NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    Terraformのドリフト検知結果をSlackに通知させてみた〜GitHub Actions利用〜

    クラウド事業推進部の石倉です。

    Terraformで管理しているインフラ環境の状態、把握してますか?
    作業途中など把握している差分ならいいですが、作業の戻し忘れによる差分などは時間が経った時に「何の変更だ?」とびっくりすることになります。

    そんな環境の状態を把握できるように、今回はTerraformのドリフト検知結果をSlackに通知させる仕組みをGitHub Actionsでやってみたのでご紹介します。 インフラ環境はAWSです。

    Terraformのディレクトリ構造

    Terraformリソースについて、今回は複数環境の想定で以下のようなディレクトリ構造にしています。

    .
    └── terraform
        ├── envs
        │   ├── dev
        │   │   ├── xxxxx.tf
        │   │   └── main.tf
        │   ├── prd
        │   │   ├── xxxxx.tf
        │   │   └── main.tf
        │   └── stg
        │       ├── xxxxx.tf
        │       └── main.tf
        └── modules
            ├── resource-a
            │   ├── main.tf
            │   └── xxxxx.tf
            └── resource-b
                ├── main.tf
                └── xxxxx.tf

    参考
    一般的なスタイルと構造に関するベスト プラクティス  |  Terraform  |  Google Cloud

    コード

    上記のTerraformのディレクトリ構造における各環境(dev, stg, prd)に対してterraform planを行い、結果をまとめSlackに通知するコードです。 処理の流れとしては以下のような流れです。

    1. 各環境に対してplanを行い差分があった環境の情報を出力する処理を並列で実行する(drift-detectionジョブ)
    2. 1の処理が終わったら差分があった場合となかった場合でSlack通知する(notify-drift、notify-no-driftジョブ)
    name: Terraform Drift Detection
    
    on:
      schedule:
        - cron: '0 8 * * *'
      workflow_dispatch:
    
    jobs:
      drift-detection:
        runs-on: ubuntu-latest
        outputs:
          output_dev: ${{ steps.export.outputs.output_dev }}
          output_stg: ${{ steps.export.outputs.output_stg }}
          output_prd: ${{ steps.export.outputs.output_prd }}
        strategy:
          matrix:
            environment: ["dev", "stg", "prd"]
          fail-fast: false
    
        steps:
          - name: Checkout repository
            uses: actions/checkout@v4
    
          - name: Setup Terraform
            uses: hashicorp/setup-terraform@v3
            with:
              terraform_wrapper: false
    
          - name: Configure AWS credentials
            uses: aws-actions/configure-aws-credentials@v4
            with:
              aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
              aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
              aws-region: ap-northeast-1
    
          - name: Terraform Init
            id: init
            run: |
              terraform init
            working-directory: ./terraform/envs/${{ matrix.environment }}
    
          - name: Terraform Plan
            id: plan
            run: |
              set +e
              terraform plan -detailed-exitcode
              echo "exit_code=$?" >> "$GITHUB_OUTPUT"
            working-directory: ./terraform/envs/${{ matrix.environment }}
    
          - name: Export environment
            if: ${{ steps.plan.outputs.exit_code == 2 }}
            id: export
            run: |
              echo "output_${{ matrix.environment }}=${{ matrix.environment }}" >> "$GITHUB_OUTPUT"
    
      notify-drift:
        if: ${{ toJson(needs.drift-detection.outputs) != '{}' }}
        runs-on: ubuntu-latest
        needs: [drift-detection]
        steps:
          - name: Collect environment
            id: collect
            run: |
              values=$(echo '${{ toJson(needs.drift-detection.outputs) }}' | jq -r '[.[]] | join(", ")')
              echo "collect_env=$values" >> "$GITHUB_OUTPUT"
    
          - name: Post a message in channel
            uses: slackapi/slack-github-action@v2.0.0
            with:
              webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
              webhook-type: incoming-webhook
              payload: |
                {
                  "blocks": [
                    {
                      "type": "section",
                      "text": {
                        "type": "mrkdwn",
                        "text": ":warning: *`${{ steps.collect.outputs.collect_env }}` 環境でドリフトを検知したよ。把握していますか?*"
                      }
                    },
                    {
                      "type": "section",
                      "text": {
                        "type": "mrkdwn",
                        "text": "<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|ワークフロー詳細>"
                      }
                    }
                  ]
                }
    
      notify-no-drift:
        if: ${{ toJson(needs.drift-detection.outputs) == '{}' }}
        runs-on: ubuntu-latest
        needs: [drift-detection]
        steps:
          - name: Post a message in channel
            uses: slackapi/slack-github-action@v2.0.0
            with:
              webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
              webhook-type: incoming-webhook
              payload: |
                {
                  "blocks": [
                    {
                      "type": "section",
                      "text": {
                        "type": "mrkdwn",
                        "text": ":ok: *ドリフトなし!*"
                      }
                    }
                  ]
                }

    各コードにおけるTips

    ドリフト検知を並列実行する

    複数環境を想定というところで今回はGitHub Actionsのmatrixを利用して各環境に対してplanを行うdrift-detectionジョブを並列実行し効率化しています。

        strategy:
          matrix:
            environment: ["dev", "stg", "prd"]
          fail-fast: false

    ワークフローでのジョブのバリエーションの実行 - GitHub Docs

    Terraform実行環境の用意

    HashiCorpが提供しているGitHubActionを利用してワークフロー上でTerraformコマンドを実行できるようにしています。

          - name: Setup Terraform
            uses: hashicorp/setup-terraform@v3
            with:
              terraform_wrapper: false

    GitHub - hashicorp/setup-terraform: Sets up Terraform CLI in your GitHub Actions workflow.

    ここで一つterraform_wrapper: falseについて、この設定はデフォルトではtrueになっているのですが後ほど説明するterraform plan -detailed-exitcodeの終了コードが正しく効かなくなってしまう事象があったためfalseにしています。

    GitHub ActionsからAWS環境への認証方法

    今回はAWS環境への認証をアクセスキーの方法でやっていますがプロジェクトで利用する場合はOpenID Connect (OIDC) での方法に変える方がベストかと思います。

          - name: Configure AWS credentials
            uses: aws-actions/configure-aws-credentials@v4
            with:
              aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
              aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
              aws-region: ap-northeast-1

    terraform planで差分があった環境だけを取得する

    detailed-exitcodeというplan結果によって終了コードを分けるオプションがあるためこちらを利用して差分があった場合に(終了コードが2)、その環境情報を出力するようにしています。

          - name: Terraform Plan
            id: plan
            run: |
              set +e
              terraform plan -detailed-exitcode
              echo "exit_code=$?" >> "$GITHUB_OUTPUT"
            working-directory: ./terraform/envs/${{ matrix.environment }}
    
          - name: Export environment
            if: ${{ steps.plan.outputs.exit_code == 2 }}
            id: export
            run: |
              echo "output_${{ matrix.environment }}=${{ matrix.environment }}" >> "$GITHUB_OUTPUT"

    https://developer.hashicorp.com/terraform/cli/commands/plan#detailed-exitcode

    このときに先ほどのterraform_wrapper: falseがtrueになっていると差分があっても終了コードが0(差分なし)になってしまう事象があります。
    https://github.com/hashicorp/setup-terraform/issues/328

    またGitHub Actionsはset -eがデフォルトで設定されておりterraform plan -detailed-exitcodeで終了コードが2になるとその時点で処理が終わってしまうのでset +eの設定をしています。
    GitHub Actions のワークフロー構文 - GitHub Docs

    ステップ間、ジョブ間の出力パラメータの利用

    plan結果の情報を次のステップに渡したり、planが失敗した環境の情報を次のジョブに渡して利用しています。

          - name: Terraform Plan
            id: plan
            run: |
    ...
    ...
              echo "exit_code=$?" >> "$GITHUB_OUTPUT"
            working-directory: ./terraform/envs/${{ matrix.environment }}
    
          - name: Export environment
            if: ${{ steps.plan.outputs.exit_code == 2 }}
      drift-detection:
        outputs:
          output_dev: ${{ steps.export.outputs.output_dev }}
          output_prd: ${{ steps.export.outputs.output_prd }}
          output_stg: ${{ steps.export.outputs.output_stg }}
    ...
    ...
        steps:
    ...
    ...
          - name: Export environment
    ...
    ...
            run: |
              echo "output_${{ matrix.environment }}=${{ matrix.environment }}" >> "$GITHUB_OUTPUT"
    
      notify-drift:
        if: ${{ toJson(needs.drift-detection.outputs) != '{}' }}

    それぞれの記述コードについては以下を参考にしました。
    GitHub Actions のワークフロー コマンド - GitHub Docs
    GitHub Actions のワークフロー構文 - GitHub Docs

    jqコマンドによる整形処理

      drift-detection:
    ...
    ...
        steps:
    ...
    ...
          - name: Export environment
    ...
    ...
            run: |
              echo "output_${{ matrix.environment }}=${{ matrix.environment }}" >> "$GITHUB_OUTPUT"
    
      notify-drift:
    ...
    ...
        steps:
          - name: Collect environment
            id: collect
            run: |
              values=$(echo '${{ toJson(needs.drift-detection.outputs) }}' | jq -r '[.[]] | join(", ")')
              echo "collect_env=$values" >> "$GITHUB_OUTPUT"

    Export environmentステップで差分があった環境の情報を出力し、notify-driftジョブでecho '${{ toJson(needs.drift-detection.outputs) }}'で取得した情報は以下のような形になっています。

    {
      "output_stg": "stg",
      "output_dev": "dev"
    }

    記事冒頭でお見せしたイメージのような「stg, dev 環境でドリフトを検知したよ」というようなメッセージで送るためにjqコマンドで値を取得・整形しています。
    最終的にはcollect_envに「stg, dev」というような環境名を, で繋げるような形に整形しています。

    参考
    jq 1.8 Manual

    Slackへの通知

    Slackへの通知にはSlackが提供しているGitHubActionを利用しています。

          - name: Post a message in channel
            uses: slackapi/slack-github-action@v2.0.0
            with:
              webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
              webhook-type: incoming-webhook

    GitHub - slackapi/slack-github-action: Send data into Slack using this GitHub Action!

    Slackチャンネルにメッセージを送信するためにWebhookURLを利用するのでSlackappを作成してWebhookURLを用意します。
    Sending messages using incoming webhooks | Slack Developer Docs

    メッセージ作成

    Slackへのメッセージの作成はBlock Kitという仕組みを利用しています。 Block Kit Builderという簡単にBlock Kitでのメッセージ作成をできるようにするツールを用意してくれているのでそれを使ってメッセージを作成しました。

          - name: Post a message in channel
            uses: slackapi/slack-github-action@v2.0.0
            with:
              webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
              webhook-type: incoming-webhook
              payload: |
                {
                  "blocks": [
                    {
                      "type": "section",
                      "text": {
                        "type": "mrkdwn",
                        "text": ":warning: *`${{ steps.collect.outputs.collect_env }}` 環境でドリフトを検知したよ。把握していますか?*"
                      }
                    },
                    {
                      "type": "section",
                      "text": {
                        "type": "mrkdwn",
                        "text": "<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|ワークフロー詳細>"
                      }
                    }
                  ]
                }
    

    Block Kit | Slack Developer Docs

    最後に

    いかがでしたでしょうか。 同じようなことをしようと考えている方の参考になれば幸いです。

    執筆者 石倉弘隆

    クラウド事業推進部 / クラウドエンジニア