Help us understand the problem. What is going on with this article?

CodePipelineのステータスをSlackへ通知

More than 1 year has passed since last update.

環境

  • ソースコードは GitHub で管理
  • ビルドは CodeBuild で実行し、dockerコンテナのイメージをECRに登録
  • デプロイ先は EC2インスタンスクラスタの ECS
  • CodePipelineで特定のブランチへのコミットをトリガーに、テスト環境へのビルド・デプロイを実行

課題

  • いつビルド・デプロイが開始されたのか?
  • いつデプロイが完了したのか?
  • CodePipelineのマネージドコンソールを見ないとわからない

目的

  • 開発メンバの誰もがテスト環境の構築状況を把握してもらいたい(デプロイ完了した?というのを無くす)
  • ビルドまたはデプロイで失敗した場合に放置されないようにしたい

解決方法

  • ビルド・デプロイの開始と完了(成功・失敗)を通知
  • 通知先はチーム内のコミュニケーションツール Slack

構成

Untitled Diagram.png

  1. CodePipelineのステータスをCloudWatchで監視
  2. Statusの変更があった場合に、Lambdaを起動
  3. Lambdaではステータス結果を、Slackへ通知

通知内容

ステータス

  • CloudWatchでは、パイプラインのステータス、ステージのステータス、アクションのステータスの監視が可能
  • パイプラインのステータスは、パイプラインの実行単位
  • ステージのステータスは、コード取得→ビルド→デプロイでパイプラインを組んであるなら、コード取得、ビルド、デプロイ毎の単位
  • アクションのステータスは、例えばビルド内のそれぞれのアクション単位

codepipeline_01.png

  • 今回はビルド・デプロイがいつ開始され完了されたかを知りたいので、パイプライン単位のステータスを通知

マネージドコンソールのURL

  • 詳細な状況をマネージドコンソールで確認できるようにするため、通知内にマネージドコンソールへのリンクをはる
  • しかし、CodePipelineでは実行単位の結果のみが表示される画面が存在しない
  • 実行単位の結果をマネージドコンソールから確認した場合は、View pipeline history のページがら参照可能

codepipeline_02.png

  • 実行結果への直リンクがないため、Slackへの通知には、CodePipelineのプロジェクト単位のページへのリンクと、実行IDも通知に含め、各自Historyから詳細結果を参照できるようにする

Slack

Incoming WebHooks

  • Post to Channel を入力し、Add Incoming WebHooks integration をクリック
    • Webhook URL: 動的に振られる。これをLambda側で呼び出す。
    • Post to Channel: Lambda側でChannelは指定するので、特に気にしなくてもいい
    • Customize Name: Slackに表示される名前(適当に)
    • Customize Icon: Slackに表示されるアイコン(適当に)

Lambda

関数の作成

  • 設計図: cloudwatch-alarm-to-slack-python3

基本的な情報

  • 名前: (任意)
  • ロール: テンプレートから新しいロールを作成
  • ロール名: (任意)
  • ポリシーテンプレート: KMSの復号化アクセス権限 (defaultのまま)

※Lambda作成後、関数の中でCodePipelineの情報を読み取るので、作成したロールに AWSCodePipelineReadOnlyAccess ポリシーを追加

SNSトリガー

  • 削除

Lambda関数のコード

lambda_function.py
import boto3
import json
import logging
import os

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

ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
SLACK_CHANNEL = os.environ['slackChannel']

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

CODEPIPELINE_URL = "https://{0}.console.aws.amazon.com/codepipeline/home?region={0}#/view/{1}"
GITHUB_URL = "https://github.com/{0}/{1}/tree/{2}"
CODEBUILD_URL = "https://{0}.console.aws.amazon.com/codebuild/home?region={0}#/projects/{1}/view"
ECS_URL = "https://{0}.console.aws.amazon.com/ecs/home?region={0}#/clusters/{1}/services/{2}/details"

SLACK_MESSAGE_TEXT = '''\
*{0}* `{1}` {3} <{2}|CodePipeline>
```execution_id: {4}```
{5}
'''

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

# ステータスアイコン
STATE_ICONS = {
    'STARTED': ':seedling:',
    'FAILED': ':sweat_drops:',
    'SUCCEEDED': ':rainbow::rainbow::rainbow:'
}

def lambda_handler(event, context):
    logger.debug("Event: " + str(event))

    region = event["region"]
    pipeline = event["detail"]["pipeline"]
    version = event["detail"]["version"]
    execution_id = event["detail"]["execution-id"]
    state = event["detail"]["state"]
    url = CODEPIPELINE_URL.format(region, pipeline)
    # 開始時はCodePipelineの詳細情報を追加
    detail = pipeline_details(pipeline, version, region) if state == 'STARTED' else ''

    # 通知内容
    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': SLACK_MESSAGE_TEXT.format(
            pipeline, state, url, STATE_ICONS.get(state, ''),
            execution_id, detail),
        'parse': 'none'
    }

    # 通知
    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の詳細を取得
def pipeline_details(name, version, region):

    codepipeline = boto3.client('codepipeline').get_pipeline(
        name = name, version = int(version))
    logger.debug("get-pipeline: " + str(codepipeline))
    stages = []
    for stage in codepipeline['pipeline']['stages']:
        actions = []
        for action in stage["actions"]:
            provider = action["actionTypeId"]["provider"]
            if provider == 'GitHub':
                action_url = GITHUB_URL.format(
                    action["configuration"]["Owner"],
                    action["configuration"]["Repo"],
                    action["configuration"]["Branch"])
                actions.append(":octocat: <{0}|{1}>".format(
                    action_url, provider))
            elif provider == 'CodeBuild':
                action_url = CODEBUILD_URL.format(
                    region,
                    action["configuration"]["ProjectName"])
                actions.append(":codebuild: <{0}|{1}>".format(
                    action_url, provider))
            elif provider == 'ECS':
                action_url = ECS_URL.format(
                    region,
                    action["configuration"]["ClusterName"],
                    action["configuration"]["ServiceName"])
                actions.append(":ecs: <{0}|{1}>".format(
                    action_url, provider))
            else:
                actions.append(provider)
        stages.append("_{0}_ ( {1} )".format(stage["name"], ' | '.join(actions)))

    return ' => '.join(stages)

