codepipelineの承認をslack上で行う

## Codepipeline上で承認を行っていた
今回、社内から要望が上がってきたので備忘録として記載しておく。これまではCodepipelineで本番環境にDeployする前段階で以下のような「承認」を経てデプロイしていた。

ところがAWSのコンソールに入るまでに「二段階認証を行うのは面倒」ということでSlack上に「承認」を行うボタンを表示させ、そこをポチったら承認フローが完了するようにした。

ただし、slackのchannnelは「管理者」のみのプライベートにして無闇矢鱈に誰でもポチポチできるような仕組みは排除するようにした。

![ScreenShot 2020-09-09 12.13.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/210ca3c0-ea6e-6951-7653-a419bc223e46.png)

## 仕組みを考える

codepipleline上のApprovalから直接何かしらの手順で投げれれば良かったが投げれないため
① SNS通知させた。
②・③ その通知をlambdaのトリガーとしてslackに通知させる
④・⑤・⑥ slackで承認ボタンを押下した際にAPIGatewayにPOSTさせてlambdaを発動させえ最終的にCodepipelineの「承認」を実施する

![slack承認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/7954cd8e-40e4-6bc3-e116-664c6dd25feb.png)


# ①のSNS通知から作る
まずは、SNSのトピックから作成した。ここは特に難しい部分はない。

![ScreenShot 2020-09-09 12.38.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/acb47c1d-5d80-3335-9b22-8de3c9f425d6.png)

次にCodepipelineの承認の設定。ここは作成したSNSトピックを指定するだけ。

![ScreenShot 2020-09-09 13.24.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/0c547fe8-50cb-e010-9441-2784f5ab889e.png)

