---
title: "AWS サーバーレス ハンズオン｜Lambda・DynamoDB・API GatewayでREST APIを作る手順【2026年版】"
url: "https://www.issoh.co.jp/tech/details/2818/"
published: 2024-06-27
updated: 2026-09-16
categories: ["AWS"]
publisher: "株式会社一創"
---

# AWS サーバーレス ハンズオン｜Lambda・DynamoDB・API GatewayでREST APIを作る手順【2026年版】

AWS Lambda、DynamoDB、API Gatewayを組み合わせたサーバーレスAPIは、構成図そのものは数年前から変わっていません。変わったのは足回りです。ハンズオン記事の多くが開発環境に指定していたAWS Cloud9は2024年7月25日に新規顧客の受付を終了し、サンプルコードで多用されていた`python3.8`と`python3.9`のランタイムも廃止されました。手順どおり進めても入口で詰まる記事が増えています。ここでは2026年8月時点の公式ドキュメントで裏の取れる経路だけを残し、テーブル作成からAPI公開・動作確認までを一本の流れでたどります。

## まとめ

- 開発環境はAWS CloudShell、またはVS CodeにAWS Toolkitを入れたローカル環境を使います。AWS Cloud9は新規のAWSアカウントからは選べません。
- Lambdaのランタイムは`python3.13`または`python3.14`です。`python3.8`は2024年10月14日、`python3.9`は2025年12月15日に廃止され、2027年2月1日からは新規作成もブロックされます。
- DynamoDBのテーブルはオンデマンド（`PAY_PER_REQUEST`）で作ります。オンデマンドと`ProvisionedThroughput`の同時指定はAPI仕様上できません（課金モード自体は後から変更できます）。
- APIキー・使用量プラン・AWS WAFが要るならREST API、要らないならHTTP APIです。米国東部（バージニア北部）の第1階層で、REST APIは100万リクエストあたり3.50 USD、HTTP APIは1.00 USDです。
- AWS CLIで統合を作るときは`aws lambda add-permission`を忘れると呼び出しに失敗します。マネジメントコンソールはこの権限を自動で付けるため、CLI手順だけが落ちる典型的な原因です。

環境の作り直しから順に手を動かします。

## 2026年に手順を作り直す理由｜Cloud9の新規提供終了とPythonランタイムの廃止

サーバーレスAPIの手順記事には、前提にしている開発環境とランタイムが現在のAWSでは選べなくなったものがあります。ここを直さないと、以降の手順は一行も実行できません。

### AWS Cloud9の代わりに使う開発環境

AWS Cloud9のユーザーガイド冒頭には「AWS Cloud9 is no longer available to new customers.」という告知が出ています。AWSのDevOpsブログは「we have made the decision to close new customer access to AWS Cloud9, effective July 25, 2024」と日付まで明示しました。すでにCloud9を使っていたアカウントは継続利用できますが、これから作るアカウントのコンソールにCloud9は現れません。

同じブログが挙げている移行先は2つです。ひとつはAWS IDE Toolkitsで、Visual Studio Code・IntelliJ・PyCharmにAWSサービスを統合するオープンソースのプラグイン群です。もうひとつはAWS CloudShellで、こちらはマネジメントコンソール内で開くブラウザベースのシェルであり、認証済み・設定済みの状態でAWSリソースを操作できます。

この記事の手順はどちらでも通ります。ブラウザだけで完結させたいならCloudShell、コードを書き続けるならVS CodeとAWS Toolkitを選んでください。ローカルで進める場合はAWS CLI v2と、boto3を動かすPython（3.10以上）を入れ、`aws configure`で認証情報を設定した状態から始めます。

