目次Category
はじめに
今回はAWSの3つのサービスを組み合わせた サーバーレスな Todo アプリを作ります
EC2インスタンスを立てずに、マネージドサービスだけで構築します
| サービス | 役割 |
|---|---|
| Lambda | サーバーが不要でリクエスト時にプログラムを実行するサービス |
| API Gateway | APIの作成・管理ができるサービス |
| DynamoDB | フルマネージド型のNoSQLデータベース |
前提条件
- ・操作・確認は AWS コンソール(Web) で進めます
- ・Python 3.14 を使用します(Lambda ランタイム)
- ・VPC 設定は不要です(Lambda を VPC 外に置くため不要)
今回構築する全体像
Phase 1 Lambda 作成(モック)→ 単体テスト
↓
Phase 2 API Gateway 接続 → GET/POST 確認
↓
Phase 3 DynamoDB 接続 → 永続化
↓
Phase 4 ローカル HTML から操作
構築手順
Step1 Lambda の作成
まず Lambda 関数を作成し、DynamoDB の代わりにメモリ上の辞書で動作確認します
① Lambda 関数の作成
AWS コンソールから Lambda を開き、関数の作成を選択
設定内容
| 項目 | 値 |
|---|---|
| 関数名 | ojt-lambda-name-01(自分の名前に合わせて変更) |
| ランタイム | Python 3.14 |
| その他 | デフォルトのまま |

関数の作成後、コードエディタに以下のコードを貼り付けてデプロイ
今回はテストなのでDynamoDB は使用せず、メモリをデータベースとして使用します
import json
# 本物のDynamoDBの代わりに、メモリ上の辞書(Dict)をデータベースとして使用
# 構造: { "ユーザー名": ["タスク1", "タスク2", ...] }
MOCK_DYNAMODB = {
"yamada": ["AWSの勉強をする", "日報を書く"],
"tanaka": ["Pythonのコードレビュー"],
}
def lambda_handler(event, context):
"""
API Gatewayからのリクエストを処理するハンドラー
"""
# 1. API Gatewayからのリクエスト情報を取得
# ※API Gatewayの**Lambda統合プロキシ**を想定
http_method = event.get("httpMethod")
path_parameters = event.get("pathParameters") or {}
query_parameters = event.get("queryStringParameters") or {}
# ルーティング処理
try:
# 【タスク取得】GET /todo?user=ユーザー名
if http_method == "GET":
username = query_parameters.get("user")
if not username:
return create_response(400, {"message": "ユーザー名(user)を指定してください"})
# DynamoDB(モック)からユーザー名(PK)で検索
tasks = MOCK_DYNAMODB.get(username, [])
return create_response(200, {"user": username, "tasks": tasks})
# 【タスク追加】POST /todo
elif http_method == "POST":
# ボディの解析
body = json.loads(event.get("body", "{}"))
username = body.get("user")
task = body.get("task")
if not username or not task:
return create_response(400, {"message": "user と task は必須です"})
# DynamoDB(モック)へデータ保存(ユーザーが未登録なら新規作成)
if username not in MOCK_DYNAMODB:
MOCK_DYNAMODB[username] = []
MOCK_DYNAMODB[username].append(task)
return create_response(201, {
"message": "タスクを追加しました",
"current_tasks": MOCK_DYNAMODB[username]
})
# 対応していないメソッド
else:
return create_response(405, {"message": f"Method {http_method} Not Allowed"})
except Exception as e:
return create_response(500, {"message": f"Internal Server Error: {str(e)}"})
def create_response(status_code, body_content):
"""
API Gateway(プロキシ統合)が認識できる形式のレスポンスを作成するヘルパー関数
CORS対策のヘッダーもあらかじめ付与
"""
return {
"statusCode": status_code,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*", # フロントエンド(HTML)からのクロスドメイン通信を許可
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
},
"body": json.dumps(body_content, ensure_ascii=False)
}コードを貼り付けたらDeployボタンを押す

関数 ~~~~ が正常に更新されましたと出れば成功です
② テストイベントで動作確認する(GET・POST)
コードが正しく動くかを、API Gateway を通さずに Lambda 単体でテストします
テストイベント 1:GET のテスト
テストタブから新しいイベントを作成を選択し、以下を入力して保存
- ・イベント名:
getTest
{
"httpMethod": "GET",
"queryStringParameters": {
"user": "yamada"
}
}
テストを実行して、yamada のタスク一覧が返ってくることを確認

statusCodeが200になっていて、bodyに上記の内容が表示されていれば成功
テストイベント 2:POST のテスト
- ・イベント名:
postTest
{
"httpMethod": "POST",
"body": "{\"user\": \"yamada\", \"task\": \"課題を終わらせる\"}"
}テストを実行して、タスクの追加が成功することを確認

