ひたすら楽してCodePipelineのイベントをslackに通知する

2018.04.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

中山(順)です。

クラスメソッドにジョインして半年。ジョインするまでは全く縁のないサービスを扱うことも非常に多く、毎日が勉強です。

今日はタイトルの通り、CodePipelineのイベントをslackに通知するということをやってみようと思います。

また、今回はひたすら楽してこれを実現することを心がけました。もう少し具体的に言うと、すでにできあがったものをできるだけ流用しました。

今回の材料

CodePipelineとは?

開発において、コードを書いて実行環境上で動作するまでには、アプリケーションのビルド・テスト・デプロイなどいくつかの手順を経ます。

CodePipelineを利用することを、それら一連の処理の実行を自動化することができます。

AWS CodePipeline は、ソフトウェアをリリースするために必要な手順のモデル化、視覚化、および自動化に使用できる継続的な配信サービスです。ソフトウェアリリースプロセスのさまざまなステージをすばやくモデル化して設定できます。AWS CodePipeline は、ソフトウェアの変更を継続的にリリースするために必要なステップを自動化します。

AWS CodePipeline とは ?

CloudWatch Events

多くのメンバーで開発を行う場合、CodePipelineによる処理がいつ実行され、そのような結果になったのかを把握したくなると思います。

CloudWatch Eventsを利用することで、CodePipelineに関するイベントを検知し、何らかの処理を実行できます。

Amazon CloudWatch Events は、Amazon Web Services(AWS) リソースの変更を示すシステムイベントのほぼリアルタイムのストリームを提供します。すぐに設定できる簡単なルールを使用して、ルールに一致したイベントを 1 つ以上のターゲット関数またはストリームに振り分けることができます。オペレーションの変更が発生すると、CloudWatch イベント はその変更を認識します。CloudWatch イベント は、オペレーションの変更に応答し、必要に応じて、応答メッセージを環境に送り、機能をアクティブ化し、変更を行い、状態情報を収集することによって、修正アクションを実行します。

Amazon CloudWatch Events とは?

CloudWatch Eventsが何らかのイベントを検知すると、イベントの詳細を含むイベントメッセージを使って何らかの処理を実行できます。 イベントメッセージの形式は、イベントの種類によって様々です。

CodePipelineにおけるステージ実行の状態変更の場合は以下のような形式になります。(公式ドキュメントより参照)

{
  "version": "0",
  "id": "CWE-event-id",
  "detail-type": "CodePipeline Stage Execution State Change",
  "source": "aws.codepipeline",
  "account": "123456789012",
  "time": "2017-04-22T03:31:47Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:codepipeline:us-east-1:123456789012:pipeline:myPipeline"
  ],
  "detail": {
    "pipeline": "myPipeline",
    "version": "1",
    "execution-id": "01234567-0123-0123-0123-012345678901",
    "stage": "Prod",
    "state": "STARTED"
  }
}

AWS CodePipeline イベント

slack incoming-webhook

チーム開発におけるコミュニケーションツールとして、slackはほぼデファクトですね。今回はslackにイベントの内容を投稿したいと思います。

slackに対してメッセージを簡単に投稿するためにincoming-webhookを利用します。詳細な設定手順などは公式ドキュメントをご確認ください。

Slack での着信 WebHook の利用

Incoming Webhooks

Serverless Application Repository

今回のメインテーマである「ひらすら楽して」の部分を実現するために、Serverless Application Repository(以下、SAR)を利用します。

SARは、サーバーレスアプリケーションをみんなで共有してひたすら楽しようぜ!というサービスです、たぶん。

今回実装したいことをどうやって実現するかを考えたとき、slackへの通知部分を実装するのがめんどくさそうだなって思いました。 で、SARでそれっぽいのが公開されてないか探したところ、ちょうどいいものがありました。 CloudWatch Alarmの内容をSNSおよびLambda経由でslackに通知してくれるようです。 このアプリで作成されるSNS TopicをCloudWatch Alarmで設定すると通知ができる、という代物のようです。

