Мечта о бессерверных вычислениях (и почему это реально)

Помните времена, когда развёртывание приложения означало аренду физического сервера, беспокойство о месте на диске и молитвы о том, чтобы ваша инфраструктура не сгорела в 3 часа ночи в воскресенье? К счастью, эти дни остались позади. Бессерверные вычисления, особенно AWS Lambda, изменили наше представление о создании и развёртывании приложений. Но вот что вам никто не скажет на конференциях: бессерверные вычисления не означают «без серверов». Это значит, что кто-то другой беспокоится о серверах, пока вы решаете реальные проблемы.

В этой статье я покажу вам, как создать готовое к продакшену бессерверное приложение с помощью AWS Lambda и API Gateway. Мы говорим не о игрушечном примере «Hello World», который исчезает при закрытии терминала. Мы создаём что-то реальное, масштабируемое и то, о чём вы не пожалеете в 2 часа ночи.

Понимание архитектуры

Прежде чем мы начнём писать код, давайте разберёмся, что мы на самом деле создаём. Типичное бессерверное приложение с Lambda и API Gateway выглядит примерно так:

graph LR A[Клиентский запрос] -->|HTTPS| B[API Gateway] B -->|Вызывает| C[Функция Lambda] C -->|Запрос/Обновление| D[DynamoDB] D -->|Ответ| C C -->|Возвращает| B B -->|JSON ответ| A

Вот что происходит:

  • API Gateway действует как ваша входная дверь, обрабатывая все входящие HTTP-запросы.
  • Функции Lambda — это ваши бессерверные вычислительные единицы, которые выполняют ваш код по требованию.
  • DynamoDB (или любая другая служба AWS) обрабатывает ваше постоянное хранилище данных.
  • Всё масштабируется автоматически в зависимости от спроса, и вы платите только за то, что используете.

Преимущество? Вы не выделяете серверы. Вы не управляете группами автомасштабирования. Вы просто пишете функции, а AWS берёт на себя остальное.

Начало работы: три способа

Существует три основных способа создания бессерверных приложений на AWS, каждый со своей степенью сложности и контроля.

Способ 1: Консоль AWS (туристический подход)

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

Способ 2: Серверная платформа (выбор прагматиков)

Моя любимая платформа для быстрого выполнения задач — Serverless Framework. Она интуитивно понятна, имеет отличную документацию и автоматически обрабатывает многие аспекты AWS.

Вот как начать:

npm install -g serverless
serverless create --template aws-nodejs-typescript --path my-awesome-api
cd my-awesome-api

Это создаёт структуру проекта со всем необходимым. Ваш файл serverless.yml — это место, где происходит волшебство: он определяет ваши функции, триггеры и инфраструктуру.

Способ 3: AWS SAM и CDK (путь для предприятий)

Для более сложных приложений AWS SAM (Serverless Application Model) и CDK (Cloud Development Kit) предлагают мощные возможности инфраструктуры как кода. Они дают вам детальный контроль над каждым аспектом вашего развёртывания, но требуют более глубокого изучения.

В этой статье я сосредоточусь на Serverless Framework, так как она сочетает в себе мощность и удобство использования.

Создание вашего первого API: реальный пример

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

Шаг 1: Настройка проекта

serverless create --template aws-nodejs-typescript --path expense-tracker
cd expense-tracker
npm install

Это даёт вам базовую структуру проекта. Теперь давайте определим нашу инфраструктуру в serverless.yml:

service: expense-tracker
provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  environment:
    EXPENSES_TABLE: expenses-${sls:stage}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
          Resource: "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/${self:provider.environment.EXPENSES_TABLE}"
functions:
  createExpense:
    handler: src/handlers/createExpense.main
    events:
      - http:
          path: expenses
          method: post
          cors: true
  listExpenses:
    handler: src/handlers/listExpenses.main
    events:
      - http:
          path: expenses
          method: get
          cors: true
  getExpense:
    handler: src/handlers/getExpense.main
    events:
      - http:
          path: expenses/{id}
          method: get
          cors: true
resources:
  Resources:
    ExpensesTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.EXPENSES_TABLE}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH

Вот что мы сделали:

  • Определили три функции Lambda для разных конечных точек.
  • Настроили разрешения IAM, чтобы наши функции Lambda могли обращаться к DynamoDB.
  • Настроили API Gateway для обработки HTTP-запросов.
  • Создали таблицу DynamoDB с оплатой по факту использования (платите только за то, что используете).

Шаг 2: Написание функций Lambda

Создайте файлы обработчиков. Первый, src/handlers/createExpense.ts:

import { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDB } from 'aws-sdk';
import { v4 as uuidv4 } from 'uuid';
const dynamodb = new DynamoDB.DocumentClient();
export const main: APIGatewayProxyHandler = async (event) => {
  try {
    const { amount, category, description } = JSON.parse(event.body || '{}');
    if (!amount || !category) {
      return {
        statusCode: 400,
        body: JSON.stringify({ message: 'amount and category are required' }),
      };
    }
    const id = uuidv4();
    const timestamp = new Date().toISOString();
    await dynamodb.put({
      TableName: process.env.EXPENSES_TABLE!,
      Item: {
        id,
        amount,
        category,
        description,
        createdAt: timestamp,
      },
    }).promise();
    return {
      statusCode: 201,
      body: JSON.stringify({
        id,
        amount,
        category,
        description,
        createdAt: timestamp,
      }),
    };
  } catch (error) {
    console.error('Error creating expense:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Failed to create expense' }),
    };
  }
};

Теперь src/handlers/listExpenses.ts:

import { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDB } from 'aws-sdk';
const dynamodb = new DynamoDB.DocumentClient();
export const main: APIGatewayProxyHandler = async () => {
  try {
    const result = await dynamodb.scan({
      TableName: process.env.EXPENSES_TABLE!,
    }).promise();
    return {
      statusCode: 200,
      body: JSON.stringify({
        expenses: result.Items || [],
        count: result.Count,
      }),
    };
  } catch (error) {
    console.error('Error listing expenses:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Failed to list expenses' }),
    };
  }
};

И src/handlers/getExpense.ts:

import { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDB } from 'aws-sdk';
const dynamodb = new DynamoDB.DocumentClient();
export const main: APIGatewayProxyHandler = async (event) => {
  try {
    const { id } = event.pathParameters || {};
    if (!id) {
      return {
        statusCode: 400,
        body: JSON.stringify({ message: 'id is