AWSへ課金せず手元で同じ構成を組む道もあります。S3・DynamoDB・LambdaのAPIをローカルのDockerコンテナで受けるLocalStackを使えば、実アカウントへデプロイせずに動作を確認できます。ただし2026年3月23日にCommunityエディションが終了し、商用利用には有償プランが要るようになりました。導入手順と現在の料金は[LocalStackとは？AWSをローカルで動かす手順と有料化後の料金・判断基準](https://www.issoh.co.jp/tech/details/17634/)にまとめています。

### Lambdaランタイムの廃止スケジュールと選択

サンプルコードに`--runtime python3.8`と書かれた手順が広く出回っています。この識別子はすでに廃止済みで、廃止後のランタイムはコンソールから関数を作成・更新できません。AWS CLIやAWS SAM、CloudFormationからは当面作成できますが、それも期限付きです。

| ランタイム識別子   | 状態                    | 廃止日             | 新規作成のブロック日      |
| ---------- | --------------------- | --------------- | --------------- |
| python3.14 | サポート中                 | 2029年6月30日（予定）  | 2029年7月31日（予定）  |
| python3.13 | サポート中                 | 2029年6月30日（予定）  | 2029年7月31日（予定）  |
| python3.12 | サポート中                 | 2028年10月31日（予定） | 2028年11月30日（予定） |
| python3.11 | サポート中（Amazon Linux 2） | 2027年6月30日（予定）  | 2027年7月31日（予定）  |
| python3.10 | サポート中（Amazon Linux 2） | 2026年10月31日（予定） | 2027年2月1日（予定）   |
| python3.9  | 廃止済み                  | 2025年12月15日     | 2027年2月1日       |
| python3.8  | 廃止済み                  | 2024年10月14日     | 2027年2月1日       |

数字はAWS Lambda開発者ガイドのランタイム表からの引用で、予定日は変更されることがあります。新規に作るなら`python3.13`が無難です。`python3.10`と`python3.11`はAmazon Linux 2ベースで、その基盤自体が2026年6月30日にサポート終了を迎えており、これから選ぶ理由はありません。ランタイムの世代を上げる操作は自動では行われず、識別子を書き換えるのは利用者側の作業です。

## DynamoDBテーブルの作成｜オンデマンドとプロビジョンドの選び分け

ユーザー情報を格納するテーブルを1つ作ります。パーティションキーは`UserID`（文字列）だけの単純な構成にします。

```
import boto3

dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-1")
table = dynamodb.create_table(
    TableName="Users",
    KeySchema=[{"AttributeName": "UserID", "KeyType": "HASH"}],
    AttributeDefinitions=[{"AttributeName": "UserID", "AttributeType": "S"}],
    BillingMode="PAY_PER_REQUEST",
)
table.wait_until_exists()
print(table.table_name, "is ready")
```

`ProvisionedThroughput`に読み書き5ユニットずつを指定する手順も見かけます。DynamoDBのCreateTable APIリファレンスは、`BillingMode`に`PROVISIONED`を指定した場合は`ProvisionedThroughput`が必須、`PAY_PER_REQUEST`を指定した場合は指定できないと定めており、両方書くとエラーになります。同リファレンスは「大半のDynamoDBワークロードには`PAY_PER_REQUEST`を推奨する」とも明記しています。学習用のテーブルはリクエストが来ない時間帯が大半なので、オンデマンドのほうが素直です。課金モード自体は作成後に切り替えられるので、迷ったらオンデマンドで始めて構いません。

`wait_until_exists()`を挟んでいるのは、CreateTableが非同期で、応答時点のステータスが`CREATING`のままだからです。このメソッドは20秒間隔で`describe_table`を呼び、25回失敗すると例外を投げます。待機を入れずに直後の`put_item`を書くと、テーブル未作成のまま失敗します。テーブル設計そのものの考え方は[AWS DynamoDBとは？特徴・使い方・料金・RDSとの違いを実装目線で解説](/column/details/3025/)で扱っています。

## Lambda関数の作成｜最小権限のIAMロールとプロキシ統合のレスポンス形式

Lambda関数には実行ロール、関数コード、デプロイパッケージの3つが必要です。ここでつまずく原因は権限とレスポンス形式に集中しています。

### 実行ロールと信頼ポリシー

まずLambdaが引き受けられるロールを作ります。信頼ポリシーの`Service`が`lambda.amazonaws.com`になっていない場合、ロールは作成できても関数にアタッチできません。

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "lambda.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

権限のほうは、テーブルを限定したインラインポリシーにします。`AmazonDynamoDBFullAccess`をアタッチする手順が広く出回っていますが、このポリシー（バージョンv15）は`dynamodb:*`と`dax:*`をリソース`*`に対して許可し、さらに`lambda:CreateFunction`と`lambda:DeleteFunction`まで含みます。アカウント内の全テーブルの削除も、Lambda関数の削除も通ってしまう権限です。学習用のアカウントでも、後で本番のテンプレートとして流用されるのはこの部分です。最初から絞ってください。

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
      "Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/Users"
    }
  ]
}
```

```
aws iam create-role --role-name UserApiLambdaRole \
  --assume-role-policy-document file://trust-policy.json