環境変数

  • kmsEncryptedHookUrl: Webhook URL を入力
  • slackChannel: #your-channel(通知したいチャネル)

暗号化の設定

  • 伝送中の暗号化のためのヘルパーの有効化 をチェック
  • 伝搬中に暗号化するKMSキー 暗号化キーのarnまたはエイリアスを指定(未作成の場合は、IAM > 暗号化キー から作成)
  • 保管時に暗号化する KMS キー: (デフォルト) aws/lambda

※キー設定後、環境変数の kmsEncryptedHookUrl の暗号化をクリック

CloudWatch

ルールの作成

  • イベントソース

    • サービス名: CodePipeline
    • イベントタイプ: CodePipeline Pipeline Execution State Change
    • 特定の条件: CANCELED, FAILED, RESUMED, STARTED, SUCCEEDED
  • ターゲット

    • Lambda関数: (作成したLambdaの名前)

実行結果

開始時
codepipelne2slack_started.png

失敗時
codepipelne2slack_failed.png

成功時
codepipelne2slack_succeeded.png

まとめ

  • 絵文字入れているのは視覚的にわかりやすくなることを期待して
  • コード上にも記載してるが、ソースの取得もとは GitHub 前提で書いているので、CodeCommitやS3などが元になる場合は、適宜変更を加える
  • CodePipeline から取得する情報をステージ単位、アクション単位にしたい場合は、CloudWatchのイベントタイプで、CodePipeline Stage Execution State Change または CodePipeline Action Execution State Change を選択(サンプルイベントが表示されるのでそれを参考に作成)
  • CodeBuild等のステータス監視も同様に「CloudWatchでステータス監視→Lambda→Slack」の流れで可能

おまけ

CodeBuild to Slacck

  • CloudWatchではサービス「CodeBuild」イベントタイプ「CodeBuild Build State Change」で監視

Lambda関数のコード

lambda_function.py
import boto3
import json
import logging
import os

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

ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
SLACK_CHANNEL = os.environ['slackChannel']

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

CODEBUILD_URL = "https://{0}.console.aws.amazon.com/codebuild/home?{0}#/builds/{1}/view/new"

SLACK_MESSAGE_TEXT = '''\
*{0}* `{1}` <{2}|CodeBuild>
'''

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

def lambda_handler(event, context):
    logger.info("Event: " + str(event))

    region = event["region"]
    build_id = event["detail"]["build-id"].split('/')[1]
    url = CODEBUILD_URL.format(region, build_id)
    project = event["detail"]["project-name"]
    state = event["detail"]["build-status"]

    # 通知内容
    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': SLACK_MESSAGE_TEXT.format(project, state, url),
        'parse': 'none'
    }

    # 通知
    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)

実行結果
codebuild2.png

ECS to slack

  • CloudWatchではサービス「ECS」イベントタイプ「状態変更」(タスクのステータスECS Task State Changeのみ)で監視

Lambda関数のコード

lambda_function.py
import boto3
import json
import logging
import os

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

ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
SLACK_CHANNEL = os.environ['slackChannel']

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

TASK_URL = "https://{0}.console.aws.amazon.com/ecs/home?region={0}#/clusters/{1}/tasks/{2}/details"
TASK_DEFINITION_URL = "https://{0}.console.aws.amazon.com/ecs/home?region={0}#/taskDefinitions/{1}"
CONTAINER_INSTANCE_URL = "https://{0}.console.aws.amazon.com/ecs/home?region={0}#/clusters/{1}/containerInstances/{2}"

SLACK_MESSAGE_TEXT = '''\
*{0}* `{1}` {2} <{3}|Task> ( _<{4}|{5}>_ )
'''

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

STATE_ICONS = {
    'RUNNING': ':runner::runner:',
    'STOPPED': ':skull_and_crossbones:'
}

def lambda_handler(event, context):
    logger.debug("HOOK_URL: " + str(HOOK_URL))
    logger.debug("Event: " + str(event))

    region = event["region"]
    cluster_id = event["detail"]["clusterArn"].split('/')[1]
    container_instance_id = event["detail"]["containerInstanceArn"].split('/')[1]
    task_id = event["detail"]["taskArn"].split('/')[1]
    task_definition_id = event["detail"]["taskDefinitionArn"].split('/')[1]
    desired_status = event["detail"]["desiredStatus"]
    last_status = event["detail"]["lastStatus"]

    task_url = TASK_URL.format(region, cluster_id, task_id)
    task_definition_url = TASK_DEFINITION_URL.format(region, task_definition_id.replace(':', '/'))
    container_instance_url = CONTAINER_INSTANCE_URL.format(region, cluster_id, container_instance_id)

    # 期待するステータスになる過程のステータスは通知しない
    if desired_status != last_status:
        return

    # 通知内容
    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': SLACK_MESSAGE_TEXT.format(
            cluster_id,
            last_status,
            STATE_ICONS.get(last_status, ''),
            task_url,
            task_definition_url,
            task_definition_id)
    }

    # 通知
    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)

実行結果
ecs.png

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした