aws

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

AWS Lambda、DynamoDB、API Gatewayを組み合わせたサーバーレスAPIは、構成図そのものは数年前から変わっていません。変わったのは足回りです。ハンズオン記事の多くが開発環境に指定していたAWS Cloud9は2024年7月25日に新規顧客の受付を終了し、サンプルコードで多用されていたpython3.8python3.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で認証情報を設定した状態から始めます。

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.10python3.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リファレンスは、BillingModePROVISIONEDを指定した場合はProvisionedThroughputが必須、PAY_PER_REQUESTを指定した場合は指定できないと定めており、両方書くとエラーになります。同リファレンスは「大半のDynamoDBワークロードにはPAY_PER_REQUESTを推奨する」とも明記しています。学習用のテーブルはリクエストが来ない時間帯が大半なので、オンデマンドのほうが素直です。課金モード自体は作成後に切り替えられるので、迷ったらオンデマンドで始めて構いません。

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

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

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

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

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

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

権限のほうは、テーブルを限定したインラインポリシーにします。AmazonDynamoDBFullAccessをアタッチする手順が広く出回っていますが、このポリシー(バージョンv15)はdynamodb:*dax:*をリソース*に対して許可し、さらにlambda:CreateFunctionlambda: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を軸にisBase64Encodedheaders(または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とは?仕組み・料金体系とコールドスタート対策・採用判断を実装者目線で解説にまとめています。

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.dumpsdefault=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の使い方をわかりやすく解説で、複数クラウドを視野に入れる場合の選択肢はServerless Frameworkとは?v4の料金・使い方・無料の代替まで【2026年最新】で確認できます。サーバーレス構成そのものの向き不向きはサーバーレスアーキテクチャとは?構成パターン・AWS実装・採用判断を実装者目線で解説にまとめています。

よくある質問

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とは|設定方法・自動削除の仕組みと期限切れ項目の扱いを実装目線で解説で解説しています。

関連記事

資料請求

RELATED POSTS 関連記事