CDK Aspects・Nag本番導入3ヶ月で気づいた、自動セキュリティ検証の現実

CDK AspectsとNagを導入してみたら、False Positiveの対処が意外と大変だった。チーム運用で見えた落とし穴と実装パターンを3ヶ月の経験から解説します。

CDK Aspects・Nagで本番導入してみた

うちのチームで先月、CDK AspectsとAWS Nagを本番環境に組み込んだんですよ。正直「これで自動でセキュリティチェックが走って楽になるな」って思ってたんですが、3ヶ月運用してみると地味だけど重い課題がいっぱい出てきた。今日はその実装記録を共有したいと思います。

実は導入する前、うちはセキュリティレビューって人手でやってたんです。デプロイするたびに「このIAMポリシー大丈夫?」「S3のパブリックアクセス許可されてない?」みたいなのをチェックリストで確認してた。これが時間かかるし、漏れるし、最悪でした。

Nagで何ができるのか、実際に試してみた

NagはAWS製のセキュリティルール検査ツールで、CloudFormationテンプレートに対して事前定義されたルールセットを実行します。AWS Construct Library(CDK)と組み合わせると、デプロイ前にセキュリティ違反を検出できるんですね。

うちが導入した構成はこんな感じです:

import { Aspects, Stack } from 'aws-cdk-lib';
import { AwsSolutions } from 'cdk-nag';

export class MyStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // S3バケット作成
    const bucket = new s3.Bucket(this, 'MyBucket', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      versioned: true,
    });

    // IAMロール
    const role = new iam.Role(this, 'MyRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });

    role.addToPrincipalPolicy(
      new iam.PolicyStatement({
        actions: ['s3:GetObject'],
        resources: [bucket.arnForObjects('*')],
        effect: iam.Effect.ALLOW,
      })
    );
  }
}

// Aspectsの適用
const app = new App();
const stack = new MyStack(app, 'MyStack');

Aspects.of(stack).add(new AwsSolutions());

これ、デプロイ時に自動でチェックが走るんですよ。最初は感動しました。でもここからが本当の始まりでした。

実装してから気づいた現実:False Positiveの嵐

導入初日、デプロイを試したら500個以上のエラーが出ました。マジで焦りました。

例えばこんなのが報告されます:

Rule: AwsSolutions-IAM1: IAM policies should not allow full "*" administrative privileges
Resource: MyRole
Message: IAM policy allows full administrative privileges

でもね、これってうちの場合は意図的なんですよ。特定の環境では広い権限が必要な場合があるんです。CloudFormation検証だと「絶対ダメ」になってしまう。

実際に運用して気づいた主な落とし穴があります:

  • IAMワイルドカード検査が厳しすぎるs3:GetObjectresources: ['bucket-name/*']って書くと「ワイルドカードはダメ」って言われる。でも現実的には必要な場合がほとんど

  • ログ保持期間 — CloudWatch Logsのデフォルト永続保持がセキュリティ問題扱いされる。これは確かに対応したいけど、すべてのログに明示的に期間指定するとコスト増える

  • VPC設定 — RDSのマルチAZ無効化が引っかかる。開発環境では必要ないのに本番ルールが強制されている

こういった「理屈は正しいんだけど、現実には困る」パターンが大量に出てくるんですよね。

チーム運用で見つけた正解パターン

う〜ん、ここまで試行錯誤した結果、うちは「ルールを段階的に有効化する」戦略に変えました。

import { NagSuppressions } from 'cdk-nag';

// 特定リソースのみ例外処理
NagSuppressions.addResourceSuppressions(role, [
  {
    id: 'AwsSolutions-IAM1',
    reason: 'この環境では広い権限が必要(データレイク用途)',
    appliesTo: ['arn:aws:iam::ACCOUNT_ID:role/MyRole'],
  },
]);

// スタック全体でルールを無効化
NagSuppressions.addStackSuppressions(stack, [
  {
    id: 'AwsSolutions-RDS1',
    reason: '開発環境なのでマルチAZは不要',
  },
]);

この方式だと、例外の理由が記録されるから後でレビューしやすいんですよ。デプロイログにも出るし、「なぜこのルール無視してるのか」ってのが履歴に残る。

実装したチェックリスト:何をどの段階で有効化するか

うちは最終的にこういう段階的な導入戦略で落ち着きました:

環境有効ルール理由
開発基本セキュリティ(S3暗号化、IAM原則など)開発速度重視。最小限の強制
ステージング本番と同じ(ただし例外あり)本番検証のため
本番すべてのルール + カスタムルール最高レベルのセキュリティ強制

