LoginSignup
12
5

More than 3 years have passed since last update.

codebuildのbuild結果をSlackに通知できるようにする(terraform)

Last updated at Posted at 2019-05-05

背景

先月から新しいプロジェクトを開発し始め、CICDの機構としてcode pipelineを採用しました。(やってみたかった!!!!)
コードがpushされるとtestのprojectが走り、masterにmergeされるとbuild→deployのprojectが走るようにしています。

そこでなのですが、いざ開発!となったときに、凄まじいスピードでmergeされていく為build結果を常にawsコンソール上で追うのが厳しくなってきました。
という経緯があり、Slack上でカジュアルにbuild結果を確認できるようにしました。
こういうのはSlackに通知するのがやっぱり便利!

image (1).png

testのprojectは、どのPRで落ちているのか参照しやすいようにPRリンクを表示するようにしました🎉

image (2).png

失敗の場合、開発者はアクションを取りたいはずなので@hereにしています。
失敗理由もここで確認でき、 view log を押すとcloud watch logsで詳細ログを確認できるようにしました😀

構成

Untitled Diagram.png

①build projectが走る
②build結果がcloud watchに通知される
③event ruleがbuild stateの変更を監視している
④変更を検知したらlambda関数を呼び出す
⑤Slackに通知される

作成手順

作成するresourceの全体像

Untitled Diagram (2).png
これで全てではないですが、全体像はこんな感じです。(codebuildは作成してある前提です)
こちらの左のほうから順に作って行きます💪

event ruleの作成

cloudwatch.tf
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 で指定します。

cloudwatch.tf
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の作成

lambda.tf
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}"
}
lambda.tf
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関数に送っています。

ci_cd_notify.py
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で落ちたのか?という情報が含まれていなかった為です。

ci_cd_notify.py
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 を作成しておきましょう。

cloudwatch.tf
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の分かりやすい記事

iam.tf
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だけだよ!という権限設定をします。

iam.tf
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に与えます。

iam.tf
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に紐付けます。

iam.tf
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です🙆
image.png

これで晴れてSlackに通知できるようになりました🎉
Slackトリガーでbuildが失敗したときだけawsコンソールを確認しに行けばいいので、
安心して開発に集中できますね👍

12
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5