デプロイ対象環境ごとに別々のSlackチャンネルに通知するGitHub Actionsの実装例

SREチームの長田です。

SRE関連の記事としては今年最初の記事になります。 今年も定期的にSREチームメンバーによる記事を投稿していく予定です。 よろしくお願いします。

さて、今回はGitHub Actionsのはなしです。

TL;DR

デプロイを実行するGitHub Actionsの実行状況を デプロイ対象環境ごとに別々のSlackチャンネルに通知する場合の実装例として、 「slackapi/slack-github-actionで通知をつくりこむ」 「Actions Workflowを分ける」 「Actions Workflow実行の入り口を分ける」 の3つを紹介します。

背景

カヤックでは「まちのコイン」という地域通貨サービスを開発・運用しています。

coin.machino.co

まちのコインの開発・運用チームの、特にサーバーサイドに関しては、 アプリケーションやインフラ構成の変更を、こまめに本番環境にデプロイしています。 このため、デプロイ操作を実施する頻度が多く、多いときには日に数回のデプロイを行うこともあります。

本記事はこのまちのコインの開発・運用チームでデプロイ操作を自動化する取り組みから得られた知見の紹介となります。

GitHub Actionsによるデプロイ

デプロイ操作の定型化手段として、GitHub Actions(以降Actionsと表記)を利用することがあります。 ActionsでWorkflowを定義しておけば、誰がやっても同じ手順でデプロイできます。

デプロイ操作なので、当然本番環境を変更することになります。 自動化されているとはいえ、本番環境の変更操作は逐一状況を把握したいものです。

また、他の開発・運用メンバーにも状況を知らせたいという要求もあります。 状況の報告はこまめに行なったほうが、作業当事者以外に安心感を与えられます。 無言で本番操作している様子は、傍から見ているメンバーからすると不安でしかありません。

とはいえ、Actions Workflowの実行ログをじっと眺めているのも手間ですし、 逐一他のメンバーに手動報告するのも面倒です *1。 せっかくデプロイ手順を自動化しているのに、状況共有だけは手動というのもおかしな話です。 カヤックでは主なコミュニケーション手段としてSlackを利用しているので、 操作の要所要所で状況がSlackに自動で通知されると便利です。

ステージング環境へのデプロイ

デプロイ操作の動作確認として、まずはステージング環境を使ってデプロイを行います。 デプロイ操作を変更して、いきなり本番環境で実行するのは相当な勇気が必要です。

ステージング環境へのデプロイは、本番環境へのデプロイと同じ手順で行います。 ActionsのWorkflow定義も、できれば同じものを使いたいですね。 環境に依存する要素をパラメータ化して、それ以外の部分は共用するのが理想です。

オペレーション専用チャンネル

カヤックの自社サービス運用開発チームでは、本番環境操作専用のSlackチャンネルを用意しています。 デプロイ操作の共有や、障害対応の進行状況などはこのチャンネルで行います。

ステージング環境へのデプロイ進行状況もSlackに通知したいわけですが、 本番環境用のオペレーションチャンネルに通知するとノイズになってしまいます。 そこでステージング環境にはステージング環境用のチャンネルを用意しています。

つまり、本番環境とステージング環境で、Actions Workflowの進行状況通知先チャンネルを分ける必要があります。

やりたいこと

前置きが長くなりましたが、やりたいのはつまりこういうことです。

  • Actions Workflowの進行状況をSlackにに通知する
  • ただし、本番環境とステージング環境で通知先チャンネルを分ける

GitHubのSlack Appを使えば解決?

SlackにGitHubのSlack Appを導入することでGitHubとSlackを連携させることができます。

slack.github.com

連携機能のひとつとして、Actions Workflowのsubscribeがあります。

例えば、Slack上で以下のようなコマンドを発行すると、 GitHubリポジトリ kayac/foobar のActions Workflow deploy が実行されたことがSlackに通知されるようになります (具体的な通知内容は、READMEにあるスクリーンショットを参照ください)。

/github subscribe kayac/foobar workflows:{name:"deploy" event:"workflow_run"}

単に通知を受けるだけであればこれで解決なのですが、 この方法ではやりたいことの2番目の要件「本番環境とステージング環境で通知先チャンネルを分ける」が実現できません。 上記のコマンドでは、Actions Workflow deploy のすべての実行がsubscribeしたチャンネルに通知されてしまいます。

