SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

SlackでChatOps!CodeDeployのBlue/Greenデプロイを操作する方法

スマートキャンプ、エンジニアの入山です。

昨年末から弊社BOXILでは、EC2からECS/Fargateへのインフラ移行作業を実施しています。

EC2運用からコンテナベースのECS運用への移行は、インフラの思想として異なる部分も多く、一筋縄ではいかないということを日々痛感しています。特に運用面に関する仕組みやノウハウは大きく異なっているため、今までと同等の運用を異なる方法で実現する必要があり、頭を悩ませることが多いです。

例えば、今まではEC2にSSHしてコマンドを投入していたが、ECS/Fargate上で同じことをどうやってやるのか…など。SSHしなくても良い運用にすることも必要ですが、今まで当たり前に出来ていたことが万が一の時にどうやっても出来なくなるのはやはり辛い問題ではないでしょうか。

また、とりあえず移行はできたけど、今までよりも運用に時間や手間が掛かるようになった…といったことも避けたいです。移行作業にあたっては、これらのマイナスのギャップを減らし、プラスのギャップが多くなるように気を付けて取り組んでいます。

今回は、移行作業の中でデプロイ時のフローに掛かる時間や手間を削減することを目的に、AWS CodeDeployのBlue/Greenデプロイの承認フローをSlackでChatOpsできるようにした話を紹介したいと思います!

背景

従来のEC2環境のデプロイフローはJenkinsで構成されており、作業者は以下の手順で操作を行いデプロイを行っていました。(作業者の操作が必要な箇所を抜粋)

1. Jenkinsへログイン
  - ID/Passwordによる認証
2. (Jenkins) Pre環境のデプロイ開始
3. (Jenkins) Prod環境のデプロイ開始
4. (Jenkins) デプロイの完了承認(旧バージョンの終了)

これに対して、ECS環境のデプロイフローはCircleCIとCodeDeploy(Blue/Greenデプロイ)で構成しており、作業者は以下の手順で操作を行う必要があります。

1. CircleCIでデプロイ用のTagを作成
2. AWSへログイン
  - ID/Passwordによる認証
  - 2要素認証
  - スイッチロール
3. (AWS) 新バージョンの本番昇格承認
4. (AWS) デプロイの完了承認(旧バージョンの終了)

操作が必要なステップ数はどちらも4ステップですが、AWSへのログインは2要素認証とスイッチロールが必要となります。そのため、従来よりもデプロイの際に手間がかかるという課題が出てきました。

この課題に対して、AWS上の承認フローをSlackで操作できたら当たり前に便利…ということで、SlackでのChatOpsを実装することにしました。

ChatOps実装後の手順は、以下となります。

1. CircleCIでデプロイ用のTagを作成
2. (Slack) 新バージョンの本番昇格承認
3. (Slack) デプロイの完了承認(旧バージョンの終了)

実装概要

CodeDeployのBlue/Greenデプロイにおける以下ステップに関する通知・操作をSlackで実施できるようにします。 (デプロイの開始処理は、他サービスをトリガーとする想定のため範囲外)

  • 【ステップ3】テストトラフィックルーティングのセットアップ完了後の待機
    • [操作] トラフィックの再ルーティング(Continue)
    • [操作] デプロイを停止(Stop)
  • 【ステップ5】: 本稼働トラフィックを置き換えタスクセットに再ルーティング完了後の待機
    • [操作] 旧タスクセットの終了(承認)
    • [操作] デプロイを停止(Stop)

構成

AWSの各サービス(SNS + Lambda, API Gateway + Lambda)とSlack App(Webhook, Intaractive Component)を連携させた、以下の構成となります。

f:id:mt_iri:20210106011920p:plain

実装

Slack App作成

Slackでは、メッセージ受信用のIncoming Webhookとコールバック用のInteractive Componentsを利用します。

Slack APIのApp管理ページから、新しいSlack Appを作成します。

f:id:mt_iri:20210106090458p:plain

Slack Appが作成されるとBasic Informationページが表示されます。このページから、Incoming WebhookやInteractive Componentsの設定ができます。

f:id:mt_iri:20210106091002p:plain

Webhook有効化

先程作成したSlack AppでIncoming Webhookを有効化します。

  1. Add features and functionalityからIncoming Webhookを選択し、Activate Incoming Webhooksをオンにします。機能が有効化されるとAdd New Webhook to Workspaceボタンが表示されます。

  2. Add New Webhook to Workspaceから、メッセージの投稿先チャンネルを選択し、Webhookを作成します。

  3. Webhookが作成されると、Webhook URLが表示されます。後述のメッセージ通知用のLambda関数にこのURLを設定します。

f:id:mt_iri:20210106092827p:plain

メッセージ通知処理(SNS + Lambda)

