⚙️

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に生成してもらい、発生したエラーも一緒に解決しました。


やったこと

  1. DynamoDBのテーブル設計(Claudeとの議論)
  2. CDKでDynamoDBテーブルを実装・デプロイ
  3. Lambda(Python)でチケットCRUD APIを実装
  4. API GatewayでAPIを公開
  5. APIの動作確認(PowerShellから叩く)
  6. Cognito認証の追加
  7. 認証付き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.idTICKET#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を提案することがあった(pointInTimeRecoverypointInTimeRecoverySpecification)。CDKはバージョンアップが速いため、提案されたコードは公式ドキュメントで確認することが重要

次回予告

次回「完結編」では以下を実施します。

  • React実装(Claude Codeで生成)
  • S3 + CloudFrontへのデプロイ
  • フロント〜バックエンドの結合確認
  • 画面キャプチャ
  • シリーズ全体の振り返り
  • GitHubリポジトリの公開

Discussion