subscribeコマンドにはActionを実行したブランチでフィルタするオプションがあるので、 例えばブランチ運用ルールを以下のように定めておけば、

  • 本番環境にデプロイするのはmainブランチのみ
  • ステージング環境にデプロイするのはstagingブランチのみ

以下のようにコマンドを発行することで通知先チャンネルを分けることができます。

# 本番環境用チャンネル
/github subscribe kayac/foobar workflows:{name:"deploy" event:"workflow_run" branch:"main"}
# ステージング用チャンネル
/github subscribe kayac/foobar workflows:{name:"deploy" event:"workflow_run" branch:"staging"}

ブランチ運用がこの条件にマッチするのであればこの方法で十分でしょう。

残念ながら私が担当しているプロダクトでは、この条件にはマッチしませんでした。 通知のためだけにブランチ運用に制限をかけるのは本末転倒です。 せめてEnvironmentでフィルタできれば・・・。

いくつかの手段

ブランチ運用に縛られない通知手段として、実際に試した方法を紹介します。

手段1 頑張ってWorkflow内通知を作り込む

Slack公式のActionが公開されているので、これを使う方法。

github.com

Actions Workflowの要所要所にSlackに通知するstepを挟めば、思い通りに通知先を切り替えられます。 Slackからsubscribeする方式ではなく、Actions Workflowからpushする方式ですね。

通知するメッセージも自由にカスタマイズできるので、必要な情報を見栄え良く送ることができます。 下記の例では、Environment variables を使用してデプロイ対象環境ごとに別々のSlack Webhook URLを使用しています。

    - name: Notify deploy start
      uses: slackapi/slack-github-action@v1.25.0
      env:
        SLACK_WEBHOOK_URL: ${{ vars.SLACK_WEBHOOK_URL }} # Environment variableを使用する例
        SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
      with:
        payload: |
          {
            "blocks": [
              {
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "@here リリース作業を開始します"
                }
              },
              {
                "type": "context",
                "elements": [
                  {
                    "type": "mrkdwn",
                    "text": ":technologist: ${{ github.actor }}"
                  }
                ]
              }
            ],
            "link_names": 1
          }

Slackにはこのようなメッセージが投稿されます。

Slackへの投稿例

ただし、この方式はActions Workflowの肥大化に繋がります。 Slack APIのpayloadはそこそこの記述量があるので、本筋のデプロイ操作が追いにくくなるという欠点があります。

また、Slackのblockメッセージの作り込みは沼です (楽しいんですけどね)。

手段2 Actions Workflowを分ける

Actions Workflow定義自体を分けてしまう方式です。 Actions Workflowが異なればsubscribe設定も個別に設定できるので、それぞれ別のSlackチャンネルに通知することができます。 手段1に比べると通知内容のカスタマイズ性はなくなりますが、Slackからsubscribeするだけなのでメンテナンス性は格段に上がります。

しかし、「定型化したデプロイ操作をステージング環境で確認する」本来の目的からずれてしまっている感は否めません。 本番環境とステージング環境で、Actions Workflowの定義が別物になってしまうので。

手段3 Actions Workflow実行の入り口を分ける

GitHub ActionsはWorkflow中から別のWorkflowを呼び出すことができます。

docs.github.com

これ利用して、本番環境とステージング環境で入口となるActions Workflowを分けることでsubscribe設定を分けられるようにしよう、という方法です。 デプロイ操作の本体は再利用可能なActions Workflowとして定義し、入口となるActions Workflowから use で呼び出します。

例えば本番環境用の入り口Workflowを以下のように定義し、

name: "production deploy"

on:
  workflow_dispatch:
    inputs:
      pr_number:
        description: |
          リリース対象のPull Request番号を指定してください
          指定したPull Requestはmainブランチにマージされます
        required: false

concurrency:
  group: ${{ github.workflow }}

jobs:
  start:
    uses: ./.github/workflows/deploy.yml # デプロイ操作本体の定義をuseで呼び出す
    with:
      # デプロイ先環境固有のパラメータを指定する
      deploy_target: production
      aws_iam_role_arn: arn:aws:iam::123456789012:role/production-github-actions-deployment
      slack_hook: ${{ secret.PRODUCTION_SLACK_WEBHOOK_URL }}
      pr_number: ${{ github.event.inputs.pr_number }}
    secrets: inherit

