AWS サーバーレスで CRUD API を作ってみた

AWS サーバーレスで CRUD API を作ってみた

2025.09.19

はじめに

お疲れ様です。あきとです。

今回は AWS Lambda・DynamoDB・API Gateway (HTTP API) を組み合わせて、シンプルな CRUD 操作システムを構築しました。

AWS 公式ドキュメントにある チュートリアル: Lambda と DynamoDB を使用して CRUD HTTP API を作成するをベースにしつつ、自分なりに設定を調整し、最小限の権限設計やコード改善を行った内容を整理しています。
これからサーバーレス構成で CRUD API を作ってみたい方や、公式チュートリアルから一歩踏み込んで理解を深めたい方の参考になれば幸いです。

使用技術

  • データベース: Amazon DynamoDB
  • API実行環境: AWS Lambda(Python 3.13)
  • APIゲートウェイ: Amazon API Gateway(HTTP API)
  • アクセス制御: AWS IAM

システム構成

AWS構成図
システム構成図 - Clients、HTTP API、Lambda、DynamoDB の連携フロー

API Gateway が外部からの HTTP リクエストを受け取り、Lambda 関数をトリガーして、DynamoDB でデータの読み書きを行う構成です。

やってみた

手順1: DynamoDB テーブル作成

まず、データを保存するための DynamoDB テーブルを作成します。

1-1. DynamoDB サービスに移動

  1. コンソール上部の検索ボックスに「DynamoDB」と入力
  2. 検索結果から「DynamoDB」を選択してクリック

1-2. テーブルを作成

  1. DynamoDB ダッシュボードで「テーブルの作成」ボタンをクリック
  2. 以下の設定を入力:
    • テーブル名: items(任意の名前)
    • パーティションキー: id(データ型:文字列)
  3. その他の設定はデフォルトのまま
  4. 「テーブルの作成」ボタンをクリック

DynamoDB作成
DynamoDB テーブル作成画面 - パーティションキーは文字列型で設定

手順2: IAM 権限の設定

Lambda 関数が DynamoDB にアクセスできるよう、適切な IAM ポリシーとロールを作成します。

2-1. IAM サービスに移動

  1. AWS マネジメントコンソール上部の検索ボックスに「IAM」と入力
  2. 検索結果から「IAM」を選択してクリック

2-2. カスタムポリシーを作成

  1. 左サイドバーの「ポリシー」をクリック
  2. 「ポリシーの作成」ボタンをクリック
  3. 「JSON」タブをクリック
  4. 既存のJSONを削除し、以下のコードを貼り付け:
			
			{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "crud",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:DeleteItem"
      ],
      "Resource": [
        "arn:aws:dynamodb:<REGION>:<ACCOUNT_ID>:table/<TABLE_NAME>"
      ]
    }
  ]
}

		
  1. 「次へ」ボタンをクリック
  2. ポリシー名に「DynamoDBCrudPolicy」(任意の名前)を入力
  3. 「ポリシーの作成」ボタンをクリック

2-3. IAM ロールを作成

  1. 左サイドバーの「ロール」をクリック
  2. 「ロールの作成」ボタンをクリック
  3. 信頼されたエンティティタイプで「AWS のサービス」を選択
  4. ユースケースで「Lambda」を選択
  5. 「次へ」ボタンをクリック
  6. 許可ポリシーで以下を検索して選択:
    • AWSLambdaBasicExecutionRole(検索ボックスに入力して選択)
    • DynamoDBCrudPolicy(先ほど作成したポリシー)
  7. 「次へ」ボタンをクリック
  8. ロール名に「LambdaDynamoDBRole」(任意の名前)を入力
  9. 「ロールの作成」ボタンをクリック

IAMロール作成
IAM ロール作成画面 - Lambda 実行ロールと DynamoDB 操作権限を付与

手順3: Lambda 関数の実装

Python 3.13 で CRUD 操作を行う Lambda 関数を作成します。

3-1. Lambda サービスに移動

  1. AWS マネジメントコンソール上部の検索ボックスに「Lambda」と入力
  2. 検索結果から「Lambda」を選択してクリック

3-2. Lambda 関数を作成

  1. Lambda ダッシュボードで「関数の作成」ボタンをクリック
  2. 一から作成を選択
  3. 以下の設定を入力:
    • 関数名: crud-api-function(任意の名前)
    • ランタイム: Python 3.13
    • アーキテクチャ: x86_64(デフォルト)
  4. 「デフォルトの実行ロールの変更」を展開
  5. 既存のロールを使用するを選択
  6. 手順2で作成した「LambdaDynamoDBRole」を選択
  7. 「関数の作成」ボタンをクリック