statusCodeが201になっていて、bodyに上記の内容が表示されていれば成功
Step2 API Gateway との接続
Lambda を API として公開するため、API Gateway を作成して接続します
① API Gateway の作成
AWS コンソールから API Gateway を開き、REST APIの構築を選択
設定内容
| 項目 | 値 |
|---|---|
| API 名 | ojt-api-name-01(自分の名前に合わせて変更) |
| その他 | デフォルトのまま |


② リソース(/todo)の作成
作成した API を開き、リソースの作成を選択
設定内容
| 項目 | 値 |
|---|---|
| リソース名 | todo |
| クロスオリジンリソース共有(CORS) | チェックを入れる |

③ GET・POST メソッドの作成
/todo リソースを選択し、メソッドの作成から GET と POST をそれぞれ追加
GET メソッドの設定
| 項目 | 値 |
|---|---|
| メソッドタイプ | GET |
| 統合タイプ | Lambda 関数 |
| Lambda プロキシ統合 | ✅ チェックを入れる |
| Lambda 関数 | 作成した Lambda 関数を選択 |

POST メソッドの設定
GET と同じく、メソッドタイプのみ POST を選択
④ API のデプロイ(dev ステージ)
API をデプロイを選択し、以下の設定でデプロイ
設定内容
| 項目 | 値 |
|---|---|
| ステージ | 新しいステージ |
| ステージ名 | dev |

デプロイ後、呼び出し URLが表示されるのでコピーしておきます

⑤ ブラウザと curl で動作確認
ブラウザで GET の確認
先ほどコピーしたURLにtodoのパスと引数を加えます
例)
https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/todo?user=yamada
yamada のタスク一覧が JSON で返ってくることを確認

curl で POST の確認
実行する環境(OS・シェル)に合わせて、以下の curl コマンドをターミナルで実行します
xxxxxxxxは自身のURLに書き換えてください
Mac / Linux / WSL / Git Bash の場合
curl -X POST https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/todo \
-H "Content-Type: application/json" \
-d '{"user": "yamada", "task": "本物のDynamoDBに保存する"}'Windows コマンドプロンプト(cmd.exe)の場合
curl -X POST https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/todo ^
-H "Content-Type: application/json" ^
-d "{\"user\": \"yamada\", \"task\": \"本物のDynamoDBに保存する\"}"
Step3 DynamoDB との接続
メモリ上のモックから、本物の DynamoDB に切り替えてデータを永続化します
① DynamoDB でテーブルを作成
AWS コンソールから DynamoDB を開き、テーブルの作成を選択
設定内容
| 項目 | 値 |
|---|---|
| テーブル名 | ojt-dynamo-name-01(自分の名前に合わせて変更) |
| パーティションキー(PK) | user(文字列) |
| ソートキー(SK) | timestamp(文字列) |
| その他 | デフォルトのまま |

② Lambda に DynamoDB の権限を付与
Lambda はデフォルトでDynamoDB を操作できないので
IAM ロールにポリシーをアタッチして、権限を付与します
設定手順
- Lambda の設定タブ → アクセス権限を開く
- ロール名のリンクをクリック(IAM コンソールが開く)
- 許可を追加→ポリシーをアタッチを選択
AmazonDynamoDBFullAccessを検索して選択 → 許可を追加


③ コードを DynamoDB 連携版に書き換え
Lambda のコードエディタを開き、以下のコードに差し替えてデプロイ
テスト版はLambdaのメモリで操作しましたが、今回はDynamoDBで操作します
TABLE_NAMEを自身のテーブル名に変えてください
import json
import boto3
from datetime import datetime, timezone, timedelta
from boto3.dynamodb.conditions import Key
# DynamoDBオブジェクトの初期化
dynamodb = boto3.resource('dynamodb')
TABLE_NAME = 'ojt-dynamo-name-01' # ← 自身で作成したテーブル名に変更
table = dynamodb.Table(TABLE_NAME)
def lambda_handler(event, context):
"""
PK(user) と SK(timestamp) を使用した、実際のDynamoDB接続コード
"""
http_method = event.get("httpMethod")
query_parameters = event.get("queryStringParameters") or {}
try:
# 【タスク取得】GET /todo?user=ユーザー名
if http_method == "GET":
username = query_parameters.get("user")
if not username:
return create_response(400, {"message": "ユーザー名(user)を指定してください"})
# ソートキーがある場合、get_itemではなく**query**を使う
# 指定したユーザー(PK)のデータを全件検索
response = table.query(
KeyConditionExpression=Key('user').eq(username)
)
items = response.get('Items', [])
# フロントエンド(JS)が扱いやすいように配列にして返す
tasks_list = []
for item in items:
tasks_list.append({
"timestamp": item.get("timestamp"),
"task": item.get("task")
})
return create_response(200, {"user": username, "tasks": tasks_list})
# 【タスク追加】POST /todo
elif http_method == "POST":
body = json.loads(event.get("body", "{}"))
username = body.get("user")
task = body.get("task")
if not username or not task:
return create_response(400, {"message": "user と task は必須です"})
# 現在時刻をソートキー(SK)にする(重複を防ぎ、並び替えを容易にするため)
# 日本時間(JST)のタイムスタンプ文字列(例: 2026-07-07T12:00:00)
jst = timezone(timedelta(hours=+9))
now_str = datetime.now(jst).isoformat()
# PK(user)とSK(timestamp)の組み合わせが新しいので、上書きされずに別データとして保存
table.put_item(
Item={
'user': username,
'timestamp': now_str,
'task': task
}
)
return create_response(201, {
"message": "タスクを追加しました",
"added_task": {
"user": username,
"timestamp": now_str,
"task": task
}
})
# 対応していないメソッド
else:
return create_response(405, {"message": f"Method {http_method} Not Allowed"})
except Exception as e:
return create_response(500, {"message": f"DynamoDB Error: {str(e)}"})
def create_response(status_code, body_content):
"""API Gateway(プロキシ統合)用レスポンス作成ヘルパー"""
return {
"statusCode": status_code,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
},
"body": json.dumps(body_content, ensure_ascii=False)
}④ テストイベントで動作確認
POST テスト
以前作成した postTest イベントを選択してテストを上書き保存して実行
{
"httpMethod": "POST",
"body": "{\"user\": \"yamada\", \"task\": \"本物のDynamoDBに保存する\"}"
}
DynamoDB コンソールの項目を探索でテーブルを開き、データが追加されていることを確認

