- はじめに
- 前提条件
- アーキテクチャと使用サービス
- Step 1: プロジェクトフォルダの作成
- Step 2: SAMテンプレートの作成(template.yaml)
- Step 3: Lambda関数の作成(src/app.py)
- Step 4: requirements.txtの作成
- Step 5: Gitリポジトリの初期化
- Step 6: samconfig.tomlの作成
- Step 7: sam build(ビルド)
- Step 8: sam deploy(デプロイ)
- Step 9: 動作テスト
- Step 10: AWS管理コンソールで確認
- Step 11: リソースの削除
- トラブルシューティング一覧
- まとめ
- SAMがやってくれていたことをコンソールで確認する
- 次のステップ
はじめに
「AWSのサーバーレスを試してみたい」「Lambda・API Gateway・DynamoDBを組み合わせて何か作ってみたい」と思っていませんか?
この記事では、AWS SAM(Serverless Application Model) を使って、メモの作成・取得・更新・削除ができるREST APIをゼロから構築するハンズオンを紹介します。
このハンズオンで実際に作るもの:
インターネット
↓ HTTPS
API Gateway(Prod ステージ)
↓ プロキシ統合
Lambda(Python 3.12)
↓ boto3
DynamoDB(メモデータを保存)この記事の特徴:
- Macユーザー向けの記事が多い中、Windows(VSCode)での実際の操作手順を詳細に解説
- 実際にハマった Windowsならではのエラーと対処法 を丁寧に紹介
- VSCode上でCMD・PowerShell・Git Bashを使い分ける際の注意点も解説
- ハンズオン完了後のリソース削除手順まで含めた完全ガイド
前提条件
以下のツールがインストール・設定済みであることを確認してください。
必要なツール
| ツール | 確認コマンド | 最低バージョン目安 |
|---|---|---|
| AWS CLI v2 | aws --version | 2.x |
| AWS SAM CLI | sam --version | 1.x |
| Python | python --version | 3.12推奨 |
| Git | git --version | - |
| VSCode | - | 最新版推奨 |
AWS認証の確認
aws sts get-caller-identity上記コマンドでアカウントIDが表示されれば認証設定済みです。表示されない場合は aws configure で認証情報を設定してください。
アーキテクチャと使用サービス
使用するAWSサービス
| サービス | 役割 | 料金 |
|---|---|---|
| API Gateway | HTTPリクエストの受付・ルーティング | 100万リクエスト/$3.50 |
| Lambda | ビジネスロジックの実行(Python) | 100万リクエスト無料枠あり |
| DynamoDB | メモデータの永続化(NoSQL) | PAY_PER_REQUESTで使った分だけ |
| S3 | SAMのデプロイパッケージ置き場 | 自動作成・少量なのでほぼ無料 |
学習目的の短時間ハンズオンであれば、ほぼ無料枠内で収まります。
ただし、ハンズオン完了後は必ずリソースを削除してください。
APIエンドポイント設計
| メソッド | パス | 説明 |
|---|---|---|
| POST | /memos | メモ作成 |
| GET | /memos | メモ一覧取得 |
| GET | /memos/{id} | メモ1件取得 |
| PUT | /memos/{id} | メモ更新 |
| DELETE | /memos/{id} | メモ削除 |
Step 1: プロジェクトフォルダの作成
フォルダ構造
memo-api-dynamodb/
├── template.yaml # SAMテンプレート(インフラ定義)
├── samconfig.toml # デプロイ設定(gitignore対象・手動作成が必要)
├── README.md
└── src/
├── app.py # Lambda関数(CRUD処理)
└── requirements.txt作業ディレクトリを開く
VSCodeでターミナルを開き、プロジェクトを管理したいディレクトリに移動します。
cd C:\my-aws\aws-learning-projects
mkdir memo-api-dynamodb
cd memo-api-dynamodb
mkdir srcStep 2: SAMテンプレートの作成(template.yaml)
memo-api-dynamodb/template.yaml を作成します。このファイルがAWSリソース全体の設計図です。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Memo API with Lambda, API Gateway, and DynamoDB
Globals:
Function:
Runtime: python3.12
Timeout: 30
MemorySize: 128
Environment:
Variables:
# Lambda関数からDynamoDBテーブル名を参照するための環境変数
TABLE_NAME: !Ref MemosTable
Resources:
# DynamoDB テーブル
MemosTable:
Type: AWS::DynamoDB::Table
Properties:
# スタック名をテーブル名に含めることで、複数環境でも名前が衝突しない
TableName: !Sub "${AWS::StackName}-Memos"
# PAY_PER_REQUEST: アクセスがない時間は課金ゼロ(学習・開発に最適)
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: memoId
AttributeType: S # S = String
KeySchema:
- AttributeName: memoId
KeyType: HASH # パーティションキー(一意のID)
# Lambda 関数(全エンドポイントを1つの関数で処理)
MemoFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: app.lambda_handler
Description: CRUD handler for Memo API
Policies:
# DynamoDBCrudPolicy: SAM組み込みのポリシーテンプレート
# GetItem / PutItem / UpdateItem / DeleteItem / Scan / Query が自動付与される
- DynamoDBCrudPolicy:
TableName: !Ref MemosTable
Events:
# POST /memos - メモ作成
CreateMemo:
Type: Api
Properties:
Path: /memos
Method: post
# GET /memos - メモ一覧取得
ListMemos:
Type: Api
Properties:
Path: /memos
Method: get
# GET /memos/{id} - メモ1件取得
GetMemo:
Type: Api
Properties:
Path: /memos/{id}
Method: get
# PUT /memos/{id} - メモ更新
UpdateMemo:
Type: Api
Properties:
Path: /memos/{id}
Method: put
# DELETE /memos/{id} - メモ削除
DeleteMemo:
Type: Api
Properties:
Path: /memos/{id}
Method: delete
# Outputs: デプロイ後に表示される情報
Outputs:
MemoApiUrl:
Description: "Memo API エンドポイント URL"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"
MemoFunctionArn:
Description: "Lambda 関数 ARN"
Value: !GetAtt MemoFunction.Arn
DynamoDBTableName:
Description: "DynamoDB テーブル名"
Value: !Ref MemosTabletemplate.yaml のポイント解説
Transform: AWS::Serverless-2016-10-31
CloudFormationをSAM用に拡張する宣言。これがないと AWS::Serverless::Function などのSAMリソースが使えません。
BillingMode: PAY_PER_REQUEST
DynamoDBの課金モード。リクエストがない時間は課金ゼロなので、学習・開発用途に最適です。
DynamoDBCrudPolicy
SAMが提供する組み込みポリシーテンプレート。自分でIAMポリシーをゼロから書く必要がなく、必要最小限の権限(CRUD操作のみ)が自動付与されます。
Step 3: Lambda関数の作成(src/app.py)
memo-api-dynamodb/src/app.py を作成します。
"""
メモアプリ API - Lambda 関数
Lambda + API Gateway + DynamoDB による CRUD API の実装
"""
import json
import os
import uuid
from datetime import datetime, timezone
import boto3
# DynamoDB クライアント初期化
# テーブル名は環境変数 TABLE_NAME から取得(template.yaml で設定)
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])
# -------------------------------------------------------
# ヘルパー関数
# -------------------------------------------------------
def success(body, status_code=200):
"""成功レスポンスを生成する共通関数"""
return {
'statusCode': status_code,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*', # CORS対応
},
'body': json.dumps(body, ensure_ascii=False),
}
def error(message, status_code=400):
"""エラーレスポンスを生成する共通関数"""
return {
'statusCode': status_code,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
'body': json.dumps({'error': message}, ensure_ascii=False),
}
def now_iso():
"""現在時刻を ISO 8601 形式(UTC)で返す"""
return datetime.now(timezone.utc).isoformat()
# -------------------------------------------------------
# CRUD 処理
# -------------------------------------------------------
def create_memo(body):
"""POST /memos - メモを新規作成する"""
if not body:
return error('リクエストボディが空です', 400)
data = json.loads(body)
title = data.get('title', '').strip()
content = data.get('content', '').strip()
if not title:
return error('title は必須です', 400)
memo_id = str(uuid.uuid4()) # UUID でユニークなIDを自動生成
timestamp = now_iso()
item = {
'memoId': memo_id,
'title': title,
'content': content,
'createdAt': timestamp,
'updatedAt': timestamp,
}
table.put_item(Item=item)
return success(item, 201)
def list_memos():
"""GET /memos - メモ一覧を取得する"""
result = table.scan() # テーブル全件取得
items = result.get('Items', [])
# createdAt の降順(新しい順)でソート
items.sort(key=lambda x: x.get('createdAt', ''), reverse=True)
return success({'memos': items, 'count': len(items)})
def get_memo(memo_id):
"""GET /memos/{id} - 指定IDのメモを1件取得する"""
result = table.get_item(Key={'memoId': memo_id})
item = result.get('Item')
if not item:
return error(f'memoId={memo_id} のメモが見つかりません', 404)
return success(item)
def update_memo(memo_id, body):
"""PUT /memos/{id} - 指定IDのメモを更新する"""
if not body:
return error('リクエストボディが空です', 400)
# 更新前に存在確認
check = table.get_item(Key={'memoId': memo_id})
if not check.get('Item'):
return error(f'memoId={memo_id} のメモが見つかりません', 404)
data = json.loads(body)
title = data.get('title', '').strip()
content = data.get('content', '').strip()
if not title:
return error('title は必須です', 400)
timestamp = now_iso()
result = table.update_item(
Key={'memoId': memo_id},
UpdateExpression='SET title = :title, content = :content, updatedAt = :updatedAt',
ExpressionAttributeValues={
':title': title,
':content': content,
':updatedAt': timestamp,
},
ReturnValues='ALL_NEW', # 更新後の全属性を返す
)
return success(result['Attributes'])
def delete_memo(memo_id):
"""DELETE /memos/{id} - 指定IDのメモを削除する"""
# 削除前に存在確認
check = table.get_item(Key={'memoId': memo_id})
if not check.get('Item'):
return error(f'memoId={memo_id} のメモが見つかりません', 404)
table.delete_item(Key={'memoId': memo_id})
return success({'message': f'memoId={memo_id} を削除しました'})
# -------------------------------------------------------
# エントリーポイント(ルーター)
# -------------------------------------------------------
def lambda_handler(event, context):
"""
API Gateway からのリクエストを受け取り、
HTTPメソッドとパスに応じて適切な関数に振り分ける
"""
http_method = event.get('httpMethod', '')
path = event.get('path', '')
path_parameters = event.get('pathParameters') or {}
body = event.get('body')
try:
if http_method == 'POST' and path == '/memos': # POST /memos - メモ作成
return create_memo(body)
elif http_method == 'GET' and path == '/memos': # GET /memos - メモ一覧取得
return list_memos()
elif http_method == 'GET' and 'id' in path_parameters: # GET /memos/{id} - メモ1件取得
return get_memo(path_parameters['id'])
elif http_method == 'PUT' and 'id' in path_parameters: # PUT /memos/{id} - メモ更新
return update_memo(path_parameters['id'], body)
elif http_method == 'DELETE' and 'id' in path_parameters: # DELETE /memos/{id} - メモ削除
return delete_memo(path_parameters['id'])
else:
return error('エンドポイントが存在しません', 404)
except json.JSONDecodeError:
return error('リクエストボディのJSONが不正です', 400)
except Exception as e:
print(f'Unexpected error: {e}') # CloudWatch Logs に記録
return error('サーバー内部エラーが発生しました', 500)Step 4: requirements.txtの作成
memo-api-dynamodb/src/requirements.txt を作成します。
# boto3 is included in the Lambda runtime, no need to list it here.
# Add external libraries here if needed.
# Example: requests==2.31.0【Windows注意点】
requirements.txtに日本語コメントを書いてはいけない
requirements.txtに日本語コメントを書くと、sam buildで以下のエラーが発生します:Build Failed Error: PythonPipBuilder:ResolveDependencies - 'cp932' codec can't decode byte 0x84 in position 43: illegal multibyte sequence原因: Windows の SAM CLI(PythonPipBuilder)は
requirements.txtをcp932(Shift-JIS)で読み込もうとします。UTF-8で書かれた日本語文字をcp932として解釈しようとするとエラーになります。対処:
requirements.txtのコメントは必ず英語で書きましょう。template.yamlのコメントはSAMがpipに直接渡さないため日本語でも問題ありません。
Step 5: Gitリポジトリの初期化
cd C:\my-aws\aws-learning-projects\memo-api-dynamodb
git init.gitignore を作成します:
# SAM関連(デプロイ設定・ビルド成果物)
samconfig.toml
.aws-sam/
# Python
__pycache__/
*.pyc
.venv/
# テスト用の一時ファイル
test_*.json
samconfig.tomlはGit管理外にする理由: リージョン名やスタック名など環境固有の設定が含まれるためです。チームで開発する場合も、各自が自分の環境に合わせて作成します。
初期コミット:
git add template.yaml src/app.py src/requirements.txt .gitignore README.md
git commit -m "memo-api-dynamodb: プロジェクトフォルダと基本ファイルを作成"Step 6: samconfig.tomlの作成
samconfig.toml は .gitignore で管理外になっているため、手動で作成します。memo-api-dynamodb/samconfig.toml を新規作成し、以下を貼り付けてください:
version = 0.1
[default.deploy.parameters]
stack_name = "memo-api-dynamodb-stack"
resolve_s3 = true
s3_prefix = "memo-api-dynamodb-stack"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
disable_rollback = true
image_repositories = []
[default.global.parameters]
region = "ap-northeast-1"【注意】
samconfig.tomlの置き場所
samconfig.tomlはmemo-api-dynamodb/フォルダの直下 に置く必要があります。
別のフォルダに置いてしまうと、sam deployで以下のエラーが発生します:Error: Missing option '--stack-name', 'sam deploy --guided' can be used to provide and save needed parameters for future deploys.
Step 7: sam build(ビルド)
sam build成功すると以下のように表示されます:
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
[*] Deploy: sam deploy --guidedStep 8: sam deploy(デプロイ)
samconfig.tomlがある場合
sam deploy初回に対話式で設定する場合(--guided)
samconfig.toml を作成していない場合は --guided オプションで対話式に設定できます。自動的に samconfig.toml が生成されます:
sam deploy --guided以下のように順番に聞かれます:
Stack Name [sam-app]: memo-api-dynamodb-stack ← これを入力
AWS Region [ap-northeast-1]: Enter(そのままEnter)
Confirm changes before deploy [Y/n]: Y(そのままEnter)
Allow SAM CLI IAM role creation [Y/n]: Y(そのままEnter)
Disable rollback [y/N]: y ← yを入力
Save arguments to configuration file [Y/n]: Y(そのままEnter)
SAM configuration file [samconfig.toml]: Enter(そのままEnter)
SAM configuration environment [default]: Enter(そのままEnter)最後に変更内容が表示されて Deploy this changeset? と聞かれるので y を入力します。
デプロイ完了の確認
デプロイが完了すると、ターミナルに Outputs が表示されます:
CloudFormation outputs from deployed stack
---------------------------------------------------------------------------------------------------------------
Outputs
---------------------------------------------------------------------------------------------------------------
Key MemoApiUrl
Description Memo API エンドポイント URL
Value https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod
---------------------------------------------------------------------------------------------------------------この URL を控えておいてください。 テストで使います。
Step 9: 動作テスト
デプロイで取得したURLを使ってAPIをテストします。
VSCode上のターミナルの選択肢
VSCodeでは複数のターミナルが使えますが、コマンドの書き方が異なります:
| ターミナル | 変数定義 | 変数参照 | curl | 特徴 |
|---|---|---|---|---|
| CMD | set VAR=値 | %VAR% | curl | Windowsデフォルト |
| PowerShell | $VAR = "値" | $VAR | curl.exe | PowerShell組み込みcurlと区別が必要 |
| Git Bash | VAR="値" | ${VAR} | curl | bash構文が使える |
CMD でのテスト手順
まずAPI URLを変数にセットします:
set API_URL=https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/ProdPOST /memos — メモを作成する
curl -s -X POST "%API_URL%/memos" -H "Content-Type: application/json" -d "{\"title\":\"初めてのメモ\",\"content\":\"DynamoDBに保存されます\"}"レスポンス例:
{
"memoId": "6150433b-4834-4cfe-81f5-266d3dad9e57",
"title": "初めてのメモ",
"content": "DynamoDBに保存されます",
"createdAt": "2026-02-20T10:00:00.000000+00:00",
"updatedAt": "2026-02-20T10:00:00.000000+00:00"
}返ってきた memoId を控えておいてください。 以降のコマンドで使います。
GET /memos — メモ一覧を取得する
curl -s "%API_URL%/memos"GET /memos/{id} — メモ1件を取得する
curl -s "%API_URL%/memos/6150433b-4834-4cfe-81f5-266d3dad9e57"PUT /memos/{id} — メモを更新する
curl -s -X PUT "%API_URL%/memos/6150433b-4834-4cfe-81f5-266d3dad9e57" -H "Content-Type: application/json" -d "{\"title\":\"更新後タイトル\",\"content\":\"内容も更新しました\"}"【Windows CMD 注意点】PUT/DELETEでのURL変数エラー
以下のように
%MEMO_ID%を使って変数でIDを指定しようとするとエラーが発生することがあります:set MEMO_ID=6150433b-4834-4cfe-81f5-266d3dad9e57 curl -s -X PUT "%API_URL%/memos/%MEMO_ID%" ...400 ERROR - Bad request (Generated by cloudfront)原因: CMDでは変数を
setしてもターミナルセッションを閉じると消えます。新しいターミナルを開いて%MEMO_ID%を使うと変数が未設定のまま%MEMO_ID%という文字列がURLに含まれてしまいます。CloudFrontは%をパーセントエンコーディングの開始として解釈するため、不正なURLとして400エラーを返します。対処: UUIDを直接URLに貼り付けて実行してください。
DELETE /memos/{id} — メモを削除する
curl -s -X DELETE "%API_URL%/memos/6150433b-4834-4cfe-81f5-266d3dad9e57"エラー確認(404テスト)
削除済みのIDで取得を試みて404が返ることを確認:
curl -s "%API_URL%/memos/6150433b-4834-4cfe-81f5-266d3dad9e57"{"error": "memoId=6150433b-4834-4cfe-81f5-266d3dad9e57 のメモが見つかりません"}Git Bash でのテスト手順(参考)
bash構文が使えるので、より読みやすく書けます:
API_URL="https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod"
MEMO_ID="6150433b-4834-4cfe-81f5-266d3dad9e57"
# POST
curl -s -X POST "${API_URL}/memos" \
-H "Content-Type: application/json" \
-d '{"title":"初めてのメモ","content":"テスト"}'
# GET一覧
curl -s "${API_URL}/memos"
# PUT
curl -s -X PUT "${API_URL}/memos/${MEMO_ID}" \
-H "Content-Type: application/json" \
-d '{"title":"更新後タイトル","content":"内容も更新"}'
# DELETE
curl -s -X DELETE "${API_URL}/memos/${MEMO_ID}"VSCodeでGit Bashターミナルを使う方法:
ターミナルパネルの右上にある+ボタンの横の∨をクリック → Git Bash を選択
Step 10: AWS管理コンソールで確認
テスト後、AWSマネジメントコンソールからも各サービスの状態を確認できます。
DynamoDB
DynamoDB → テーブル → memo-api-dynamodb-stack-Memos → 「テーブルアイテムの探索」
作成したメモのデータが保存されていることを確認できます。
Lambda
Lambda → 関数 → memo-api-dynamodb-stack-MemoFunction-XXXX
関数の設定・環境変数・モニタリングが確認できます。
CloudWatch Logs(エラー確認)
Lambda → 対象関数 → 「モニタリング」タブ → 「CloudWatch Logs を表示」
エラーが発生した場合はここでログを確認します。print() で出力した内容もここに記録されます。
Step 11: リソースの削除
課金を止めるために、ハンズオン完了後は必ずリソースを削除してください。
sam delete --stack-name memo-api-dynamodb-stack --region ap-northeast-1対話式で確認が入ります:
Are you sure you want to delete the stack memo-api-dynamodb-stack in the region ap-northeast-1 ? [y/N]: y
Are you sure you want to delete the folder memo-api-dynamodb-stack in S3 which contains the artifacts? [y/N]: y両方 y で進めると数分で削除が完了します。
削除完了の確認
aws cloudformation describe-stacks --stack-name memo-api-dynamodb-stack --region ap-northeast-1An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id memo-api-dynamodb-stack does not existこのメッセージが表示されれば削除完了です。
注意: DynamoDB テーブル内のデータも同時に削除されます。必要なデータは事前にエクスポートしてください。
トラブルシューティング一覧
実際のハンズオン中に遭遇したエラーと対処法をまとめました。
| 症状 | 原因 | 対処 |
|---|---|---|
sam build が 'cp932' codec can't decode... エラー | requirements.txt に日本語コメントが含まれている | コメントを英語にする |
sam deploy が Missing option '--stack-name' エラー | samconfig.toml がない、または置き場所が違う | memo-api-dynamodb/ 直下に作成する |
| PUT/DELETE で CloudFront 400 Bad Request | URL に %MEMO_ID% が未展開のまま含まれている | UUIDを直接URLに貼り付ける |
CMD で 'API_URL' は認識されていません | bash構文(API_URL="..." )をCMDで実行した | CMDでは set API_URL=... 形式を使う |
PowerShell で curl がエラー | PowerShell組み込みの Invoke-WebRequest が呼ばれている | curl.exe と明示的に .exe を付ける |
| API が 500 を返す | Lambda内部エラー | CloudWatch Logsでエラー内容を確認 |
まとめ
今回のハンズオンで実現したこと:
- SAMテンプレート1ファイルでLambda・API Gateway・DynamoDBの3サービスを一括定義
- CRUD 5エンドポイント(POST/GET/GET{id}/PUT/DELETE)を持つREST APIを構築
sam build→sam deploy→sam deleteのサーバーレスアプリのライフサイクルを体験- Windows特有のハマりポイント(cp932エラー・CMDの変数・curlの書き方の違い)を解消
Macで書かれたAWSチュートリアルをWindowsで試してエラーが出た経験がある方も多いと思います。この記事でそのハマりポイントが解決できれば幸いです。
SAMがやってくれていたことをコンソールで確認する
「SAMは便利だけど、裏で何をやっているのかわからない」と感じた方向けに、全く同じAPIをAWSコンソールのみで手動構築する手順を比較記事としてまとめました。
コンソールで手動構築すると何が起きるか(一部抜粋):
| SAMの記述1行 | コンソールでやること |
|---|---|
DynamoDBCrudPolicy | IAMロール作成 + インラインポリシーのJSON作成 |
Events: Type: Api(5行) | API Gatewayのリソース5個 + メソッド5個 + デプロイ |
sam delete | 4サービスを依存関係の逆順に個別手動削除 |
操作ステップ数はSAMの約5ステップに対してコンソール手動は約50〜60クリック。SAMの価値をコンソール操作を通じて実感できます。
- AWSコンソールだけでメモアプリAPIを構築する手順【SAMとの比較付き】

次のステップ
このハンズオンを完成させたら、以下の発展的な内容にもチャレンジしてみてください:
- API Key認証の追加 — 外部からの不正アクセスを防ぐ
- Cognito認証の追加 — ユーザーごとにメモを管理する
- フロントエンドと連携 — React/Vue.jsからこのAPIを呼び出す
- CI/CDパイプラインの構築 — GitHub ActionsでSAMデプロイを自動化
コメント