コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

Terraformの各種Providerバージョンアップ作業を自動化した取り組み

こんにちは、コドモン SRE チームの渡辺です。

コドモンでは、AWSなどのインフラリソースをTerraformで管理しています。先日、私たちのチームでは、各種Providerのバージョンアップ作業を自動化しました。

本記事では、自動化の仕組みについて紹介します。Terraformを運用されている方の役に立てれば幸いです。

自動化する以前の課題

私たちのチームでは、dependbotを利用してProviderのバージョンアップPR作成を自動で行っていました。

作成されたPRをSREチームのメンバーが確認し、以下の作業を手動で行っていました。

  1. terraform planの結果に差分があるか確認する(全環境分)
  2. 差分がある場合、内容を確認し対応を検討する
  3. 差分がない場合、PRをApproveしてマージする

しかし、各Providerのバージョンアップは頻繁に行われるため、毎日のようにPRが作成されます。このため、チーム内の負荷が高まるにつれ、ProviderバージョンアップPRの対応が遅れてしまうことが多々ありました。

このような状況で、週次のふりかえり会において、この問題についてチーム内で議論しました。議論の結果、週次担当を決めて対応するなどの案もありましたが、手動で行っている作業は機械的に判断しているため自動化を進める方針になりました。

自動化の仕組み

今回は、GitHub Actions(以下 GHA)を利用して自動化を進めていきました。流れは以下のとおりになります。

  1. 全環境のterraform planの結果をGitHub Storageにアップロード
  2. artifactをダウンロードし、すべてのPlan結果で差分がないか検証する
  3. 差分がない場合、PRをマージする

私たちのチームでは、Terraformを管理しているリポジトリでPRを作成すると、変更のあったコードに関係する環境のterraform planが並列実行されるようにしています。Providerバージョンアップの場合は、すべての環境でterraform planが走ります。

まずは、並列実行した全環境のterraform planの結果をGitHub Storageにアップロードします。

その後のJobで全環境分のファイルをダウンロードし、差分の有無を検証します。一つでも差分がある環境があれば、Job3は実行しないようにしています。

GHAは、以下のようになります。

  plan-workdir:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
    outputs:
        workdirs: ${{ steps.filter.outputs.workdirs }}
    steps:
      - uses: actions/checkout@v3
      - uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: .github/filters.yml # フィルターファイルを設定する
      - name: filter
        id: filter
        run: |
          ここでterraform planを実行するディレクトリを$GITHUB_OUTPUTに設定する

  terraform_plan:
    needs: plan-workdir
    if: needs.plan-workdir.outputs.workdirs != '[]'
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
      pull-requests: write
    strategy:
      matrix:
        workdir: ${{ fromJSON(needs.plan-workdir.outputs.workdirs) }}
    - uses: actions/checkout@v3

    省略(terraformのセットアップなど)

    - name: Terraform plan dependbot
      if: github.event.pull_request.user.login == 'dependabot[bot]'
      id: dependbot_tfplan
      run: |
        # 標準出力をファイルにリダイレクトするとGitHub Actionsで実行ログが確認できないため、outオプションでバイナリをファイルに書き込む
        tfcmt -var target:$WORK_DIR plan -patch -- terraform plan -lock=false -out=tfplan_binary.txt
        
        FILENAME=$(echo "plan-result-$WORK_DIR.txt" | tr "/" "-")
        terraform show tfplan_binary.txt > $FILENAME
        echo "FILENAME=$FILENAME" >> $GITHUB_OUTPUT
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        WORK_DIR: ${{ matrix.workdir }}

    - name: Save result
      if: github.event.pull_request.user.login == 'dependabot[bot]'
      uses: actions/upload-artifact@v3
      with:
        name: tfplan-result-${{ github.sha }}
        path: ${{ matrix.workdir }}/${{ steps.dependbot_tfplan.outputs.FILENAME }}
        if-no-files-found: error

  terraform_plan_results:
    needs: terraform_plan
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - run: exit 1

  check_terraform_plan_results:
    if: github.event.pull_request.user.login == 'dependabot[bot]'
    needs: terraform_plan
    runs-on: ubuntu-latest
    outputs:
      has_changes: ${{ steps.check.outputs.has_changes }}
    steps:
      - name: Download all results
        uses: actions/download-artifact@v3
        with:
          name: tfplan-result-${{ github.sha }}

      - name: Check results
        id: check
        run: |
          for file in ./*.txt; do
            if ! grep -q "No changes." "$file" | grep -q "Your infrastructure matches the configuration." "$file"; then
              echo "has_changes=true" >> $GITHUB_OUTPUT
              echo "Changes detected in $file"
              break
            fi
          done

  auto_merge:
    needs: check_terraform_plan_results
    if: needs.check_terraform_plan_results.outputs.has_changes != 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v3
      - name: Merge PR
        run: |
          gh pr comment $PR_NUMBER --body "All Terraform plan results showed No Changes."
          gh pr review $PR_NUMBER --approve
          gh pr merge $PR_NUMBER --merge
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}

  remove_artifacts:
    needs: check_terraform_plan_results
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Remove artifacts
         run: |
           echo "remove artifact_name: $ARTIFACT_NAME"
           artifact_ids=$(gh api /repos/$GITHUB_REPO/actions/artifacts --jq ".artifacts[] | select(.name == \"$ARTIFACT_NAME\").id")
           for id in $artifact_ids; do
             echo "delete artifacts id: $id"
             gh api -X DELETE /repos/$GITHUB_REPO/actions/artifacts/$id
           done
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           ARTIFACT_NAME: tfplan-result-${{ github.sha }}
           GITHUB_REPO: ${{ github.repository }}

upload-artifactでアップロードしたファイルは、GitHub Storageに保管されます。GitHub Storageには容量制限があるため、remove_artifactsで検証後のファイル群を削除しています。

このアクションが実行され、terraform planに差分がない場合は、以下のようにPRが自動でマージされます。

なお、baseブランチのRequire status checks to pass before mergingterraform_plan_resultsを設定しています。このため、どこか一つの環境でもterraform_planが失敗した場合は、PRをマージできないようになっています。

まとめ

今回、Terraformの各種Providerのバージョンアップ作業を自動化する取り組みを行った結果、トイルを削減できました。

今後も人が機械的に行っている作業を抽出し自動化するというプロセスを実施し、チームが抱えている手動作業については、積極的に自動化を進めていこうと思います。

最後に

コドモンでは、今後も新しいサービスの開発や既存サービスのリプレイスなどを予定しており、一緒に盛り上げてくださる方を募集しています。

一緒に働くことに興味を持ちましたら、ぜひ求人情報もご覧ください!