AWS CDKを学びたいけれど、CloudFormationも含めて未経験だと、最初の一歩がかなり重く感じます。
今回は「ドキュメントを読むだけで終わらせない」ために、Redmine風のチケット管理アプリをAWS CDKで作り切る挑戦をしました。あわせて、設計の壁打ちや実装支援にClaude / Claude Codeを使い、AIを作業の代行ではなく自分の学習と開発を広げる自己拡張として活用しました。
この記事は、Zennで公開した全4回シリーズのまとめです。細かい手順よりも、完成までのロードマップ、各章で何を判断し、どこで詰まり、何を学んだかに絞って整理します。
元シリーズ
| 章 | タイトル | 主な内容 |
|---|---|---|
| 1 | 構想編 | 作るもの、技術選定、無料利用枠、DynamoDB採用理由 |
| 2 | 環境構築編 | AWS CLI、CDK Bootstrap、Claude Code、cdk init |
| 3 | バックエンド編 | DynamoDB、Lambda、API Gateway、Cognito |
| 4 | 完結編 | React、S3、CloudFront、結合確認、振り返り |
ソースコードはこちらです。
完成形
作ったのは、スクラム開発で使うことを想定したチケット管理アプリです。
- チケット種別: Epic / Story / Task / Bug / Spike
- 階層構造: 親チケットと子チケット
- ステータス: open / in_progress / done / closed
- 優先度: low / medium / high / critical
- 認証: Cognito
- フロントエンド: React
- 配信: S3 + CloudFront
- API: API Gateway + Lambda
- データストア: DynamoDB
全体像はこうです。
完成までのロードマップ
今回の挑戦は、ざっくり次の4ステップで進みました。
Step 1: 何を作るか決める
-> チケット管理アプリ、無料枠、DynamoDB、CDKの方針を決定
Step 2: 開発環境を整える
-> AWS CLI、CDK、Bootstrap、Claude Code、cdk synthまで確認
Step 3: バックエンドを作る
-> DynamoDB設計、Lambda CRUD API、API Gateway、Cognito認証
Step 4: フロントエンドと配信を仕上げる
-> React、S3、CloudFront、結合確認、振り返り
単に実装を積み上げたというより、設計の判断を小さく切り、動かしながら理解する流れでした。
Step 1: 構想を固める
最初に決めたのは「何を作るか」と「何を学ぶか」です。
学習ゴールは2つでした。
- AWS CDKを実践的に学ぶ
- Claude Codeを使ったAI協働開発を体験する
AWS CDKだけを学ぶなら小さなLambdaでもよかったのですが、それだと実務の構成に近づきません。そこで、認証、API、DB、フロント、配信まで含むチケット管理アプリにしました。
技術選定で一番大きかった判断は、データベースです。
| 候補 | 良い点 | 見送った / 採用した理由 |
|---|---|---|
| Aurora Serverless | 親子関係をRDBとして自然に扱える | 無料枠に収めにくいため見送り |
| DynamoDB | 無料利用枠が期限なしで使いやすい | 隣接リストパターンで親子関係を表現して採用 |
DynamoDBはRDBのように後から自由に検索する設計ではなく、先にアクセスパターンを決める必要があります。この制約が、逆に設計を学ぶよい題材になりました。
Step 2: 環境構築で土台を作る
次に、Windowsネイティブ環境で開発環境を整えました。WSLは使っていません。
主な環境は以下です。
| ツール | バージョン |
|---|---|
| OS | Windows 11 |
| Node.js | v24.15.0 |
| npm | 11.13.0 |
| Python | 3.13.5 |
| AWS CLI | 2.34.37 |
| AWS CDK | 2.1120.0 |
| Claude Code | 2.1.123 |
ここで大事だったのは、いきなりデプロイしないことです。
まずは以下を確認しました。
aws sts get-caller-identity
npx cdk synth
cdk synthはCloudFormationテンプレートを生成するだけなので、AWS上にリソースは作られません。学習中は、synthで確認してからdeployする流れを習慣にすると安心です。
最初にハマったこと: cdk initは空ディレクトリでないと失敗する
GitHubでリポジトリを作ったとき、README.mdとLICENSEが自動生成されていました。その状態で以下を実行すると失敗します。
cdk init app --language typescript
エラーの理由はシンプルで、cdk initは空ディレクトリでないと実行できないためです。
Claude Codeに相談したところ、最初は.bakにリネームして退避しようとしましたが、それでも非空ディレクトリとして検知されて失敗しました。次に、ファイルをプロジェクト外へ退避して再実行し、成功しました。
この時点で、Claude Codeは「一発で正解を出す道具」ではなく、失敗を観測して次の手を打つ開発エージェントとして使える感触がありました。
Step 3: バックエンドを作る
バックエンド編では、DynamoDB、Lambda、API Gateway、Cognitoを実装しました。
DynamoDBはアクセスパターンから設計する
まず、必要な操作を整理しました。
チケット操作
- 作成
- 一覧取得
- 詳細取得
- 更新
- 削除
階層操作
- 子チケット一覧取得
- 親チケット取得
絞り込み
- ステータスで絞り込み
親子関係は、隣接リストパターンで表現しました。
PK SK 内容
TICKET#001 METADATA チケット本体
TICKET#001 CHILD#TICKET#002 子チケットへの参照
TICKET#002 METADATA 子チケット本体
TICKET#002 PARENT#TICKET#001 親チケットへの参照
GSIは最小限に絞りました。
| GSI | PK | SK | 用途 |
|---|---|---|---|
| GSI-1 | entity_type | created_at | 全チケット一覧 |
| GSI-2 | status | created_at | ステータス絞り込み |
担当者やチケット種別での絞り込みも候補にありましたが、学習用途としては複雑さが増えすぎるため、今回は見送りました。ここは「作れるものを全部作る」より、学習の焦点を保つ判断です。
Lambda + API GatewayでCRUD APIを作る
APIは次の形にしました。
POST /tickets
GET /tickets
GET /tickets/{id}
PUT /tickets/{id}
DELETE /tickets/{id}
GET /tickets/{id}/children
実装中には、地味だけれど実務でも踏みそうな問題が出ました。
- DynamoDBの
statusが予約語なので、更新時にエスケープが必要 - DynamoDBの
Decimal型はそのままJSON化できない - PowerShellから日本語JSONを送ると文字化けする
- URL中の
#が%23のままLambdaに渡るケースがある
特に日本語の文字化けは印象的でした。PowerShellから送ったUTF-8の日本語が、API Gateway側でLatin-1として扱われて文字化けしていました。
解決策として、Lambda側で以下のように復元しました。
body.encode('latin-1').decode('utf-8')
また、チケットIDに含まれる#については、Lambda側でURLデコードするようにしました。
from urllib.parse import unquote
ticket_id = unquote(path_params.get('id', ''))
こういう「動くはずなのに動かない」場面で、Claude Codeに原因の切り分けを依頼できたのはかなり助かりました。
Cognito認証を追加する
API GatewayにCognito Authorizerを追加し、トークンなしでは401 Unauthorized、トークン付きではAPIを呼べる状態にしました。
Cognitoで注意したのはプランです。
新規UserPool作成時はデフォルトでEssentialsプランになるため、無料枠を意識するならLiteプランを明示的に指定する必要があります。ここは見落とすと予期しない課金につながるため、かなり重要なポイントでした。
Step 4: フロントエンドと配信を仕上げる
最後にReactアプリを作り、S3 + CloudFrontで配信しました。
画面構成は4つです。
/login
- Cognito認証
/
- チケット一覧
- ステータス絞り込み
- 新規作成
- ログアウト
/tickets/new
- チケット作成フォーム
/tickets/:id
- チケット詳細
- ステータス更新
- 子チケット一覧
- 子チケット作成
React実装では、認証済みユーザーだけが画面に入れるPrivateRouteや、チケットIDの#をencodeURIComponent()でエンコードする対応を入れました。
フロントエンドでハマったこと: global is not defined
amazon-cognito-identity-jsを使ったところ、Vite環境で次のエラーが出ました。
Uncaught ReferenceError: global is not defined
原因は、ライブラリがNode.jsのglobal変数を前提にしている一方で、ブラウザ環境にはそれがないためです。
vite.config.tsに以下を追加して解決しました。
define: {
global: 'globalThis'
}
S3 + CloudFrontでSPAを配信する
配信構成は、プライベートS3 + CloudFront OACにしました。
CloudFrontではSPA対応として、403と404をindex.htmlへ返す設定が必要です。特にOAC + プライベートS3では、存在しないパスが404ではなく403になることがあります。
ここを404だけにしてしまうと、/tickets/:idのようなReact Routerのパスを直接開いたときに壊れます。403も拾うのがポイントです。
走りきって得た学び
今回のシリーズで一番大きかった学びは、CDKの文法そのものよりも、インフラを小さく作って、確認して、壊せる状態にしておくことの大切さでした。
CDKで特に大事だと感じたのは次の点です。
-
cdk bootstrapが何を作るのか理解しておく -
cdk synthで確認してからcdk deployする - 学習用途では
removalPolicy: DESTROYを検討する - CDKは更新が速いので、生成コードを公式情報で確認する
- 無料枠を使う場合も、請求アラートは必ず設定する
Claude Codeについては、次の使い方が自分には合っていました。
- 設計はClaudeと壁打ちして、選択肢とトレードオフを整理する
- 実装はClaude Codeに任せつつ、生成内容は自分で読む
- エラー時は、現象、期待値、ログを渡して原因分析を依頼する
- 最終判断は自分で行う
AIに全部任せるというより、自分の理解速度と試行回数を増やすために使う感覚です。
時間と心のゆとりはどこで生まれたか
仕事をしながら新しい技術を学ぶとき、つらいのは「調べることが多すぎて前に進んでいる感じがしない」時間です。
今回、Claude / Claude Codeを使ってよかったのは、そこをかなり圧縮できたことでした。
- 選択肢を整理してもらう
- エラー原因の仮説を出してもらう
- 修正案を複数試してもらう
- 記事化するときに構成を壁打ちする
そのぶん、自分は「なぜその設計にするのか」「本当にそのコードでよいのか」「どこを学びとして残すのか」に時間を使えました。
これは単なる時短ではなく、学習の密度を上げて、心の余白を作る使い方だったと思います。
次にやるなら
今回スコープ外にした機能もいくつかあります。
- 階層チケットの再帰取得
- スプリント管理機能
- 担当者・種別でのGSI追加
- Cognitoのセルフサインアップ
- DynamoDBからAurora Serverlessへの移行
- GitHub Actions + CDKのCI/CD
このあたりは、次の挑戦としてちょうどよさそうです。
おわりに
AWS CDK未経験から始めて、認証、API、DB、フロント、配信まで含むチケット管理アプリを作り切ることができました。
CDKは、最初は覚えることが多く見えます。ただ、synth -> deploy -> destroyの流れを小さく回せるようになると、インフラをコードとして扱う感覚がかなり掴みやすくなります。
そしてClaude Codeは、コードを代わりに書かせるだけの存在ではありませんでした。設計の壁打ち、エラーの切り分け、実装のたたき台作りを通じて、自分の挑戦を前に進めるための自己拡張として使えました。
同じように、仕事をしながらAWS CDKやAI活用を学びたい方の参考になればうれしいです。