Lambda関数作成
Lambda 関数作成画面 - 既存の IAM ロールを使用

3-3. Lambda 関数のコードを編集

  1. 関数作成後、コードエディターが表示されます
  2. デフォルトの lambda_function.py の内容をすべて削除
  3. 以下のコードを貼り付け:
			
			import os
import json
from decimal import Decimal
import boto3

# ====== DynamoDB テーブル設定 ======
TABLE_NAME = os.environ.get("TABLE_NAME")
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(TABLE_NAME)

# ====== 共通ユーティリティ ======
def to_json_safe(value):
    """DynamoDB の Decimal 型を JSON で扱いやすい float に変換"""
    if isinstance(value, Decimal):
        return float(value)
    if isinstance(value, dict):
        return {k: to_json_safe(v) for k, v in value.items()}
    if isinstance(value, list):
        return [to_json_safe(v) for v in value]
    return value

def resp(status, body):
    """API Gateway (HTTP API v2) に返すレスポンス形式を統一"""
    return {
        "statusCode": status,
        "headers": {
            "Content-Type": "application/json",
        },
        "body": json.dumps(to_json_safe(body), ensure_ascii=False),
    }

def parse_json_body(event):
    """HTTP リクエストボディを JSON にパースする"""
    raw = event.get("body")
    if not raw:
        return None, "request body is required"
    try:
        return json.loads(raw), None
    except json.JSONDecodeError:
        return None, "invalid JSON body"

def require_id(event):
    """pathParameters から id を取得"""
    item_id = (event.get("pathParameters") or {}).get("id")
    if not item_id:
        return None, "id is required"
    return item_id, None

# ====== 各ルート処理 ======
def handle_get(item_id):
    """GET /items/{id} → DynamoDB から 1件取得"""
    res = table.get_item(Key={"id": item_id})
    item = res.get("Item")
    if not item:
        return resp(404, {"message": f"Item not found: {item_id}"})
    return resp(200, item)

def handle_put(item_id, payload):
    """PUT /items/{id} → DynamoDB に新規作成または更新 (Upsert)"""
    item = {"id": item_id}

    if "name" in payload:
        item["name"] = payload["name"]
    if "price" in payload:
        item["price"] = Decimal(str(payload["price"]))
    if "tags" in payload and isinstance(payload["tags"], list):
        item["tags"] = payload["tags"]

    table.put_item(Item=item)
    return resp(200, {"message": "upserted", "item": item})

def handle_delete(item_id):
    """DELETE /items/{id} → DynamoDB から削除"""
    res = table.delete_item(Key={"id": item_id}, ReturnValues="ALL_OLD")
    old = res.get("Attributes")
    if not old:
        return resp(404, {"message": f"Item not found: {item_id}"})
    return resp(200, {"message": "deleted", "item": old})

# ====== Lambda ハンドラ ======
def lambda_handler(event, context):
    """API Gateway HTTP API v2 から呼ばれるメイン処理"""
    route_key = event.get("routeKey")

    try:
        if route_key == "GET /items/{id}":
            item_id, err = require_id(event)
            if err:
                return resp(400, {"message": err})
            return handle_get(item_id)

        elif route_key == "PUT /items/{id}":
            item_id, err = require_id(event)
            if err:
                return resp(400, {"message": err})
            payload, perr = parse_json_body(event)
            if perr:
                return resp(400, {"message": perr})
            return handle_put(item_id, payload)

        elif route_key == "DELETE /items/{id}":
            item_id, err = require_id(event)
            if err:
                return resp(400, {"message": err})
            return handle_delete(item_id)

        return resp(400, {"message": f"Unsupported route: {route_key}"})

    except Exception as e:
        return resp(500, {"message": "internal error", "detail": str(e)})

		
  1. 「Deploy」ボタンをクリックしてコードを保存

3-4. 環境変数を設定

Lambda 関数で DynamoDB テーブル名を参照できるよう環境変数を設定します。

  1. Lambda 関数のページで「設定」タブをクリック
  2. 左サイドバーの「環境変数」をクリック
  3. 「編集」ボタンをクリック
  4. 「環境変数を追加」ボタンをクリック
  5. 以下を入力:
    • キー: TABLE_NAME
    • : items(手順1で作成したテーブル名)
  6. 「保存」ボタンをクリック

Lambda環境変数
Lambda 環境変数設定画面 - TABLE_NAME に DynamoDB テーブル名を設定

手順4: API Gateway 設定

HTTP リクエストを受け取り、Lambda 関数を呼び出すための API Gateway を設定します。

4-1. API Gateway サービスに移動

  1. AWS マネジメントコンソール上部の検索ボックスに「API Gateway」と入力
  2. 検索結果から「API Gateway」を選択してクリック

