第1回では「Security Hub は集約と採点の道具で、採点は AWS Config で動く」という概念を
整理しました。今回はそれを AWS CDK(TypeScript)で実際に形にします。
この記事でわかること
- Security Hub CSPM を CDK で構築するときの全体の流れ(事前確認 → ゲート設定 → デプロイ → 検証)
- なぜ東京だけでなく us-east-1 にも必要になるのか(CloudFront・WAFv2)と、finding aggregator による東京集約
- 標準を FSBP 単独にする理由と
enableDefaultStandards: false - 「リージョンに1個」しか作れないリソースと衝突しないゲートパターン
- デプロイ後に「本当に動いているか」を確認する read-only コマンド一式
前提: 第1回の概念(SH の仕事は集約+採点 / 採点の実体は Config ルール / 有効化はリージョン単位)。
用語に迷ったら第1回の用語集へ。CDK v2 のプロジェクトを触ったことがある想定です。
シリーズ: ① 概念編 → ② CDK 構築編(本記事) → ③ トラブル編
1. 今回のゴールと進め方
先にゴールをはっきりさせておきます。この記事が終わったとき、手元にはこういう状態ができています:
ゴール(完成条件)
- FSBP 標準の採点が実際に回っている(recorder が録画中で、コントロールが PASSED / FAILED を出す)
- us-east-1 の findings も含めて、東京の Security Hub 一画面で見られる
- 以上がすべてコード(CDK)で管理されていて、別アカウントにも同じ構成を再現できる
進め方は4ステップ。この順番自体が本記事の目次でもあります:
- 設計を理解する(§2) — 「東京だけ」で考え始めて、2つの壁にぶつかりながら最終形に辿り着く
- アカウントの現状を確認して「ゲート」を設定する(§3) — 既存リソースと衝突しないための下ごしらえ
- CDK で実装してデプロイする(§4) — 核心コードは3つだけ
- 本当に動いているか検証する(§5) — read-only コマンドで実体を確認
2. 設計 — 「東京だけ」から始めて、2つの壁にぶつかる
2.1 まず素朴に考える: 東京リージョンだけで良いのでは?
第1回の知識で素直に組むなら、こうなるはずです:
東京に AWS Config(recorder)を置き、東京で Security Hub CSPM + FSBP を有効化する。
これで東京リージョンのリソースの採点は回ります。ワークロードが東京だけなら、実際これでほぼ足ります。
ところが、構築を進めると2つの壁にぶつかります。
2.2 壁① — CloudFront と WAF がいつまでも採点されない
FSBP には CloudFront.x や WAF.x というコントロールがあるのに、CloudFront を使っていてもfindings が一向に出ない。
これはCloudFront と、CloudFront に紐づく WAFv2 Web ACL は、AWS 内部では us-east-1(バージニア北部)のリソースとして存在するからです。
第1回のとおり Security Hub CSPM の採点はリージョン単位 —東京の Hub は東京のリソースしか見えないので、us-east-1 にいる CloudFront は永遠に採点対象外になります。
結論①: CloudFront・WAF を採点したければ、us-east-1 にも Security Hub CSPM + Config(recorder)が必要。つまり最低でも「東京 + us-east-1」の2リージョン構成になる。
2.3 壁② — 今度は画面が2つに割れる
us-east-1 にも Hub を立てると、今度は findings の置き場が2か所になります。
「セキュリティの状況は Security Hub を見ればわかる」はずが、2つのリージョンのコンソールを巡回する運用に逆戻りしてしまう。
これを解決するのが第1回 §7 で予告した**クロスリージョン集約(finding aggregator)**です。集約リージョン(=東京)に aggregator を置いて us-east-1 をリンクすると、us-east-1 の findingsが 東京の Security Hub にも自動で現れます。
運用者は東京だけ見ればよい。
結論②: 2リージョンに Hub を置きつつ、aggregator で東京に集約する。
通知(EventBridge)の配線も東京1か所で済むようになる。
2.4 たどり着いた完成形

