Изучайте serverless на AWS шаг за шагом — Файловое хранилище

Изучайте serverless на AWS шаг за шагом — Файловое хранилище

В прошлой статье я рассказал об основах взаимодействия с базами данных DynamoDB с помощью CDK. В этой статье я расскажу о том, как хранить данные в виде файлов с помощью Amazon S3. Эта серия будет продолжаться! Следите за мной в twitter или DEV, чтобы получить уведомление о публикации следующей статьи!

Бессерверное хранение файлов с помощью Amazon S3

Две недели назад я рассказал об основах взаимодействия с базами данных DynamoDB. Эти базы данных отлично подходят для хранения структурированных данных, состоящих из небольших элементов: предельный размер одного элемента составляет 400 КБ. Однако что, если вы хотите хранить большие файлы? В этом случае следует использовать Amazon S3, сервис, позволяющий хранить файлы в облаке.

Amazon S3 очень мощный, потому что он может хранить практически бесконечное количество данных и оставаться доступным 99,99999999999% времени. Это также очень дешево, поскольку вы платите только за используемое хранилище и объем передаваемых данных.

Вместе мы собираемся создать небольшой клон dev.to, позволяющий пользователям публиковать, размещать и читать статьи. Содержимое статей, которое может быть довольно большим, мы будем хранить в Amazon S3, а метаданные (название статьи, автор и т.д.) - в DynamoDB: это будет отличный обзор предыдущей статьи!

В Amazon S3 файлы хранятся в ведрах. Ведро - это контейнер для файлов, и оно имеет уникальное имя. Каждое ведро может содержать почти бесконечное количество файлов. Вы также можете создавать подпапки в ведре и хранить файлы в этих подпапках.

Вот небольшая схема архитектуры, которую мы будем строить:

Схема архитектуры

Приложение будет содержать 3 лямбда-функции, базу данных DynamoDB, ведро S3 и REST API, взаимодействующие между собой.

Как создать ведро S3 с помощью CDK?

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

Создать ведро очень просто, нужно лишь добавить следующий код в файл learn-serverless-stack.ts, в конструктор:

// Previous code

export class LearnServerlessStack extends cdk.Stack {
  // Previous code
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Previous code
    const articlesBucket = new cdk.aws_s3.Bucket(this, 'articlesBucket', {});
  }
}

Видите, ничего сложного! Обратите внимание, что я не указал никакого имени для ведра, CDK автоматически сгенерирует его за меня. Если вы хотите указать имя, вы можете сделать это, передав его в качестве параметра, но имейте в виду, что имя должно быть уникальным в мире, иначе развертывание будет неудачным.

Создание базы данных DynamoDB и трех функций Lambda

Давайте создадим таблицу DynamoDB для хранения метаданных наших статей. Мы также создадим три функции Lambda: одну для создания статьи, одну для списка всех статей и одну для чтения конкретной статьи.

// Previous code

// Create the database
const articlesDatabase = new cdk.aws_dynamodb.Table(this, 'articlesDatabase', {
  partitionKey: {
    name: 'PK',
    type: cdk.aws_dynamodb.AttributeType.STRING,
  },
  sortKey: {
    name: 'SK',
    type: cdk.aws_dynamodb.AttributeType.STRING,
  },
  billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST,
});

// Create the publishArticle Lambda function
const publishArticle = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'publishArticle', {
  entry: path.join(__dirname, 'publishArticle', 'handler.ts'),
  handler: 'handler',
  environment: {
    BUCKET_NAME: articlesBucket.bucketName,
    TABLE_NAME: articlesDatabase.tableName,
  },
});

// Create the listArticles Lambda function
const listArticles = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'listArticles', {
  entry: path.join(__dirname, 'listArticles', 'handler.ts'),
  handler: 'handler',
  environment: {
    BUCKET_NAME: articlesBucket.bucketName,
    TABLE_NAME: articlesDatabase.tableName,
  },
});

// Create the getArticle Lambda function
const getArticle = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'getArticle', {
  entry: path.join(__dirname, 'getArticle', 'handler.ts'),
  handler: 'handler',
  environment: {
    BUCKET_NAME: articlesBucket.bucketName,
  },
});

Если вернуться к схеме архитектуры, то можно увидеть, что существуют взаимодействия между функциями Lambda и S3 Bucket и таблицей DynamoDB:

  • Ведро S3 взаимодействует с publishArticle и getArticle -> я передал имя моего нового ведра в качестве переменной окружения функциям Lambda.
  • Таблица DynamoDB взаимодействует с publishArticle и listArticles -> я передал имя моей новой таблицы в качестве переменной окружения функциям Lambda.

Эти переменные окружения будут использоваться функциями Lambda для взаимодействия с S3 Bucket и таблицей DynamoDB.

Предоставьте разрешения функциям Lambda и создайте REST API

Разрешения являются основой безопасности в приложениях AWS. Если вы не предоставите явных разрешений вашим функциям Lambda, они не смогут взаимодействовать с ведром S3 и таблицей DynamoDB. Мы будем использовать методы grantRead и grantWrite для предоставления разрешений нашим Lambda-функциям.

