背景
CDKはCloudFormationやTerraformと違って○○を作るとしたらという情報が少なく、最初の学習ハードルが気持ち高いので、積極的にCDK関連を書くことで誰かの役に立てればいいなあと。
今回はECRにContainerimageがpushされたらイメージスキャンが走り、その結果をChatbotを経由してSlackに通知させる動きを作っていきます。
イメージ図
2種類のスキャン
AWS re:Invent 2021でInspectorがリニューアルされて、これまでのEC2に対する脆弱性管理だけでなく、ECRにpushされたコンテナイメージも対象となりました。これにより、コンテナイメージに対するスキャニングは、ECRの機能としての基本スキャンとInspectorの機能としての拡張スキャンの2種類のオプションを選択することが出来るようになりました。
基本スキャン
脆弱性データベース
ClairプロジェクトのCommon Vulnerabilities and Exposures(CVE)データベースを使用
スキャン検知対象
- OS パッケージ
拡張スキャン
脆弱性データベース
mazon Inspector は、Snyk と提携して、脆弱性データベースに追加の脆弱性インテリジェンスを受け取りました。
スキャン検知対象
- OS パッケージ(各OSのversionは下記一覧参照)
- Alpine Linux
- Amazon Linux
- CentOS Linux
- Debian Server
- Oracle Linux
- Red Hat Enterprise Linux
- OpenSUSE Leap
- SUSE Linux Enterprise Server
- Ubuntu Server
- プログラミング言語パッケージ
- C#
- Golang
- Java
- Javascript
- PHP
- Python
- Ruby
- Rust
事前準備
ChatbotとSlack連携
事前にchatbotがslackworkspaceにアクセスする権限を付与しておきましょう。
- AWS Chatbotコンソールより「チャットクライアント」を「Slack」にして「クライアントを設定」を押下する
- アクセス権限をリクエスト画面が表示されたら対象のSlackワークスペースを確認して「許可する」を押下する
CDK
-
Version:2.8.0
-
作成されるリソース
- SNSTopic
- SNSTopicPolicy
- ECRRepository
- EventBridgeRule
- Chatbot
※CDKではまだ拡張スキャンを有効化出来なさそうなので、今回は基本スキャンで実装していきます。
自分が見つけられていないだけで、こういうやり方で出来るよというのがありましたらコメント下さい!
cdk.json
今回はContextに、prefixと対象となるSlackワークスペースID、SlackチャンネルIDを置いています。
{
"app": "npx ts-node --prefer-ts-exts bin/src.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
"@aws-cdk/core:stackRelativeExports": true,
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
"@aws-cdk/aws-lambda:recognizeVersionProps": true,
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"prefix": "sampletest",
"slackWorkspaceId":"xxxxxxxxx",
"slackChannelId":"xxxxxxxxx"
}
}
app
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
import { SampleStack } from "../lib/sample-stack";
const app = new cdk.App();
const sampleStack = new SampleStack(app, `sample`);
stack
import * as cdk from "aws-cdk-lib";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as sns from "aws-cdk-lib/aws-sns";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as chatbot from "aws-cdk-lib/aws-chatbot";
export interface SampleStackProps {
readonly repo: ecr.Repository
readonly snsTopic: sns.Topic
}
export class SampleStack extends cdk.Stack {
public readonly repo: ecr.Repository
public readonly snsTopic: sns.Topic
// ECR Repository
private createRepository(name: string): ecr.Repository {
const repo = new ecr.Repository(this, `${name}`, {
repositoryName: name,
imageScanOnPush: true, // push時にスキャン
imageTagMutability: ecr.TagMutability.MUTABLE,
removalPolicy: cdk.RemovalPolicy.DESTROY,
lifecycleRules:[
{
maxImageAge: cdk.Duration.days(30),
}
],
});
return repo;
}
// SNS Topic
private createSndTopic(name: string): sns.Topic {
const snsTopic = new sns.Topic(this, `${name}`, {
displayName: "ECR Scan Nottification",
topicName: name,
});
return snsTopic;
}
// ECR Scan Event
private createScanEventRule(name: string, repo: ecr.Repository, sns: sns.Topic): events.Rule {
const eventRule = new events.Rule(this, `${name}`, {
description: 'ecr scan completed',
eventPattern: {
source: ['aws.ecr'],
detailType: ['ECR Image Scan'],
detail: {
'repository-name': [`${repo.repositoryName}`], // 作成したecrリポジトリが対象
'scan-status': ['COMPLETE'],
'image-tags': [{"prefix": "v-" }] // "v-"を含むタグで実行
},
},
ruleName: `${name}-scan-notification`,
});
eventRule.addTarget(new targets.SnsTopic(sns)) // 作成したSNSをターゲットに指定
return eventRule;
}
// Chatbot
private createChatbot(name: string, sns: sns.Topic): chatbot.SlackChannelConfiguration {
const slackWorkspaceId = this.node.tryGetContext("slackWorkspaceId"); // Contextで指定したslackworkspaceidを取得
const slackChannelId = this.node.tryGetContext("slackChannelId"); // Contextで指定したslackchannelidを取得
const slackchatbot = new chatbot.SlackChannelConfiguration(this, `${name}`, {
slackChannelConfigurationName: name,
slackWorkspaceId: slackWorkspaceId, // 事前にコンソール上でchatbotがslackworkspaceにアクセスする権限を与えていること
slackChannelId: slackChannelId,
loggingLevel: chatbot.LoggingLevel.ERROR,
notificationTopics: [sns],
});
return slackchatbot;
}
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const prefix = this.node.tryGetContext("prefix"); // Contextで指定したprefixを取得
this.snsTopic = this.createSndTopic(`${prefix}-sns-topic`);
this.repo = this.createRepository(`${prefix}-repo`);
this.createScanEventRule(`${prefix}-event-rule`, this.repo, this.snsTopic);
this.createChatbot(`${prefix}-chatbot`, this.snsTopic);
}
}
デプロイ
cdk deploy -c slackWorkspaceId=<対象のSlackワークスペースID> -c slackChannelId=<対象のSlackチャンネルID>
作成リソース確認
ECR
push時にスキャンするECRリポジトリの作成確認
Chatbot
SNS
ChatbotがEndpointに設定されいるSNSの作成確認
EventBridge
下記イベントパターンのEventRuleの作成確認
- 対象リポジトリ、スキャンステータス、image-tagが合致
動作確認
- ECRにログイン
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
- tagを付けてECRにpush
docker tag amazon/aws-cli:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/sampletest-repo:v-1.0.0
docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/sampletest-repo:v-1.0.0
- Slack通知確認
- EventBridgeでも一応確認
同じ時間にトリガーされている事が確認出来ますね
まとめ
CDK(v2)でECRスキャン結果をSlackへ通知してみました
慣れてくるまでが大変でしたが扱えるようになってくると楽しい
今回はローカルからECRに直接pushしましたが、CodebuildやGitHub Actions経由であってもECRリポジトリにコンテナイメージが置かれたタイミングで自動的に基本スキャンが走るのでCI部分はお好みで
拡張スキャンも対応されたらそちらでもまた書いていきたいと思います。