緑=CDK で新規作成、灰破線=既存活用(§3 のゲート)、青破線=次のステップ。us-east-1 → 東京の集約矢印が §2.3 の答え。*
壁①②の答えに加えて、あと3つの設計判断が入っています。いずれもそのままコスト設計でもあります:
| 判断 | 内容 | 理由 |
|---|---|---|
| 標準は FSBP 単独 |
enableDefaultStandards: false にして FSBP だけ明示的に有効化 |
既定のままだと FSBP + CIS が自動で両方有効になり、同じ問題が2重に findings 化され検査料金も増える。まず FSBP 1本で始めるのが定石 |
| グローバルタイプの記録は東京のみ | us-east-1 の Config recorder は WAFv2・CloudFront の7タイプ限定 | IAM などのグローバルタイプを両リージョンで記録すると構成アイテム(CI)課金が二重になる。AWS も「グローバルリソースの記録は1リージョンのみ」を推奨 |
| 集約ホームは東京 | aggregator を東京に置き us-east-1 をリンク | 運用の主拠点が東京のため。通知の配線も東京1か所で済む |
3. 実アカウントの現実 — 「リージョンに1個」問題とゲートパターン
設計が決まったら次はデプロイ…の前に、教科書どおりに行かないポイントがあります。GuardDuty の detector、Security Hub の Hub、Config の recorder はいずれも 1リージョンに1個しか存在できません。
そして実アカウントには、過去に誰かが有効化したリソースが残っていることがよくあります(筆者のケースでは GuardDuty が既に有効でした)。
問題は、CloudFormation に「既にあればスキップ」という機能が無いこと。テンプレートに書いた以上、必ず作ろうとし、既存があれば already exists エラーでスタック全体がロールバックします。
ゲートパターン。デプロイ前の read-only 確認 → .env のゲート値 → 「無いものだけ作る」。確認を怠ると already exists → ロールバック。*
3.1 デプロイ前の存在確認(read-only)
| リソース | 確認コマンド | 「存在しない」ときの出力 |
|---|---|---|
| Security Hub | aws securityhub describe-hub --region ap-northeast-1 |
InvalidAccessException: ... not subscribed |
| GuardDuty | aws guardduty list-detectors --region ap-northeast-1 |
"DetectorIds": [] |
| Config recorder | aws configservice describe-configuration-recorders |
"ConfigurationRecorders": [] |
| CloudTrail | aws cloudtrail describe-trails |
"trailList": [] |
3.2 ゲートの実装 — .env フラグ + if 文だけ
大げさな仕組みは要りません。確認結果を .env のフラグに落とし、スタック側は
フラグが true のときだけ construct を生成します:
// env-config.ts — "true" という文字列のときだけ true(既定 = 未設定 = false = 作らない)
function flag(value: string | undefined): boolean {
return value?.trim().toLowerCase() === "true";
}
# .env — デプロイ前の確認結果を反映する
CREATE_SECURITYHUB=true # 未サブスクライブを確認済み → 作る
CREATE_GUARDDUTY=false # 既存 detector あり → 絶対に作らない
CREATE_CONFIG_RECORDER=true # recorder 不在を確認済み → 作る
// スタック側 — ゲートが true のものだけテンプレートに載る
if (config.createSecurityHub) {
cspm = new SecurityHubCspm(this, "SecurityHub");
}
ポイント: 既定値は必ず false(作らない) にする。「確認していないものは作らない」に倒しておけば、設定漏れがあっても事故は「作られなかった」で済む。逆(既定 true)だと設定漏れ = ロールバック事故になる。
4. CDK 実装 — 核心コード3つ
4.1 Security Hub CSPM + FSBP 単独有効化
import * as securityhub from "aws-cdk-lib/aws-securityhub";
export class SecurityHubCspm extends Construct {
readonly hub: securityhub.CfnHub;
constructor(scope: Construct, id: string) {
super(scope, id);
const stack = cdk.Stack.of(this);
this.hub = new securityhub.CfnHub(this, "Hub", {
enableDefaultStandards: false, // 既定(FSBP+CIS 自動有効化)を止める
});
// FSBP だけを明示的に購読。ARN はリージョンごとに異なる点に注意
const fsbp = new securityhub.CfnStandard(this, "FsbpStandard", {
standardsArn: `arn:${stack.partition}:securityhub:${stack.region}::standards/aws-foundational-security-best-practices/v/1.0.0`,
});
fsbp.addDependency(this.hub); // Hub 有効化前に購読しようとして失敗するのを防ぐ
}
}
CfnStandard と CfnHub は独立リソースなので、依存を張らないと CloudFormation が並列作成し「Hub がまだ無いのに標準を購読 → 失敗」が起き得ます。addDependency を忘れずに。
4.2 Finding Aggregator(東京がホーム)
// 東京スタック側。このスタックのリージョン(東京)が自動的に集約ホームになる
const aggregator = new securityhub.CfnFindingAggregator(this, "FindingAggregator", {
regionLinkingMode: "SPECIFIED_REGIONS",
regions: ["us-east-1"], // リンクするリージョン
});
aggregator.addDependency(cspm.hub); // これも Hub が先
4.3 us-east-1 の Config は7タイプ限定
// us-east-1 スタックの Config recorder — グローバルタイプは記録しない(東京の担当)
recordingGroup: {
allSupported: false,
includeGlobalResourceTypes: false, // ← 二重課金防止の要
resourceTypes: [
"AWS::WAFv2::WebACL", "AWS::WAFv2::RuleGroup", "AWS::WAFv2::IPSet",
"AWS::WAFv2::RegexPatternSet", "AWS::CloudFront::Distribution",
],
},
上記5タイプが「実際に自分で作る可能性がある」もの。AWS::WAFv2::ManagedRuleSet は AWS/マーケットプレイスのルールベンダー専用リソースなので通常は不要、AWS::CloudFront::StreamingDistribution(RTMP配信)は2020年に非推奨化され新規作成できないため、両方とも一覧から外して問題ありません。
Config recorder 自体の CDK 実装には大きな落とし穴があります。
recorder と delivery channel を素直に L1 リソースで書くと、どちらを先に作っても失敗する「循環デッドロック」に必ずハマります。筆者は実際に CREATE_FAILED とハングを1回ずつ経験しました。この顛末と解決策(AwsCustomResource ×3)は第3回で詳しく扱います。
5. デプロイと「本当に動いているか」の確認
スタックは「東京」「us-east-1」の2つ。デプロイ後、次の read-only コマンドで実体を確認します(コンソールを眺めるより確実で、そのまま手順書にもなります):
# ① Config recorder が実際に記録中か(最重要 — 第1回 §5 参照)
aws configservice describe-configuration-recorder-status --region ap-northeast-1
aws configservice describe-configuration-recorder-status --region us-east-1
# → 期待値: "recording": true, "lastStatus": "SUCCESS"
# ② Security Hub が FSBP 単独で有効か
aws securityhub get-enabled-standards --region ap-northeast-1
# → aws-foundational-security-best-practices/v/1.0.0 が1件だけ
# ③ 集約の確認 — ホームとリンク先
aws securityhub list-finding-aggregators --region ap-northeast-1
aws securityhub get-finding-aggregator --region ap-northeast-1 --finding-aggregator-arn <ARN>
# → 期待値: "FindingAggregationRegion": "ap-northeast-1", "Regions": ["us-east-1"]
# ④ しばらくして: us-east-1 発の findings が東京に見えるか
aws securityhub get-findings --region ap-northeast-1 \
--filters '{"Region":[{"Value":"us-east-1","Comparison":"EQUALS"}]}' --max-items 10
焦らないポイント2つ
① 標準のステータスが READY になるまで時間がかかります。デプロイ直後に
INCOMPLETE / NO_AVAILABLE_CONFIGURATION_RECORDER と表示されても、①の確認で recorder がrecording: true` なら AWS 側の状態更新待ちであることが多い(第1回 §5 の「recorder がそもそも無い」状況とは、recorder の実在で見分けられます)。
② 集約直後は us-east-1 の findings が東京に現れるまでラグがあります。day-0 で空でも正常。
6. コストの話 — この設計自体がコストガードレール
- 30日無料トライアル: Security Hub CSPM はリージョン×アカウント単位で初回30日無料。GuardDuty も同様。まず検証用アカウントで findings の量と料金メーターを観察してから本番へ、が安全です。
- AWS Config が主なコスト源: 記録開始直後は既存全リソース分の構成アイテム(CI)が一括記録される「初期バースト」があります(一時的なもの)。以後は変更量に比例。
- 設計判断がそのまま節約: FSBP 単独(検査数を抑える)+ グローバルタイプは1リージョンのみ記録 (CI の二重課金防止)。§2.4 の表の「理由」列は全部コストの話でもあります。
7. まとめと次回予告
- 進め方は4ステップ — 設計 → 事前確認+ゲート → 実装+デプロイ → 検証
- 「東京だけ」で始めると2つの壁(CloudFront が採点されない / 画面が割れる)にぶつかる → 答えは us-east-1 追加 + finding aggregator で東京集約(図C)
- 標準は
enableDefaultStandards: false+ FSBP 単独から始める - 「リージョンに1個」リソースはゲートパターンで守る — 事前確認 →
.envフラグ →
無いものだけ作る。既定は必ず false(図D) - デプロイ後は read-only コマンドで「実体」を確認。
recording: trueが試金石
次回(第3回・最終回): この構築、実は一度で成功していません。AWS Config の recorder とdelivery channel が互いを前提とする循環デッドロックで、CREATE_FAILED → ハング → と2連敗しました。CloudFormation のイベントログを追いながら原因を特定し、AwsCustomResource ×3 で解決するまでの一部始終を、エラーメッセージの原文つきで振り返ります。
シリーズ: 第1回 概念編 / 第2回(本記事)