CodeDeployのイベントをSlackに通知するためのLambda関数を作成します。

シンプルなイベントの通知処理に加えて、操作が必要なフローに対応したイベントの場合は、コールバック用のインタラクティブメッセージボタンが表示されます。

前項で登場したSlackのWebhook URLを、環境変数SLACK_WEBHOOK_URLに設定します。

Python サンプルコード(メッセージ通知)

import os
import json
import urllib.request

SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']

def lambda_handler(event, context):
    # print("Received event: " + json.dumps(event))
    message = event['Records'][0]['Sns']['Message']

    message_dict = json.loads(message)
    deployment_id = message_dict['deploymentId']

    if "status" in message_dict:
        deployment_status = message_dict['status']
    elif "instanceStatus" in message_dict:
        deployment_status = message_dict['instanceStatus']

    slack_color = "good"
    slack_title = f"Deployment ID: {deployment_id} ({deployment_status})"
    slack_value = None
    slack_actions = None

    # Statusに合わせて通知内容を設定
    # テストトラフィックへのルーティング完了(READY)
    if deployment_status == "READY":
        slack_value = "新バージョンのデプロイが完了しました。\nリリース作業を継続する場合は、Continueを押してください。"
        action_type = "approval_ready"

    # 置き換えタスクセットへの再ルーティング(Succeeded)
    elif deployment_status == "Succeeded":
        slack_value = "新バージョンへのトラフィック切り替えが完了しました。\nリリース作業を完了する場合は、Continueを押してください。\n(旧バージョンのコンテナは停止されます)"
        action_type = "approval_termination"

    # デプロイ停止
    elif deployment_status == "ABORTED":
        slack_color = "danger"
        slack_value = "デプロイが中止されました"
    else:
        slack_color = "warning"

    if deployment_status == "READY" or deployment_status == "Succeeded":
        slack_actions = [
            {
                "name": "deployment",
                "text": "Continue",
                "style": "danger",
                "type": "button",
                "value": json.dumps({"actionType": action_type, "deploymentId": deployment_id}),
                "confirm": {
                    "title": "リリース継続確認",
                    "text": "リリース作業を継続します",
                    "ok_text": "Yes",
                    "dismiss_text": "No"
                }
            },
            {
                "name": "deployment",
                "text": "Stop",
                "type": "button",
                "value": json.dumps({"actionType": "stop_deployment", "deploymentId": deployment_id}),
                "confirm": {
                    "title": "リリース中止確認",
                    "text": "リリース作業を中止します",
                    "ok_text": "Yes",
                    "dismiss_text": "No"
                }
            }
        ]

    data = {
        "username": "CodeDeploy",
        "attachments": [
            {
                "color": slack_color,
                "fallback": "You are unable to promote a build",
                "callback_id": "wopr_game",
                "attachment_type": "default",
                "fields": [
                    {
                        "title": slack_title,
                        "value": slack_value
                    }
                ],
                "actions": slack_actions
            }
        ]
    }
    headers = {'content-type': 'application/json'}
    req = urllib.request.Request(SLACK_WEBHOOK_URL, json.dumps(data).encode('utf-8'), headers)
    with urllib.request.urlopen(req) as res:
        body = res.read()

次に、SNSトピックを作成し、上記Lambda関数を呼び出すサブスクリプションを作成します。

  • SNSトピック
    • タイプ: スタンダード
  • サブスクリプション
    • プロトコル: AWS Lambda
    • エンドポイント: 上記Lambda関数

CodeDeployのイベントトリガー設定

CodeDeployでのBlue/Greenデプロイ時の各イベントをSNSに発行する設定を行います。

デプロイに関する設定は、以下の通り設定されている前提として省略します。

  • デプロイ設定
    • デプロイタイプ: Blue/Green
    • Load balancer: テストリスナーあり
    • トラフィックの再ルーティング: トラフィックを再ルーティングするタイミングを指定します(例:1時間)

CodeDeployのコンソールから該当アプリケーションを選択後、デプロイグループの編集ページの詳細 – オプション項目から以下のトリガーを作成します。イベントは、デプロイ中に通知・操作したいタイミングに関連するイベントに絞っています。

  • トリガー
    • イベント: デプロイの停止デプロイの準備が完了しましたインスタンスの成功
    • SNS: 前項のSNSトピック

f:id:mt_iri:20210106155127p:plain

ここまでの設定完了後に、CodeDeployでBlue/Greenデプロイを実行すると、各イベントをトリガーに以下のような通知やボタンがSlackに表示されるようになります!

f:id:mt_iri:20210106162656p:plain

CodeDeploy操作処理(API Gateway + Lambda)

SlackのIntaractive Componentsからのコールバック結果に応じて、CodeDeployを操作するLambda関数を作成します。