// Previous code
articlesBucket.grantWrite(publishArticle);
articlesDatabase.grantWriteData(publishArticle);

articlesDatabase.grantReadData(listArticles);

articlesBucket.grantRead(getArticle);

Наконец, давайте подключим наши функции Lambda к REST API. Судя по прошлым статьям, API уже существует, и нам просто нужно добавить к нему новый ресурс. Мы создадим новый ресурс под названием articles и добавим к нему три метода: POST, GET и GET /{id}.

// Previous code
const articlesResource = myFirstApi.root.addResource('articles');

articlesResource.addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(publishArticle));
articlesResource.addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(listArticles));
articlesResource.addResource('{id}').addMethod('GET', new cdk.aws_apigateway.LambdaIntegration(getArticle));

И мы закончили с инфраструктурой! Осталось написать код наших лямбда-функций (самая интересная часть 😎).

Взаимодействие с ведром S3 и таблицей DynamoDB

Публикация статьи

Начнем с лямбда-функции publishArticle. Эта функция будет вызываться, когда пользователь захочет опубликовать статью. Она получит название, содержание и автора статьи из тела запроса, сохранит статью в S3 Bucket и ее метаданные в таблице DynamoDB.

import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';

const dynamoDBClient = new DynamoDBClient({});
const s3Client = new S3Client({});

export const handler = async (event: { body: string }): Promise<{ statusCode: number; body: string }> => {
  // parse the request body
  const { title, content, author } = JSON.parse(event.body) as { title?: string; content?: string; author?: string };

  if (title === undefined || content === undefined || author === undefined) {
    return Promise.resolve({ statusCode: 400, body: 'Missing title or content' });
  }

  // generate a unique id for the article
  const id = uuidv4();

  // store the article metadata in the database PK = article, SK = ${id}
  await dynamoDBClient.send(
    new PutItemCommand({
      TableName: process.env.TABLE_NAME,
      Item: {
        PK: { S: `article` },
        SK: { S: id },
        title: { S: title },
        author: { S: title },
      },
    }),
  );

  // store the article content in the bucket
  await s3Client.send(
    new PutObjectCommand({
      Bucket: process.env.BUCKET_NAME,
      Key: id,
      Body: content,
    }),
  );

  // return the id of the article
  return { statusCode: 200, body: JSON.stringify({ id }) };
};

В приведенном выше коде я использовал пакеты @aws-sdk/client-dynamodb и @aws-sdk/client-s3 для взаимодействия с таблицей DynamoDB и ведром S3. Я использовал команды PutItemCommand и PutObjectCommand для хранения метаданных и содержимого статьи в таблице DynamoDB и ведре S3.

Если вам нужно освежить в памяти, как взаимодействовать с DynamoDB, ознакомьтесь с моей последней статьей;

Обратите внимание, что я использовал переменные окружения process.env.TABLE_NAME и process.env.BUCKET_NAME для получения имени таблицы DynamoDB и ведра S3, эти переменные окружения были установлены в CDK ранее!

Получить статью

Лямбда-функция getArticle будет вызываться, когда пользователь захочет получить статью. Она получит id статьи из параметров пути запроса и вернет содержимое статьи, хранящееся в S3 Bucket.

import { GetObjectCommand, GetObjectCommandOutput, S3Client } from '@aws-sdk/client-s3';

const client = new S3Client({});

export const handler = async ({
  pathParameters: { id },
}: {
  pathParameters: { id: string };
}): Promise<{ statusCode: number; body: string }> => {
  let result: GetObjectCommandOutput | undefined;

  // get the article content from the bucket using the id as the key
  try {
    result = await client.send(
      new GetObjectCommand({
        Bucket: process.env.BUCKET_NAME,
        Key: id,
      }),
    );
  } catch {
    result = undefined;
  }

  if (result?.Body === undefined) {
    return { statusCode: 404, body: 'Article not found' };
  }

  // transform the body of the response to a string
  const content = await result.Body.transformToString();

  // return the article content
  return {
    statusCode: 200,
    body: JSON.stringify({ content }),
  };
};

В приведенном выше коде я использовал пакет @aws-sdk/client-s3 для взаимодействия с S3 Bucket. Я использовал команду GetObjectCommand для получения содержимого статьи из ведра S3. Указанный ключ id - это id статьи (помните, что я использовал этот id для создания объекта в PublishArticle).

Команда GetObjectCommand возвращает объект GetObjectCommandOutput, который содержит тело ответа. Если запрос не встречает проблем, я использую метод transformToString, чтобы преобразовать тело в строку и вернуть ее.

Перечислить статьи

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

import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb';

const client = new DynamoDBClient({});