aws iam put-role-policy --role-name UserApiLambdaRole \
  --policy-name UsersTableAccess \
  --policy-document file://users-table-policy.json

aws iam attach-role-policy --role-name UserApiLambdaRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
```

3つ目の`AWSLambdaBasicExecutionRole`はCloudWatch Logsへの書き込み権限です。これが無いとログが1行も残らず、後述の502の原因調査ができません。

### 関数コードとレスポンス形式

API GatewayのLambdaプロキシ統合では、関数の戻り値の形が決まっています。開発者ガイドは`statusCode`を軸に`isBase64Encoded`・`headers`（または`multiValueHeaders`）・`body`で構成したJSONを返すよう求めています。ヘッダーを返す必要がなければ`headers`系のキーは省略できますが、それ以外の形は許されず、開発者ガイドは「If the function output is of a different format, API Gateway returns a 502 Bad Gateway error response.」と続けます。DynamoDBから取得した項目をそのまま`return`すると502になるのは、この規定によります。

```
import json
import os

import boto3

TABLE = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])


def _response(status, payload):
    return {
        "statusCode": status,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps(payload, ensure_ascii=False, default=str),
    }


def lambda_handler(event, context):
    method = event.get("httpMethod")
    body = json.loads(event["body"]) if event.get("body") else {}

    if method == "POST":
        user_id = body.get("UserID")
        if not user_id:
            return _response(400, {"message": "UserID is required"})
        TABLE.put_item(Item={"UserID": user_id, "Name": body.get("Name", "")})
        return _response(201, {"UserID": user_id})

    if method == "GET":
        params = event.get("queryStringParameters") or {}
        user_id = params.get("UserID")
        if not user_id:
            return _response(400, {"message": "UserID is required"})
        item = TABLE.get_item(Key={"UserID": user_id}).get("Item")
        if item is None:
            return _response(404, {"message": "User not found"})
        return _response(200, item)

    return _response(405, {"message": "Method not allowed"})
```

`default=str`を渡しているのは、DynamoDBが数値を`Decimal`で返し、`json.dumps`がそのままでは変換できないためです。数値属性を1つでも持たせた瞬間に踏むので、最初から入れておきます。ただしこの指定では数値がJSONの文字列として出力される（`Decimal("3")`が`"3"`になる）ため、クライアント側で数値として扱いたい場合は`int()`や`float()`へ明示的に変換するエンコーダーを書いてください。GET側でも`UserID`の有無を先に確かめているのは、空文字をパーティションキーにすると`ValidationException`で落ちるためです。DynamoDBは、テーブルやインデックスのキーとして使う属性に空文字を許しません。テーブル名を環境変数から読むのも同じ発想で、ハードコードするとステージごとに関数を分ける羽目になります。このコードは`lambda_function.py`として保存します。

### zipパッケージからのデプロイ

boto3はLambdaのPythonランタイムに同梱されているため、この関数に外部依存はありません。ファイル1つをzipで固めれば足ります。

```
zip function.zip lambda_function.py

