Блог Amazon Web Services

Загрузка файлов в Amazon S3 напрямую из веб-приложений и мобильных приложений

Оригинал статьи: ссылка (James Beswick, Senior Developer Advocate)

В веб-приложениях и мобильных приложениях пользователи довольно часто имеют возможность загружать файлы. Приложение может позволять пользователям загружать PDF-файлы и другие документы, а также медиа-файлы, например, фотографии и видео. В каждом современном веб-сервере есть механизмы, реализующие такую функциональность. Обычно в архитектуре с использованием серверов процесс выглядит следующим образом:

Процесс загрузки с использованием серверов

  1. Пользователь загружает файл на сервер приложения.
  2. Сервер приложения сохраняет загруженный файл во временное хранилище для обработки.
  3. Приложение перемещает файл в базу данных, на файловый сервер или объектное хранилище для постоянного хранения.

Несмотря на простоту, процесс может иметь существенные побочные эффекты, влияющие на производительность сервера при высокой нагрузке. Медиа-файлы, как правило, имеют большой размер, поэтому их передача может занимать достаточно большую часть сетевого канала или процессорного времени сервера. Кроме того, вам нужно следить за состоянием загрузки, чтобы убедиться, что файл был полностью загружен, а также, при необходимости, осуществлять повторные попытки загрузки и обрабатывать ошибки.

Описанный выше процесс загрузки файлов может представлять собой непростую задачу для приложений подверженных всплескам трафика. Например, в веб-приложении, которое специализируется на отправке поздравлений, большая часть трафика приходится на праздничные дни. Если тысячи пользователей попытаются загрузить медиа-файлы примерно в одно время, это потребует масштабирования сервера приложения, а также обеспечения достаточной пропускной способности сети.

Загружая эти файлы напрямую в Amazon S3, вы можете избежать проксирования запросов через сервер приложения. Это может значительно уменьшить сетевой трафик и использование CPU, а также позволит вашему серверу обрабатывать другие запросы в нагруженные периоды. S3 также отличается высокой доступностью и надёжностью, что делает этот сервис идеальным хранилищем для загруженных пользователями файлов.

В этой статье я покажу, как реализовать бессерверную (serverless) загрузку файлов, а также преимущества такого подхода. Этот паттерн также используется в веб-приложении Happy Path, описанном в серии статей по ссылке. Вы можете скачать исходный код из этого блог-поста в GitHub-репозитории.

Обзор бессерверной загрузки в S3

Когда вы загружаете файл сразу в бакет S3, вам необходимо вначале запросить подписанный URL от самого сервиса Amazon S3. После этого вы сможете загрузить файл напрямую в S3 с использованием этого URL. Указанный процесс состоит из двух шагов с точки зрения фронтенда вашего приложения:

Бессерверная загрузка в S3

  1. Вызов на точку доступа Amazon API Gateway, который, в свою очередь, вызывает Lambda-функцию getSignedURL. Эта функция получает подписанный URL от бакета S3.
  2. Прямая загрузка файла из приложения в бакет S3.

Чтобы развернуть пример такого загрузчика в S3 в вашем аккаунте AWS, выполните следующие действия:

  1. Перейдите в репозиторий и установите все необходимые компоненты, указанные README.md.
  2. В окне терминала выполните команды:
    git clone https://github.com/aws-samples/amazon-s3-presigned-urls-aws-sam
    cd amazon-s3-presigned-urls-aws-sam
    sam deploy --guided
  3. При появлении запроса введите s3uploader в поле Stack Name, а также выберите желаемый регион для установки. Когда установка будет завершена, запомните значение поля APIendpoint в выводе.

Тестирование приложения

Я покажу два способа протестировать установленное приложение. Первый – с помощью Postman, который позволяет напрямую отправлять API-запрос, а затем загружать бинарный файл с использованием подписанного URL. Второй – с использованием простого приложения-фронтенда, которое покажет, как осуществляется интеграция с API.

Для тестирования с использованием Postman:

  1. Cкопируйте точку доступа API из вывода, показанного при развёртывании приложения.
  2. В интерфейсе Postman вставьте адрес точки доступа API в поле с названием Enter request URL.
  3. Нажмите Send.
    Тестирование с помощью Postman
  4. После завершения запроса в секции Body вы сможете увидеть ответ в формате JSON. Атрибут uploadURL содержит подписанный URL, скопируйте его в буфер обмена.
  5. Нажмите иконку «+» рядом со списком вкладок, чтобы создать новый запрос.
  6. В выпадающем списке смените метод с GET на PUT. Вставьте скопированный URL в поле Enter request URL.
  7. Перейдите на вкладку Body, затем в списке ниже выберите опцию binary.
    Выберите опцию binary в Postman
  8. Нажмите Select file и выберите файл JPG для загрузки.
    Нажмите Send. После завершения загрузки файла вы должны увидеть ответ 200 OK.
    Ответ 200 OK в Postman
  9. Перейдите в консоль S3 и откройте созданный во время установки приложения бакет. В нём вы увидите файл JPG, загруженный через Postman.
    Загруженный в S3 объект

