[CDK]全リージョンのGuardDutyを有効にしてSNS+AWS ChatbotでSlack通知する構成

2021.11.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

吉川@広島です。

【初心者向け】AWSの脅威検知サービスAmazon GuardDutyのよく分かる解説と情報まとめ | DevelopersIO

全リージョンのGuardDutyを有効化してSlack通知する構成を作るにあたり、

  1. 全リージョンでGuardDuty有効化
  2. 全リージョンでSNSを作成し、GuardDutyと紐付ける
  3. AWS Chatbotを設定
  4. 全リージョンのSNSをChatbotに紐付ける

これを手動でやるのはなかなか大変です。

そこでCDKを使って自動化してみました。

環境

  • npm 8.1.3
  • node 14.18.1
  • aws-cdk 1.132.0

CDKプロジェクト構築

mkdir guardduty-alarm
cd guardduty-alarm
npx aws-cdk init -l typescript
npm i @aws-cdk/aws-{chatbot,events,events-targets,guardduty,iam,sns}

CDKコード

GuardDuty+SNSスタック

// lib/guardduty-stack.ts

import * as cdk from '@aws-cdk/core'
import * as sns from '@aws-cdk/aws-sns'
import * as iam from '@aws-cdk/aws-iam'
import * as guardduty from '@aws-cdk/aws-guardduty'
import * as events from '@aws-cdk/aws-events'
import * as eventsTargets from '@aws-cdk/aws-events-targets'

export class GuarddutyStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    /**
     * SNS
     */
    const topic = new sns.Topic(this, 'topic')

    topic.addToResourcePolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ['SNS:Publish'],
        principals: [new iam.ServicePrincipal('events.amazonaws.com')],
        resources: [topic.topicArn],
      })
    )

    // AWS Chatbotとの紐付けで必要になるため、出力する
    new cdk.CfnOutput(this, 'output', {
      exportName: 'sns-topic-arn',
      value: topic.topicArn,
    })

    /**
     * GuardDuty
     */
    new guardduty.CfnDetector(this, 'guardduty-detector', {
      enable: true,
    })

    /**
     * CloudWatch Events
     */
    new events.Rule(this, 'rule', {
      eventPattern: {
        source: ['aws.guardduty'],
        detailType: ['GuardDuty Finding'],
      },
      targets: [new eventsTargets.SnsTopic(topic)],
    })
  }
}

SNSトピックARNをChatbotスタックに受け渡すためにOutputしています。

AWS Chatbotスタック

// lib/chatbot-stack.ts

import * as cdk from '@aws-cdk/core'
import * as iam from '@aws-cdk/aws-iam'
import * as chatbot from '@aws-cdk/aws-chatbot'
import * as fs from 'fs'
export interface MyProps {
  readonly slackChannelId: string
  readonly slackWorkspaceId: string
  readonly regions: ReadonlyArray<string>
}

export class ChatbotStack extends cdk.Stack {
  constructor(
    scope: cdk.Construct,
    id: string,
    props?: cdk.StackProps & MyProps
  ) {
    super(scope, id, props)

    // outputs.jsonからSNSトピックARNの配列を取得する
    let snsTopicArns: string[] = []
    if (fs.existsSync('./outputs.json')) {
      const outputs: Record<string, Record<string, string>> = JSON.parse(
        fs.readFileSync('./outputs.json').toString()
      )
      snsTopicArns = props!.regions.map(
        (region) => outputs[`guardduty-stack-${region}`]['snstopicarnoutput']
      )
    }

    /**
     * Chatbot
     */
    const chatbotRole = new iam.Role(this, 'chatbot-role', {
      assumedBy: new iam.ServicePrincipal('sns.amazonaws.com'),
    })

    chatbotRole.addToPolicy(
      new iam.PolicyStatement({
        resources: ['*'],
        actions: ['cloudwatch:*'],
      })
    )

    new chatbot.CfnSlackChannelConfiguration(this, 'slack-channel-config', {
      configurationName: 'guardduty-slack-channel-config',
      slackChannelId: props!.slackChannelId,
      slackWorkspaceId: props!.slackWorkspaceId,
      snsTopicArns,
      iamRoleArn: chatbotRole.roleArn,
      loggingLevel: 'ERROR',
    })
  }
}

// outputs.jsonからSNSトピックARNの配列を取得する の箇所が悩んだ点で、最初はクロススタック参照を使おうとしていました。

