[CDK]全リージョンのGuardDutyを有効にしてSNS+AWS ChatbotでSlack通知する構成
吉川@広島です。
【初心者向け】AWSの脅威検知サービスAmazon GuardDutyのよく分かる解説と情報まとめ | DevelopersIO
全リージョンのGuardDutyを有効化してSlack通知する構成を作るにあたり、
- 全リージョンでGuardDuty有効化
- 全リージョンでSNSを作成し、GuardDutyと紐付ける
- AWS Chatbotを設定
- 全リージョンの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への通知設定ができました。