Для тестирования с использованием приложения-фронтенда:

  1. Скопируйте файл index.html из репозитория с примером в бакет S3.
  2. Поменяйте права доступа к объекту, чтобы сделать его публично доступным на чтение.
  3. Перейдите в веб-браузере на публичный URL файла index.html.
    Тестовое приложение-фронтенд
  4. Нажмите Choose file, затем выберите JPG-файл, который хотите загрузить. Нажмите Upload image. После завершения загрузки вы увидите сообщение с подтверждением.
    Результат загрузки в тестовом приложении
  5. Перейдите в консоль S3 и откройте созданный во время установки приложения бакет. В нём вы увидите второй файл JPG, который вы загрузили из браузера.
    Второй загруженный в S3 объект

Понимание процесса загрузки в S3

Для загрузки объектов в S3 из веб-приложения вам необходимо настроить в бакете S3 правила Cross-Origin Resource Sharing (CORS). Эти правила задаются в виде документа XML. При использовании AWS SAM, вы можете настроить CORS в описании ресурса в шаблоне AWS SAM:

   S3UploadBucket:
    Type: AWS::S3::Bucket
    Properties:
      CorsConfiguration:
        CorsRules:
        - AllowedHeaders:
            - "*"
          AllowedMethods:
            - GET
            - PUT
            - HEAD
          AllowedOrigins:
            - "*"

В политике выше разрешены любые заголовки и любые адреса-источники запроса. Для приложений в производственной среде рекомендуется использовать более строгие политики.

В первом шаге процесса обработки запроса точка доступа API передаёт вызов в Lambda-функцию, которая создаёт запрос на подписанный URL. Lambda-функция содержит следующий код:

const AWS = require('aws-sdk')
AWS.config.update({ region: process.env.AWS_REGION })
const s3 = new AWS.S3()
const URL_EXPIRATION_SECONDS = 300

// Main Lambda entry point
exports.handler = async (event) => {
  return await getUploadURL(event)
}

const getUploadURL = async function(event) {
  const randomID = parseInt(Math.random() * 10000000)
  const Key = `${randomID}.jpg`

  // Get signed URL from S3
  const s3Params = {
    Bucket: process.env.UploadBucket,
    Key,
    Expires: URL_EXPIRATION_SECONDS,
    ContentType: 'image/jpeg'
  }
  const uploadURL = await s3.getSignedUrlPromise('putObject', s3Params)
  return JSON.stringify({
    uploadURL: uploadURL,
    Key
  })
}

Вначале она создаёт имя (в случае S3 оно называется «ключ») для загружаемого объекта путём генерации случайного числа. Объект s3Params указывает тип загружаемого файла, а также срок действия подписанного URL. В нашем случае он действителен в течение 300 секунд. Подписанный URL затем возвращается в JSON-объекте, также содержащем ключ объекта, который будет использоваться вызывающим приложением.

Подписанный URL содержит токен безопасности, разрешающий загрузку только указанного объекта в бакет. Чтобы успешно создать такой токен, исходный код, вызывающий getSignedUrlPromise должен иметь права вызова s3:putObject на этот бакет. Назначение политики S3WritePolicy для доступа Lambda-функции к указанному бакету происходит в шаблоне AWS SAM.

Загружаемый объект должен иметь такое же название файла и его тип, как было указано в параметрах. При соответствии значениям параметров объект может быть загружен несколько раз, если процесс загрузки начинается до истечения срока действия токена. По-умолчанию, срок действия равен 15 минутам, но вы можете указать более короткое время в зависимости от вашего сценария.

Результатом запроса от приложения-фронтенда к точке доступа API будет подписанный URL. Теперь приложение может использовать метод PUT для загрузки бинарных данных в этот URL:

let blobData = new Blob([new Uint8Array(array)], {type: 'image/jpeg'})
const result = await fetch(signedURL, {
  method: 'PUT',
  body: blobData
})