CodeDeployの操作は、AWS SDKを利用したAPIコールによって行います。そのため、Lambdaの実行ロールにCodeDeployへのアクセス権限が必要となります。

CodeDeploy — Boto3 Docs 1.16.49 documentation

また、Slackからのリクエスト検証用のVerification Tokenを、環境変数SLACK_VERIFICATION_TOKENに設定します。Verification Tokenは、Basic InformationページのApp Credentials項目に表示されています。

Python サンプルコード(CodeDeploy操作)

import json
import os
import boto3

from base64 import b64decode
from urllib.parse import parse_qs

client = boto3.client('codedeploy')
SLACK_VERIFICATION_TOKEN = os.environ['SLACK_VERIFICATION_TOKEN']


#Triggered by API Gateway
def lambda_handler(event, context):
    # print("Received event: " + json.dumps(event))
    if event['isBase64Encoded']:
        body = parse_qs(b64decode(event['body']).decode())
    else:
        body = parse_qs(event['body'])

    payload = json.loads(body['payload'][0])
    action_value = json.loads(payload['actions'][0]['value'])

    # Validate Slack token
    if SLACK_VERIFICATION_TOKEN == payload['token']:
        if action_value['actionType'] == "approval_ready":
            approval_ready(action_value['deploymentId'])
            slack_text = "リリース作業の継続を受け付けました。トラフィックを切り替えます。"
        elif action_value['actionType'] == "approval_termination":
            approval_termination(action_value['deploymentId'])
            slack_text = "リリース作業の継続を受け付けました。旧バージョンのコンテナを停止します。"
        elif action_value['actionType'] == "stop_deployment":
            stop_deployment(action_value['deploymentId'])
            slack_text = "リリース作業の中止を受け付けました。"
        else:
            return {
                "isBase64Encoded": "false",
                "statusCode": 403,
                "body": "An error occurred."
            }

        return  {
            "isBase64Encoded": "false",
            "statusCode": 200,
            "body": slack_text
        }
    else:
        return  {
            "isBase64Encoded": "false",
            "statusCode": 403,
            "body": "This request does not include a vailid verification token."
        }


def approval_ready(deployment_id):
    response = client.continue_deployment(
                            deploymentId=deployment_id,
                            deploymentWaitType='READY_WAIT',
                        )
    print(response)

def approval_termination(deployment_id):
    response = client.continue_deployment(
                            deploymentId=deployment_id,
                            deploymentWaitType='TERMINATION_WAIT',
                        )
    print(response)

def stop_deployment(deployment_id):
    response = client.stop_deployment(
                            deploymentId=deployment_id,
                            autoRollbackEnabled=False,
                        )
    print(response)

次に、コールバックを受け取るためのAPI Gatewayを作成します。

今回は、HTTP APIで以下のように設定して作成します。

  • API
    • APIタイプ: HTTP API
    • 統合: Lambda
    • Lambda関数: 上記Lambda関数
  • ルート
    • メソッド: POST
    • リソースパス: (default)
    • 統合ターゲット: (default)
  • ステージ
    • ステージ名: $default (default)
    • 自動デプロイ: Enabled (default)

HTTP APIを指定することで、ステージのデプロイなどを気にすることなく、シンプルにAPI Gatewayを利用出来ます。(作成された時点でデプロイまで完了され、利用可能な状態になります。)

Interactive Components有効化

最後にSlack App側でInteractive Componentsを有効化します。

この設定により、Slack上のインタラクティブメッセージボタンの操作が、API Gatewayにリクエストされるようになります。

  1. Add features and functionalityからInteractive Componentsを選択し、Interactivityをオンにします。機能が有効化されるとRequest URL項目が表示されます。

  2. Request URL項目に、API GatewayのエンドポイントURLを入力します。

f:id:mt_iri:20210106164841p:plain

動作確認

CodeDeployでBlue/Greenデプロイを実行して、ステップ毎にSlackへ以下の通知が表示され、Slack側のボタン操作でCodeDeployのステップが進めば実装は完了です!

  • 【ステップ3】テストトラフィックルーティングのセットアップ完了後の待機

f:id:mt_iri:20210106165719p:plain

  • 【ステップ5】: 本稼働トラフィックを置き換えタスクセットに再ルーティング完了後の待機

f:id:mt_iri:20210106165831p:plain

終わりに

今回は、AWS CodeDeployのBlue/Greenデプロイの承認フローをSlackでChatOpsできるようにした話を紹介しました!

ChatOpsを上手く活用することで簡単に運用の効率化ができるだけでなく、運用の効率化は長期的に効果を発揮するため、検討する価値は大いにあるのではないでしょうか。 また、今回と同様の仕組み(AWS SDKからのAPIコール)を使うことで、CodeDeployだけでなく様々なAWSサービスを操作できるので、そちらも是非試してみてください!