javascriptおよびpythonでLambda Functionが書かれたものが用意されていました。やってることはどれも同じです。 ただし、Lambdaの受信するメッセージがCloudWatch Alarmのメッセージの形式であることを前提としていました。 逆に、この部分をCodePipeline用に修正してあげるだけで使えそうです。

ちなみに、awslabsにコードが公開されていますので、他のサンプルアプリも含めて参考にしていただくといいのではないでしょうか?

awslabs/serverless-application-model

https://github.com/awslabs/serverless-application-model/tree/master/examples/apps/cloudwatch-alarm-to-slack

https://github.com/awslabs/serverless-application-model/tree/master/examples/apps/cloudwatch-alarm-to-slack-python

https://github.com/awslabs/serverless-application-model/tree/master/examples/apps/cloudwatch-alarm-to-slack-python3

やってみた

構成図

これから作るものはこんな感じです。

作業の流れ

作業の流れはおおよそ以下のような感じです。

  • 前準備
    • KMSキーの作成
    • incoming-Webhook URLの発行
  • サーバーレスアプリケーションのデプロイ
  • サーバーレスアプリケーションの修正
  • イベントルールの作成
  • 動作確認

CodePipelineにおけるパイプラインの作成はすでに行われているものとします。

KMSキーの作成

Lambda Functionの環境変数にWebhook URLを設定するのですが、その際の暗号化に利用します。 同時に、Lambda Functionの実行時にWebhook URLを復号するためにも利用します。 そのために、前準備として作成します。 "Key-Id"は後で利用するのでメモしておいてください。

aws kms create-key \
    --description "Developers.IO"
{
    "KeyMetadata": {
        "Origin": "AWS_KMS",
        "KeyId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "Description": "Developers.IO",
        "KeyManager": "CUSTOMER",
        "Enabled": true,
        "KeyUsage": "ENCRYPT_DECRYPT",
        "KeyState": "Enabled",
        "CreationDate": 1522595642.592,
        "Arn": "arn:aws:kms:ap-northeast-1:XXXXXXXXXXXX:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "AWSAccountId": "XXXXXXXXXXXX"
    }
}

incoming-Webhook URLの発行

App ディレクトリに移動し、incoming-Webhook URLを発行しましょう。 以下のURLに移動し、着信Webフックの設定を行います。

https://(Work space name).slack.com/apps

「設定を追加」をクリックします。

メッセージを投稿したいチャンネルを選択し、「着信Webフックインテグレーションの追加」をクリックします。

Webhook URLが生成されますので、これをコピーします。 後で利用するのでメモしておいてください。

サーバーレスアプリケーションのデプロイ

Lambdaのコンソールに移動し、「関数の作成」をクリックします。

最初に、「サーバーレスアプリケーションのリポジトリ」をクリックします。 その後、「cloudwatch-alarm-to-slack」で検索します。 今回は、「cloudwatch-alarm-to-slack-python3」を利用します。 AWS謹製で、なんとなく安心感があります。

"Application name"に任意のアプリケーション名を設定します。 "KeyIdParameter"に前準備で作成した鍵のIDを設定します。 パラメーターを設定したら、「Deploy」をクリックします。

しばらくするとデプロイが完了します。

KeyIdParameterに設定するIDが同時に作成されるIAM Roleのポリシーに反映されるはずなのですが、なぜか反映されませんでした。CFnテンプレートに問題があるのかもしれません。めんどうですが、Lambda Function用のIAM Roleのポリシーを編集し、前準備で作成したKMSキーによる復号権限を付与してください。Resourceの部分にKMSキーのARNを設定すればOKのはずです。

サーバーレスアプリケーションの修正

環境変数の設定

Lambda Functionの設定画面に移動します。 すると、何かエラーが出ているかと思います。 下の方にスクロールすると、どうやら環境変数部分の設定が足りていないようです。

まず、Webhook URLを入力します。 この際、KMSで暗号化された状態で保存します。 手順については以下の画像を見てください。

