Мечта о бессерверных вычислениях (и почему это реально)
Помните времена, когда развёртывание приложения означало аренду физического сервера, беспокойство о месте на диске и молитвы о том, чтобы ваша инфраструктура не сгорела в 3 часа ночи в воскресенье? К счастью, эти дни остались позади. Бессерверные вычисления, особенно AWS Lambda, изменили наше представление о создании и развёртывании приложений. Но вот что вам никто не скажет на конференциях: бессерверные вычисления не означают «без серверов». Это значит, что кто-то другой беспокоится о серверах, пока вы решаете реальные проблемы.
В этой статье я покажу вам, как создать готовое к продакшену бессерверное приложение с помощью AWS Lambda и API Gateway. Мы говорим не о игрушечном примере «Hello World», который исчезает при закрытии терминала. Мы создаём что-то реальное, масштабируемое и то, о чём вы не пожалеете в 2 часа ночи.
Понимание архитектуры
Прежде чем мы начнём писать код, давайте разберёмся, что мы на самом деле создаём. Типичное бессерверное приложение с Lambda и API Gateway выглядит примерно так:
Вот что происходит:
- 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
