CDK Aspects・Nag導入で痛感した、自動セキュリティ検証の現実的な話
CDK AspectsとNagを本番運用3ヶ月。False Positiveの嵐を乗り越えて、IaC品質を本当に改善した工夫と落とし穴を実体験から解説します
CDK AspectsとNagで痛感した、自動検証の地味だけど大事な話
つい先月、うちのチームで CDK Aspects と AWS Nag を本番導入してから3ヶ月になる。最初は「セキュリティルール自動化すれば楽になるっしょ」くらいの軽い気持ちだったんだけど、実際に運用してみると、単なる検証ツールじゃなくて、IaC 品質を根本から変える仕組みなんだってことに気づいた。
個人的に思ったのは、自動化系のツールって導入時の満足度と実運用のギャップが大きいことが多いんですよ。Nag も最初は「これで脆弱性チェックが自動化されるぞ」と導入したら、False Positive の嵐で、むしろチームの負担が増えた時期もあった。でも工夫次第で、本当に価値のある仕組みにできるってわかった。今回は、そういった実際に苦労した部分も含めて、どうやって実運用レベルで機能させるかを共有したい。
Aspects の基本から、なぜ Nag が必要なのか
Aspects っていうのは CDK の訪問者パターン(Visitor Pattern)を使った機構で、スタック内のすべてのリソースに対して任意のルールを適用できる。Nag はそれをセキュリティ・ベストプラクティス面から具体化したルールセットってイメージだ。
実際のコード例でいくと、こんな感じ。
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag';
import * as s3 from 'aws-cdk-lib/aws-s3';
export class MyStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Nag ルール適用
const bucket = new s3.Bucket(this, 'MyBucket', {
versioned: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.KMS,
enforceSSL: true,
});
// 特定ルール抑制(必ずコメント付けること)
NagSuppressions.addResourceSuppressions(bucket, [
{
id: 'AwsSolutions-S1',
reason: 'Development environment logs not required',
},
]);
}
}
// CDK App で Nag を有効化
import { App } from 'aws-cdk-lib';
import { Aspects } from 'aws-cdk-lib';
const app = new App();
const stack = new MyStack(app, 'MyStack');
// AWS Solutions Checks を有効化
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
app.synth();
cdk synth を実行すると、Nag がスタック内のすべてのリソースを走査して、ルール違反を検出する。この時点で CI に統合できるから、本番デプロイ前に自動チェックが走る。
実際のエラー出力はこんな感じ:
[ERROR] AwsSolutions-S1: S3 bucket version is disabled
Resource: MyBucket [LogicalId: MyBucket]
Rule: S3 Bucket should have versioning enabled
Recommendation: Enable versioning on your S3 bucket to maintain a history of all versions of objects.
うちが導入してみて感じたのは、単体で使うと False Positive が多すぎて、むしろ開発体験が落ちるってことだった。だから、いかに賢く Suppression(ルール抑制)を使いこなすかが、実運用のカギになる。
False Positive との付き合い方、実装のコツ
導入当初、チームから「Nag の警告が多すぎて CI がいつも失敗する」という不満が出た。原因を分析してみると、大きく3つのパターンがあった。
1. ルール設定が厳しすぎる
Nag はデフォルトで AWS Solutions Checks を適用される。これは本当に厳しくて、開発環境では過剰なルールも含まれる。例えば、開発環境の Lambda に CloudWatch Logs 暗号化を強制したり、小さい RDS にバックアップ保持期間 35 日を要求したり。
そこで、うちは環境別にルールセットを分ける実装にした:
interface NagConfig {
environment: 'development' | 'staging' | 'production';
rules: string[];
}
const nagConfigs: Record<string, NagConfig> = {
development: {
environment: 'development',
rules: [
'AwsSolutions-EC2', // セキュリティグループ関連
'AwsSolutions-S1', // S3 バージョニング
],
},
production: {
environment: 'production',
rules: [
'AwsSolutions-EC2',
'AwsSolutions-S1',
'AwsSolutions-RDS',
'AwsSolutions-KMS', // 本番は KMS も強制
'AwsSolutions-Lambda',
],
},
};
const app = new App();
const stack = new MyStack(app, 'MyStack');
const env = process.env.ENVIRONMENT || 'development';
const config = nagConfigs[env];
Aspects.of(app).add(
new AwsSolutionsChecks({
verbose: true,
})
);
実装としては、環境ごとに有効化するルールを制御することで、開発時の「ノイズ」を減らせる。
2. 意図的な例外をドキュメント化する
Nag Suppression を使うときに、チームで「何でこれを除外したのか」が曖昧になりがちだ。そこで、除外する際には必ず理由コメントを付けて、Confluence にルール除外リストをまとめるようにした:
// ❌ 悪い例:理由が不明確
NagSuppressions.addResourceSuppressions(table, [
{ id: 'AwsSolutions-DDB', reason: 'Not applicable' },
]);
// ✅ 良い例:具体的な理由
NagSuppressions.addResourceSuppressions(table, [
{
id: 'AwsSolutions-DDB3',
reason: 'DynamoDB Stream not required. Development table for test data only. Approved by security team on 2026-04-15.',
},
]);
これを Confluence に「Nag Suppression Registry」として一覧化して、定期的(四半期ごと)に見直す。実際に「あ、もうこのルール除外って不要だな」って判明することもある。
3. カスタムルール実装で、実際のニーズに合わせる
Nag の標準ルールだけでは足りなくて、うちのチーム固有のルール(例:タグの強制、リソース命名規則など)が必要だった。これは Aspects で自分たちのカスタムルール実装できる:
import { IConstruct } from 'constructs';
import { IInspectable, IInspectionMessage, InspectionResults } from 'cdk-nag';
export class CustomTaggingCheck implements IInspectable {
constructor(private readonly requiredTags: string[]) {}
inspect(construct: IConstruct): InspectionResults {
const results = new InspectionResults();
// リソースが ITaggable か判定
if ('tags' in construct && construct.tags) {
const tags = Object.keys(construct.tags);
const missing = this.requiredTags.filter(tag => !tags.includes(tag));
if (missing.length > 0) {
results.fail(
construct,
`Missing required tags: ${missing.join(', ')}. All resources must have Environment, Owner, CostCenter tags.`
);
} else {
results.pass(construct, 'All required tags present');
}
}
return results;
}
}
// 使用例
const app = new App();
const stack = new MyStack(app, 'MyStack');
Aspects.of(app).add(
new CustomTaggingCheck(['Environment', 'Owner', 'CostCenter'])
);
Aspects.of(app).add(new AwsSolutionsChecks());
こんな形で、標準ルール + カスタムルールを組み合わせることで、チームの「本当に必要なチェック」だけを走らせられる。
実装から CI 統合、本番運用の流れ
AWS Nag は単体では何もしないので、実際に効果を出すには CI パイプラインに組み込む必要がある。うちの CDK Pipeline の構成はこんな感じだ:
graph TB
subgraph SourceStage["Source"]
A[GitHub Repository]
end
subgraph SynthStage["Synth & Validation"]
B["npm ci<br/>cdk synth"]
C["Nag Validation<br/>cdk synth with Aspects"]
D["cfn-lint"]
end
subgraph BuildStage["Build"]
E["Build & Test"]
F["Unit Tests<br/>Integration Tests"]
end
subgraph DeployStage["Deploy"]
subgraph Dev["Dev Environment"]
G1["CloudFormation Deploy"]
G2["Smoke Tests"]
end
subgraph Prod["Prod Environment"]
H1["Manual Approval"]
H2["CloudFormation Deploy"]
H3["Health Check"]
end
end
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G1
G1 --> G2
G2 --> H1
H1 --> H2
H2 --> H3
style C fill:#ff9999
style H1 fill:#ffcc99
Nag チェックが Synth ステージに統合されるので、CloudFormation テンプレート生成時点で既に検証が終わっている。これ重要なんですよ。わざわざ別ステップを作らずに、cdk synth 実行時に自動的にチェックが走る。
CI の実装はこんな感じ(GitHub Actions の例):
name: CDK Validation and Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: CDK Synth with Nag validation
run: npm run build && cdk synth
env:
CDK_CONTEXT_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'development' }}
- name: Upload CloudFormation templates
uses: actions/upload-artifact@v4
with:
name: cdk-outputs
path: cdk.out/
deploy-dev:
needs: validate
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Deploy to Dev
run: cdk deploy --require-approval never --context environment=development
env:
AWS_REGION: us-east-1
deploy-prod:
needs: validate
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Deploy to Prod
run: cdk deploy --require-approval any-change --context environment=production
env:
AWS_REGION: us-east-1
重要な設定ポイント:
- Nag は
cdk synthの時点で走る — つまり synth ステップが失敗したら、CloudFormation デプロイには進まない - 環境ごとにルールセット分ける —
CDK_CONTEXT_ENVで開発 vs 本番を制御 - Suppression はコード化 — git に含めるので、レビュープロセスで監視できる
マルチアカウント・複数チーム運用で気をつけたこと
うちのチームは今、3つの AWS アカウント(dev、staging、prod)を管理していて、複数のプロジェクトから CDK スタックがデプロイされる。こういう環境では Nag の「一律ルール強制」が問題になることが多い。
例えば:
- DataTeam: DynamoDB に対する要件が厳しい(Stream 必須など)
- WebTeam: Lambda のカスタムランタイム使ってて、標準ルール外
- MLTeam: SageMaker リソースに特殊な要件
こういう異なるニーズに対応するために、うちは「ルールセット」という概念を導入した:
// lib/nag-configs.ts
export interface RuleSetConfig {
name: string;
suppressions: Record<string, string[]>; // リソースタイプごとに suppress する rule ID
customRules: IInspectable[];
}
export const ruleSetConfigs: Record<string, RuleSetConfig> = {
'web-team': {
name: 'Web Application Stack',
suppressions: {
'AWS::Lambda::Function': ['AwsSolutions-L1'], // Runtime check は除外
},
customRules: [],
},
'data-team': {
name: 'Data Processing Stack',
suppressions: {
'AWS::DynamoDB::Table': [], // DynamoDB は全ルール強制
},
customRules: [new DataPipelineSecurityCheck()],
},
'ml-team': {
name: 'ML Pipeline Stack',
suppressions: {
'AWS::SageMaker::NotebookInstance': ['AwsSolutions-SMNotebook'], // Notebook は特別扱い
},
customRules: [new MLModelGovernanceCheck()],
},
};
そして、スタック定義時にどのルールセットを使うか指定:
export interface MyStackProps extends StackProps {
ruleSet?: string; // 'web-team', 'data-team', 'ml-team'
}
export class MyStack extends Stack {
constructor(scope: Construct, id: string, props?: MyStackProps) {
super(scope, id, props);
// リソース定義...
const config = ruleSetConfigs[props?.ruleSet || 'default'];
// スタック定義後に、このスタック固有のルール適用
if (config.customRules.length > 0) {
config.customRules.forEach(rule => {
Aspects.of(this).add(rule);
});
}
}
}
これで、各チームが「自分たちのルールセット」を持ちながら、全社レベルのセキュリティルール(AwsSolutionsChecks)も保証できる。
実運用 3 ヶ月で気づいた、本当に大事なこと
導入から 3 ヶ月運用してきて、個人的に「これが実は重要だな」って思ったことを3つ挙げるとしたら:
1. Suppression の見直し会議を定期実施する
Nag suppression は「一度追加したら終わり」じゃなくて、組織が成長するに連れて「あ、もうこのルール除外って不要だ」って判明する。うちは四半期ごとに「Nag Suppression Review」という会議をセキュリティチームと一緒にやるようにした。結果、導入時の 45 個の suppression が、今は 28 個に減った。つまり、セキュリティベストプラクティスへの準拠が進んだってことだ。
2. False Positive と向き合うのに時間がかかることを最初から認める
「導入したら翌日から完全に自動化」ってのは幻想だ。うちのチームも最初 2 週間は CI ビルド失敗の嵐で、毎日誰かが Nag ルール対応に追われてた。正直、ツール導入の ROI が見えたのって、実は 4 週間目くらい。ここは忍耐が必要なんですよ。
3. ドキュメントが本当に大事
Nag のルール ID(AwsSolutions-S1 とか AwsSolutions-RDS2 とか)は、ぱっと見で何のルールか分からない。うちは GitHub Wiki に「Nag ルール一覧」というページを作って、各ルール ID に対して「何をチェックするのか」「どうやって対応するのか」をまとめた。これが後から新しく入ったエンジニアのオンボーディングで本当に役に立ってる。
まとめ
CDK Aspects と AWS Nag は、IaC の品質を自動化するめちゃ強力な仕組みだ。ただし、単にツール導入しただけでは意味なくて、チームの工夫と継続的な改善があって初めて価値が出る。
実際に動かすなら、次のアクションがおすすめ:
- 小さく始める — まずは
AwsSolutionsChecksだけで synth 試してみて、どんな警告が出るか把握する - Suppression は理由付きで — 例外を認める際には必ず「なぜこのルール除外なのか」をドキュメント化
- 環境ごとに無情に分ける — 開発と本番で異なるルール強度を持つのは健全。全環境一律ルール強制は避ける
- 定期的に見直す — 四半期ごとに suppression リストを見直して、本来のベストプラクティス準拠に向けて徐々に調整していく
正直、セキュリティ自動化って「魔法」じゃなくて「地道な改善」なんですよ。でも、その地道さが積み重なると、チームの信頼性と生産性が劇的に上がる。この体験、一度やると他の方法には戻れなくなると思う。