はじめに
個人で開発している旅行のサービス(Web と iPhone のアプリ)の AWS を、3 週間ほどで CloudFormation の管理下に入れた。
RDS や S3 のようにデータを持つ資源は、作り直さずに resource import で取り込んだ。調査・テンプレート・手順書・レビューは AI(Claude Code)が行い、人(私)は承認と、商用での実行だけを持った。
この記事では、その進め方と、AI が書いた変更を機械で止める仕組みを書く。
対象読者
- コンソールで作ったものと SAM が混ざった AWS を運用していて、IaC にしたい人
- データを持つ資源を、作り直さずに管理下へ入れたい人
- AI エージェントにインフラを触らせるのが怖い人
この記事で分かること
- resource import で、作り直さずに取り込む進め方
- スタックの分け方
- AI が書いた変更を止める、変更セットの許可リストのゲート
- 取り込んだ後に崩れないための、週 1 回のドリフト検知
前提
| 項目 | 内容 |
|---|---|
| 構成 | Amplify(Web)、API Gateway + Lambda(Python)、RDS PostgreSQL、S3 |
| 環境 | 商用(prod)と検証(stg)の 2 つ |
| ツール | SAM / CloudFormation、GitHub Actions |
| 体制 | 人は 1 人。AI は Claude Code(設計・実装・レビュー) |
1. 出発点: 半分だけ IaC だった
API と Lambda は SAM のテンプレートにあった。一方で、RDS・S3・VPC・Amplify・DNS・証明書・共用の IAM ロールは、コンソールで作って育ったものだった。
困っていたのは次の 3 つ。
- 何がどこにあるか分からない: 使われていないセキュリティグループや古い Lambda、空のスタックが残っていた
- 環境の境目があいまい: 検証の Lambda が、商用と同じ共用の実行ロールを使っていた
-
デプロイに歯止めがない: CI は長期のアクセスキーで動き、
sam deploy --no-confirm-changesetで変更セットを見ずに実行していた
最初にしたのは、今の構成図を描くことだった。AI に draw.io の MCP で図を描かせ、資源の棚卸し表を作った。IaC にする範囲は、この表から決めた。
2. 方針: 作り直さずに取り込む
ツールは、SAM / CloudFormation を続けることにした。
| 観点 | SAM / CFN を続ける + import | CDK に移る | Terraform に移る |
|---|---|---|---|
| 既存のスタックと CI | そのまま使える | 移すか併存させる | 移すか併存させる |
| 既存の資源の取り込み | resource import | 結局 CFN の import に頼る | terraform import |
| 状態の管理 | CFN に任せる | CFN に任せる | state 用の S3 とロックが要る |
| 変更の確かめ方 | 変更セットで置き換えの有無が分かる | 合成の後の変更セット(1 段増える) | plan |
決め手は、変更セットで「置き換え(Replacement)」の有無を機械的に確かめられることだった。AI が書いたテンプレートでも、データを持つ資源が作り直されないことを、人の目に頼らずに確かめられる。
取り込むときに守る条件も、先に決めた。
- データを持つ資源とネットワークには、
DeletionPolicy: RetainとUpdateReplacePolicy: Retainを付ける - 取り込みは、必ず IMPORT の変更セットで行う
- テンプレートの値は、取り込む時点の実物に合わせる(取り込みと設定の変更を混ぜない)
- 検証で先に取り込み、差分が 0 なのを確かめてから商用に進む
- 商用の操作は手順書を作り、人が承認してから行う
- IaC の変更は、すべて批判的なレビューにかける
3. AI と人の分担
| 仕事 | 担い手 | 決まり |
|---|---|---|
| 調べる | AI | AWS は読み取りのコマンドだけ。実行したコマンドは全件を後で確かめる |
| 設計・手順書 | AI が書き、人が承認 | 判断が要るところは案を並べて人に出す |
| テンプレート | AI | 変更は PR。merge は人 |
| 批判的なレビュー | 別の AI | 実装の経緯を渡さない、新しい文脈で見る |
| 商用での実行・削除 | 人 | 手順書に沿って、人が手で行う |
レビューは、書いた AI とは別の AI に頼んだ。当時は OpenAI の Codex、今は Claude の Opus に頼んでいる。
レビューの指摘で多かったのは、置き換えの見落とし、削除保護の付け忘れ、権限の付けすぎ。1 つの手順書で 3〜5 回まわすこともあった。
失敗も 1 つ書いておく。構成図を描かせたとき、AI がテンプレートを読まずに推測で書いたところがあり、レビューで Major が 3 件出た(認可の確かめ方など)。それからは「事実はテンプレート・コード・AWS の読み取りで確かめる」を決まりにしている。
4. スタックの分け方
変える頻度と、デプロイの仕方で分けた。
| スタック | 中身 | デプロイ |
|---|---|---|
app-{env}(SAM) |
API Gateway・Lambda・環境ごとの実行ロール | CI(ゲートつき) |
data-{env} |
RDS・S3・DB の資格情報 | 手動(手順書と承認) |
frontend |
Amplify・CloudFront Functions | 手動 |
ci |
OIDC のプロバイダー・デプロイ用のロール・成果物のバケット | 手動 |
shared-network |
VPC・サブネット・セキュリティグループ | 手動(検証にも商用と同じゲート) |
shared-dns |
Route 53 のゾーンとレコード | 同上 |
shared-edge(us-east-1) |
証明書・WAF | 同上 |
分けるときの決まりは 5 つ。
- 取り込みは、SAM でない素の CloudFormation のスタックに入れる。アプリのスタックは CI が毎回デプロイするので、データを置くと影響が広がる
- データと共有基盤は、CI が自動でデプロイしないスタックに置く
- 1 つのもの(セキュリティグループのルール・ロールのポリシー)を、2 つのスタックが持たない。新しく作るスタックでは、ルールやポリシーを本体のプロパティに書かず、独立した資源にする
- 消す予定のものは取り込まない。取り込んでから消すより安全で、手間も少ない
-
参照の向きは上から下だけ。同じリージョンは
!ImportValue、us-east-1 の証明書の ARN はパラメータで渡し、CI がshared-edgeの出力と一致するかを確かめる
共用の実行ロールは取り込まず、環境ごとのロールに分けた。分けるときにコードが実際に使う操作を洗い出し、AmazonS3FullAccess のような広すぎる権限も外した。
5. 取り込みの進め方
順番は次のとおり。前の段の検査に合格しないと、次に進まない。
- 使い捨ての資源で、取り込めるかどうかを検証
data-stg-
ci(OIDC とゲートへの切り替えと一緒に) - 実行ロールの分割(検証 → 商用)
data-prodshared-network-
shared-dns・shared-edge frontend
取り込んだ直後の検査は、毎回同じ 5 つにした。
| # | 検査 | 合格の条件 |
|---|---|---|
| A1 | drift detection | 取り込んだ資源がすべて IN_SYNC
|
| A2 | drift detection の対象外の型の照合 | 実物とテンプレートの差がない |
| A3 | 何も変えないテンプレートで変更セットを作る | 変更なし |
| A4 | 影響のない更新(タグの追加など)を 1 回 | 成功し、置き換え・削除がない |
| A5 | 疎通の確認 | 取り込む前と同じに動く |
つまずいたところ
DNS の単純なレコードは取り込めない
単純ルーティングのレコードは、import の対象にできなかった。そこで、今の値をそのままテンプレートに書いて「作成」した。値が完全に同じならエラーなく管理下に入り、違えば失敗して既存のレコードは変わらない。
SAM のスタックへの IMPORT が拒まれた
検証の画像のバケットのポリシーを SAM のスタックに取り込もうとしたら、API の Deployment の作り直しが同じ変更セットに出てきて、IMPORT が通らなかった。ポリシーは、バケットと同じ data-stg に持たせることにした。
値が同じでも、書き方を変えると置き換えになる
テンプレートに直書きした ID(サブネット・セキュリティグループなど)を !ImportValue に置き換えようとした。値は同じなのに、一部の資源で変更セットに Replacement: True が出た。
置き換えるのはやめ、直書きのまま、共有のスタックの出力と一致するかを照合のスクリプトで守ることにした。変更セットは作っただけで実行していない。作ってから実行する流れにしておいたおかげで、手前で気づけた。
6. CI/CD: AI が書いた変更を、機械で止める
変更セットの許可リストのゲート
sam deploy で直に実行するのをやめ、変更セットを作ってから判定するようにした。
-
sam build→sam package(成果物を S3 へ) -
aws cloudformation create-change-set(名前は CI の実行ごとに 1 つ) -
describe-change-setで、変更の一覧をすべて読む - 判定のスクリプトで、全件が許可リストに当たるかを見る
- すべて許されれば
execute-change-set。1 つでも外れたら実行せずに失敗し、変更セットは残す
判定は許可リスト方式にした。どの規則にも当たらない変更は、すべて止めて人に回す。考え方を簡単に書くと、次のようになる(実物より単純にした例)。
# 簡略化した例。実物は資源の型・プロパティごとに規則を持つ
AUTO_OK = {
("AWS::Lambda::Function", "Code"),
("AWS::Lambda::Function", "Environment"),
}
def judge(change: dict) -> str:
rc = change["ResourceChange"]
if rc["Action"] in ("Remove", "Import"):
return "stop" # 削除と取り込みは人へ
if rc.get("Replacement") in ("True", "Conditional"):
return "stop" # 置き換えは人へ
for d in rc.get("Details", []):
key = (rc["ResourceType"], d["Target"].get("Name"))
if key not in AUTO_OK:
return "stop" # 許可リストにないものは人へ
return "auto"
止まる変更の例は、データを持つ資源の置き換えや削除、関数の実行ロールの付け替え、判定できない変更。判定のスクリプトには単体の試験を 53 件付けた。
AI がテンプレートを書き換えても、人が書き換えても、同じゲートを通る。AI に「気をつけて」と頼むより、通らない形にしておくほうが確かだ。
商用は、手動の起動だけ
商用のデプロイは workflow_dispatch だけにし、起動した人がデプロイしてよい人かを workflow の中で確かめる。無料のプランの非公開リポジトリでは、GitHub の Environments の承認が使えなかったため、この形にした。
7. 運用: 週 1 回のドリフト検知
取り込んだ後に、コンソールで誰かが(あるいは AI が)手で変えると、テンプレートと実物がずれる。そこで、毎週月曜に定期実行で確かめている。
- すべてのスタックの drift detection
- drift detection の対象外の型(セキュリティグループのルール・バケットのポリシー・DNS のレコードなど)を、スクリプトで実物と照合
- 管理の外のスタック(確かめのために作った使い捨てのスタックなど)が残っていないか
- 不合格なら、Issue を自動で作る(定期実行の失敗の通知は、届く人が限られるため)
運用を始めてから多かったのは、テンプレートを足したのに、ドリフト検知の対象に登録し忘れることだった。10 月は週に 1 つほど起きた。今は PR の CI で、登録されていないテンプレートや、どの方法でも見ていない資源の型があれば落とすようにしている。
8. まとめ
構成図を描いてから、ドリフト検知の運用を始めるまで、3 週間ほどだった。
効いたのは次の 3 つ。
- 変更セットを作ってから実行する: 置き換えを機械的に見つけられる。AI の変更も、人の変更も同じゲートで止まる
- 検証で先に取り込み、差分 0 を確かめてから商用: 毎回同じ 5 つの検査で、手順書の型ができた
- 書いた AI と別の AI がレビューする: 置き換え・削除保護の漏れ・権限の付けすぎを、商用の前に拾えた
残っているのは、取り込まなかった古い資源の片付けと、構成図を IaC にした後の形に描き直すこと。
AI にインフラを任せるときに大事なのは、AI を信じることでも疑うことでもなく、間違えても止まる形を先に作ることだと思う。
この開発の体制(PM ひとりと AI の組織)の話は、note に書いた。