Step4 ローカル HTML からの接続
ブラウザから API を呼び出すためのシンプルな HTML ファイルを作成します
① todo.html の作成
HTML・CSS・JavaScript をひとつのファイルにまとめた構成
todo.html 内の API_URL を、自分のエンドポイント URL(/todo まで)に変更してください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>サーバーレス Todoリスト</title>
</head>
<body>
<div class="container">
<h1>📝 Todo リスト</h1>
<!-- ユーザー名入力 + 検索ボタン -->
<div class="form-group">
<input type="text" id="username" placeholder="ユーザー名を入力(例: yamada)">
<button class="search-btn" onclick="fetchTodos()">検索</button>
</div>
<hr>
<!-- タスク入力 + 追加ボタン -->
<div class="form-group">
<input type="text" id="taskContent" placeholder="新しいタスクを入力">
<button onclick="addTodo()">追加</button>
</div>
<ul id="todoList"></ul>
</div>
<script>
// TODO: あなたのAPI GatewayのURL(/todoまで)に書き換えてください
const API_URL = "【ここにあなたのAPI GatewayのURLを貼り付ける】";
// 1. 【GET】ユーザー名で検索してタスク一覧を取得する関数
async function fetchTodos() {
const username = document.getElementById("username").value.trim();
if (!username) {
alert("ユーザー名を入力してください");
return;
}
try {
const response = await fetch(`${API_URL}?user=${encodeURIComponent(username)}`);
if (!response.ok) throw new Error("データの取得に失敗しました");
const data = await response.json();
displayTodos(data.tasks);
} catch (error) {
alert("エラーが発生しました: " + error.message);
}
}
// 2. 【POST】新しいタスクを追加する関数
async function addTodo() {
const username = document.getElementById("username").value.trim();
const taskContent = document.getElementById("taskContent").value.trim();
if (!username || !taskContent) {
alert("ユーザー名とタスクの両方を入力してください");
return;
}
try {
const response = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user: username, task: taskContent })
});
if (!response.ok) throw new Error("タスクの追加に失敗しました");
document.getElementById("taskContent").value = "";
fetchTodos();
} catch (error) {
alert("エラーが発生しました: " + error.message);
}
}
// 3. タスク配列を画面(HTML)に描画するヘルパー関数
function displayTodos(tasks) {
const todoListUl = document.getElementById("todoList");
todoListUl.innerHTML = "";
if (tasks.length === 0) {
todoListUl.innerHTML = "<li>タスクはありません</li>";
return;
}
tasks.forEach(item => {
const li = document.createElement("li");
const displayTime = item.timestamp
? item.timestamp.substring(0, 16).replace("T", " ")
: "";
li.innerHTML = `
<span>${escapeHtml(item.task)}</span>
<span class="timestamp">${displayTime}</span>
`;
todoListUl.appendChild(li);
});
}
// 4. セキュリティ対策(XSS対策):文字エスケープ関数
function escapeHtml(str) {
return str.replace(/[&<>"']/g, match => {
const escape = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return escape[match];
});
}
</script>
</body>
</html>② ブラウザで動作確認
todo.html をブラウザで開き、以下の流れで動作確認
確認手順
- ユーザー名に
yamadaと入力して検索→ タスク一覧が表示されることを確認 - タスク入力欄に文字を入力して追加→ リストに反映されることを確認
- DynamoDBの設定画面で値を追加しhtmlで反映されているか確認

まとめ
これで作成は以上です
削除できるサービスは消しておきましょう
片付け
演習が終わったら、不要なリソースを削除します
- ・Lambda
- ・API Gateway
- ・DynamoDB
同じカテゴリーの記事
同じカテゴリの記事を読む