AWS CDKでクロススタック参照をしてみた | DevelopersIO

しかし、クロススタック参照はリージョンをまたげないため、今回は使用できませんでした。

他の案として、 aws cloudformation describe-stacks の操作をAWS SDK for Node.jsで行ってOutpusの値を収集する手も考えました。

スタックの情報とリストの取得 - AWS CloudFormation

上記でもできそうでしたが、今回は

AWS CDK Toolkit (cdk command) - AWS Cloud Development Kit (CDK)

If your stack declares AWS CloudFormation outputs, these are normally displayed on the screen at the conclusion of deployment. To write them to a file in JSON format, use the --outputs-file flag. cdk deploy --outputs-file outputs.json MyStack

このCDKの --outputs-file を使って解決することにしました。このオプションを付与するとCloudFormationのOutputsをJSONファイルとして出力してくれるので、それをNode.jsでパースすることでARNの値を取得することができました。

binコード

// bin/guardduty-alarm.ts

#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from '@aws-cdk/core'
import { GuarddutyStack } from '../lib/guardduty-stack'
import { ChatbotStack } from '../lib/chatbot-stack'

const app = new cdk.App()

// https://docs.aws.amazon.com/ja_jp/general/latest/gr/rande.html
const regions: ReadonlyArray<string> = [
  'us-east-2',
  'us-east-1',
  'us-west-1',
  'us-west-2',
  // 'af-south-1',
  // 'ap-east-1',
  'ap-south-1',
  'ap-northeast-3',
  'ap-northeast-2',
  'ap-southeast-1',
  'ap-southeast-2',
  'ap-northeast-1',
  'ca-central-1',
  // 'cn-north-1',
  // 'cn-northwest-1',
  'eu-central-1',
  'eu-west-1',
  'eu-west-2',
  // 'eu-south-1',
  'eu-west-3',
  'eu-north-1',
  // 'me-south-1',
  'sa-east-1',
]
const guarddutyStacks = regions.map(
  (region) =>
    new GuarddutyStack(app, `guardduty-stack-${region}`, {
      env: { region },
    })
)

const chatbotStack = new ChatbotStack(app, 'chatbot-stack', {
  env: { region: 'ap-northeast-1' },
  slackWorkspaceId: 'xxxxxxxx', // SlackワークスペースID
  slackChannelId: 'xxxxxxxxxx', // SlackチャンネルID
  regions,
})

for (const guarddutyStack of guarddutyStacks) {
  chatbotStack.addDependency(guarddutyStack)
}

chatbotスタックはguarddutyスタックに依存するため、addDependencyで指定することで実行順をコントロールしています。

いくつかコメントアウトしているリージョンがありますが、これらについては

The security token included in the request is invalid

のエラーが発生していました。

リージョンとゾーン - Amazon Elastic Compute Cloud

ドキュメントによると、これらのリージョンはオプトインステータスが必須とあるので、利用するには有効化手続きを行う必要があるという理解をしています。ただ、この点あまり詳しくないため、間違いがあればご指摘いただければと思います。

デプロイ手順

まず、GuardDutyのスタックをデプロイします。スタックの数が多くすべて承認するのが大変なので --require-approval never を付与します。

npx cdk deploy 'guardduty-stack-*' --require-approval never --outputs-file outputs.json

完了すると、以下のようなoutputs.jsonが作成されています。

{
  "guardduty-stack-ap-northeast-1": {
    "snstopicarnoutput": "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:guardduty-stack-ap-northeast-1-topicxxxxxxxxxxxxxxxxxxxxx"
  },
  "guardduty-stack-ap-northeast-2": {
    "snstopicarnoutput": "arn:aws:sns:ap-northeast-2:xxxxxxxxxxxx:guardduty-stack-ap-northeast-2-topicxxxxxxxxxxxxxxxxxxxxx"
  },
  "guardduty-stack-ap-northeast-3": {
    "snstopicarnoutput": "arn:aws:sns:ap-northeast-3:xxxxxxxxxxxx::guardduty-stack-ap-northeast-3-topicxxxxxxxxxxxxxxxxxxxxx"
  }
}

実際にはリージョン数ぶんの要素があるのでもっと多いです。

意図通りoutputs.jsonが出力されていることが確認できたらchatbot-stackをデプロイします。

npx cdk deploy chatbot-stack

以上でGuardDutyの有効化とSlackへの通知設定ができました。

参考