К этому моменту приложение работает напрямую с сервисом S3, а не с вашим API или Lambda-функцией. S3 возвращает код 200 после успешного завершения загрузки.

Для приложений, в которых ожидается загрузка большого количества файлов пользователями, такой подход позволяет простым образом перенаправить большой объём сетевого трафика в S3, разгрузив бэкенд-инфраструктуру вашего приложения.

Добавление аутентификации в процесс загрузки

В настоящий момент точка доступа API открыта публично, и её может использовать любой сервис в интернете. Это значит, что любой может загрузить файл JPG после получения подписанного URL. В большинстве систем, работающих в производственной среде, разработчики хотят использовать аутентификацию, чтобы контролировать, кто имеет доступ к API, и кто может загружать файлы в бакеты S3.

Вы можете ограничить доступ к API с помощью использования авторизатора (authorizer). В нашем примере используются HTTP API, которые поддерживают авторизаторы JWT. Благодаря этому, вы можете контролировать доступ к API с помощью поставщика удостоверений (identity provider), например, сервиса Amazon Cognito или Auth0.

Приложение Happy Path позволяет загружать файлы только зарегистрированным пользователям и использует Auth0 в качестве поставщика удостоверений. Репозиторий с примером содержит второй шаблон AWS SAM, templateWithAuth.yaml, который показывает, как вы можете добавить авторизацию к вашему API:

  MyApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      Auth:
        Authorizers:
          MyAuthorizer:
            JwtConfiguration:
              issuer: !Ref Auth0issuer
              audience:
                - https://auth0-jwt-authorizer
            IdentitySource: "$request.header.Authorization"
        DefaultAuthorizer: MyAuthorizer

Значения атрибутов issuer и audience необходимо взять из конфигурации Auth0. При указании этого авторизатора в качестве авторизатора по-умолчанию, он будет использоваться для всех запросов в указанном API. Прочитайте первую статью о приложении Ask Around Me, чтобы узнать больше о конфигурации Auth0 и использовании авторизаторов с HTTP API.

После добавления аутентификации веб-приложение отправляет JWT-токен в заголовках запроса:

const response = await axios.get(API_ENDPOINT_URL, {
  headers: {
    Authorization: `Bearer ${token}`
        }
})

API Gateway проверяет этот токен перед вызовом Lambda-функции getUploadURL. Это гарантирует, что только аутентифицированные пользователи могут загружать объекты в бакет S3.

Изменение ACL и создание публично доступных объектов

В текущем сценарии загруженный объект не доступен публично. Чтобы разрешить публичные права на чтение объекта, необходимо их настроить с помощью access control list (ACL). В S3 доступны предварительно настроенные ACL, в том числе public-read, который делает объект доступным для чтения всем в интернете. Установите необходимый ACL с использованием объекта params перед вызовом s3.getSignedUrl:

const s3Params = {
  Bucket: process.env.UploadBucket,
  Key,
  Expires: URL_EXPIRATION_SECONDS,
  ContentType: 'image/jpeg',
  ACL: 'public-read'
}

Так как у Lambda-функции должен быть необходимый доступ к бакету для подписи запроса, убедитесь, что ей были выданы права PutObjectAcl. Чтобы добавить их с помощью AWS SAM, используйте следующую политику:

        - Statement:
          - Effect: Allow
            Resource: !Sub 'arn:aws:s3:::${S3UploadBucket}/'
            Action:
              - s3:putObjectAcl

Заключение

Многие веб-приложения и мобильные приложения позволяют пользователям загружать данные, в том числе большие медиа-файлы, такие как изображения и видео. В традиционном приложении с использованием серверов это может вызывать высокую нагрузку на сервер приложения, а также использовать значительную часть пропускной способности сети.

Реализуя загрузку файлов напрямую в Amazon S3, описанный в данной статье подход снимает сетевую нагрузку с вашего сервиса. Это может сделать ваше приложение более масштабируемым и улучшить его способность обрабатывать пиковый трафик.

В этой статье мы рассмотрели пример приложения, реализующего такой паттерн, и описали процесс создания подписанного URL в S3. Затем мы рассмотрели, как протестировать этот URL с использованием Postman и через веб-приложение. В конце мы рассмотрели, как добавить аутентификацию, и как сделать загруженные объекты доступными публично.

Чтобы узнать больше, посмотрите это видео с описанием процесса, в котором показано, как загрузить файлы напрямую в S3 из фронтенда веб-приложения. Больше ресурсов для обучения бессерверным технологиям вы можете найти по ссылке https://serverlessland.com/.