はじめに
私は株式会社GENEROSITYのSREです。約150アカウントをAWS Organizations + Control Towerで運用しています。
先日、あるプロダクトで Amazon Bedrock の Claude Haiku 4.5 を使いたいという依頼を受けたのですが、Control Tower のリージョン拒否(SCP)が正面から立ちはだかり、想像以上に奥行きのある対応になりました。この記事は、そのときの 検証 → 失敗 → 最終案 の記録です。
結論から書きます。
- リージョン拒否には2種類ある。LZ全体版(
AWS-GR_REGION_DENY)は例外を作れず、OU単位で無効化もできない。- 解は、OU単位版(
CT.MULTISERVICE.PV.1)へ切り替え、対象OUだけExemptedActionsにbedrock:InvokeModelを指定すること。- 大阪を管理対象リージョンに追加しないので、コスト増はゼロ。
- 切替は 新しい仕組みを先に全OUへ用意してから、最後に旧版を外す ことで、統制を緩めず無停止で行える。
同じ壁に当たった方の地図になれば幸いです。基本的な設定手順は、私が救われた下記記事が詳しいので、本記事は「やってみて分かったこと」に重心を置きます。
なお、固有のアカウントID・OU ID・SCP ID・プロダクト名は伏せ、構成を一般化しています。
1. 事象:なぜ弾かれるのか
Claude Haiku 4.5 には前提があります。
- オンデマンドの単一モデル呼び出しができない。必ず推論プロファイル経由で呼ぶ。
- 国内向けの
jp.推論プロファイルは、内部で 東京(ap-northeast-1)+大阪(ap-northeast-3) にルーティングされる。
私たちの環境は Control Tower のリージョン拒否で 東京とバージニア北部以外を全面拒否していました。したがって推論プロファイルを叩くと、内部で大阪へ流れた瞬間に SCP で弾かれます。
bedrock:InvokeModel ... with an explicit deny in a service control policy
2. 課題:なぜ素直に例外を作れないのか
「対象OUだけ大阪の Bedrock を通す」を素直にやろうとすると、SCP の原則に阻まれます。
- SCPのDenyはAllowで上書きできない … 別のSCPで「大阪を許可」と足しても、既存のDenyは消えない。
- CT管理のSCPは手編集できない … 直接書き換えても、LZ更新で元に戻る(ドリフトの原因)。
つまり LZ全体版のリージョン拒否が生きている限り、特定OUだけ穴を開ける方法がない。ここが出発点でした。
守りたかった条件は次の4つです。
- リージョン統制は維持する(大阪を全社に開放しない=最小権限)
- コスト増を避ける
- 既存の稼働アカウントへ影響を出さない
- ダウンタイムを出さない
3. 検証 → 失敗 → 最終案
失敗①:専用OUの「SCPだけ」を差し替える
最初の発想は「対象OU用の専用OUを作り、そこに当たっているリージョン拒否SCPを、大阪の Bedrock だけ許可する内容に差し替える」でした。しかし前章のとおり、そのSCPは CT管理で手編集不可、別SCPを足しても Denyは上書き不可。この方向は成立しません。
失敗②:専用OUの「コントロールだけ」を無効化する
ならば大元のコントロールをOUから外せないか。list-enabled-controls はOUごとに返り、SCP実体IDもOUで別々。「OU単位で管理されている=OU単位で外せる」と考え、実際に無効化コマンドを叩きました。とはいえ、対象は当時まだアカウントの入っていない空のOUで、しかも結果は次のとおり——AWSに拒否され、状態は何も変わっていません(リージョン拒否はそのまま有効なまま)。
ValidationException: AWS-GR_REGION_DENY control can only be disabled
through the landing zone settings.
LZ全体版のリージョン拒否は、OU単位で無効化できない(ON/OFFのマスタースイッチはLZ側)。見た目の per-OU 表示と、無効化の可否は別物でした。この方向も成立しません。
一時候補(不採用):全社開放
代替として「Control Tower の管理対象リージョンに大阪を追加する(=全OU・全アカウントで大阪を許可)」も検討しました。操作は単純ですが、大阪が全社で開いて最小権限が崩れ、大阪への baseline 展開で コスト増も生じます。条件1・2に反するため、最終的に不採用としました。
最終案:OU単位版へ切り替え、対象OUだけ例外化
行き着いたのが、リージョン拒否そのものを OU単位版(CT.MULTISERVICE.PV.1) に置き換える案です。
- 全OUに
CT.MULTISERVICE.PV.1を適用(許可=東京+バージニア北部)。 -
Bedrockを使うOUだけ
ExemptedActionsにbedrock:InvokeModelを追加。 - LZ全体版のリージョン拒否は無効化する。
- 大阪は管理対象リージョンに追加しない(baseline展開なし=コスト増ゼロ)。
生成されるSCPは Deny + NotAction の形です。ExemptedActions に入れた操作は NotAction(=除外リスト)に載るので、許可外リージョンでもその操作だけは Deny されずに通ります。
{
"Effect": "Deny",
"NotAction": [
"iam:*", "kms:*", "aws-marketplace:*", "...(グローバルサービス群)...",
"bedrock:InvokeModel" // ← ExemptedActions で追加した1行
],
"Condition": {
"StringNotEquals": { "aws:RequestedRegion": ["ap-northeast-1", "us-east-1"] },
"ArnNotLike": { "aws:PrincipalArn": ["arn:aws:iam::*:role/AWSControlTowerExecution"] }
},
"Resource": "*"
}
4. 検証Orgでやってみて分かったこと
本番の統制をいきなり触るのは怖いので、本番と同じ構成を再現した使い捨ての検証用 Organizationsで、案を丸ごと通しで実証しました。ここで拾えた知見が、本番の設計をほぼ決めました。
-
OU単位版は成立する:
CT.MULTISERVICE.PV.1が有効化でき、AllowedRegions/ExemptedActions/ExemptedPrincipalArnsが使える。対象OUにExemptedActions=bedrock:InvokeModelを指定すると、生成SCPのNotActionにbedrock:InvokeModelが入る。 -
有効化は「新規SCP作成」ではなく「既存SCPのUpdate」:CloudTrail上は
UpdatePolicyで、SCPの枚数は増えない。SCPには1OUあたりの枚数・バイト数の上限があるので、これは地味に重要。 - LZ拒否を外したときのSCPの消え方がOUで違う:専用SCPとして配られているOUはSCPごと削除、他ガードレールと同居しているOUは該当の文だけ削除してSCPは存続。事前に把握できたおかげで、本番で慌てずに済んだ。
-
大阪の
bedrock:InvokeModelがSCPを通過することの確認方法:除外を効かせた状態で大阪のモデルを呼ぶと、SCPのDenyではなく Bedrock側のValidationExceptionが返る。これは「SCPを通り抜けてBedrockまで到達した」証拠なので、除外が効いていることを決定的に示せる。 -
疎通:
jp.の Haiku 4.5 で Converse 実行に成功。モデルアクセスも AVAILABLE。 - OU移動に伴う再登録は非破壊:1アカウントあたり約3.5分。Configレコーダーの再起動なし・CloudTrail不変・アプリ相当のマーカーも無傷で行えることを実測。
5. 本番:無停止で切り替える
本番は「先に代替(OU単位版)を全OUへ配り切ってから、最後に旧版(LZ全体版)を外す」という順序で実施しました。逆順(先にLZ拒否を消す)にすると、一瞬でも統制が緩む窓ができるためです。常に拒否が二重にかかった状態を保ったまま、主役だけ入れ替えるイメージです。
-
全OUに
CT.MULTISERVICE.PV.1を適用(対象OUのみExemptedActions=bedrock:InvokeModel)。子OUは継承のため対象外。 - 最終確認(全OUで新コントロール有効・対象OUのみ除外・SCP枚数が上限内かを確認し、問題なければ次へ)。
-
LZ全体版のリージョン拒否を無効化。LZ更新は実測約24分。管理対象リージョンは東京+バージニアのまま(=コスト増ゼロ)、drift は
IN_SYNC。 - 切替確認 → 対象アカウントを除外OUへ移動 → 移動先OUで再登録(各約3.5分・非破壊)。
影響チェック(実機で対比)
- 除外していないOUの既存アカウント:東京OK / 大阪のEC2拒否 / 大阪のBedrock拒否 / その他拒否(従来どおり)
- 移動した対象アカウント:東京OK / 大阪のEC2拒否 / 大阪のBedrock通過
「例外は対象OUだけ、それ以外は今までどおり」がきれいに対比で示せました。ダウンタイムゼロ、既存への影響ゼロ、コスト増ゼロで着地です。
なお本番の切替は深夜に実施し、作業開始から動作検証の完了まで約1時間40分(うちLZ更新の待ちが約30分)でした。
6. まとめ:できたこと
当初は「LZ全体版のリージョン拒否を残したまま、特定OUだけ穴を開ける」ことに阻まれました。しかし発想(=どのコントロールを使うか)を変えることで、当初の想定より良い設定に着地できました。
- リージョン拒否を OU単位版
CT.MULTISERVICE.PV.1に切り替え、対象OUだけExemptedActions=bedrock:InvokeModelで例外化。 - 統制維持・コスト増ゼロ・無停止で、対象アカウントだけ大阪の Bedrock を解放できた。
余談:この最終案に辿り着けたのは、AIだけの力ではなかった
正直に書くと、失敗①②のあと、私は一度「全社開放しかない」と結論づけ、実際に上司へその案(全社開放)で報告しました。AIと何度も検証を重ね、実際にエラーまで取ったことで、かえって「できない」という確信が強くなっていたのです。
その結論に再検討の余地をくれたのは、報告を受けた上司の「それ、本当にできないの?」という一言と、そこで共有してもらった一次情報(冒頭の記事)でした。AIは与えた前提の中では驚くほど緻密に検証してくれますが、「前提そのものが違うのでは」という問いは、人との対話が連れてきてくれる。AIを使い倒すときこそ、人に相談することの価値を実感した一件でした。
おわりに
リージョン統制を保ちつつ個別の要望に応える、という板挟みは、マルチアカウント運用では避けて通れないテーマだと思います。今回は「対象OUだけ・無停止・コスト増ゼロ」という納得のいく落とし所にたどり着けました。同じ課題に取り組む方の一助になれば幸いです。最後までお読みいただき、ありがとうございました!