AWS CDK × Claude Codeでチケット管理アプリを作る【バックエンド編】
この記事について
本記事は「AWS CDK × Claude Codeでチケット管理アプリを作る」シリーズの第3回です。
| # | タイトル |
|---|---|
| 1 | 構想編 |
| 2 | 環境構築編 |
| 3 | バックエンド編(本記事) |
| 4 | 完結編(React・S3・CloudFront・振り返り) |
Claudeとの進め方
構想編と同様のスタイルで進めています。設計はClaudeと議論して方針を決め、実装はClaude Codeに生成してもらい内容を確認しながら進めました。Zenn記事の執筆もClaudeに提案してもらいながら行っています。
この記事(バックエンド編)ではアクセスパターンの整理・GSI選定・APIの仕様決定をClaudeと議論しながら進めました。実装はClaude Codeに生成してもらい、発生したエラーも一緒に解決しました。
やったこと
- DynamoDBのテーブル設計(Claudeとの議論)
- CDKでDynamoDBテーブルを実装・デプロイ
- Lambda(Python)でチケットCRUD APIを実装
- API GatewayでAPIを公開
- APIの動作確認(PowerShellから叩く)
- Cognito認証の追加
- 認証付きAPIの動作確認
DynamoDBの設計(Claudeとの議論)
DynamoDBはRDBと違い、先にどんなクエリをするか決めてから設計する必要があります。まずClaudeとアクセスパターンを整理しました。
アクセスパターンの整理
チケット操作
├── チケットを作成する
├── チケット一覧を取得する
├── チケット詳細を取得する
├── チケットを更新する
└── チケットを削除する
階層操作
├── 子チケット一覧を取得する
└── 親チケットを取得する
絞り込み
└── ステータスで絞り込む(文字列検索はスコープ外)
隣接リストパターン
親子関係のあるチケットをDynamoDBで表現するために隣接リストパターンを採用しました。
PK SK 内容
TICKET#001 METADATA チケット本体
TICKET#001 CHILD#TICKET#002 子チケットへの参照
TICKET#002 METADATA 子チケット本体
TICKET#002 PARENT#TICKET#001 親チケットへの参照
PK=TICKET#001, SK begins_with CHILD#とクエリすると子チケット一覧が取れます。
GSIとは
GSI(グローバルセカンダリインデックス)とは、メインテーブルのPK/SK以外の属性で効率よく検索するための仕組みです。
メインテーブル
└── PK=TICKET#001で検索できる
GSI-2(ステータス用)
└── status=openで検索できる
GSIがない場合、ステータスで絞り込むには全件スキャンが必要になります。データが増えると遅くなるため、よく使う検索パターンにはGSIを設けます。
GSIの選定について
当初以下のGSIを検討しました。
| GSI候補 | PK | 用途 | 採用 |
|---|---|---|---|
| GSI-1 | entity_type | 全チケット一覧 | ✅ |
| GSI-2 | status | ステータス絞り込み | ✅ |
| GSI-3 | assignee | 担当者絞り込み | ❌ |
| GSI-4 | ticket_type | 種別絞り込み | ❌ |
Claudeとの議論で以下の観点から2つに絞りました。
- GSI-3(担当者):学習用途では優先度低。発展編で追加予定
- GSI-4(種別):階層構造で代替できるため不要
GSIが増えるほどコストと複雑さが上がるため、学習用途ではシンプルに保つ判断をしました。
GSIの設計(確定版)
| GSI | PK | SK | 用途 |
|---|---|---|---|
| GSI-1 | entity_type | created_at | 全チケット一覧 |
| GSI-2 | status | created_at | ステータス絞り込み |
CDKでDynamoDBテーブルを実装
Claude Codeを起動して以下のように指示しました。Claude Codeへの指示は日本語で書いて問題ありません。
DynamoDBのテーブルをCDKで実装してほしい。
テーブル名:TicketTable
PK:PK(文字列)/ SK:SK(文字列)
GSI-1:entity_type + created_at
GSI-2:status + created_at
removalPolicy: DESTROY(学習用途のため)
billingMode: PAY_PER_REQUEST(無料枠対応)
生成されたコードを確認すると、pointInTimeRecoveryの指定に古いAPIが使われていました。Claude Codeが最新API(pointInTimeRecoverySpecification)に自動で修正してくれました。
デプロイ・確認・削除
npx cdk deploy # デプロイ
# AWSコンソールでDynamoDBテーブルとGSIを確認
npx cdk destroy # 削除
Lambda(Python)でCRUD APIを実装
Claude Codeに以下のAPI仕様を伝えてLambda関数を生成してもらいました。
POST /tickets チケット作成(parent_idがあれば子チケット)
GET /tickets 全チケット一覧(status絞り込み対応)
GET /tickets/{id} チケット詳細
PUT /tickets/{id} チケット更新
DELETE /tickets/{id} チケット削除
GET /tickets/{id}/children 子チケット一覧
Claude Codeは自律的に以下の対応も行ってくれました。
-
statusがDynamoDBの予約語のため、UPDATE時にExpressionAttributeNamesでエスケープ - DynamoDBの
Decimal型のJSONシリアライズ対応(DecimalEncoderクラス)
CDKにLambda + API Gatewayを追加
追加したリソース
├── Lambda(Python 3.13)
├── LambdaにDynamoDB読み書き権限を付与
└── API Gateway(RestApi)
├── /tickets
├── /tickets/{id}
└── /tickets/{id}/children
APIの動作確認
PowerShellからAPIを叩いて動作確認しました。
# チケット作成
$bytes = [System.Text.Encoding]::UTF8.GetBytes('{"title":"テスト用チケット","ticket_type":"Task","status":"open","priority":"high","created_by":"user"}')
Invoke-RestMethod `
-Uri "https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/tickets" `
-Method POST `
-ContentType "application/json; charset=utf-8" `
-Body $bytes
# チケット一覧取得
Invoke-RestMethod `
-Uri "https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/tickets" `
-Method GET
APIの動作確認でハマった場面
①日本語の文字化け
PowerShellからPOSTリクエストを送ると日本語が文字化けしました。
title = "テスト用チケット"
→ DynamoDBに "ãã¹ãç¨ãã±ãã" として保存される
原因:API GatewayがHTTP/1.1仕様のデフォルト「ISO-8859-1(Latin-1)」でバイト列を文字列化するため、UTF-8の日本語が文字化けしていました。
解決:Claude Codeが_parse_body関数に以下のロジックを追加しました。
# Latin-1でエンコードされたUTF-8を正しく復元
body.encode('latin-1').decode('utf-8')
②DELETEのURLデコード問題
DELETE /tickets/TICKET%232760da51-...を実行するとTicket not foundエラーが発生しました。
原因:URLの#はフラグメント識別子として予約されており、API GatewayがURL中の%23をデコードしません。そのためpathParameters.idがTICKET#xxxではなくTICKET%23xxxのまま渡されていました。
解決:Lambda側でurllib.parse.unquote()を使ってデコードするよう修正しました。
from urllib.parse import unquote
ticket_id = unquote(path_params.get('id', ''))
Cognito認証の追加
Cognitoとは
AWSが提供する認証・認可サービスです。ユーザー登録・ログイン・JWTトークンの発行を担います。
CDKでCognitoを実装
Claude Codeに以下を指示して実装しました。
Cognito UserPool(Liteプラン)
サインイン:メールアドレス
自己サインアップ:有効
パスワード:8文字以上・大小文字・数字必須
removalPolicy: DESTROY
Cognito UserPool Client
認証フロー:USER_PASSWORD_AUTH
シークレットなし
API GatewayにCognito Authorizerを追加
全エンドポイントに適用
ユーザー登録と動作確認
# ユーザー登録
aws cognito-idp sign-up `
--client-id "UserPoolClientId" `
--username "test@example.com" `
--password "Test1234!" `
--region ap-northeast-1
# メール確認をスキップ(テスト用)
aws cognito-idp admin-confirm-sign-up `
--user-pool-id "UserPoolId" `
--username "test@example.com" `
--region ap-northeast-1
# ログインしてトークン取得
$auth = aws cognito-idp initiate-auth `
--client-id "UserPoolClientId" `
--auth-flow USER_PASSWORD_AUTH `
--auth-parameters USERNAME="test@example.com",PASSWORD="Test1234!" `
--region ap-northeast-1 | ConvertFrom-Json
$token = $auth.AuthenticationResult.IdToken
認証なしでAPIを叩くと401 Unauthorizedが返り、トークン付きで叩くと正常にレスポンスが返ることを確認しました。
Claude Codeとの協働で気づいたこと
良かった点
- DynamoDBの予約語問題・Decimal型の対応など、自分では見落としがちな実装上の注意点を自動で対処してくれた
- 文字化けやURLデコード問題の根本原因を的確に特定して修正してくれた
AIが間違えた場面
- CDKの古いAPIを提案することがあった(
pointInTimeRecovery→pointInTimeRecoverySpecification)。CDKはバージョンアップが速いため、提案されたコードは公式ドキュメントで確認することが重要
次回予告
次回「完結編」では以下を実施します。
- React実装(Claude Codeで生成)
- S3 + CloudFrontへのデプロイ
- フロント〜バックエンドの結合確認
- 画面キャプチャ
- シリーズ全体の振り返り
- GitHubリポジトリの公開
Discussion