最後にメッセージを投稿するチャンネル名を設定し、一旦「保存」をクリックします。

コードの修正

次に、コードを修正します。

既存のコードはCloudWatch Alarmのメッセージを処理する前提のコードとなっています。 具体的には、以下のハイライトされた部分を修正する必要があります。

  • イベントメッセージから必要な情報を抽出(27~30行目)
  • slackに投稿するメッセージを生成(34行目)
import boto3
import json
import logging
import os

from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError


# The base-64 encoded, encrypted key (CiphertextBlob) stored in the kmsEncryptedHookUrl environment variable
ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
# The Slack channel to send a message to stored in the slackChannel environment variable
SLACK_CHANNEL = os.environ['slackChannel']

HOOK_URL = "https://" + boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL))['Plaintext'].decode('utf-8')

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))

    alarm_name = message['AlarmName']
    #old_state = message['OldStateValue']
    new_state = message['NewStateValue']
    reason = message['NewStateReason']

    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': "%s state is now %s: %s" % (alarm_name, new_state, reason)
    }

    req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

今回、CodePipelineのステージ実行の状態変更を契機にslackへメッセージを投稿したいと思います。 その際に発行されるイベントメッセージの形式は以下のとおりです。(再掲)

{
  "version": "0",
  "id": "CWE-event-id",
  "detail-type": "CodePipeline Stage Execution State Change",
  "source": "aws.codepipeline",
  "account": "123456789012",
  "time": "2017-04-22T03:31:47Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:codepipeline:us-east-1:123456789012:pipeline:myPipeline"
  ],
  "detail": {
    "pipeline": "myPipeline",
    "version": "1",
    "execution-id": "01234567-0123-0123-0123-012345678901",
    "stage": "Prod",
    "state": "STARTED"
  }
}

今回、コードを以下のように修正しました。

import boto3
import json
import logging
import os

from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError


# The base-64 encoded, encrypted key (CiphertextBlob) stored in the kmsEncryptedHookUrl environment variable
ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
# The Slack channel to send a message to stored in the slackChannel environment variable
SLACK_CHANNEL = os.environ['slackChannel']

HOOK_URL = "https://" + boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL))['Plaintext'].decode('utf-8')

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))

    region = message['region']
    pipeline_name = message['detail']['pipeline']
    stage_name = message['detail']['stage']
    state = message['detail']['state']

    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': "State %s (Pipeline %s) is now %s : https://%s.console.aws.amazon.com/codepipeline/home?region=%s#/view/%s" % (stage_name, pipeline_name, state, region, region, pipeline_name)
    }

    req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

コードを修正したら、「保存」をクリックしましょう。

イベントルールの作成

最後にイベントルールを作成します。

CloudWatch Eventsのコンソールに移動し、「ルールの作成」をクリックします。

「イベントパターン」を選択し、サービス名に「CodePipeline」、イベントタイプに「CodePipeline Stage Execution State Change」を指定します。

併せて、ターゲットに「SNSトピック」、トピックにSARでLambda Functonと同時に作成したSNS Topicを、入力の設定に「一致したイベント」を選択します。

設定できたら、「設定の詳細」をクリックします。

最後に、名前と説明を入力し、「ルールの作成」をクリックします。

これで作業は完了です。

動作確認

すでにあるパイプラインでリリースを実行してみます。

今回利用するパイプラインは、「Source」「Build」「Staging」の3つのステージで構成されています。

リリースしたところ、以下のようにslackにメッセージが投稿されました。(Buildが失敗していますが、今回はそこが目的ではないので無視します。)

まとめ

ひたすら楽してslackへの通知を行ってみました。いかがでしょうか?

CI/CDだとか、DevOpsだとか、自動化だとか聞くと、敷居が高いとか思ってしまったりしていませんか? そういう方が結構いるのではないかと思いますが、簡単に始める事ができるようにCode兄弟のようなマネージドサービスやSARによる様々なアプリケーションがあります。

モジモジしてる暇があったら、はじめましょう。

現場からは以上です。