aws lambda create-function \
  --function-name UserFunction \
  --runtime python3.13 \
  --role arn:aws:iam::123456789012:role/UserApiLambdaRole \
  --handler lambda_function.lambda_handler \
  --environment "Variables={TABLE_NAME=Users}" \
  --zip-file fileb://function.zip
```

ただしAWSの開発者ガイドは、ランタイム同梱のSDKに頼るのは追加パッケージを入れられない場合に限り、通常はデプロイパッケージかLayerにSDKを含めるよう推奨しています。自動ランタイム更新でSDKのバージョンが変わっても壊れないようにするためです。実行時間やコールドスタートを含めた料金の考え方は[AWS Lambdaとは？仕組み・料金体系とコールドスタート対策・採用判断を実装者目線で解説](/tech/details/15407/)にまとめています。

## API Gatewayでの公開｜REST APIとHTTP APIの判断基準

ここで最初の分岐が来ます。API GatewayにはREST APIとHTTP APIがあり、AWSの公式チュートリアルや近年の解説はHTTP APIを使う一方、`create-rest-api`で書かれた手順も多く残っています。どちらが正しいかではなく、必要な機能で決まります。

### REST APIとHTTP APIの機能差と単価

| 機能                     | REST API | HTTP API |
| ---------------------- | -------- | -------- |
| APIキー・使用量プラン           | あり       | なし       |
| クライアント単位のレート制限         | あり       | なし       |
| AWS WAF連携              | あり       | なし       |
| プライベートエンドポイント          | あり       | なし       |
| リクエスト検証・キャッシュ          | あり       | なし       |
| 実行ログ・X-Rayトレース         | あり       | なし       |
| JWTオーソライザー             | なし       | あり       |
| 自動デプロイ                 | なし       | あり       |
| 料金（100万リクエスト・米国東部第1階層） | 3.50 USD | 1.00 USD |

機能の対応表はAPI Gateway開発者ガイドの比較ページ、料金はAPI Gatewayの料金ページ（米国東部バージニア北部）の値です。どちらも利用量が増えるほど単価が下がる段階制で、リージョンによっても変わります。実際の見積もりは公式の料金ページで自分のリージョンを選んで確認してください。判断の軸は単純で、APIキーを配って利用量を制御する予定があるか、AWS WAFを前段に置く要件があるならREST API、社内の内部APIやモバイルアプリのバックエンドのように鍵の配布が不要ならHTTP APIです。この記事は元の手順に合わせてREST APIで進めます。

HTTP APIに切り替える場合、関数コードも直す必要があります。ペイロード形式2.0ではHTTPメソッドが`event["httpMethod"]`ではなく`event["requestContext"]["http"]["method"]`に入り、パスは`rawPath`になります。またAWS CLI・CloudFormation・SDKから統合を作る場合は`payloadFormatVersion`の明示が必須です（マネジメントコンソールは最新版を既定で選びます）。

### REST APIでの公開手順

```
API_ID=$(aws apigateway create-rest-api --name UserAPI --query id --output text)
ROOT_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query "items[?path=='/'].id" --output text)

RES_ID=$(aws apigateway create-resource --rest-api-id $API_ID \
  --parent-id $ROOT_ID --path-part users --query id --output text)

aws apigateway put-method --rest-api-id $API_ID --resource-id $RES_ID \
  --http-method ANY --authorization-type NONE

aws apigateway put-integration --rest-api-id $API_ID --resource-id $RES_ID \
  --http-method ANY --type AWS_PROXY --integration-http-method POST \
  --uri arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:123456789012:function:UserFunction/invocations

aws lambda add-permission --function-name UserFunction \
  --statement-id apigw-invoke --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:ap-northeast-1:123456789012:$API_ID/*/*/users"