ステージング環境用の入り口Workflowは以下のように定義します。

name: "staging deploy"

on:
  workflow_dispatch: {} # ステージング環境のデプロイではPR番号を指定しない

concurrency:
  group: ${{ github.workflow }}

jobs:
  start:
    uses: ./.github/workflows/deploy.yml # 本番環境と同じものをuseする
    with:
      deploy_target: staging
      aws_iam_role_arn: arn:aws:iam::123456789012:role/staging-github-actions-deployment
      slack_hook: ${{ secret.STAGING_SLACK_WEBHOOK_URL }}
      pr_number: null # null固定
    secrets: inherit

そのうえで、入口となるWorkflowをSlackチャンネルからsubscribeすれば、 「本番環境とステージング環境で通知先チャンネルを分ける」事ができるわけです。

# 本番環境用チャンネル
/github subscribe kayac/foobar workflows:{name:"production deploy" event:"workflow_run"}
# ステージング環境用チャンネル
/github subscribe kayac/foobar workflows:{name:"staging deploy" event:"workflow_run"}

環境ごとに異なるパラメータ定義が簡単になるというメリットもあります。 *2。 上記の例では with:./.github/workflows/deploy.yml に渡しているパラメータがこれにあたります。

例えば「ステージング環境ではmainブランチのPRマージを行わない」 という要件があるとします。 Actions Workflow全体を本番環境とステージング環境で共通化し、 マージ対象PRの番号を workfrow_dispatch.inputs.pr_number で入力する場合、 各stepに以下のような、対象環境を考慮した条件分岐を追加する必要があるでしょう。

    - name: Merge pull request
      if: ${{ github.event.inputs.target_env == "production" && github.event.inputs.pr_number != null }}
      run: |
        # マージ処理

入口となるWorkflowを分けた場合、ステージング環境へのデプロイでは pr_number が常に null になることを保証することができます。 条件分岐が不要になるわけではないですが、対象環境の考慮は不要になります。

    - name: Merge pull request
      if: ${{ github.event.inputs.pr_number != null }} # 対象のPRがあるかどうかのみを考慮すればよい
      run: |
        # マージ処理

まとめ

個人的には手段3がお気に入りです。 しかし、「Actions Workflowのメンテナンス性よりも通知のわかりやすさを優先したい」なら手段1が有効でしょうし、 「Actions Workflowのテンプレートを用意し、そのテンプレートから実際に使用するActions Workflowの定義を生成する」仕組みを用意すれば手段2のデメリットを解決できます。 もちろん、今回紹介しなかった手段で実現することもできるでしょう。 開発・運用チームの状況にあった方法を選択するのが良いでしょう。

Workflowの作り込み、特に人間が干渉する必要があるものの作り込みは沼です。 特に本筋とは関係のない、体験向上部分の作り込みには終りがありません。 仕事としてやるからには、ちょうどいい塩梅を見つけてコスパよく実現していきたいですね。

補足

  • Slackからのsubscribeで済ませるためには、実行する操作をActions Workflowのstepに分割する必要があります。 subscribeによる通知はstep単位の進行状況なので、ひとつのstepで複数の操作を行ってしまうと、 その中の進行状況を通知から知ることができなくなってしまいます
  • Actions Workflowのstepには適切な名前(name)を付けましょう。 subscribeでの通知はstep名で表示されます。 適切な名前付けはActions Workflowの可読性向上にもつながるでしょう
  • 「ステージング環境へのデプロイ」でさらっと「環境に依存する要素をパラメータ化して」と書きましたが、これにも色々な手段があります。 長くなってしまうのでまた別の機会に・・・

カヤックでは自動化が好きなエンジニアも募集しています!

hubspot.kayac.com

*1:週に1回や月に1回程度のデプロイしか行わないのであれば、これくらいの手間はかけてもいいかもしれません。 が、たとえば私が担当しているまちのコインは日に数回以上デプロイを行うので、毎回手動で報告するのはチリツモでまあまあのコストになってきます

*2:これについてはEnvironment variablesを使うほうがきれいかもしれませんが、リポジトリ管理外なってしまうので変更履歴が追えないのが気になります。秘匿する必要がない値はリポジトリ内に含めたい・・・