CDK Aspects + cdk-nag本番導入3ヶ月で正直に話す、自動セキュリティ検証の現実
「なぜCIで止めなかったの?」──その一言がきっかけでした。CDK Aspects + cdk-nagをマルチアカウント本番に導入して3ヶ月、誤検知との戦いや運用の実態を包み隠さず公開します。
先日、うちのチームのCDKスタックが本番でセキュリティ監査に引っかかった。S3バケットのSSL強制が抜けていて、しかも同じミスが3つのスタックで繰り返されていた。「なぜCIで止めなかったの?」と言われてぐうの音も出なかった。それを機にCDK Aspects + cdk-nagを本格導入したのが3ヶ月前。今日はその導入記録と、実際に運用して気づいたことを正直に書いていく。
CDK Aspectsって何者? 導入前に理解したかったこと
Aspectを一言でいうと、「CDKのConstruct treeを丸ごとトラバースして、任意の処理を注入できる仕組み」だ。OOPのAOP(Aspect Oriented Programming)に近いイメージで、スタック定義後にすべてのリソースを横断的に操作できる。
最初は「なんか難しそう」と思って敬遠していたんだけど、実装してみると拍子抜けするくらいシンプルだった。
import { IAspect, Aspects } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
// カスタムAspect: S3バケット全てにSSL強制を適用
class EnforceS3SSLAspect implements IAspect {
visit(node: IConstruct): void {
if (node instanceof s3.CfnBucket) {
const policy = new s3.BucketPolicy(node, 'SSLPolicy', {
bucket: s3.Bucket.fromBucketName(node, 'Ref', node.ref),
});
// 注意: CfnBucketに対して直接操作する場合はCfnBucketPolicyで
console.log(`[EnforceSSL] Visiting: ${node.node.path}`);
}
}
}
// スタックへの適用はたった1行
Aspects.of(app).add(new EnforceS3SSLAspect());
注意点として、visit()内でConstructを作ると無限ループになることがある。実際に最初のカスタムAspectで盛大にハマった。Aspectの中でAspectを追加するのは厳禁、リソースのプロパティ変更に留めるのが安全だと身をもって学んだ。
cdk-nag 2.x の実際の威力と誤検知との戦い
cdk-nagはApplied Composerチームが管理しているCDK用のセキュリティルールエンジン。2026年現在、バージョン2.34.xが安定版で、AWS Solutions Library Security Checklist / NIST / PCI DSS / HIPAAに対応したルールセットが揃っている。
npm install cdk-nag
導入自体は本当に簡単で、既存のCDKコードにほぼ手を加えなくていいのが地味に助かる。
import { App, Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag';
const app = new App();
const stack = new MyProductionStack(app, 'ProdStack');
// これだけでAWS Solutionsのルールセット全部有効化
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
app.synth();
実行すると、cdk synthのタイミングで違反が報告される。
[Error at /ProdStack/ApiGateway/Resource] AwsSolutions-APIG2:
The REST API does not have request validation enabled.
[Error at /ProdStack/RDSCluster/Resource] AwsSolutions-RDS6:
The RDS cluster does not have IAM Database Authentication enabled.
[Warning at /ProdStack/Lambda/Resource] AwsSolutions-L1:
The Lambda function is not configured to use the latest runtime version.
最初にうちのスタックに適用したら47個のエラーが出た。正直、目が点になった。「うちってこんなにセキュリティ穴だらけだったの?」と。
実際には47個のうち、真に修正が必要なものは約20個、残り27個は過検知または意図的な設計判断だった。ここが一番つらかった部分で、NagSuppressionsを使って誤検知を個別に抑制する作業が2週間かかった。
// 特定リソースへの抑制
NagSuppressions.addResourceSuppressions(
myLambdaFunction,
[
{
id: 'AwsSolutions-IAM4',
reason: 'AWSLambdaBasicExecutionRole は最小権限ポリシー。社内セキュリティレビュー済み #TICKET-1234',
},
],
true // 子Constructにも適用
);
// スタックレベルでの一括抑制(慎重に使う)
NagSuppressions.addStackSuppressions(stack, [
{
id: 'AwsSolutions-CFR4',
reason: '内部利用のみのS3オリジン。CloudFrontでHTTPSは別途強制済み',
},
]);
皆さん、Suppressionにはちゃんとreasonを書いてください。 3ヶ月後に自分で見返すと「なぜ抑制したのか」が全くわからなくなる。チケット番号や意思決定の根拠を残すのが最低限のマナーだと、痛い目を見て気づいた。
カスタムNagルールで会社独自の標準を強制する
cdk-nagの既成ルールだけでは会社固有の要件を満たせない場面が出てくる。うちの場合は「タグポリシー」と「特定リージョン以外へのデプロイ禁止」がそれだった。
import { NagRules, NagMessageLevel } from 'cdk-nag';
import { IConstruct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
// カスタムルール: 必須タグの強制
class CompanyTaggingRule implements NagRules {
readonly ruleId = 'Company-TAG001';
readonly info = '全リソースに CostCenter・Team・Environment タグが必要';
readonly explanation = '社内コスト配賦ルールに基づく要件';
readonly level = NagMessageLevel.ERROR;
check(node: IConstruct): string | undefined {
const requiredTags = ['CostCenter', 'Team', 'Environment'];
const tags = cdk.Tags.of(node);
// タグ情報はCfnリソースから取得
if (node instanceof cdk.CfnResource) {
const cfnTags = (node as any).tags?.renderTags() as
| { key: string; value: string }[]
| undefined;
const tagKeys = cfnTags?.map((t) => t.key) ?? [];
const missingTags = requiredTags.filter((t) => !tagKeys.includes(t));
if (missingTags.length > 0) {
return `必須タグが不足しています: ${missingTags.join(', ')}`;
}
}
return undefined; // 問題なし
}
}
ここは正直まだ試行錯誤中で、タグの取得方法がCDKバージョンによって微妙に変わる。2.180.x(2026年6月時点の最新安定版)ではCfnResourceのcfnOptions.metadata経由でタグを参照する方法が安定している。
本番CI/CDへの組み込みパターン
「ローカルで通れば良い」だと意味がない。CDK Aspects + Nagの本当の価値はCI/CDに組み込んでデプロイを自動で止めることにある。うちのCodePipeline V2との統合はこんな構成になっている。
graph TB
subgraph Developer["👨💻 開発者"]
PR["Pull Request"]
end
subgraph CICD["CodePipeline V2"]
direction TB
Source["Source Stage\n(CodeCommit/GitHub)"]
Build["Build Stage\n(CDK Synth + Nag)"]
Approve["Manual Approval\n(本番デプロイ前)"]
Deploy["Deploy Stage\n(CFn ChangeSet)"]
end
subgraph NagCheck["Nag検証ステップ"]
Synth["cdk synth"]
NagResult{"Nag Error = 0?"}
Report["SecurityHub\n検証レポート送信"]
end
subgraph AWSEnv["AWS環境"]
direction TB
subgraph Dev["開発アカウント"]
DevStack["Dev Stack"]
end
subgraph Staging["ステージングアカウント"]
StagingStack["Staging Stack"]
end
subgraph Prod["本番アカウント"]
ProdStack["Prod Stack"]
end
end
PR --> Source
Source --> Build
Build --> Synth
Synth --> NagResult
NagResult -- "YES" --> Report
NagResult -- "NO / エラー" --> BuildFail["ビルド失敗\nPR ブロック"]
Report --> Approve
Approve --> Deploy
Deploy --> DevStack
Deploy --> StagingStack
Deploy --> ProdStack
buildspec.ymlでの実装はこんな感じ。
# buildspec.yml
version: 0.2
phases:
install:
runtime-versions:
nodejs: 20
commands:
- npm ci
pre_build:
commands:
- echo "CDK Nag セキュリティ検証開始"
build:
commands:
# cdk synthは内部でAspectsを実行するのでNagも同時に動く
- npx cdk synth --all 2>&1 | tee /tmp/synth_output.txt
# エラーが1件でもあればビルド失敗
- |
NAG_ERRORS=$(grep -c "\[Error" /tmp/synth_output.txt || true)
echo "Nag エラー数: $NAG_ERRORS"
if [ "$NAG_ERRORS" -gt "0" ]; then
echo "❌ セキュリティ違反が検出されました。デプロイを中断します。"
cat /tmp/synth_output.txt | grep "\[Error"
exit 1
fi
echo "✅ セキュリティ検証通過"
# SecurityHubへの結果送信(オプション)
- node scripts/send-nag-report-to-securityhub.js /tmp/synth_output.txt
post_build:
commands:
- echo "CDK Nag 検証完了"
artifacts:
files:
- cdk.out/**/*
これを導入してから2ヶ月で、セキュリティ起因のPRレビュー指摘が約70%減った。数字が出るとチームの反応も変わってくる。最初は「また余計なツール入れやがって」みたいな雰囲気だったのが、今は「Nagが通ったから大丈夫でしょ」という信頼感が出てきた。この変化が一番うれしかった。
3ヶ月運用してわかった実際のメリット・限界・コスト
xychart-beta
title "CDK Nag導入前後のセキュリティレビュー指摘件数"
x-axis ["1月", "2月", "3月(導入)", "4月", "5月", "6月"]
y-axis "指摘件数" 0 --> 20
bar [15, 18, 12, 7, 5, 4]
line [15, 18, 12, 7, 5, 4]
よかったこと
1. 「あとで直す」が消えた
今まで「このS3バケット、パブリックブロック設定漏れてるけどとりあえずデプロイして後で直そう」が本当によくあった。Nagがブロックするので「後で」が物理的にできなくなった。強制力があるツールって本当に大事だと実感した。
2. コードレビューの議論が減った
セキュリティ設定の議論は「Nagルールに準拠しているか」という客観的な基準に変わった。「このタイムアウト設定は30秒がいいんじゃないか」みたいな主観的な議論が減って、本質的な設計レビューに集中できるようになった。
3. 新人オンボーディングが楽になった
「うちのチームのセキュリティ標準」を口頭で教える必要がなくなった。コードとして定義されているので、新しいメンバーが入ってきても自動的に学べる。個人的には、これが一番地味に効いている。
正直しんどかったこと
誤検知の抑制管理が地味に重い
NagSuppressionsが増えていくと、それ自体の管理コストが発生する。どのSuppressionsが本当に必要で、どれが技術的負債なのかを定期的にレビューするサイクルを作らないと、「とりあえず全部抑制」になってしまう危険がある。最初の2週間はひたすらこの作業で、正直しんどかった。
pie title Nag エラー 47件の分類(初回実行時)
"要修正(真の問題)" : 21
"意図的設計(正当な抑制)" : 18
"ルールの誤検知" : 8
既存スタックへの適用は段階的に
既存スタックに一気にNagを適用すると、エラーが大量に出てCIが全部落ちる。うちは「新規スタックは必須、既存スタックは3ヶ月でゼロエラーを目指す」という移行計画を立てて段階的に対応した。
この辺の移行戦略はAWS Organizationsのマルチアカウント運用で失敗した話でも書いたアカウント分離の話と絡んでいて、どのアカウントから先にNagを導入するかの優先順位決めも結構悩んだ。
コンプライアンス要件との組み合わせ
SOC2審査でCloudTrail・Config設計が半年泥沼になった実録でも触れたけど、SOC2やISMSの準拠ではCDK Nagだけでは不十分で、Config RulesやSecurityHubとの組み合わせが必須になる。Nagはあくまで「デプロイ前のシフトレフト」であって、デプロイ後のドリフト検知はConfigに任せるという役割分担が重要だ。
実際の本番構成図
うちのチームで実際に動いているCDK Nag統合環境の全体像。
graph TB
subgraph Dev["開発者環境"]
IDE["VSCode / Cursor\ncdk-nag npm package"]
LocalCheck["ローカル cdk synth\n即時フィードバック"]
end
subgraph Management["管理アカウント"]
subgraph CICD_VPC["VPC: CI/CD"]
subgraph AZ1["AZ-1a"]
CodePipeline["CodePipeline V2"]
CodeBuild["CodeBuild Fleet\ncdk synth + Nag"]
end
end
SecurityHub["Security Hub\nNag結果インポート"]
AuditLog["CloudTrail\n検証ログ"]
end
subgraph Tooling["ツールアカウント"]
NagReport["Nag Report S3\n検証履歴保管"]
Dashboard["CloudWatch Dashboard\nNag エラートレンド"]
end
subgraph Target["デプロイ対象アカウント"]
subgraph ProdVPC["VPC: Production"]
subgraph ProdAZ1["AZ-1a"]
ECS["ECS Fargate"]
RDS[("Aurora PostgreSQL")]
end
subgraph ProdAZ2["AZ-1c"]
ECS2["ECS Fargate"]
RDS2[("Aurora Replica")]
end
ALB["ALB"]
SG["Security Group\nNag強制適用済み"]
end
subgraph Bucket["S3"]
AppBucket["App Bucket\nSSL強制・暗号化"]
end
WAF["WAF v2"]
end
IDE --> LocalCheck
LocalCheck --> CodePipeline
CodePipeline --> CodeBuild
CodeBuild --> |"Nag Error = 0?\nYES"| SecurityHub
CodeBuild --> |"Nag Error > 0\nビルド失敗"| AuditLog
CodeBuild --> NagReport
NagReport --> Dashboard
SecurityHub --> |"承認後デプロイ"| Target
WAF --> ALB
ALB --> ECS
ALB --> ECS2
ECS --> RDS
ECS2 --> RDS2
ECS --> AppBucket
ルールセット別の使い分け
cdk-nagには複数のルールセットがあって、最初はどれを使えばいいか迷うと思う。正直「全部入れとけば万全でしょ」と思いがちだけど、それをやると誤検知の嵐で死ぬ。実際に試した感想ベースでまとめた。
| ルールセット | 対象コンプライアンス | 厳しさ | うちでの利用 | コメント |
|---|---|---|---|---|
| AwsSolutionsChecks | AWS Best Practices | ★★★☆ | 全スタック必須 | バランスがいい。最初はこれから始めるべき |
| NistSP80053R5Checks | NIST SP 800-53 Rev.5 | ★★★★★ | 本番のみ任意 | 厳しすぎて誤検知も多い。金融・官公庁向け |
| PCIDSS321Checks | PCI DSS 3.2.1 | ★★★★☆ | 決済基盤のみ | カード決済扱うなら必須 |
| HIPAASecurityChecks | HIPAA | ★★★★★ | 未使用 | ヘルスケア系向け |
| CIS14Checks | CIS Benchmark 1.4 | ★★★★☆ | セキュリティ審査前 | 年1回の審査前に一時的に有効化 |
まずAWSSolutionsChecksだけを入れて安定させてから、必要に応じて追加するのを強くおすすめする。最初から全部入れると誤検知と戦う時間で死ぬ、本当に。
CDK関連ではCDK Pipelines 2026年版でマルチアカウントデプロイの仕組みを詳しく書いているので、セットで読むと構成の全体像がつかみやすい。
まとめ
3ヶ月本番で使って見えてきたことを正直にまとめると:
-
CDK Aspects + cdk-nagはIaCのセキュリティシフトレフトとして本物に使える。 ただし導入コストを甘く見ると痛い目を見る。既存スタックは段階移行を計画してから始めること。
-
誤検知との戦いは避けられない。 NagSuppressionsは必ずチケット番号・理由・承認者を記録する運用にしないと、後から「なぜ抑制しているのか」が追えなくなる。
-
CI/CDに組み込んで初めて価値が出る。 ローカルで動かすだけだと「後で直す」が生き残る。ビルドを止める権限を持たせることが肝。
-
カスタムルールで会社独自標準をコード化できる。 タグポリシー、命名規則、デプロイ可能リージョンなど、口頭で教えていたことをコードにできる。
-
ConfigやSecurityHubとの組み合わせが前提。 Nagはデプロイ前の検証。デプロイ後のドリフト検知はConfigに任せる役割分担を最初から設計すること。
次のアクション: まずは既存スタックの1つにAwsSolutionsChecksだけを追加してcdk synthしてみてください。何個エラーが出るか見るだけでも、チームの現状把握になる。うちは47個出てビビったけど、それが「始まり」だった。エラーゼロになったときの達成感は地味に気持ちいいですよ。