設定ファイル方式も導入しました:

{
  "nagRulesets": {
    "development": {
      "enabled": [
        "AwsSolutions-S1",
        "AwsSolutions-IAM4",
        "AwsSolutions-EC2"
      ]
    },
    "production": {
      "enabled": "all",
      "exemptions": [
        {
          "ruleId": "AwsSolutions-RDS1",
          "resourceId": "legacy-db",
          "expiresAt": "2026-12-31",
          "reason": "レガシーDBの移行予定"
        }
      ]
    }
  }
}

こうすることで、例外が一時的なものか恒久的なものか明確になるんです。期限を切ることで「いつまでに対応する」ってのが可視化される。個人的には、このファイル方式がチーム全体の判断基準を統一するのに地味に便利でした。

実装してから3ヶ月:チームで運用してわかったこと

あ、一つ大事なのが、CI/CDパイプラインに組み込むタイミングなんです。うちは最初、デプロイの最後に走らせてたんですが、これだとエラーで止まってしまう。結局、デプロイが何度もリトライされるハメに。

いまは「警告レベル」と「エラーレベル」に分けてます:

# CDK synthでNagを実行
cdk synth --context env=production

# 警告レベルはログに出力、エラーレベルは失敗
npm run cdk:nag -- --report-format=json > nag-report.json

カスタムスクリプトで結果を解析するようにしました:

// カスタムスクリプトで結果を解析
const report = require('./nag-report.json');
const errors = report.filter(r => r.severity === 'error');
const warnings = report.filter(r => r.severity === 'warning');

if (errors.length > 0) {
  process.exit(1); // デプロイ中止
}

if (warnings.length > 0) {
  console.warn(`警告: ${warnings.length}件`);
  // 通知を送る
}

こうすることで、本当に危険なセキュリティ違反だけでデプロイをブロックして、軽微な問題は通知しながら進める。実務的ですよね。

本番導入で見えたメリットと課題

ぶっちゃけ、メリットはかなりでかいです。導入後3ヶ月で、手動レビューで見落としていたセキュリティ設定ミスが3件出てきました。以前だったら本番環境で発見されていたはずです。

コード例を一つ:

// BAD: これはNagで引っかかる
const bucket = new s3.Bucket(this, 'DataBucket', {
  // publicReadAccess: true,  // 不適切
  // serverAccessLogsPrefix: undefined,  // ログなし
});

// GOOD: Nagで合格する設定
const bucket = new s3.Bucket(this, 'DataBucket', {
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
  encryption: s3.BucketEncryption.S3_MANAGED,
  versioned: true,
  serverAccessLogsPrefix: 'access-logs/',
  serverAccessLogsBucket: logBucket,
});

ただし課題もあります。一番の課題は「ルールメンテナンスコスト」なんですよ。Nagのルールセット自体がアップデートされるんですが、そのたびに新しい警告が増える。

うちは月1回、ルール更新をレビューする会議を設定しました。どのルールを有効化するか、どれを除外するか。これをドキュメント化して、新しく入ったメンバーに共有する。こういった継続的な対応が案外大変です。

実装・運用の流れを可視化すると

導入から運用までのフローはこんな感じです:

graph LR
    A["コード作成"] --> B["CDK synth"]
    B --> C["Nagチェック"]
    C --> D{エラーあり?}
    D -->|yes| E["エラーレベルで失敗"]
    E --> F["開発者が修正"]
    F --> A
    D -->|no| G{警告あり?}
    G -->|yes| H["通知・ログ出力"]
    G -->|no| I["デプロイ実行"]
    H --> I
    I --> J["本番環境反映"]

このフローだと、エラーは確実に止めるけど、警告は見守る形になります。

まとめ

CDK Aspects・Nagは確かに強力だけど、運用があるんだってのが実感です。

導入初期は「自動化で楽になる」と思ってたんですが、実際には「セキュリティチェックのやり方が変わる」くらいの認識の方がいいですね。設定ファイルの管理、ルール更新への対応、チーム間でのルール判断の統一…こういったことが必要になってくる。

でも逆に考えると、これらが整備されれば、本当に重要なセキュリティ判断に時間を割けるようになるんです。うちのチームでは導入後、セキュリティレビュー時間が70%削減されました。その分、アーキテクチャレビューとか本質的なセキュリティ検討に人力を回せるようになった。

もし同じようにIaCでセキュリティを強化したいなら、試す価値はあります。ただ「設定して終わり」じゃなく「運用フロー込みで考える」ってのが成功のカギですね。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事