
AIによるAWS操作を安全に。Claude Code × Bedrockで作るsudo的なIAM権限昇格
AI エージェントが AWS を直接操作する場面が増えています。前回の記事では Kiro CLI のカスタムエージェント機能を使い、sudo 的な IAM 権限昇格の仕組みを紹介しました。
今回は同じ IAM 三層分離のアーキテクチャを「Claude Code + Amazon Bedrock」で試してみました。
前提: Bedrock バックエンドの構成
本記事では Claude Code の LLM バックエンドとして「Amazon Bedrock」を利用する構成を採用しました。Bedrock 経由の場合、Claude Code は内部で bedrock:InvokeModel を呼ぶため、AWS_PROFILE で指定するロールに Bedrock 権限が必要です。そのため、ReadOnly と Elevated のロールに Bedrock 権限を付与しています(EC2 ロールには付与しません。理由はアーキテクチャの節で説明します)。
なお、Claude Max サブスクリプションや Anthropic API キーを利用する場合、LLM の呼び出しは Bedrock を経由しません。その場合は Bedrock 権限の付与は不要です。本記事のテンプレートをそのまま使うと、意図せず Bedrock の API 課金が発生する可能性があるため、ご自身の利用形態に合わせて構成を選択してください。
アーキテクチャ
IAM ロールを三層に分離し、AssumeRole チェーンで段階的に権限を昇格させます。
| ロール | 権限 | 用途 |
|---|---|---|
EC2 ロール (claudecode-ec2-*) |
SSM + AssumeRole(ReadOnly) | インスタンスの基盤 |
ReadOnly (claudecode-readonly-*) |
ReadOnlyAccess + Bedrock + AssumeRole(Elevated) | 参照操作 |
Elevated (claudecode-elevated-*) |
PowerUserAccess + Bedrock | 変更操作 |
EC2 ロールには Bedrock 権限を付与していません。プロファイル指定なしで claude を起動した場合はエラーになるため、最小権限の原則を守りつつ、誤操作のフェイルセーフとしても機能します。
この設計には副次的なセキュリティ効果もあります。Claude Code はコード実行時に Docker を利用することがあるため、ec2-user に Docker 実行権限(docker グループ)を付与しています。Docker グループへの所属はホスト OS の root 権限に等しい強い権限ですが、本構成では EC2 の IAM ロール自体に AWS リソースへのアクセス権限(AssumeRole と SSM 以外)を一切持たせていません。万が一、AI が生成したコードが Docker コンテナからホスト OS にエスケープしたとしても、AWS 環境に対する被害は IAM レベルでブロックされます。
前回の Kiro CLI 版との違いは、Bedrock 権限(bedrock:InvokeModel 等)を ReadOnly と Elevated のロールに付与する点です。Claude Code 自体が Bedrock を使って動作するため、どのプロファイルで起動しても Claude Code が使えるようにしています。
仕組み
AWS_PROFILE による権限切替
Claude Code は AWS_PROFILE 環境変数で使用する AWS プロファイルが決まります。プロファイルごとに異なる IAM ロールを紐づけることで、起動時に権限を選択できます。
# ReadOnly で起動
AWS_PROFILE=claudecode-readonly claude
# Elevated で起動
AWS_PROFILE=claudecode-elevated claude
Kiro CLI では /agent swap でセッション内のプロファイルを切り替えられましたが、Claude Code ではセッションの再起動が必要です。この違いについては後述します。
AWS CLI config
~/.aws/config にプロファイルを定義します。CloudFormation テンプレートの UserData で自動配置されるため、手動作成は不要です。
[profile claudecode-readonly]
role_arn = arn:aws:iam::<ACCOUNT_ID>:role/claudecode-readonly-<STACK_NAME>
credential_source = Ec2InstanceMetadata
role_session_name = claudecode-readonly-session
[profile claudecode-elevated]
role_arn = arn:aws:iam::<ACCOUNT_ID>:role/claudecode-elevated-<STACK_NAME>
source_profile = claudecode-readonly
role_session_name = claudecode-elevated-session
credential_source = Ec2InstanceMetadata により、EC2 インスタンスプロファイルの認証情報を起点に AssumeRole します。source_profile = claudecode-readonly により、elevated は readonly を経由してチェーンします。role_session_name を設定することで、CloudTrail のログに claudecode-readonly-session や claudecode-elevated-session と記録され、どのプロファイルで操作したかが一目で分かります。
Bedrock 権限の共有
Bedrock 権限は共有マネージドポリシーとして定義し、ReadOnly と Elevated のロールにアタッチしています。
BedrockPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub claudecode-bedrock-${AWS::StackName}
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- bedrock:InvokeModel
- bedrock:InvokeModelWithResponseStream
- bedrock:ListFoundationModels
- bedrock:ListInferenceProfiles
Resource: '*'
IAM ロールの循環参照回避
前回の Kiro CLI 版と同じく、信頼ポリシーで Principal: root + Condition: ArnEquals を使って循環参照を回避しています。
ReadOnlyRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub claudecode-readonly-${AWS::StackName}
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
Action: sts:AssumeRole
Condition:
ArnEquals:
aws:PrincipalArn: !Sub arn:aws:iam::${AWS::AccountId}:role/claudecode-ec2-${AWS::StackName}
UserData
UserData で行うのは以下の4つです。
- Claude Code のインストール(
dnf install nodejs→npm install -g @anthropic-ai/claude-code) - Bedrock 環境変数の永続化(
/etc/profile.d/に配置し、ログインシェルで確実に読み込まれるようにする) ~/.aws/configの配置(3プロファイル定義)- Docker のセットアップ(Claude Code がコード実行時にサンドボックスとして利用する場合があるため)
# Claude Code
dnf install -y unzip git jq nodejs docker
npm install -g @anthropic-ai/claude-code
# Bedrock 環境変数(/etc/profile.d/ でログインシェルに確実に反映)
printf '#!/bin/bash\nexport CLAUDE_CODE_USE_BEDROCK=1\nexport ANTHROPIC_MODEL=us.anthropic.claude-sonnet-4-6\nexport AWS_REGION=${AWS::Region}\n' > /etc/profile.d/claude-bedrock.sh
chmod 644 /etc/profile.d/claude-bedrock.sh
Kiro CLI 版ではエージェント JSON の配置が必要でしたが、Claude Code では不要です。AWS_PROFILE 環境変数だけで権限が決まります。
テンプレート全体は記事末尾の「参考情報」に掲載しています。
デプロイ
aws cloudformation create-stack \
--stack-name claudecode-profile-test \
--template-body file://cfn-claudecode.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--region us-west-2
スタック作成完了後、SSM で接続します。
aws ssm start-session --target <InstanceId> --region us-west-2
sudo su - ec2-user
UserData の実行完了を待ちます。SSM 接続可能な状態でもまだ処理中の可能性があるため、必ず以下のコマンドで完了を確認してください。
cloud-init status --wait
$ cloud-init status --wait
..............................................................
status: done
status: done と表示されたら、一度 exit してから再度 sudo su - ec2-user でログインし直してください。UserData で .bashrc に追加されたエイリアス(claude-ro / claude-el)や /etc/profile.d/ の環境変数がシェルに反映されます。
exit
sudo su - ec2-user
<details>
<summary>cloud-init が失敗した場合のトラブルシュート</summary>
# 正常終了の確認("Cloud-init ... finished" と所要時間が最終行に出ること)
sudo tail -1 /var/log/cloud-init-output.log
Cloud-init v. 22.2.2 finished at ... Up 130.36 seconds
# エラーがないことの確認
sudo grep -i error /var/log/cloud-init-output.log
</details>
検証: ReadOnly プロファイル
ReadOnly プロファイルで Claude Code を起動しました。
AWS_PROFILE=claudecode-readonly claude
参照操作(成功)
S3 バケット一覧の取得を依頼しました。
❯ aws s3 ls を実行して
● Bash(aws s3 ls)
⎿ 2024-11-12 07:00:43 athena-123456789012
2024-11-12 07:00:43 athena-123456789012-apne1
2024-11-09 03:27:04 athena-temp-ap-northeast-1-000000000000
… +35 lines
ReadOnly プロファイルで参照操作が正常に動作しました。
変更操作(拒否)
S3 バケットの作成を依頼しました。
❯ aws s3 mb s3://claudecode-test-0223-delete-me を実行して
● Bash(aws s3 mb s3://claudecode-test-0223-delete-me)
⎿ Error: Exit code 1
make_bucket failed: s3://claudecode-test-0223-delete-me An error occurred
(AccessDenied) when calling the CreateBucket operation: User:
arn:aws:sts::123456789012:assumed-role/claudecode-readonly-claudecode-profile-test/claudecode-readonly-session
is not authorized to perform: s3:CreateBucket on resource:
"arn:aws:s3:::claudecode-test-0223-delete-me" because no identity-based
policy allows the s3:CreateBucket action
ReadOnly プロファイルでは変更操作が IAM によって拒否されました。エラーメッセージに claudecode-readonly-session というセッション名が表示されており、role_session_name の設定が機能していることも確認できました。
検証: Elevated プロファイル
Claude Code を終了し、Elevated プロファイルで再起動しました。
AWS_PROFILE=claudecode-elevated claude
変更操作(成功)
同じ S3 バケット作成を依頼しました。
❯ aws s3 mb s3://claudecode-test-0223-delete-me を実行して
● Bash(aws s3 mb s3://claudecode-test-0223-delete-me)
⎿ make_bucket: claudecode-test-0223-delete-me
Elevated プロファイル(PowerUserAccess)では変更操作が成功しました。後片付けとしてバケットを削除しました。
❯ aws s3 rb s3://claudecode-test-0223-delete-me を実行して
● Bash(aws s3 rb s3://claudecode-test-0223-delete-me)
⎿ remove_bucket: claudecode-test-0223-delete-me
検証: エイリアスでの起動
UserData で設定されたエイリアスを使って Claude Code を起動し、Bedrock 経由で応答が返ることを確認しました。
$ claude-ro -p "say hello"
Hello! How can I help you today?
$ claude-el -p "say hi"
Hi! How can I help you today?
どちらのプロファイルでも Bedrock 経由(Sonnet 4.6)で正常に応答が返りました。エイリアスにより AWS_PROFILE=claudecode-readonly claude と毎回入力する手間が省けます。
検証結果まとめ
| 操作 | ReadOnly | Elevated |
|---|---|---|
aws s3 ls(参照) |
✅ 成功 | ✅ 成功 |
aws s3 mb(変更) |
❌ AccessDenied | ✅ 成功 |
aws s3 rb(削除) |
— | ✅ 成功 |
claude-ro / claude-el 起動 |
✅ Bedrock 応答OK | ✅ Bedrock 応答OK |
AWS_PROFILE の切替だけで、Claude Code の AWS 操作権限を IAM レベルで制御できることが確認できました。
Kiro CLI との比較
前回の記事で紹介した Kiro CLI と比較します。
| 観点 | Kiro CLI | Claude Code |
|---|---|---|
| 権限切替 | /agent swap elevated |
AWS_PROFILE=... claude |
| セッション継続 | 維持される | 再起動が必要 |
| コンテキスト引継 | 自動(同一セッション) | --resume またはファイル経由 |
| 制御レイヤー | プロンプト + profile_name |
環境変数(OS レベル) |
| IAM 構造 | 三層(共通) | 三層 + Bedrock 権限 |
| エージェント定義 | JSON ファイル必要 | 不要 |
Kiro CLI は /agent swap でセッション内のプロファイルを切り替えられるため、コンテキストが途切れません。一方、Claude Code は環境変数でプロファイルが決まるため、プロンプトに依存しない強制力があります。
コンテキスト引継の工夫
Claude Code でプロファイルを切り替えるにはセッションの再起動が必要です。コンテキストの断絶を補う方法がいくつかあります。
「claude --resume」を使うと、直前のセッションの会話履歴を復元できます。プロファイルが変わっても、前のセッションで何を調査したかを引き継げます。
「CLAUDE.md」をプロジェクトルートに配置すると、Claude Code が毎回自動で読み込みます。作業方針や背景情報を書いておけば、セッションが切れても共通のコンテキストを維持できます。
実運用では以下のようなワークフローになります。
# 1. ReadOnly で調査・計画
claude-ro
# → 調査結果を plan.md に保存させる
# → /exit
# 2. Elevated で実行
claude-el --resume
# → plan.md を読んで実行
まとめ
Claude Code + Bedrock でも、IAM の三層分離による権限制御が正常に機能することを確認しました。CloudFormation テンプレートで環境を一発構築でき、AWS_PROFILE の切替だけで ReadOnly / Elevated を使い分けられます。
IAM の三層ロール構成(EC2 → ReadOnly → Elevated)は、AI による AWS 操作のリスク軽減に共通で使える汎用パターンです。前回の Kiro CLI 版と合わせて、自分のチームや利用ツールに合った運用を検討してみてください。
参考情報: CloudFormation テンプレート
検証に使用した CloudFormation テンプレートを掲載します。aws cloudformation create-stack でデプロイし、SSM で接続後すぐに検証を開始できます。クリーンアップは aws cloudformation delete-stack のみです。
cfn-claudecode.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Claude Code + Bedrock with IAM profile switching (ReadOnly / Elevated)
Parameters:
Subnet:
Type: AWS::EC2::Subnet::Id
SecurityGroup:
Type: AWS::EC2::SecurityGroup::Id
InstanceType:
Type: String
Default: t4g.small
VolumeSize:
Type: Number
Default: 10
AmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64
Resources:
BedrockPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub claudecode-bedrock-${AWS::StackName}
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- bedrock:InvokeModel
- bedrock:InvokeModelWithResponseStream
- bedrock:ListFoundationModels
- bedrock:ListInferenceProfiles
Resource: '*'
ReadOnlyRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub claudecode-readonly-${AWS::StackName}
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
Action: sts:AssumeRole
Condition:
ArnEquals:
aws:PrincipalArn: !Sub arn:aws:iam::${AWS::AccountId}:role/claudecode-ec2-${AWS::StackName}
ManagedPolicyArns:
- arn:aws:iam::aws:policy/ReadOnlyAccess
- !Ref BedrockPolicy
Policies:
- PolicyName: AllowAssumeElevated
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/claudecode-elevated-${AWS::StackName}
ElevatedRole:
Type: AWS::IAM::Role
DependsOn: ReadOnlyRole
Properties:
RoleName: !Sub claudecode-elevated-${AWS::StackName}
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
Action: sts:AssumeRole
Condition:
ArnEquals:
aws:PrincipalArn: !Sub arn:aws:iam::${AWS::AccountId}:role/claudecode-readonly-${AWS::StackName}
ManagedPolicyArns:
- arn:aws:iam::aws:policy/PowerUserAccess
- !Ref BedrockPolicy
Ec2Role:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub claudecode-ec2-${AWS::StackName}
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Policies:
- PolicyName: AllowAssumeReadOnly
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/claudecode-readonly-${AWS::StackName}
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub claudecode-ec2-${AWS::StackName}
Roles:
- !Ref Ec2Role
Ec2Instance:
Type: AWS::EC2::Instance
DependsOn: InstanceProfile
Properties:
InstanceType: !Ref InstanceType
ImageId: !Ref AmiId
SubnetId: !Ref Subnet
SecurityGroupIds:
- !Ref SecurityGroup
IamInstanceProfile: !Ref InstanceProfile
MetadataOptions:
HttpTokens: required
HttpEndpoint: enabled
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeSize: !Ref VolumeSize
VolumeType: gp3
Tags:
- Key: Name
Value: !Sub claudecode-${AWS::StackName}
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -eux
dnf install -y unzip git jq nodejs docker
# Claude Code
npm install -g @anthropic-ai/claude-code
# Docker
usermod -aG docker ec2-user
systemctl enable docker
systemctl start docker
# swap 512MB
dd if=/dev/zero of=/swapfile bs=1M count=512
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile swap swap defaults 0 0' >> /etc/fstab
# Bedrock 環境変数
printf '#!/bin/bash\nexport CLAUDE_CODE_USE_BEDROCK=1\nexport ANTHROPIC_MODEL=us.anthropic.claude-sonnet-4-6\nexport AWS_REGION=${AWS::Region}\n' > /etc/profile.d/claude-bedrock.sh
chmod 644 /etc/profile.d/claude-bedrock.sh
# AWS CLI config
mkdir -p /home/ec2-user/.aws
printf '[default]\nregion = ${AWS::Region}\n\n[profile claudecode-readonly]\nrole_arn = ${ReadOnlyRole.Arn}\ncredential_source = Ec2InstanceMetadata\nrole_session_name = claudecode-readonly-session\nregion = ${AWS::Region}\n\n[profile claudecode-elevated]\nrole_arn = ${ElevatedRole.Arn}\nsource_profile = claudecode-readonly\nrole_session_name = claudecode-elevated-session\nregion = ${AWS::Region}\n' > /home/ec2-user/.aws/config
# エイリアス
printf '\nalias claude-ro="AWS_PROFILE=claudecode-readonly claude"\nalias claude-el="AWS_PROFILE=claudecode-elevated claude"\n' >> /home/ec2-user/.bashrc
chown -R ec2-user:ec2-user /home/ec2-user/.aws
Outputs:
InstanceId:
Value: !Ref Ec2Instance
ReadOnlyRoleArn:
Value: !GetAtt ReadOnlyRole.Arn
ElevatedRoleArn:
Value: !GetAtt ElevatedRole.Arn
SsmConnectCommand:
Value: !Sub aws ssm start-session --target ${Ec2Instance} --region ${AWS::Region}
デプロイ、クリーンアップ
デプロイコマンド:
aws cloudformation create-stack \
--stack-name claudecode-profile-test \
--template-body file://cfn-claudecode.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=Subnet,ParameterValue=<YOUR_SUBNET_ID> \
ParameterKey=SecurityGroup,ParameterValue=<YOUR_SG_ID> \
--region us-west-2
クリーンアップ:
aws cloudformation delete-stack --stack-name claudecode-profile-test --region us-west-2










