HardПрактика3 min

Lambda и DynamoDB

Детальная реализация Lambda-функции счётчика посещений с DynamoDB и API Gateway на AWS

Архитектура бэкенда CRC на AWS

Браузер → API Gateway (HTTP API) → Lambda → DynamoDB
                                      ↓
                                  CloudWatch Logs

DynamoDB: создание и настройка

# Создаём таблицу
aws dynamodb create-table \
  --table-name visitor-counter \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --tags Key=project,Value=crc

# Начальная запись
aws dynamodb put-item \
  --table-name visitor-counter \
  --item '{"id": {"S": "visitors"}, "count": {"N": "0"}}'

# Проверка
aws dynamodb get-item \
  --table-name visitor-counter \
  --key '{"id": {"S": "visitors"}}'

Lambda: полная реализация

Код функции

# lambda_function.py
import json
import logging
import boto3
from decimal import Decimal

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Initialize outside handler (reused between invocations)
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('visitor-counter')

CORS_HEADERS = {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': 'https://resume.ivanpetrov.com',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type'
}

def lambda_handler(event, context):
    """Main handler for visitor counter."""
    logger.info(f"Event: {json.dumps(event)}")

    try:
        # Atomic increment using UpdateExpression
        response = table.update_item(
            Key={'id': 'visitors'},
            UpdateExpression='ADD #c :val',
            ExpressionAttributeNames={'#c': 'count'},
            ExpressionAttributeValues={':val': Decimal('1')},
            ReturnValues='UPDATED_NEW'
        )

        count = int(response['Attributes']['count'])
        logger.info(f"Visitor count: {count}")

        return {
            'statusCode': 200,
            'headers': CORS_HEADERS,
            'body': json.dumps({'count': count})
        }

    except Exception as e:
        logger.error(f"Error: {e}", exc_info=True)
        return {
            'statusCode': 500,
            'headers': CORS_HEADERS,
            'body': json.dumps({'error': 'Service temporarily unavailable'})
        }

Зависимости

# requirements.txt
boto3>=1.34.0

boto3 уже включён в Lambda runtime, но фиксация версии в requirements.txt обеспечивает воспроизводимость.

Деплой

# Создаём IAM Role
aws iam create-role \
  --role-name lambda-crc-role \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {"Service": "lambda.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }]
  }'

# Прикрепляем политику
aws iam put-role-policy \
  --role-name lambda-crc-role \
  --policy-name crc-dynamodb \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": ["dynamodb:UpdateItem", "dynamodb:GetItem"],
        "Resource": "arn:aws:dynamodb:us-east-1:*:table/visitor-counter"
      },
      {
        "Effect": "Allow",
        "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
        "Resource": "arn:aws:logs:us-east-1:*:*"
      }
    ]
  }'

# Упаковка и деплой
cd backend && zip -r ../function.zip . && cd ..

aws lambda create-function \
  --function-name visitor-counter \
  --runtime python3.12 \
  --architectures arm64 \
  --handler lambda_function.lambda_handler \
  --role arn:aws:iam::123456789012:role/lambda-crc-role \
  --zip-file fileb://function.zip \
  --timeout 10 \
  --memory-size 128 \
  --environment 'Variables={TABLE_NAME=visitor-counter}'

API Gateway: HTTP API

# Создаём HTTP API
API_ID=$(aws apigatewayv2 create-api \
  --name crc-api \
  --protocol-type HTTP \
  --cors-configuration '{
    "AllowOrigins": ["https://resume.ivanpetrov.com"],
    "AllowMethods": ["GET", "OPTIONS"],
    "MaxAge": 86400
  }' --query 'ApiId' --output text)

# Интеграция с Lambda
INT_ID=$(aws apigatewayv2 create-integration \
  --api-id $API_ID \
  --integration-type AWS_PROXY \
  --integration-uri arn:aws:lambda:us-east-1:123456789012:function:visitor-counter \
  --payload-format-version "2.0" \
  --query 'IntegrationId' --output text)

# Маршрут
aws apigatewayv2 create-route \
  --api-id $API_ID \
  --route-key "GET /count" \
  --target "integrations/$INT_ID"

# Стадия с auto-deploy
aws apigatewayv2 create-stage \
  --api-id $API_ID \
  --stage-name prod \
  --auto-deploy

# Permission для API Gateway вызывать Lambda
aws lambda add-permission \
  --function-name visitor-counter \
  --statement-id apigateway \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:us-east-1:123456789012:$API_ID/*/GET/count"

echo "API URL: https://$API_ID.execute-api.us-east-1.amazonaws.com/prod/count"

Тестирование

# Тест API
curl https://API_ID.execute-api.us-east-1.amazonaws.com/prod/count
# {"count": 1}

# Повторный вызов
curl https://API_ID.execute-api.us-east-1.amazonaws.com/prod/count
# {"count": 2}

# Проверка CORS
curl -v -H "Origin: https://resume.ivanpetrov.com" \
  https://API_ID.execute-api.us-east-1.amazonaws.com/prod/count

Мониторинг

# Просмотр логов Lambda
aws logs tail /aws/lambda/visitor-counter --follow

# Метрики
aws cloudwatch get-metric-statistics \
  --namespace AWS/Lambda \
  --metric-name Invocations \
  --dimensions Name=FunctionName,Value=visitor-counter \
  --start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 3600 \
  --statistics Sum

Проверь себя

Зачем Lambda добавляется permission для API Gateway?

Почему Lambda использует arm64 архитектуру?

Какой billing-mode DynamoDB рекомендуется для CRC?

Что такое payload-format-version 2.0 в интеграции API Gateway с Lambda?

Зачем используется ExpressionAttributeNames '#c' вместо прямого имени 'count'?