export const handler = async (): Promise<{ statusCode: number; body: string }> => {
  // Query the list of the articles with the PK = 'article'
  const { Items } = await client.send(
    new QueryCommand({
      TableName: process.env.TABLE_NAME,
      KeyConditionExpression: 'PK = :pk',
      ExpressionAttributeValues: {
        ':pk': { S: 'article' },
      },
    }),
  );

  if (Items === undefined) {
    return { statusCode: 500, body: 'No articles found' };
  }

  // map the results (un-marshall the DynamoDB attributes)
  const articles = Items.map(item => ({
    id: item.SK?.S,
    title: item.title?.S,
    author: item.author?.S,
  }));

  // return the list of articles (title, id and author)
  return {
    statusCode: 200,
    body: JSON.stringify({ articles }),
  };
};

Эта функция Lambda менее интересна, так как она работает только с DynamoDB, я использую команду QueryCommand для перечисления всех элементов таблицы с PK = ‘article’. (помните, что я установил PK = ‘article’ и SK = ’${id}’, когда хранил метаданные статьи в PublishArticle).

Пора протестировать наше приложение!

Сначала необходимо развернуть приложение. Для этого нужно выполнить следующую команду:

npm run cdk deploy

Как только вы закончите, перейдите в Postman и протестируйте ваш новый API!

Давайте создадим статью с помощью POST-вызова, содержащего правильное тело:

Создайте первую статью

Затем вы можете перечислить все статьи с помощью вызова GET:

Список статей (1 статья)

И, наконец, вы можете получить содержимое только что созданной статьи с помощью вызова GET:

Получить первую статью

Но теперь, чтобы увидеть силу S3, давайте создадим вторую статью с гораздо, гораздо, гораздо большим содержанием:

Создание второй статьи

Вы можете видеть, что список статей обновляется автоматически:

И когда вы получаете содержимое второй статьи, вы видите, что полезная нагрузка намного больше, чем 800kB!!! Это никогда бы не поместилось в элемент DynamoDB! Вот в чем сила S3!

Список статей (2 статьи)
Получить вторую статью

Улучшение качества нашего приложения

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

  • Мы можем зарегистрировать статьи в S3 Bucket с помощью класса хранения "Intelligent-Tiering", который будет автоматически перемещать данные в класс хранения "Infrequent Access", когда к ним не обращаются в течение некоторого времени.
  • Мы можем заблокировать публичный доступ к нашему S3 Bucket, за исключением случаев, когда доступ к нему осуществляет функция Lambda
  • Мы можем обеспечить шифрование нашего ведра S3.
  • ...

Для этого давайте обновим конфигурацию нашего ведра S3:

// Previously, we had:
const articlesBucket = new cdk.aws_s3.Bucket(this, 'articlesBucket', {});

// Now, we have:
const articlesBucket = new cdk.aws_s3.Bucket(this, 'articlesBucket', {
  lifecycleRules: [
    // Enable intelligent tiering
    {
      transitions: [
        {
          storageClass: cdk.aws_s3.StorageClass.INTELLIGENT_TIERING,
          transitionAfter: cdk.Duration.days(0),
        },
      ],
    },
  ],
  blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL, // Enable block public access
  encryption: cdk.aws_s3.BucketEncryption.S3_MANAGED, // Enable encryption
});

Существует множество других небольших улучшений, которые вы можете сделать не только с этим Bucket, но и с вашими Lambdas или API! Я создал инструмент под названием sls-mentor, который поможет вам улучшить качество вашего бессерверного приложения. Он проверит ваш код и вашу конфигурацию и даст вам рекомендации по улучшению вашего приложения!

По сути, это большой облачный линтер, не стесняйтесь проверить его, чтобы узнать больше о AWS!

Домашнее задание 🤓

Мы создали простое приложение для публикации и чтения статей. Но оно не идеально, есть некоторые вещи, которые мы можем улучшить:

  • Мы не можем ни удалить статью, ни обновить ее!
  • Нет управления пользователями, любой может опубликовать статью и просмотреть все статьи. Основываясь на моей последней статье, вы можете использовать userId в качестве PK таблицы статей. Таким образом, вы сможете выводить список статей только пользователя, и только он сможет удалять свои статьи.
  • Если у вас есть желание, вы можете попробовать хранить в S3 изображения обложек для каждой статьи и возвращать их при ПОЛУЧЕНИИ статьи.

Вот это программа! Надеюсь, вам понравилась эта статья, и вы узнали что-то новое. Если у вас есть вопросы, не стесняйтесь обращаться ко мне!

Помните, что код, описанный в этой статье, вы можете найти в моем репозитории.

Заключение

Я планирую продолжать эту серию статей раз в два месяца. Я уже рассказал о создании простых лямбда-функций и REST API, а также о взаимодействии с базами данных DynamoDB. Вы можете следить за этим прогрессом на моем репозитории! Я буду освещать новые темы, такие как хранение файлов, создание приложений, управляемых событиями, и многое другое. Если у вас есть какие-либо предложения, не стесняйтесь обращаться ко мне!

Я буду очень признателен, если вы отреагируете и поделитесь этой статьей со своими друзьями и коллегами. Это очень поможет мне расширить свою аудиторию. Также не забудьте подписаться на рассылку, чтобы быть в курсе, когда выйдет следующая статья!