4-2. HTTP API を作成

  1. API Gateway ダッシュボードで「API を作成」ボタンをクリック
  2. HTTP API の「構築」ボタンをクリック
  3. 以下の設定を入力:
    • API名: crud-http-api(任意の名前)
    • 統合
      • 「統合を追加」をクリック
      • 統合タイプ: Lambda 関数
      • Lambda 関数: 手順3で作成した crud-api-function を選択
  4. 「次へ」ボタンをクリック

4-3. ルートを設定

  1. ルートを設定画面で以下のルートを追加:
    • GET /items/{id}
    • PUT /items/{id}
    • DELETE /items/{id}
  2. 各ルートの統合で、手順3で作成したLambda関数を選択
  3. 「次へ」ボタンをクリック
  4. ステージを定義画面でデフォルト設定のまま「次へ」をクリック
  5. 確認と作成画面で設定を確認し、「作成」ボタンをクリック

APIルート設定
API ルート設定画面 - CRUD 操作用のエンドポイントを定義

4-4. API URL を確認

  1. API 作成後、呼び出し URL が表示されます
  2. この URL をメモしておきます(動作確認で使用)

手順5: 動作確認・テスト

作成した API エンドポイントを curl コマンドで動作確認します。

5-1. 環境変数を設定

手順4で確認した API URL を使用します。

			
			BASE="URL"
ID="1"

		

5-2. 作成(PUT)

新しいアイテムを作成します。

			
			curl -sS -X PUT "$BASE/items/$ID" \
  -H "Content-Type: application/json" \
  -d '{"name":"my item","price":1000,"tags":["a","b"]}' | jq

		

レスポンス:

			
			{
  "message": "upserted",
  "item": {
    "id": "1",
    "name": "my item",
    "price": 1000.0,
    "tags": ["a", "b"]
  }
}

		

5-3. 取得(GET)

作成したアイテムを取得します。

			
			curl -sS "$BASE/items/$ID" | jq

		

レスポンス:

			
			{
  "id": "1",
  "name": "my item",
  "price": 1000.0,
  "tags": ["a", "b"]
}

		

5-4. 更新(PUT)

既存のアイテムを更新します。

			
			curl -sS -X PUT "$BASE/items/$ID" \
  -H "Content-Type: application/json" \
  -d '{"name":"updated item","price":12345,"tags":["x","y","z"]}' | jq

		

レスポンス:

			
			{
  "message": "upserted",
  "item": {
    "id": "1",
    "name": "updated item",
    "price": 12345.0,
    "tags": ["x", "y", "z"]
  }
}

		

5-5. 更新後の確認(GET)

更新されたアイテムを確認します。

			
			curl -sS "$BASE/items/$ID" | jq

		

レスポンス:

			
			{
  "id": "1",
  "name": "updated item",
  "price": 12345.0,
  "tags": ["x", "y", "z"]
}

		

5-6. 削除(DELETE)

アイテムを削除します。

			
			curl -sS -X DELETE "$BASE/items/$ID" | jq

		

レスポンス:

			
			{
  "message": "deleted",
  "item": {
    "id": "1",
    "price": 12345.0,
    "name": "updated item",
    "tags": ["x", "y", "z"]
  }
}

		

5-7. 削除後の確認(GET → 404期待)

削除されたアイテムの取得を試行し、404エラーが返されることを確認します。

			
			curl -sS -i "$BASE/items/$ID"

		

レスポンス:

			
			HTTP/2 404 
date: Thu, 18 Sep 2025 12:17:02 GMT
content-type: application/json
content-length: 32
apigw-requestid: RGO54jj4tjMEMTA=

{"message": "Item not found: 1"}

		

これで、CREATE(作成)、READ(取得)、UPDATE(更新)、DELETE(削除)のすべての操作が正常に動作することが確認できました。

まとめ

本記事では、AWS Lambda・DynamoDB・API Gateway (HTTP API) を組み合わせた CRUD 操作システムの構築手順を紹介しました。
AWS 公式チュートリアルをベースとしつつ、IAM ポリシーを最小権限で設計するなど、より実践的な構成を検証しました。

特に今回は Lambda プロキシ統合 を利用したことで、リクエストとレスポンスを Lambda 側で柔軟に制御できるようになりました。その結果、エラーハンドリングや DynamoDB 特有のデータ型変換(Decimal → float)を含め、レスポンス形式を統一して返せるように改善できました。

チュートリアルの内容をそのまま実施するだけでなく、自分で構成を調整しながら検証することで、各サービスに関する理解をより深めることができました。

参考資料


アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社WEBサイトをご覧ください。

この記事をシェアする

FacebookHatena blogX

関連記事