## Slackの設定を行う
最初に  [Slackアプリの設定](https://api.slack.com/apps)でアプリの設定を行う。

新規のアプリの作成を行う。名前は適当にわかり易い名前で
![ScreenShot 2020-09-09 16.52.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/c0ff7c18-b04b-fea8-af3b-83ed1f7d6cfd.png)


このあたりをポチポチやっていく。どこのchannelにあてがうのか?とか設定があるが感覚でわかる。
![ScreenShot 2020-09-09 16.56.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/9b2aeb30-e712-de0c-36a1-b18ad8fc9022.png)

次にTokenを取得する。

![ScreenShot 2020-09-09 16.48.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/b76674ff-b4ad-0bb2-4fd8-1286c955890a.png)

最後に赤枠の中の「Vertification Token」を取得
![ScreenShot 2020-09-09 16.54.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/9b6f339c-7778-dfb4-2c38-99aec6c7ffa1.png)

## lambdaの作成
ようやく下準備ができたところでlambdaを作成する。

lambdaの環境変数を設定する。今回は、値の部分は暗号化をしていないが暗号化しておいたほうがベター。

![ScreenShot 2020-09-15 11.24.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/f38fa153-8fc2-75b8-cb21-fd7cad255032.png)


自分のMACで実施した作業ログ(ライブラリの導入)

```
$ mkdir data
$ docker run -it -v $PWD/data:/data --rm python:3.7 /bin/bash
$ cd /data
$ pip3 install slackclient -t ./
```

## スクリプト作成
今回はライブラリに記載のあった以下のapi_callを使おうかと思う。

```
import logging
import json
import os
import boto3
import traceback

from slack import WebClient
from slack.errors import SlackApiError


def lambda_handler(event, context):
    token = os.environ["SLACK_API_TOKEN"]
    channel_id = os.environ["channel_id"]

    client = WebClient(token=token)

    message = event["Records"][0]["Sns"]["Message"]
    data = json.loads(message)
    token = data["approval"]["token"]
    codepipeline_name = data["approval"]["pipelineName"]

    if codepipeline_name == "ml-age-api":
        msg_text = "```承認してもよろしいでしょうか?```"

    attachments_json = [
        {
            "fallback": "Upgrade your Slack client to use messages like these.",
            "color": "#258ab5",
            "attachment_type": "default",
            "callback_id": "the_greatest_war",
            "actions": [
                {
                    "name": "ok",
                    "text": "承認",
                    "value": token + ',' + codepipeline_name,
                    "style": "primary",
                    "type": "button",
                    "confirm": {
                        "title": "承認しますか?",
                        "text": "本当によろしいですか?",
                        "ok_text": "OK",
                        "dismiss_text": "Cancel"
                    }
                },
                {
                    "name": "cancel",
                    "text": "キャンセル",
                    "style": "danger",
                    "value": token + ',' + codepipeline_name,
                    "type": "button"
                }
            ]
        }
    ]

    try:
        response = client.chat_postMessage(
            channel=channel_id,
            text=msg_text,
            attachments=attachments_json
        )

        assert response["ok"]
    except Exception:
        print(traceback.format_exc())

```

ここで、ポイントとなる部分は下記の部分である。

```
"value": token + ',' + codepipeline_name,
```

「承認」をする際に必要な `put_approval_result` を見ていただくと分かる通り `pipelineName` と `token` が必要になる。よってここで送ってしまえと思いこのようにした。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codepipeline.html#CodePipeline.Client.put_approval_result

さらにここには理由があり複数のCodepipelineが動いているためlambdaをその都度作りたくなく出来れば使いまわしをしたかったためである。

## 承認用のlambdaの作成
上記は、slackにボタンを送信するためのlambdaで今回はAPIGatewayから「承認」または「却下」を受け取ってCodepipelineにわたすためのlambdaである。

```
# coding: utf-8
import json
import urllib.request
import boto3

stage_name = "Approval"
action_name = "Approval"

def lambda_handler(event, context):
    client = boto3.client("codepipeline")
    msg = event["body"]  # httpでpostされたpayloadの格納

    if not msg:
        return {
            'statusCode': 403,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': "Forbidden"
        }

    msg_decode = urllib.parse.unquote(msg)
    msg_replace = msg_decode.replace("payload=", '')
    new_msg = json.loads(msg_replace)

    token = new_msg["actions"][0]["value"]
    approval_data = token.split(',')
    pipeline_name = approval_data[1]
    token = approval_data[0]

    if new_msg["actions"][0]["name"] == "ok":
        client.put_approval_result(
            pipelineName=pipeline_name,
            stageName=stage_name,
            result={
                'summary': 'Approval',
                'status': 'Approved',
            },
            token=token,
            actionName=action_name
        )

        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': "```承認されました```"
        }

    else:
        client.put_approval_result(
            pipelineName=pipeline_name,
            stageName=stage_name,
            result={
                'summary': 'Rejected',
                'status': 'Rejected',
            },
            token=token,
            actionName=action_name
        )

        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': "```却下されました```"
        }

```

下記の2つはCodepipeline上のStageの名前とActionの名前
```
stage_name = "Approval"
action_name = "Approval"
```

この部分
↓
![ScreenShot 2020-09-15 15.01.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/6dd6675b-25a2-06ff-d782-b83a0a37db79.png)

次にこのlambdaに
「AWSCodePipelineApproverAccess」
を付与する


##APIGatewayの設定
APIGatewayはPostメソッドで作成を行う。統合リクエストでLambda-Proxyを選択して先に作った。APIGatewayから受け取るLambdaに指定する。

![ScreenShot 2020-09-15 11.18.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/85089949-d041-4ca1-1f0e-793834c706eb.png)

あとは、APIをデプロイして終了。ここでデプロイしたURLを控える。

### Slackの設定
httpsでAPIGatewayにPOSTするために以下の設定を行う。

![ScreenShot 2020-09-15 11.14.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/9e116073-f19e-3fe0-7983-e0a07c920f5f.png)

## 実行してみる

![ScreenShot 2020-09-15 15.05.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79473/a2c4225b-5802-6540-a21d-d2a765e2d763.png)

こんな感じで出て「承認」と「キャンセル」をポチって確認終了。

## 投稿時のエラー

承認をslack投稿時に以下のようなエラーが出る場合がある

```
{ "ok": false, "error": "not_in_channel" }
```

この場合、slackの投稿したいchannelで

```
/invite @ボット名
```
とすればchannelにボットが追加されて投稿ができるようになる。