aws apigateway create-deployment --rest-api-id $API_ID --stage-name prod
```

4つ目の`add-permission`が、CLI手順だけが動かない最大の理由です。開発者ガイドは`put-integration`の`--credentials`にIAMロールを渡す方法を示したうえで、「Instead of supplying an IAM role for credentials, you can use the add-permission command to add resource-based permissions. This is what the API Gateway console does.」と補足しています。コンソールで統合を選ぶと裏でこのリソースベースポリシーが付与されるため、コンソール前提で書かれた手順をCLIに書き写すと権限だけが抜け落ちます。なお`123456789012`とリージョン名は自分のAWSアカウントID・利用リージョンに置き換えてください（`$API_ID`などのシェル変数は上のコマンドで設定済みです）。

統合のHTTPメソッドに`POST`を指定しているのはLambdaの仕様です。クライアントが`GET`で呼ぶ場合も、API GatewayからLambdaへの呼び出しは`POST`でなければなりません。`--integration-http-method`にクライアント側のメソッドを書くと動きません。

## 動作確認とつまずきどころ｜502 Bad GatewayとCORS

デプロイが終わったらcurlで叩きます。エンドポイントは`https://<api-id>.execute-api.<region>.amazonaws.com/<stage>/users`の形です。

```
curl -X POST "https://$API_ID.execute-api.ap-northeast-1.amazonaws.com/prod/users" \
  -H "Content-Type: application/json" \
  -d '{"UserID": "u-001", "Name": "Taro"}'

curl "https://$API_ID.execute-api.ap-northeast-1.amazonaws.com/prod/users?UserID=u-001"
```

POSTの直後にGETを叩いて404が返ることがあります。`get_item`は既定で結果整合性のある読み込みを行うためで、DynamoDBのAPIリファレンスも「GetItem provides an eventually consistent read by default.」と明記しています。確実に読みたい場合は`ConsistentRead=True`を渡すか、数秒待って再実行してください。

### 502 Bad Gatewayが返るときの切り分け

502はAPI Gatewayが関数の戻り値を解釈できなかったという意味です。原因はほぼ4つに絞られます。`statusCode`を含まないオブジェクトを返している、`body`を文字列化せずdictのまま返している、関数が例外で終了している、レスポンスが同期呼び出しの上限6MBを超えている、のいずれかです。どれもCloudWatch Logsの`/aws/lambda/UserFunction`ロググループに実際のトレースが出るので、推測せずログを開いてください。前掲のコードで`json.dumps`と`default=str`を最初から入れているのは、2つ目を潰し、3つ目の主要因である`Decimal`の変換失敗を先回りするためです。4つ目は`scan`の結果をそのまま返す実装に変えた瞬間に踏みます。

### ブラウザからの呼び出しが失敗するときのCORS設定

curlは通るのにフロントエンドから呼ぶと失敗する場合、CORSです。プロキシ統合ではAPI Gateway側の設定だけでは足りません。開発者ガイドは「To enable CORS for the Lambda proxy integration, you must add Access-Control-Allow-Origin:{domain-name} to the output headers.」と述べており、関数が返す`headers`に自分でこのヘッダーを入れる必要があります。前掲の`_response`なら、ヘッダーを次のように変えます。

```
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",
        },
```

本番では`*`ではなく自社ドメインを指定します。プリフライトの`OPTIONS`リクエストはAPI Gateway側で処理させるか、関数内で`OPTIONS`を受けて204を返す実装にします。

## 手作業のCLIから抜けるタイミング｜AWS SAMへの書き直し

ここまでの手順は、リソースの関係を頭に入れるには有効です。一方で2回目以降には向きません。CLIで作った構成には差分適用の仕組みがなく、ステージを増やす、関数を分ける、テーブルを追加するといった変更のたびに手順書が伸びていきます。ロールバックの手段も自分で用意することになります。

同じ構成をAWS SAMのテンプレートで書き直すのが、ハンズオン直後の復習として最も効率的です。テンプレート1枚にテーブル・関数・APIの3リソースが収まり、`sam deploy`で差分だけが適用されます。CloudFormationとの関係や導入手順は[AWS SAMとは？CloudFormationとの違い・CLIの使い方をわかりやすく解説](/tech/details/3823/)で、複数クラウドを視野に入れる場合の選択肢は[Serverless Frameworkとは？v4の料金・使い方・無料の代替まで【2026年最新】](/tech/details/8464/)で確認できます。サーバーレス構成そのものの向き不向きは[サーバーレスアーキテクチャとは？構成パターン・AWS実装・採用判断を実装者目線で解説](/tech/details/13125/)にまとめています。

