背景
先月から新しいプロジェクトを開発し始め、CICDの機構としてcode pipelineを採用しました。(やってみたかった!!!!)
コードがpushされるとtestのprojectが走り、masterにmergeされるとbuild→deployのprojectが走るようにしています。
そこでなのですが、いざ開発!となったときに、凄まじいスピードでmergeされていく為build結果を常にawsコンソール上で追うのが厳しくなってきました。
という経緯があり、Slack上でカジュアルにbuild結果を確認できるようにしました。
こういうのはSlackに通知するのがやっぱり便利!
testのprojectは、どのPRで落ちているのか参照しやすいようにPRリンクを表示するようにしました🎉
失敗の場合、開発者はアクションを取りたいはずなので@hereにしています。
失敗理由もここで確認でき、 view log
を押すとcloud watch logsで詳細ログを確認できるようにしました😀
構成
①build projectが走る
②build結果がcloud watchに通知される
③event ruleがbuild stateの変更を監視している
④変更を検知したらlambda関数を呼び出す
⑤Slackに通知される
作成手順
作成するresourceの全体像
これで全てではないですが、全体像はこんな感じです。(codebuildは作成してある前提です)
こちらの左のほうから順に作って行きます💪
event ruleの作成
resource "aws_cloudwatch_event_rule" "lambda" {
name = "ci_cd_notify"
description = "codebuildのbuild結果をslack通知します"
event_pattern = <<PATTERN
{
"source": [
"aws.codebuild"
],
"detail-type": [
"CodeBuild Build State Change"
],
"detail": {
"build-status": [
"FAILED",
"STOPPED",
"SUCCEEDED"
]
}
}
PATTERN
}
codebuildの結果として "FAILED","STOPPED","SUCCEEDED" が通知されたときに、この event rule
が発火するようになります。
そして、発火した後どこのリソースにアクションしに行くのか?というところを event target
で指定します。
resource "aws_cloudwatch_event_target" "lambda" {
rule = "${aws_cloudwatch_event_rule.lambda.name}" #先ほど作成したrule
target_id = "ci_cd_notify"
arn = "${aws_lambda_function.ci_cd_notify.arn}" #このあと作成します
}
これで event rule
が発火するとlambda関数が呼び出されるようになりました🎉
lambdaの作成
resource "aws_lambda_function" "ci_cd_notify" {
function_name = "ci_cd_notify"
handler = "ci_cd_notify.lambda_handler"
role = "${aws_iam_role.role_for_lambda.arn}" #このあと作成します
runtime = "python3.7"
filename = "${data.archive_file.lambda_data.output_path}"
source_code_hash = "${data.archive_file.lambda_data.output_base64sha256}"
}
data "archive_file" "lambda_data" {
type = "zip"
source_file = "${path.module}/lambda_py/ci_cd_notify.py"
output_path = "${path.module}/lambda_py/ci_cd_notify.zip"
}
lambdaのpythonファイルは、lambda.tfと同じ階層にlambda_pyという名前のディレクトリを用意し、その配下に置きます。
handler
まずはhandlerの内容です👇
第一引数のeventは、cloudwatchが渡してくれる情報、つまり codebuildのbuildが終わったよ!という情報 になります。
build結果や、source情報(誰がcodebuildを発火したのか)やbuild時のログが含まれています。それらを使って、Slack上に表示したい情報のみ切り出して、post_slack関数に送っています。
import json
import urllib.request
import urllib.parse
import boto3
def lambda_handler(event, context):
source_version = ''
phase_txt = ''
print(event)
project = event['detail']['project-name']
status = event['detail']['build-status']
additional_info = event['detail']['additional-information']
build_id = additional_info['logs']['stream-name']
log_link = additional_info['logs']['deep-link']
phases = additional_info['phases']
for phase in phases:
if 'phase-status' in phase:
if phase['phase-status'] == 'FAILED' and 'phase-context' in phase:
phase_txt = phase['phase-context']
post_slack(build_id, project, status, phase_txt, log_link)
Slackに送る部分
Slackのattchmentについては、 公式ドキュメント のシュミレータを利用して良い感じに組み立てました。
また、PRリンクを組み立てる部分で、boto3を使ってcodebuildのAPIをたたいて詳細ログを取得しています。cloudwatchからもらえる情報にはどのPRで落ちたのか?という情報が含まれていなかった為です。
def post_slack(build_id, project, status, phase_txt, link):
# test build projectの場合はPRlinkを作成する
pr_link = ''
if project == 'web-test' or project == 'api-test':
client = boto3.client('codebuild')
response = client.batch_get_builds(
ids=[
'%s:%s' % (project, build_id),
]
)
print(response)
for build in response['builds']:
if 'sourceVersion' in build:
version = build['sourceVersion']
if version.startswith('pr'):
link_end = version.replace('pr', 'pull')
pr_link = 'https://github.com/xxx/yyy/%s' % link_end
data_dict = {"channel": "#channel_name", "text": get_text(status, project), "attachments": [{ \
"author_name": project, \
"author_link": "https://ap-northeast-1.console.aws.amazon.com/codesuite/codebuild/projects/%s/history" % (project), \
"title": pr_link, \
"title_link": pr_link, \
"text": phase_txt, \
"color": get_color(status), \
"actions": [{"type": "button", "text": "view log", "url": link}] \
}]}
data = json.dumps(data_dict).encode()
headers = {'Content-Type': 'application/json'}
request = urllib.request.Request(
"https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxx",
data=data,
headers=headers,
method="POST"
)
with urllib.request.urlopen(request, data=data) as response:
response_body = response.read().decode('utf-8')
print(response_body)
def get_text(status, project):
if status == 'FAILED':
return '<!here> %s が失敗しました😢' % project
else:
return ''
def get_color(status):
if status == 'FAILED':
return 'danger'
else:
return 'good'
後は、lambdaのログも確認したいので、cloud watchの log group
を作成しておきましょう。
resource "aws_cloudwatch_log_group" "lambda_logs" {
name = "/aws/lambda/${aws_lambda_function.ci_cd_notify.function_name}"
retention_in_days = 14
}
assume roleの設定(lambda自身のお話)
lambdaがassume roleを実行できるようにします。
このiamリソースは、先ほどlambdaを作成したとき既に紐づけました。
(余談:assume roleの分かりやすい記事)
resource "aws_iam_role" "role_for_lambda" {
name = "ci_cd_notify"
assume_role_policy = "${data.aws_iam_policy_document.lambda_assume_policy.json}"
force_detach_policies = false
}
data "aws_iam_policy_document" "lambda_assume_policy" {
statement {
sid = ""
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
permmissionの設定(event rule→lambda)
lambda関数を呼び出せるのはevent ruleだけだよ!という権限設定をします。
resource "aws_lambda_permission" "allow_cloudwatch" {
statement_id = "AllowExecutionFromCloudWatchEventRule"
action = "lambda:InvokeFunction"
function_name = "${aws_lambda_function.ci_cd_notify.function_name}"
principal = "events.amazonaws.com"
source_arn = "${aws_cloudwatch_event_rule.lambda.arn}"
}
policyの設定(lambda→cloud watch &codebuild)
lambdaがcloud watchにログを出力することを許可する、また、codebuildのログを取得することを許可する権限をlambdaに与えます。
data "aws_iam_policy_document" "for_notify_lambda" {
statement {
effect = "Allow"
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = ["arn:aws:logs:*:*:*"]
}
statement {
effect = "Allow"
actions = ["codebuild:*"]
resources = [
"arn:aws:codebuild:ap-northeast-1:417025923863:project/api-test",
"arn:aws:codebuild:ap-northeast-1:417025923863:project/web-test",
]
}
}
resource "aws_iam_policy" "for_notify_lambda" {
name = "notify_lambda"
policy = "${data.aws_iam_policy_document.for_notify_lambda.json}"
}
このpolicyをattachmentでlambdaに紐付けます。
resource "aws_iam_role_policy_attachment" "for_notify_lambda" {
role = "${aws_iam_role.role_for_lambda.name}"
policy_arn = "${aws_iam_policy.for_notify_lambda.arn}"
}
applyを実行し、awsコンソール上のlambdaの画面で以下のような図が表示されていればOKです🙆
これで晴れてSlackに通知できるようになりました🎉
Slackトリガーでbuildが失敗したときだけawsコンソールを確認しに行けばいいので、
安心して開発に集中できますね👍