AI に dev は触らせたい、本番は見せるだけにしたい — 鏡の構造で権限の差を表す
AWS をマルチアカウント化したら、Claude に dev 環境を見せる手段がなくなった。ついでに dev ではバッチを回せるようにし、本番は閲覧に徹させたい。長期キーは消せず、キーの置き場所に迷い、権限の非対称がコードに漏れるのが気持ち悪かった。最終的に「身元は 1 つ、ロールは両アカウントで鏡、差分は追加ポリシー 1 枚」に落ち着くまでの判断の記録。
アカウントを分けたら、Claude が dev を見られなくなった
個人で運用しているサービス群を、1 つの AWS アカウントから AWS Organizations によるマルチアカウント構成に移した。本番(prod)と開発(dev)でアカウントを分け、dev の資材は丸ごと新しいアカウントに作り直した。
困ったのは、その後だった。開発の主体はすでに Claude Code on the web に移っていて、Claude には閲覧専用の IAM ユーザーを 1 つ払い出してあった。アカウントが 1 つだった頃は、それで dev も本番も全部見えた。アカウントを分けた瞬間、Claude が見られるのは本番だけになった。 dev 環境で何が起きているかを見てもらう手段が、なくなった。
アカウントが 1 つなら問題にすらならなかったことが、分けた途端に問題になる。マルチアカウント化の副作用としては地味だが、AI に開発を任せている身には効く。
ついでに、ずっと引っかかっていたこと
どうせ Claude の権限を作り直すなら、前から気になっていたことも片付けたかった。
自作サービスの一つに、定期実行の Lambda でデータを取り込み・集約するバッチがいくつかある。改修するたびに動作を確かめたいが、定期実行なので次の起動を最長 1 時間くらい待ちながら開発していたこともある。待ちたくなければ手で起動するしかない。
ところが Claude には実行権限がない。Claude がパラメータを示して、自分が CLI から回す、という往復になる。しかも自分は外出先で開発していることが多く、手元で CLI を叩ける状態にないことがほとんどだった。AWS のモバイルアプリからできなくはないが、画面は小さいし面倒だ。
権限を渡したいとはずっと思っていた。ただ、本番のバッチまで回せるようにするのは違う、とも思っていた。1 アカウントの時代は、この 2 つを両立させる素直な方法がなかった。アカウントを分けたことで、dev と本番で「できること」を変えられるようになった。これは好都合だった。
長期キーは、消せなかった
少し前に、IAM のアクセスキーが漏洩してアカウントに制限を受けたことがある。マルチアカウント化の過程で、使っていない長期キーはほぼ全部掃除した。できるなら、Claude の長期キーもなくしたかった。
GitHub Actions は OIDC で長期キーをなくせた。同じことが Claude Code on the web でもできないか調べたが、クラウドセッションのコンテナには、AWS に対して自分の身元を証明する手段(OIDC トークンなど)がない。
代替案もいくつか検討して、全部捨てた。
- 一時認証情報を人が都度入れる:12 時間程度で切れる。毎回入れ直す運用は回らない。
- IAM Roles Anywhere:証明書の秘密鍵が長期の秘密になるだけで、実質的に何も変わらない。
- GitHub Actions を中継役にして、OIDC で AWS を読ませる:リポジトリが public なので、ワークフローのログや成果物から閲覧結果が誰でも見えてしまう。
無理なものは無理だろう、と割とすぐ諦めた。あるなら最初からそれでいけている。長期キーは 1 本だけ残す。妥協だが、それ以外の鍵の掃除はできたので良しとした。そのかわり、将来 OIDC が使えるようになったら、信頼ポリシーの差し替えだけで移れる構造にしておくことを条件にした。
最初の案が、気持ち悪かった
最初に Claude が出してきた案はこうだった。
環境変数(長期キー): prod の閲覧専用ユーザー
├─ そのまま使う → prod を閲覧
└─ AssumeRole する → dev アカウントのロール → dev を操作
キーは本番の既存ユーザーのまま。本番はそのユーザーの権限で直接見て、dev だけロールを経由する。技術的には何の問題もないし、変更も小さい。
でも、これが気持ち悪かった。本番と dev は、基本的に鏡にしておきたい。せっかく環境を分けたのに、「こっちは dev の資材、こっちは prod の資材」とコード上で作りが分かれるのは美しくない。
もちろん、環境をまたぐものは例外だ。本番のデータを dev にコピーする同期処理などは、そもそも両方にまたがるのが役割なので、非対称で当然。でも Claude のアクセス権限は違う。基本の資材はミラーリングしているのに、ロールだけ非対称になるのは気持ち悪すぎる。
鏡にする:身元は 1 つ、ロールは両アカウントで同じ
分けたのは「キーを持つ身元」と「権限を持つロール」だ。
キー保持ユーザー(prod)── AssumeRole ──┬→ prod: claude ロール = 共通の閲覧ポリシー
└→ dev : claude ロール = 共通の閲覧ポリシー + dev 用の操作ポリシー
- キー保持ユーザーの権限は、両アカウントの
claudeロールへのsts:AssumeRoleだけ。自分自身では何も見られない。 claudeロールは、両アカウントで同じスタック・同じ名前・同じ信頼関係・同じ閲覧ポリシーにする。- dev と本番の違いは、dev にだけ付く追加ポリシー 1 枚に集約する。
こうすると、非対称は残るが、構造は対称になる。CDK で言えば、同じスタックに環境ごとの props を渡しているだけだ。コードを読めば、差分がどこにあるかが一目で分かる。
Claude 側の設定も対称になる。AWS CLI の設定ファイルにプロファイルを 2 つ書くだけだ。
[profile prod]
role_arn = arn:aws:iam::<prod のアカウント ID>:role/claude
source_profile = claude-key
region = us-east-1
[profile dev]
role_arn = arn:aws:iam::<dev のアカウント ID>:role/claude
source_profile = claude-key
region = us-east-1
既定のプロファイルは作らない。プロファイルを指定しないとキー保持ユーザーの権限になり、何も見えない。毎回どちらのアカウントかを明示させることで、うっかり本番を触る事故を防ぐ。
キーは、どっちの持ち物か
鏡にしても、キー保持ユーザーだけはどちらかのアカウントに置くしかない。ここで少し迷った。
本番の持ち物だろうとは思っていた。でも、主に使うのは dev だ。「本番が主だから prod」とも「開発が主だから dev」とも言える。どっちでも説明はつきそうな感覚だった。
決め手は、使う頻度ではなくそのユーザーを操作できる主体は、どこまで届くかだった。キー保持ユーザーは、本番のロールを引き受けられる身元だ。これを dev に置くと、dev アカウントで IAM を操作できる主体なら誰でも、そのユーザーに新しいキーを発行して本番を覗けてしまう。
しかも、その経路は実在した。dev のデプロイに使う GitHub Actions の OIDC ロールは、デプロイの都合で iam:CreateAccessKey を含む権限を持っている。そして dev 用のロールは、プルリクエストのワークフローからも引き受けられる。dev に置けば、PR のワークフローからキーを発行して本番を見る道ができる。
本番に置けば、そのユーザーを操作できるのは本番のデプロイロールと管理者だけで、どちらも元から本番を好きにできる。新しく得る権限はない。そこから dev に届くのは「信頼度の高い側から低い側へ」なので問題にならない。
まあ、そりゃそうか、という結論だった。
実は途中で、Claude は逆向きの案も出していた。「うっかり本番を触る事故を減らしたいなら、dev を既定にして、キーを dev 側に置き、本番はロールで見る」という案だ。事故防止としては筋が通って見える。だが上の理由で、身元を低い側に置くのはまずい。この案は Claude 自身が取り下げ、事故防止は「既定のプロファイルを作らない」ことで担保した。
dev に何を許すか:実行はいい、中身を変えるのはダメ
dev の追加ポリシーには、Lambda の実行(lambda:InvokeFunction)を入れる。一方で、Lambda のコードや設定の更新(lambda:UpdateFunctionCode / UpdateFunctionConfiguration)は入れない。線引きは「実行は可、何が実行されるかの変更は不可」にした。
理由は、dev から本番に届く経路が 1 本だけあるからだ。本番のデータを dev にコピーする同期用の Lambda は、本番のテーブルを読むロールを引き受けられる。もし Claude が Lambda のコードを書き換えられたら、この Lambda に任意のコードを載せて、本番のテーブルを直接読めてしまう。個人情報の読み取りを明示的に拒否している閲覧ポリシーも、この経路では効かない。
この経路は意識していた。ただ、同期用の Lambda はコピーしかさせないものだし、権限で絞れば問題ないだろうと思っていた。実行だけなら、動くのは CI がデプロイしたコードだけで、権限は広がらない。この線引きにしておけば、今後 dev の権限を広げるときも同じ基準で判断できる。
実行を許す関数は、手で回す場面がある定期実行のバッチ 7 本に絞った。そのうち 3 本は OpenAI の API を叩くので、回すたびに費用がかかる。それでも入れた。改修の中心になるバッチだし、Claude が不審な動きをすることはないだろうと思っている。回すのは、結局こちらが依頼したときだけだ。
その後
切り替えてから、開発がスムーズになった。Claude から「反映したので、こちらで回します」という返事が来ることが増えた。1 時間待つことも、外出先で小さい画面と格闘することもなくなった。
本番は、相変わらず見るだけだ。
まとめ
- アカウントを分けると、AI に渡していた「1 つの閲覧権限」の前提が崩れる。1 アカウントでは問題にならなかったことが問題になる。
- 環境ごとに権限の差をつけたいとき、非対称を構造に持ち込まず、同じ構造に環境ごとの差分を 1 枚だけ足す形にすると、コードが鏡のまま保てる。
- 長期キーを持つ身元は、利用頻度ではなく、届く範囲のうち最も信頼度の高いアカウントに置く。
- 開発環境で AI に許す操作は、「実行は可、何が実行されるかの変更は不可」で線を引くと、環境をまたぐ経路があっても権限が漏れにくい。
- 長期キーは残ったが、OIDC が使えるようになれば、信頼ポリシーを差し替えてキー保持ユーザーを消すだけで済むようにしてある。