## よくある質問

### AWS Cloud9がコンソールに見当たりません。何を使えばよいですか？

AWS Cloud9は2024年7月25日に新規顧客への提供を終了しました。既存の利用者はそのまま使えますが、これから作るアカウントでは選べません。AWSが移行先として案内しているのはAWS CloudShellとAWS IDE Toolkits（VS Code・IntelliJ・PyCharm向けプラグイン）の2つです。ブラウザで完結させたいならCloudShellを選んでください。

### 認可タイプをNONEのままにしても問題ありませんか？

ハンズオンでは動かすことを優先して`--authorization-type NONE`にしていますが、この状態のエンドポイントはURLを知っていれば誰でも呼び出せます。動作確認が済んだら消すか、認可を足してください。REST APIならIAM認可・Amazon Cognitoオーソライザー・Lambdaオーソライザー、HTTP APIならIAM認可・JWTオーソライザー・Lambdaオーソライザーが選べます。社内利用ならIAM認可、一般ユーザー向けならCognitoかJWTが出発点です。

### REST APIとHTTP APIのどちらで作るべきですか？

APIキーと使用量プラン、AWS WAF、リクエスト検証、キャッシュ、X-Rayトレースのいずれかが必要ならREST APIです。必要ないならHTTP APIのほうが安く、米国東部（バージニア北部）の第1階層では100万リクエストあたり1.00 USDと3.50 USDの差があります。ただしHTTP APIではペイロード形式2.0が新しい既定になり（AWS CLI・CloudFormation・SDKから統合を作る場合は`payloadFormatVersion`の明示が必須です）、関数側でイベントの読み取り位置を変える必要があります。

### APIを呼ぶと502 Bad Gatewayが返ります。何を確認すればよいですか？

関数の戻り値の形式です。プロキシ統合では`statusCode`と文字列化された`body`を含むJSONを返す必要があり、それ以外の形式ならAPI Gatewayは502を返します。DynamoDBの項目をそのまま返している、`json.dumps`を通していない、関数が例外で落ちている、のいずれかに該当していないかをCloudWatch Logsで確認してください。

### ハンズオンで作ったリソースはどこまで削除すればよいですか？

DynamoDBテーブル、Lambda関数、REST API、IAMロールの4つに加えて、CloudWatch Logsのロググループ（`/aws/lambda/UserFunction`）を削除します。ロググループはLambda関数とは別のリソースなので、関数を消しても残り、保存期間を無期限のままにしていると保管料が発生し続けます。項目に有効期限を持たせて自動削除する方法は[DynamoDB TTLとは｜設定方法・自動削除の仕組みと期限切れ項目の扱いを実装目線で解説](/tech/details/3677/)で解説しています。

## 関連記事

- [サーバーレスアーキテクチャとは？構成パターン・AWS実装・採用判断を実装者目線で解説](/tech/details/13125/)
- [AWS Lambdaとは？仕組み・料金体系とコールドスタート対策・採用判断を実装者目線で解説](/tech/details/15407/)
- [AWS DynamoDBとは？特徴・使い方・料金・RDSとの違いを実装目線で解説](/column/details/3025/)
- [AWS SAMとは？CloudFormationとの違い・CLIの使い方をわかりやすく解説](/tech/details/3823/)
- [DynamoDB TTLとは｜設定方法・自動削除の仕組みと期限切れ項目の扱いを実装目線で解説](/tech/details/3677/)

---

出典: [AWS サーバーレス ハンズオン｜Lambda・DynamoDB・API GatewayでREST APIを作る手順【2026年版】](<https://www.issoh.co.jp/tech/details/2818/>)（株式会社一創）
