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

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

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

![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137322/494e995f-ea46-967d-707c-4df0c262b993.png)

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

![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137322/69b423b1-0e92-121e-95a7-0c2b5c7f7cc5.png)

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

## 構成
![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137322/6cf9287a-9973-7672-5428-ff4dcaad15c8.png)


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

## 作成手順
### 作成するresourceの全体像
![Untitled Diagram (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137322/dcecbffc-e7a5-0144-fef8-16a4e36fe69d.png)
これで全てではないですが、全体像はこんな感じです。（codebuildは作成してある前提です）
こちらの左のほうから順に作って行きます💪

### event ruleの作成
```python: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` で指定します。

```python: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の作成
```python: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}"
}
```

```python: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関数に送っています。

```python: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については、 [公式ドキュメント](https://api.slack.com/docs/message-attachments) のシュミレータを利用して良い感じに組み立てました。

また、PRリンクを組み立てる部分で、boto3を使ってcodebuildのAPIをたたいて詳細ログを取得しています。cloudwatchからもらえる情報にはどのPRで落ちたのか？という情報が含まれていなかった為です。

```python: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` を作成しておきましょう。

```python: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の分かりやすい記事](https://dev.classmethod.jp/cloud/aws/iam-role-and-assumerole/)）

```python: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だけだよ！という権限設定をします。

```python: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に与えます。

```python: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に紐付けます。

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137322/8bb9da59-a0a8-14de-c64b-465cca38170f.png)

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