AIによるAWS操作を安全に。Claude Code × Bedrockで作るsudo的なIAM権限昇格

AIによるAWS操作を安全に。Claude Code × Bedrockで作るsudo的なIAM権限昇格

Claude Code と Amazon Bedrock を組み合わせ、AWS 操作の権限分離を実現。最新モデル Sonnet 4.6 を使い、AWS_PROFILE の切り替えで「sudo 的」な ReadOnly / Elevated 運用を安全に行う仕組みを解説します。EC2 ロールの最小権限設計により、Docker 経由のリスクにも IAM レベルで備えます。
2026.02.24

AI エージェントが AWS を直接操作する場面が増えています。前回の記事では Kiro CLI のカスタムエージェント機能を使い、sudo 的な IAM 権限昇格の仕組みを紹介しました。

https://dev.classmethod.jp/articles/kiro-cli-custom-agent-sudo/

今回は同じ 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-sessionclaudecode-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つです。

  1. Claude Code のインストール(dnf install nodejsnpm install -g @anthropic-ai/claude-code
  2. Bedrock 環境変数の永続化(/etc/profile.d/ に配置し、ログインシェルで確実に読み込まれるようにする)
  3. ~/.aws/config の配置(3プロファイル定義)
  4. 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

参考

この記事をシェアする

FacebookHatena blogX

関連記事