1. はじめに
クラウド上でインフラリソースを構築する際、セキュリティとコンプライアンスの確保は最重要課題の一つです。特に、IaC(Infrastructure as Code)を用いてインフラを構築する場合、実装段階でベストプラクティスに準拠しているかを確認するのは容易ではありません。そのため、設定ミスによるセキュリティインシデントを防ぐには、デプロイ前の段階で潜在的な問題を検出し、適切に対処することが重要となります。
「cdk-nag」は、AWS CDKで実装されたインフラリソースのうち、安全でないインフラストラクチャを示唆する可能性のあるパターンを、静的な手法で探すツールです。これにより、AWSのセキュリティベストプラクティスに準拠していない設定を事前に検出できます。
本記事では、cdk-nagで検出されたセキュリティ違反を効率的に修正する方法として、「AWS CDK MCP Server」と「Claude Code」を組み合わせた自動化アプローチを紹介します。踏み台用EC2インスタンスを構築するサンプルコードを例に、セキュリティ違反の検出から、AIを活用した自動修正までの一連の流れを実践的に解説します。
⚠️ 注意事項
本記事の手順に従ってAWSリソースをデプロイすると、EC2インスタンス、CloudWatch Logsなどの料金が発生します。検証が終了したら、不要な料金の発生を防ぐため、必ず「3.7. 削除方法」の手順に従ってリソースを削除してください。「AwsSolutions-EC29」により終了保護を有効化した場合は、削除前に必ず「WorkServer.DisableTerminationProtectionCommand」に表示されるコマンドを実行してください。実行しないと削除に失敗します。
2. 活用する技術
2.1. AWS CDK (Cloud Development Kit)
プログラミング言語を使用してクラウドインフラストラクチャをコードとして定義できるオープンソースの開発フレームワークです。TypeScript、Python、Java、C#などの使い慣れた言語でAWSリソースを定義し、AWS CloudFormationを通じてデプロイできます。従来のYAMLやJSONベースの定義と比較して、型安全性、IDE支援、コードの再利用性などの利点があります。
参考: https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html
2.2 cdk-nag
AWS CDKのコードに対してセキュリティとコンプライアンスのベストプラクティスチェックを自動実行するオープンソースツールです。CDKのAspect機能を利用して、合成(synth)時にインフラストラクチャコードを静的に分析し、潜在的なセキュリティリスクやコンプライアンス違反を検出します。以下の特徴があります。
- 複数のコンプライアンス基準に対応
- 違反の重要度別分類(Error、Warning)
- 特定のリソースに対する抑制(Suppression)機能
- デプロイ前の早期問題検出による手戻りコストの削減
参考: https://github.com/cdklabs/cdk-nag
2.3. AWS CDK MCP Server
MCP(Model Context Protocol)に対応したサーバーで、AWS CDK やcdk-nag に関する専門的な情報をAIアシスタントに提供します。これにより、AIがCDKコードの構造やcdk-nag ルールの詳細を正確に理解し、適切な修正案を生成できるようになります。
参考: https://github.com/awslabs/mcp/tree/main/src/cdk-mcp-server
2.4. Claude Code
Anthropic 社が提供するAIアシスタント「Claude」をコマンドラインから利用できるツールです。ターミナル上で直接AIと対話しながら、コードの修正やファイル操作を実行できます。MCPサーバーと連携することで、専門的なドメイン知識を持つAIアシスタントとして機能します。
参考: https://docs.claude.com/ja/docs/claude-code/overview
2.5. カスタムスラッシュコマンド
Claude Code で独自のコマンドを定義し、複雑な作業フローを自動化する機能です。事前に定義したタスクの手順をAIに実行させることで、cdk-nag エラーの分析から修正まで一連の作業を「/fix-nag
」のような簡単なコマンドで実行できるようになります。
参考: https://docs.claude.com/ja/docs/claude-code/slash-commands
3. 前提条件
$ cdk --version 2.1022.0 (build b0e6bc0)
$ python --version Python 3.12.11
$ claude --version 1.0.128 (Claude Code)
$ uvx --version uv-tool-uvx 0.6.12
※ バージョン情報は2025年9月時点の筆者の環境であり参考値です。最新バージョンをご利用ください。
4. サンプルコード
4.1. ファイル構成
. │ app.py │ cdk.json └─ pyproject.toml
4.2. app.py
import aws_cdk as cdk from aws_cdk import CfnOutput, Stack from aws_cdk import aws_iam as iam from aws_cdk import aws_ec2 as ec2 from constructs import Construct class WorkServerStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # create vpc, subnet and route table vpc = ec2.Vpc( self, "Vpc", max_azs=1, ip_addresses=ec2.IpAddresses.cidr("172.16.0.0/24"), subnet_configuration=[ ec2.SubnetConfiguration( name="Public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=26, ), ], nat_gateways=0, # NAT Gateway is not created (cost saving) ) # create instance role bastion_role = iam.Role( self, "BastionRole", assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"), # type: ignore ) # https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/getting-started-create-iam-instance-profile.html bastion_role.add_to_policy( iam.PolicyStatement( actions=[ "ssm:UpdateInstanceInformation", "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel", ], resources=["*"], ) ) # create ec2 instance bastion_instance = ec2.Instance( self, "BastionInstance", vpc=vpc, instance_type=ec2.InstanceType.of( instance_class=ec2.InstanceClass.T3, instance_size=ec2.InstanceSize.MICRO, ), machine_image=ec2.MachineImage.latest_amazon_linux2023(), vpc_subnets=ec2.SubnetSelection( subnet_type=ec2.SubnetType.PUBLIC, ), role=bastion_role, # type: ignore associate_public_ip_address=True, require_imdsv2=True, ) CfnOutput( self, "StartSessionCommand", value=f"aws ssm start-session --target {bastion_instance.instance_id}", ) app = cdk.App() WorkServerStack(app, "WorkServer") app.synth()
4.3. cdk.json
{ "app": "uv run python app.py" }
4.4. pyproject.toml
[project] name = "worker-server" version = "1.0.0" description = "Building a working EC2 instance." readme = "README.md" requires-python = ">=3.12" dependencies = [ "aws-cdk-lib>=2.217.0", "cdk-nag>=2.37.37", "constructs>=10.4.2", ]
4.5. 環境構築
上記ファイル構成にファイルを配置後、以下のコマンドを実行し、必要なライブラリをインストールします。
$ uv sync
以下のような出力が表示されると成功です。
Using CPython 3.12.9 Creating virtual environment at: .venv Resolved 16 packages in 11ms Installed 15 packages in 63ms + attrs==25.3.0 + aws-cdk-asset-awscli-v1==2.2.242 + aws-cdk-asset-node-proxy-agent-v6==2.1.0 + aws-cdk-cloud-assembly-schema==48.11.0 + aws-cdk-lib==2.217.0 + cattrs==25.2.0 + cdk-nag==2.37.37 + constructs==10.4.2 + importlib-resources==6.5.2 + jsii==1.114.1 + publication==0.0.3 + python-dateutil==2.9.0.post0 + six==1.17.0 + typeguard==2.13.3 + typing-extensions==4.15.0
4.6. デプロイ方法
デプロイをする場合は、以下のコマンドを実行します。
$ cdk deploy
Do you wish to deploy these changes (y/n)?
と確認された場合、 y
と入力します。
デプロイに成功すると「✅ WorkServer」が表示されます。
以下のように「WorkServer.StartSessionCommand」に記載されているコマンドを実行すると、EC2へ接続できます。
$ aws ssm start-session --target i-xxx # インスタンスIDは任意のもの Starting session with SessionId: xxx sh-4.2$
4.7. 削除方法
以下のコマンドを実行します。
$ cdk destroy
Are you sure you want to delete: WorkServer (y/n)?
と確認された場合、y
と入力します。
削除に成功すると「✅ WorkServer: destroyed」が表示されます。
5. cdk-nag の適用
実際にcdk-nag を適用します。具体的には、cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True))
を追加するだけです。
import aws_cdk as cdk from aws_cdk import CfnOutput, Stack from aws_cdk import aws_iam as iam from aws_cdk import aws_ec2 as ec2 from cdk_nag import AwsSolutionsChecks # 追加 from constructs import Construct class WorkServerStack(Stack): ... # 省略 app = cdk.App() WorkServerStack(app, "WorkServer") cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True)) # 追加 app.synth()
この状態で、デプロイを実施します。
$ cdk deploy
すると、以下のようなエラーが発生し、デプロイをする前(synth段階) で停止します。
[Error at /WorkServer/Vpc/Resource] AwsSolutions-VPC7: The VPC does not have an associated Flow Log. VPC Flow Logs capture network flow information for a VPC, subnet, or network interface and stores it in Amazon CloudWatch Logs. Flow log data can help customers troubleshoot network issues; for example, to diagnose why specific traffic is not reaching an instance, which might be a result of overly restrictive security group rules. [Error at /WorkServer/BastionRole/DefaultPolicy/Resource] AwsSolutions-IAM5[Resource::*]: The IAM entity contains wildcard permissions and does not have a cdk-nag rule suppression with evidence for those permission. Metadata explaining the evidence (e.g. via supporting links) for wildcard permissions allows for transparency to operators. This is a granular rule that returns individual findings that can be suppressed with 'appliesTo'. The findings are in the format 'Action::<action>' for policy actions and 'Resource::<resource>' for resources. Example: appliesTo: ['Action::s3:*']. [Error at /WorkServer/BastionInstance/Resource] AwsSolutions-EC26: The resource creates one or more EBS volumes that have encryption disabled. With EBS encryption, you aren't required to build, maintain, and secure your own key management infrastructure. EBS encryption uses KMS keys when creating encrypted volumes and snapshots. This helps protect data at rest. [Error at /WorkServer/BastionInstance/Resource] AwsSolutions-EC28: The EC2 instance/AutoScaling launch configuration does not have detailed monitoring enabled. Monitoring data helps make better decisions on architecting and managing compute resources. [Error at /WorkServer/BastionInstance/Resource] AwsSolutions-EC29: The EC2 instance is not part of an ASG and has Termination Protection disabled. Termination Protection safety feature enabled in order to protect the instances from being accidentally terminated. Found errors
今回発生したエラーの内容は、以下の通りです。
No. | ルール名 | レベル | リソース | 問題の内容 | 対応が必要な理由 |
---|---|---|---|---|---|
1 | AwsSolutions-VPC7 | Error | Vpc | VPCにFlow Logが関連付けられていない | ネットワークフロー情報をキャプチャしてトラブルシューティングに活用するため。特定のトラフィックがインスタンスに到達しない理由の診断などに必要 |
2 | AwsSolutions-IAM5 | Error | InstanceRole(Resource::*) | IAMエンティティにワイルドカード権限が含まれており、証跡の抑制ルールがない | ワイルドカード権限の透明性を確保し、オペレーターに根拠を提供するため |
3 | AwsSolutions-EC26 | Error | Instance | EBSボリュームで暗号化が無効になっている | 保存時データの暗号化によりセキュリティを強化し、KMSキーを使用することで独自のキー管理インフラの構築・維持が不要になるため |
4 | AwsSolutions-EC28 | Error | Instance | EC2インスタンスで詳細モニタリングが有効化されていない | コンピュートリソースのアーキテクチャと管理についてより良い意思決定を行うため |
5 | AwsSolutions-EC29 | Error | Instance | EC2インスタンスがASGの一部でなく、終了保護が無効 | インスタンスが誤って終了されることを防ぐ安全機能として必要 |
6. cdk-nag エラーの修正
前章では、cdk-nag を適用したことで、デプロイ前に5つのセキュリティ上の問題点が検出されました。これらの問題を一つずつ手作業で修正することも可能ですが、本記事ではAIを活用してこの修正プロセスを効率化、自動化するアプローチを紹介します。
この自動修正を実現するために、AIアシスタントである「Claude Code」と、それに専門知識を提供する「AWS CDK MCP Server」を連携させます。
- AWS CDK MCP Server: cdk-nagルールの詳細情報やCDKのベストプラクティスをAIに提供
- Claude Code: AIと対話しながらコード修正を実行
- カスタムスラッシュコマンド: 修正フローを定型化し、再現性のある運用を実現
ℹ️ なぜMCPサーバーが必要なのか?
汎用的なAIは、一般的なプログラミングの知識は豊富ですが、cdk-nag の特定のルール(例: AwsSolutions-VPC7)が何を意味し、AWS CDK のコード上で具体的にどう修正すればよいか、といった専門的なドメイン知識は必ずしも持っていません。AWS CDK MCP Server は、AWS CDK やcdk-nag に関する専門的な情報をAI に提供します。これにより、AI が専門的なドメイン知識ををもって実装/提案を行うことができるようになります。
6.1. 事前準備
MCPサーバーを使えるようにするため、以下のコマンドを実行します。
$ claude mcp add cdk-mcp-server uvx awslabs.cdk-mcp-server@latest --env FASTMCP_LOG_LEVEL=ERROR
以下のコマンドを実行し、正常に設定されたか確認します。
$ claude mcp list
「cdk-mcp-server: uvx awslabs.cdk-mcp-server@latest – ✓ Connected」が表示されたら、成功です。
カスタムスラッシュコマンドを使えるようにするため、以下のファイルを、「.claude/commands/fix-nag.md」へ配置します。
--- allowed-tools: mcp__cdk-mcp-server__ExplainCDKNagRule, Bash(cdk synth), Read, Edit, Glob, Grep, MultiEdit description: CDK Nagアラートを解消するためのカスタムスラッシュコマンド argument-hint: [rule-id] [comment] (例: AwsSolutions-APIG2 "セキュリティ上の理由で抑制") --- ## コンテキスト CDK Nagアラートを解消するために以下の手順を実行します: 1. **ルール分析**: CDK MCPを使用してルールの詳細を把握 2. **コード修正**: CDKコードを修正 3. **cdk-nag確認**: cdk synthを実行して結果確認 ## 引数 - `$ARGUMENTS`: 修正対象のCDK Nagルール (例: AwsSolutions-APIG2) - オプション: コメント(抑制理由など)を指定可能 ## タスク実行 ### 1. ルール分析 引数で指定されたCDK Nagルール(`$ARGUMENTS`)について、CDK MCPを使用して詳細な情報を取得します。 ### 2. 現在のアラート状況確認 - cdk synth ### 3. CDKコード修正 CDKコードを特定し、ルールに準拠するよう修正します。 コメントが指定されている場合は、NagSuppression抑制を適用し、コメントを理由として記録します。 ### 4. テスト実行と結果確認 修正後、`cdk synth`を実行してCDK Nagスキャンの結果を確認します。 --- **使用例**: - `/fix-nag AwsSolutions-EC29` (修正対応) - `/fix-nag AwsSolutions-IAM5 SSM Agent が使用する ssmmessages/ec2messages/UpdateInstanceInformation はリソースレベル権限をサポートせず、Resource は '*' に限定されるため。` (抑制対応)
6.2. 修正
「AwsSolutions-EC29」をClaude Code を用いて修正します。
以下のコマンドを実行します。
$ claude -p "/fix-nag AwsSolutions-EC29"
しばらくすると、以下のような出力がされます。
✅ **AwsSolutions-EC29 アラート解消完了** **【内容】** EC2インスタンスがAutoScaling Groupに属しておらず、Termination Protection(終了保護)が無効になっていることによるセキュリティアラート **【修正内容】** EC2インスタンスのTermination Protection(終了保護)を有効化しました: - `cfn_instance.disable_api_termination = True`を追加 - app.py:80-82行目に修正を適用 - AwsSolutions-EC29ルールへの準拠を明示したコメントを追加
実際にソースを確認すると、以下のようなコードが追加されていました。
# Enable termination protection to comply with AwsSolutions-EC29 cfn_instance = host.instance.node.default_child cfn_instance.disable_api_termination = True
Claude Code のログを確認したところ、MCPを介してcdk-nag の情報を取得していました。
なお、AIはL1コンストラクトを直接修正しましたが、ec2.Instance
は「disable_api_termination」パラメータが存在するため、エスケープハッチをせず設定ができます。
# create ec2 instance bastion_instance = ec2.Instance( self, "BastionInstance", vpc=vpc, # ... (中略) ... disable_api_termination=True, # こちらの方がよりシンプル )
6.3. 明示的に抑制
「AwsSolutions-IAM5」について、この「*」は問題ないものとし、抑制を依頼します。
以下のコマンドを実行します。
$ claude -p "/fix-nag AwsSolutions-IAM5 SSM Agent が使用する ssmmessages/ec2messages/UpdateInstanceInformation はリソースレベル権限をサポートせず、Resource は '*' に限定されるため。"
しばらくすると、以下のような出力がされます。
## ✅ AwsSolutions-IAM5 アラート解消完了 **【内容】** IAMエンティティにワイルドカード権限が含まれており、CDK Nagルール抑制が証拠と共に提供されていないため警告が発生していました。 **【修正内容】** SSM Agentが使用する特定のアクション(ssmmessages/ec2messages/UpdateInstanceInformation)がリソースレベル権限をサポートしないため、`NagSuppressions.add_resource_suppressions_by_path`を使用してDefaultPolicyリソースに対する抑制を適用しました。抑制理由として「SSM Agent が使用する ssmmessages/ec2messages/UpdateInstanceInformation はリソースレベル権限をサポートせず、Resource は '*' に限定されるため。」を記録し、`appliesTo: ["Resource::*"]`で特定のワイルドカードリソースに対してのみ抑制を適用しています。
実際にソースを確認したところ、以下のようなコードが追加されていました。
# Suppress AwsSolutions-IAM5 for SSM Agent permissions that don't support resource-level permissions NagSuppressions.add_resource_suppressions_by_path( self, "/WorkServer/BastionRole/DefaultPolicy/Resource", [ { "id": "AwsSolutions-IAM5", "reason": "SSM Agent が使用する ssmmessages/ec2messages/UpdateInstanceInformation はリソースレベル権限をサポートせず、Resource は '*' に限定されるため。", "appliesTo": ["Resource::*"] } ] )
なお、パスによる指定はパスを把握する必要があります。以下のようにNagSuppressions.add_resource_suppressions
を利用すると、そのような手間を省くことができます。
NagSuppressions.add_resource_suppressions( bastion_role.node.find_child("DefaultPolicy"), [ { "id": "AwsSolutions-IAM5", "reason": ( "SSM Agent が使用する ssmmessages/ec2messages/UpdateInstanceInformation は " "リソースレベル権限をサポートせず、Resource は '*' に限定されるため。" ), "appliesTo": [ "Resource::*", ], } ], )
6.4. 修正後
上記の対応を繰り返し、修正が完了したものは以下になります。
(一部手動修正を含む)
import aws_cdk as cdk from aws_cdk import CfnOutput, Stack from aws_cdk import aws_iam as iam from aws_cdk import aws_ec2 as ec2 from aws_cdk import aws_logs as logs from cdk_nag import AwsSolutionsChecks, NagSuppressions from constructs import Construct class WorkServerStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # create vpc, subnet and route table vpc = ec2.Vpc( self, "Vpc", max_azs=1, ip_addresses=ec2.IpAddresses.cidr("172.16.0.0/24"), subnet_configuration=[ ec2.SubnetConfiguration( name="Public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=26, ), ], nat_gateways=0, # NAT Gateway is not created (cost saving) ) # Add VPC Flow Logs to comply with AwsSolutions-VPC7 flow_log_group = logs.LogGroup( self, "VpcFlowLogGroup", retention=logs.RetentionDays.ONE_WEEK, removal_policy=cdk.RemovalPolicy.DESTROY, ) ec2.FlowLog( self, "VpcFlowLog", resource_type=ec2.FlowLogResourceType.from_vpc(vpc), destination=ec2.FlowLogDestination.to_cloud_watch_logs(flow_log_group), ) # create instance role bastion_role = iam.Role( self, "BastionRole", assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"), # type: ignore ) # https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/getting-started-create-iam-instance-profile.html bastion_role.add_to_policy( iam.PolicyStatement( actions=[ "ssm:UpdateInstanceInformation", "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel", ], resources=["*"], ) ) # suppress AwsSolutions-IAM5 for BastionHost IAM policy necessary for SSM Session Manager functionality NagSuppressions.add_resource_suppressions( bastion_role.node.find_child("DefaultPolicy"), [ { "id": "AwsSolutions-IAM5", "reason": ( "SSM Agent が使用する ssmmessages/ec2messages/UpdateInstanceInformation は " "リソースレベル権限をサポートせず、Resource は '*' に限定されるため。" ), "appliesTo": [ "Resource::*", ], } ], ) # create ec2 instance bastion_instance = ec2.Instance( self, "BastionInstance", vpc=vpc, instance_type=ec2.InstanceType.of( instance_class=ec2.InstanceClass.T3, instance_size=ec2.InstanceSize.MICRO, ), machine_image=ec2.MachineImage.latest_amazon_linux2023(), vpc_subnets=ec2.SubnetSelection( subnet_type=ec2.SubnetType.PUBLIC, ), role=bastion_role, # type: ignore block_devices=[ ec2.BlockDevice( device_name="/dev/xvda", volume=ec2.BlockDeviceVolume.ebs( volume_size=8, volume_type=ec2.EbsDeviceVolumeType.GP3, delete_on_termination=True, encrypted=True, ), ) ], associate_public_ip_address=True, require_imdsv2=True, # Enable detailed monitoring to comply with AwsSolutions-EC28 detailed_monitoring=True, # Enable termination protection to comply with AwsSolutions-EC29 disable_api_termination=True, ) CfnOutput( self, "StartSessionCommand", value=f"aws ssm start-session --target {bastion_instance.instance_id}", ) CfnOutput( self, "DisableTerminationProtectionCommand", value=f"aws ec2 modify-instance-attribute --instance-id {bastion_instance.instance_id} --no-disable-api-termination", ) app = cdk.App() WorkServerStack(app, "WorkServer") cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True)) app.synth()
上記修正後に、$ cdk deploy
を実行すると、cdk-nag のエラーなくデプロイに成功します。
一方で、$ cdk destroy
を実行すると、以下のエラーにより失敗します。
WorkServer: destroy failed ToolkitError: The stack named WorkServer is in a failed state. You may need to delete it from the AWS console
これは「AwsSolutions-EC29」により、終了保護=インスタンスを削除しようとするとエラーとなる設定を入れたためです。
削除をする場合は「WorkServer.DisableTerminationProtectionCommand」に記載されているコマンドを実行したうえで、$ cdk destroy
を実行します。
aws ec2 modify-instance-attribute --instance-id i-xxx --no-disable-api-termination # インスタンスIDは任意のもの
7. まとめ
- AWS CDK にて実装されたインフラリソースに対して、セキュリティ/コンプライアンスが守れているか静的にチェックする方法として、cdk-nag を紹介しました
- cdk-nag を採用すると、
cdk deploy
実行時の synth 段階で静的解析が走り、問題のある設定をデプロイ前に検知できます - 問題を検知した際の対応を自動化するための手法として、「AWS CDK MCP Server」、「Claude Code カスタムスラッシュコマンド」を紹介しました
- AWS CDK MCP Server により、AI が cdk-nag ルールや CDK の文脈を把握しやすくなり、修正案の提示や作業支援が可能になります
- Claude Code カスタムスラッシュコマンドにより、修正フローをコマンド化(定型化)することができるため、再現性のある運用がしやすくなります
- 最終判断とレビューは人手で行ってください。例えば、「6.2. 修正」のケースは、より簡潔にL2 コンストラクトのパラメータ変更で対応できます