はじめに

サーバーレスアーキテクチャを使えば、サーバーを管理せずにアプリケーションを構築・実行できます。AWS Lambdaはイベントに応じてコードを実行し、API Gatewayは完全マネージドなHTTPエンドポイントを提供します。DynamoDBと組み合わせれば、スケーラブルでコスト効率の良いAPIスタックが完成します。

このチュートリアルでは、タスク管理システムの完全なCRUD REST APIを構築します。

前提条件

  • AWSアカウント(Free Tier対象)
  • AWS CLI設定済み(aws configure
  • SAM CLIインストール済み
  • Python 3.12以上
  • REST APIの基本知識
  • プロジェクトセットアップ

    まず、新しいSAMプロジェクトを作成します:

    # SAM CLIをインストール
    

    pip install aws-sam-cli

    # プロジェクトを初期化

    sam init --runtime python3.12 --name task-api --app-template hello-world

    cd task-api

    プロジェクト構造

    task-api/
    

    ├── template.yaml # SAM/CloudFormationテンプレート

    ├── src/

    │ ├── handlers/ # Lambda関数ハンドラー

    │ │ ├── create_task.py

    │ │ ├── get_task.py

    │ │ ├── list_tasks.py

    │ │ ├── update_task.py

    │ │ └── delete_task.py

    │ └── utils/ # 共通ユーティリティ

    │ ├── dynamodb.py

    │ └── response.py

    ├── tests/

    ├── requirements.txt

    └── samconfig.toml

    ステップ1:SAMでインフラを定義する

    template.yamlを編集します:

    AWSTemplateFormatVersion: '2010-09-09'
    

    Transform: AWS::Serverless-2016-10-31

    Description: サーバーレスタスク管理API

    Globals:

    Function:

    Timeout: 10

    Runtime: python3.12

    MemorySize: 256

    Environment:

    Variables:

    TABLE_NAME: !Ref TasksTable

    Architectures:

    - arm64 # Graviton2 — より安価で高速

    Resources:

    # DynamoDBテーブル

    TasksTable:

    Type: AWS::DynamoDB::Table

    Properties:

    TableName: tasks

    BillingMode: PAY_PER_REQUEST

    AttributeDefinitions:

    - AttributeName: task_id

    AttributeType: S

    - AttributeName: user_id

    AttributeType: S

    KeySchema:

    - AttributeName: user_id

    KeyType: HASH

    - AttributeName: task_id

    KeyType: RANGE

    # API Gateway

    TaskApi:

    Type: AWS::Serverless::Api

    Properties:

    StageName: prod

    Cors:

    AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"

    AllowHeaders: "'Content-Type,Authorization'"

    AllowOrigin: "'*'"

    # Lambda関数(各CRUDエンドポイント)

    CreateTaskFunction:

    Type: AWS::Serverless::Function

    Properties:

    CodeUri: src/

    Handler: handlers.create_task.handler

    Events:

    CreateTask:

    Type: Api

    Properties:

    RestApiId: !Ref TaskApi

    Path: /tasks

    Method: POST

    Policies:

    - DynamoDBCrudPolicy:

    TableName: !Ref TasksTable

    ステップ2:ユーティリティモジュールを構築する

    DynamoDBヘルパー

    import os
    

    import boto3

    from boto3.dynamodb.conditions import Key

    _table = None

    def get_table():

    """Lambda実行間でDynamoDB接続を再利用する。"""

    global _table

    if _table is None:

    dynamodb = boto3.resource('dynamodb')

    _table = dynamodb.Table(os.environ['TABLE_NAME'])

    return _table

    def put_item(item: dict) -> dict:

    table = get_table()

    table.put_item(Item=item)

    return item

    def query_by_user(user_id: str, limit: int = 50) -> list[dict]:

    table = get_table()

    response = table.query(

    KeyConditionExpression=Key('user_id').eq(user_id),

    Limit=limit,

    ScanIndexForward=False

    )

    return response.get('Items', [])

    レスポンスヘルパー

    import json
    

    from decimal import Decimal

    class DecimalEncoder(json.JSONEncoder):

    def default(self, obj):

    if isinstance(obj, Decimal):

    return int(obj) if obj % 1 == 0 else float(obj)

    return super().default(obj)

    def success(body, status_code=200):

    return {

    'statusCode': status_code,

    'headers': {

    'Content-Type': 'application/json',

    'Access-Control-Allow-Origin': '*',

    },

    'body': json.dumps(body, cls=DecimalEncoder)

    }

    def error(message, status_code=400):

    return success({'error': message}, status_code)

    ステップ3:CRUDハンドラーを実装する

    タスク作成

    import json
    

    import uuid

    from datetime import datetime, timezone

    from utils.dynamodb import put_item

    from utils.response import success, error

    def handler(event, context):

    body = json.loads(event.get('body', '{}'))

    title = body.get('title', '').strip()

    if not title:

    return error('title is required')

    now = datetime.now(timezone.utc).isoformat()

    task = {

    'task_id': str(uuid.uuid4()),

    'user_id': body.get('user_id', 'default'),

    'title': title,

    'description': body.get('description', ''),

    'status': 'pending',

    'priority': body.get('priority', 'medium'),

    'created_at': now,

    'updated_at': now,

    }

    put_item(task)

    return success(task, 201)

    ステップ4:ローカルテスト

    # ビルド
    

    sam build

    # ローカルAPI起動(Docker必要)

    sam local start-api --port 3000

    # テスト

    curl -X POST http://localhost:3000/tasks \

    -H 'Content-Type: application/json' \

    -d '{"title": "Lambdaを学ぶ", "priority": "high"}'

    curl http://localhost:3000/tasks

    ステップ5:ミドルウェアでリクエスト検証を追加

    import json
    

    import logging

    import traceback

    from functools import wraps

    from utils.response import error

    logger = logging.getLogger()

    logger.setLevel(logging.INFO)

    def api_handler(func):

    """一貫したエラーハンドリングとロギングのデコレーター。"""

    @wraps(func)

    def wrapper(event, context):

    logger.info(json.dumps({

    'method': event.get('httpMethod'),

    'path': event.get('path'),

    'request_id': context.aws_request_id,

    }))

    try:

    return func(event, context)

    except json.JSONDecodeError:

    return error('リクエストボディのJSONが不正です', 400)

    except Exception as e:

    logger.error(traceback.format_exc())

    return error('Internal server error', 500)

    return wrapper

    ステップ6:AWSへデプロイ

    # 初回デプロイ(ガイド付き)
    

    sam deploy --guided

    # 以降のデプロイ

    sam deploy

    ステップ7:APIキー認証を追加

      TaskApi:
    

    Type: AWS::Serverless::Api

    Properties:

    StageName: prod

    Auth:

    ApiKeyRequired: true

    UsagePlan:

    CreateUsagePlan: PER_API

    Throttle:

    BurstLimit: 50

    RateLimit: 20

    Quota:

    Limit: 10000

    Period: MONTH

    コスト見積もり

    | サービス | 無料枠 | 無料枠超過後 |

    |----------|--------|-------------|

    | Lambda | 月100万リクエスト | 100万リクエストあたり$0.20 |

    | API Gateway | 月100万コール | 100万コールあたり$3.50 |

    | DynamoDB | 25GBストレージ | GB/月あたり$0.25 |

    低トラフィックAPI(約1万リクエスト/日)の場合、月額コストは無料枠内で$0、超過後は約$3です。

    まとめ

    完全なサーバーレスREST APIを構築しました:

  • ✅ Lambda + DynamoDBでのCRUD操作
  • ✅ API GatewayによるHTTPルーティング
  • ✅ SAM CLIでのローカルテスト
  • ✅ APIキー認証とレート制限
  • ✅ CloudWatch監視とアラート
  • ✅ Infrastructure as Code(SAM/CloudFormation)
  • 参考リンク

  • AWS SAM ドキュメント
  • Lambda ベストプラクティス
  • API Gateway REST API ガイド