💬

Github ActionsによるTerraform Testに対応したCI/CDの実装 v0.3.0

2024/04/29に公開

今回のバージョンアップでinfra-testing-google-sampleプロジェクトにCI/CDの実装が追加されたことにより、ようやく現場での稼働がイメージ出来るレベルになった。

他にも今回のバージョンではpre-commit時に行うチェックをCIと同期、Terraformや各種プロバイダのバージョンアップといった改修を行なっているが、この記事ではCI/CDの実装に焦点を当てて詳しく解説したい。

(なおGithub Actionsの実装に慣れきっている人であれば先にコードを読んで、もし分からない点があれば下記内容を確認するといった進め方のほうが、確実に速く読み進めることが出来る気がしている)

infra-testing-google-sample のチェックアウト

CI/CDを自分の環境で試す場合は、Githubから以下のコマンドでプロジェクトのチェックアウトを行う必要がある。

(なお空のGCPプロジェクトの用意やサービスAPIの有効化等についてはREADME.mdスクリプトのドキュメントなどを参考にして各自で行う必要がある)

$ cd /path/to/work-dir

# 以下のどちらかのコマンドでローカルにclone
$ git clone https://github.com/erueru-tech/infra-testing-google-sample.git
$ gh repo clone erueru-tech/infra-testing-google-sample

# 新規チェックアウトであれば不要だが、既にclone済みの場合必要
$ git fetch

# この記事に紐づくバージョンに切り替え
$ git checkout -b 0.3.4 refs/tags/0.3.4

CI/CD 運用

具体的なCI/CDワークフローの設定内容について触れる前に、まず今回追加されたCI/CDを使ってどのようにインフラの運用を行うのかについて説明をしたい。

GitHub Flowで開発を行なっていると仮定して、まずmainブランチから変更をpullしてきてfeatureブランチを作成するところから始まる。

このfeatureブランチの状態をv0.2.0の記事でも説明したように個人のsandbox環境のtier1に適用したのち、HCLファイルの修正やテストを実行して、作業が完了したらGithubに変更をpushする。

変更コミットを元にmainブランチに向けたプルリクエスト(以後PR)を作成するが、このPR作成のタイミングでGithub Actions上でCIのワークフローが発火する。

ワークフローではHCLファイル等のバリデーションやテストを行ったのち、問題が無ければstaging環境に対してterraform planを実行して、その結果をPRコメントに書き込む。

次にPRのレビューが開始されることになるが、レビュアーが指摘した点を修正してコミット&pushを行うとPR作成時と同様にCIのワークフローが実行される。

そしてレビュアーが修正内容とplan結果に問題が無いと判断したら、approveを行ったのちPRのマージを行う。

PRがmainブランチにマージされると、staging環境にterraform applyコマンドを実行するワークフローが開始される。

stagingへのプロビジョニングが完了したら動作確認を行なって、最後にmainブランチからreleaseブランチに向けたPRを作成してマージを行うことで、本番環境へのプロビジョニングを行うワークフローが実行されて、一連の改修作業は完了といった流れになる。

(releaseブランチは開発においては一切使用しない、本番デプロイ用途限定のトリガーブランチになる)

CI/CD ワークフロー

ここからはGithub ActionsによるCI/CDワークフローを記述した設定ファイルの詳細について、上から順に一つずつ説明をしていくことにする。

まずPR作成時および修正コミットのpush時に実行されるワークフローの設定ファイルであるtest.yamlから説明する。

🔗 test.yaml

🔗 トリガー

on:
  pull_request:
    branches:
      - main
    types: [opened, synchronize]

上記コードはワークフローの発火トリガーとなる。

この設定は先にも説明した通り、mainブランチに向けたPRを新規に作成した場合(opened)と修正コミットをpushした場合(synchronize)にワークフローを開始するという意味になる。

🔗 同時実行数制御

concurrency:
  group: test
  cancel-in-progress: false

concurrencyを定義することでワークフローの同時実行数を1に設定することが出来る。

これは複数のPRから同じタイミングでコミットが行われた際に、test環境のtier1に対するterraform applyやテストコード内で発生するapplyが衝突しないようにするための設定となる。

なおgroupの値をstg.yamlやprod.yamlでも同じ値にしてしまうと、全環境で1度に1つのワークフローしか実行できなくなるので注意が必要になる。

cancel-in-progress:falseは既に同じgroup名のワークフローが実行済みの場合に、そのワークフローが完了するまで待機する挙動になる。

(cancel-in-progressはデフォルトの値がfalseなので設定を省略しようと思えば出来るが、一応明示している)

cancel-in-progress:trueにすると既に実行済みのワークフローをキャンセルして、後続のワークフローを開始するようになる。

なお個人的には、後からやって来たコミットは先にワークフローを実行しているコミットの変更を考慮していない可能性が高く、場合によってはtest環境が壊れる可能性も考えられるため、出来れば"後続のワークフローをキャンセル"するオプションが欲しいと感じている。

🔗 パーミッション

Github Actionsではワークフローからコミット履歴やプルリクエスト、その他Githubの各種機能に対して読み書きを行う権限を個別に設定することが出来る。

なお、infra-testing-google-sampleプロジェクトではtest.yamlを含む全てのワークフローで、以下のように全てのアクセス許可をデフォルトで無効にしていて、必要に応じて各ジョブにて個別に権限を与えるようにしている。

permissions: {}

(アクセススコープに関する詳細は公式ドキュメントを参照)

🔗 validation ジョブ

ここからはワークフローを構成する各ジョブについての説明になる。

まず最初に実行されるvalidationジョブではその名の通り、HCLファイルや各種設定ファイルに対する静的チェックを行うジョブとなる。

以下ではvalidationジョブを構成するステップを順に追っていく。

プロジェクトチェックアウト

まず最初のステップとして、ソースコードプロジェクトのチェックアウトを行なっている。

(この定義はジョブ毎に書くのが冗長なので、書かなくてよい方法をもし知っていたら教えてほしい)

- name: checkout project
  # v4.1.1 ref. https://github.com/actions/checkout/releases/tag/v4.1.1
  uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

ちなみにactions/checkoutのようなオープンソースのアクションでは、利用方法としてactions/checkout@v4のようなバージョン指定で説明しているケースがほとんどだが、これはv4系の最新バージョンに攻撃的なプログラムが混入してしまった場合でも利用してしまうことになるため、セキュリティ的には推奨されない。

基本的には@v4.1.1のようにバージョンを固定すれば、4.1.1バージョンにさえ問題がなければ上記のような事態は発生しない。

しかし細かいことを言えば、リリースバージョンはいくらでも作り直せるものであることから、コミットハッシュの方がさらに安全ということで上記のような指定を行なっている。

コミットハッシュを調べる方法だが、まずリリースノートを確認すると以下のように短縮コミットハッシュが表示されているので、この値を元にコミット履歴を検索してフルのコミットハッシュを探すといった流れになる。

テストコード実行対象モジュールのチェック
- name: check test_matrix.yaml
  run: |
    ./scripts/check_test_matrix.sh

プロジェクトのチェックアウトが完了したら、まず最初にcheck_test_matrix.shというスクリプトを実行する。

これはプロジェクト内の全Terraformモジュールのテストコードを実行するようにtest_matrix.yamlが定義されているかチェックするためのスクリプトとなる。

この説明だけだと分かりづらいので、さらに掘り下げてコードベースで説明する。

まずテストコードの実行対象となるモジュールを定義しているtest_matrix.yamlは以下のようになる。

modules1:
  - network
  - github
modules2:
  - db

module1グループにnetworkモジュールとgithubモジュール、module2グループにdbモジュールを定義している。

各グループは後述するモジュールのテストコード実行時に、それぞれに1ジョブが割り当てられて並列でテスト実行を行う。

dbモジュールのテストは1回に20分近くかかるのに対して、networkモジュールやgithubモジュールは数分程度でテストが完了するため、総テスト実行時間の最小化のために上記のような設定となっている。

今後モジュールが追加された場合、テスト実行時間の合計が20分を超えるまではmodules1グループに追記し続け、20分を超えたらmodules3グループを新たに定義するといった運用を行うことで、テスト実行時間を一定以内に抑えることが出来る。

なおtest_matrix.yamlは手動で定義する性質から、新しく作成したモジュールを定義し忘れる可能性があるため、その対策としてcheck_test_matrix.shを実行している。

/path/to/infra-testing-google-sample/scripts/check_test_matrix.sh
#!/bin/bash

set -eu

TEST_MATRIX_MODULES=$(yq .*.[] .github/data/test_matrix.yaml | sort)
MODULE_DIRS=$(ls -1 terraform/modules/ | grep -v scripts | sort)
...

if [[ "$TEST_MATRIX_MODULES" != "$MODULE_DIRS" ]]; then
  echo "The modules defined in test_matrix.yaml don't match those in the directory."
  exit 1
fi

TEST_MATRIX_MODULESにはtest_matrix.yamlからモジュール名だけを抽出した結果を格納していて、一方のMODULE_DIRSmodulesディレクトリ内のscriptsを除く全ディレクトリ名を格納している。

このスクリプトを実行するとTEST_MATRIX_MODULESMODULE_DIRSの値はどちらも以下のようになる。

# yq .*.[] .github/data/test_matrix.yaml | sort # でも同じ結果
$ ls -1 terraform/modules/ | grep -v scripts | sort
db
github
network
TFLintのインストール、実行

次はHCLファイルの静的チェックツールであるTFLintのインストールとキャッシュに関するステップを定義している。

- name: cache tflint CLI
  id: tflint-cache
  uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9
  with:
    path: |
      /usr/local/bin/tflint
      ~/.tflint.d/
    key: tflint-${{ env.TFLINT_VERSION }}-${{ hashFiles('terraform/.tflint.hcl') }}

- name: install tflint if the cache doesn't exist
  if: steps.tflint-cache.outputs.cache-hit != 'true'
  run: |
    curl -s https://raw.githubusercontent.com/terraform-linters/tflint/${{ env.TFLINT_VERSION }}/install_linux.sh | sudo bash
    cd terraform && tflint --init

最初のステップではactions/cacheアクションを使用して、Github Actionsのキャッシュからtflintのバイナリ(/usr/local/bin/tflint)とプラグイン(~/.tflint.d/)のrestoreを行っている。

もちろん初回実行時はキャッシュが存在しないため、2番目のステップでtflintコマンドのバイナリとプラグインのインストールを行なっているが、ジョブ(validation)が正常終了すると最初のステップのpathで指定されたファイルやディレクトリは自動的にキャッシュに登録されて、次回以降のワークフロー実行ではそれらを再利用出来るようになる。

なおキャッシュのkeyはtflint-${{ env.TFLINT_VERSION }}-${{ hashFiles('terraform/.tflint.hcl') }}となっているが、これはTFLintのバージョンを変更した場合やプラグインのバージョンが定義されている.tflint.hclファイルに変更が発生した場合に、キャッシュミスとなり再度ダウンロードとインストールを行うようになっている。

なおキャッシュは7日間以上アクセスされていない場合や、リポジトリ内でキャッシュを合計10GB以上使用している場合に自動的に削除される。

(キャッシュの詳細に関する公式ドキュメントはこちら)

tflintコマンドとプラグインの準備が完了したら、プロジェクト内の全HCLファイルに対してTFLintを使用したチェックを行うスクリプトを実行している。

- name: run tflint
  run: |
    ./scripts/tflint.sh

tflint.shの内容については説明が長くなるため省略するが、基本的にenvironmentsディレクトリとmodulesディレクトリ配下にあるHCLファイルを格納するディレクトリへ順番に移動して、tflintコマンドを実行しているだけとなる。

terraformのインストール
- name: install terraform CLI
  uses: ./.github/actions/setup
  with:
    auth_gcloud: "false"

自作のsetupアクションを実行しているが、このアクションでは特定のバージョンのterraform CLIのインストールとGCPに対してアクセスを行うためのWorkload Identity連携による認証を任意で実行する。

validationジョブではGCPにアクセスをする必要がないため、auth_gcloud:"false"を指定して意図的にGCPにアクセス出来ないようにしている。

HCLファイルのフォーマットチェック
- name: run terraform fmt
  run: |
    ./scripts/tffmt.sh

このステップではinfra-testing-google-sampleプロジェクト内の全てのHCLファイルのフォーマットをチェックしている。

フォーマットチェックを行うtffmt.shでは以下のような処理が行われている。

/path/to/infra-testing-google-sample/scripts/tffmt.sh
#!/bin/bash

set -eu

cd terraform
if [[ -z ${CI:-} ]]; then
  terraform fmt -recursive
else
  terraform fmt -recursive -check
fi

処理はいたって簡単で、terraformディレクトリ配下に移動したのち、terraform fmt -recursive -checkによって再起的にHCLファイルを探索してフォーマットチェックを行なっているだけである。

前バージョンの記事でも説明したとおり、if [[ -z ${CI:-} ]]; thenの条件はpre-commitなどによってローカル環境からtffmt.shを実行した場合はCIという環境変数が定義されていないために真となり、Github Actions上で実行すると環境変数CI=trueが暗黙的に付与されているために偽となる。

バリデーション
- name: run terraform validate
  run: |
    ./scripts/tfvalidate.sh

tfvalidate.shは先に紹介したtflint.shと同じ探索方法で、environmentsディレクトリとmodulesディレクトリ配下でterraform validateコマンドを実行しているだけとなる。

なおterraform validateコマンドの実行前には必ずterraform initコマンドの実行が必要になるが、これが原因でスクリプトの実行に1分近くかかることから、コミット時のpre-commitではチェックしていない。

そのためCIで初めて気づく可能性もあるが、もしVSCodeで開発していて、かつHashiCorp Terraformプラグインを導入していれば同等のチェックをエディタ上で行なってくれる。

# VSCodeにHashiCorp Terraformプラグインをインストール
$ code --install-extension hashicorp.terraform

validationジョブで実行されるステップの説明は以上となる。

test_matrix.yamlのチェック、TFLint、フォーマットチェック、validateコマンドの実行が完了したら、次はテストコードの実行準備を行うジョブであるtest-prepが開始される。

🔗 test-prep ジョブ

test-prepジョブではプロジェクトのチェックアウトを行なったのち、以下のステップを実行する。

test-prep:
...
  steps:
...
    - name: setup terraform and gcloud CLI
      uses: ./.github/actions/setup
      with:
        workload_identity_provider: ${{ secrets.TEST_WORKLOAD_IDENTITY_PROVIDER }}
        service_account: ${{ secrets.TEST_SERVICE_ACCOUNT }}

    - name: run apply command to tier1 in test
      run: |
        ./terraform/environments/test/tier1/apply.sh
      env:
        TF_VAR_service: ${{ secrets.TEST_SERVICE }}
        TF_VAR_env: test

validationジョブでもsetupアクションは実行したが、ここではテストコード実行の前準備としてtest環境のtier1のリソースのプロビジョニングを行う必要があるため、GCPに接続するために必要なWorkload Identity Provider名やサービスアカウントといった情報をパラメータ経由で渡している。

(Workload Identity連携に関する公式記事はこちら)

認証が完了したら、test環境に対してtier1で定義されたリソースをapplyするシェルを実行して、テスト時の依存解決に必要なリソースをプロビジョニングしている。

なお基本的にこのジョブが実行されたらGithubのコンソール上からワークフローをキャンセルしてはいけない点に注意しなければいけない。

あとGithub Actionsを利用したことがない人向けにsecretsを説明すると、これはGithubのリポジトリに対して権限を持つ人のみが設定可能な環境変数のようなものである。

このsecretsで設定した値が標準出力に出力されようとすると、値の代わりに****のようにマスクされた状態で出力されるようになる。

🔗 test ジョブ

testジョブでは先ほど紹介したtest_matrix.yamlの定義を元に、複数のジョブを並列で起動してテストコードの実行を行う。

他のジョブとは異なり、並列実行を行うためにstrategy設定を定義している。

test:
...
  strategy:
    fail-fast: false
    matrix:
      shard: [modules1, modules2]
  steps:
     ...
     - name: run tests
        run: |
          while IFS= read MODULE; do
            ./terraform/modules/$MODULE/test.sh
          done <<< "$(yq -r .${{ matrix.shard }}[] ./.github/data/test_matrix.yaml)"
        env:
          TF_VAR_service: ${{ secrets.TEST_SERVICE }}
          TF_VAR_env: test

fail-fast:falseは1つのジョブが失敗しても他のジョブの実行を継続するための設定となっている.

次のshard:[...]ではmodules1とmodules2というジョブを定義しているが、これによって2つのジョブが並列実行されるようになる。

なおこの値はどんな文字列でも良いのだが、ここではtest_matrix.yamlのグループ名を用いている。

$(yq -r .${{ matrix.shard }}[] ./.github/data/test_matrix.yaml)が実行されると、modules1ジョブの場合、while文で定義されているMODULE変数の中にnetwork、githubといった文字列が順番に渡され、各モジュールディレクトリ内に存在するtest.shを実行することで、テストコードの逐次実行を行なっている。

🔗 plan ジョブ

バリデーションチェック、テストコードがすべてパスしたら、staging環境へのプロビジョニング準備としてterraform planコマンドを実行する。

plan:
...
  steps:
...
    - name: run terraform plan in stg
      uses: ./.github/actions/tfcmd
      with:
        service: ${{ secrets.STG_SERVICE }}
        env: stg
        command: plan
        gh_token: ${{ secrets.GITHUB_TOKEN }}

planコマンドの実行は自作のtfcmdアクションを使用して行なっているが、これはterraform planコマンドの実行が完了したら、PRに対して以下のようなコマンド実行結果をコメントするようになっている。

リソースのdestroyが発生する場合はterraformのログ出力時のカラーと同様に赤に変わる。

(destroyは発生しないがリソースのchangeが予定されている場合には橙色になる)

PRのコメントで文字の装飾を行う方法についてはこちらのGistページを参考にした。

🔗 slack ジョブ

slack:
...
  steps:
...
    - name: send the workflow result to the slack channel
      uses: ./.github/actions/slack
      with:
        conclusion: ${{ needs.plan.result }}
        channel_id: ${{ vars.SLACK_CHANNEL_ID }}
        webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}

このslackジョブは全てのワークフローにおいて必ず最後に実行されるようになっていて、ワークフローの実行結果がSlackに通知される。

ここでは自作のslackアクションを呼び出しているが、内部ではSlack公式のslackapi/slack-github-actionアクションを実行していて、以下のようなメッセージがchannel_idおよびwebhook_urlで指定した宛先に通知されるようになっている。

全ジョブが成功した場合は緑のメッセージが通知されて、途中のジョブでエラーが発生したり、ワークフローのキャンセル等が行われた場合には赤のメッセージが通知されるようになっている。

この仕組みについて触れておくと、conclusionに${{ needs.plan.result }}という値を渡しているが、これはslackを除いて最後の実行ジョブであるplanジョブが成功した場合はsuccessという文字列が渡され、planジョブ到達前のジョブでエラー終了した場合にはskipped、planジョブ自体が失敗した場合はfailure、ワークフローが途中でキャンセルされた場合にはcancelledになる。

そしてslackアクション内では${{ inputs.conclusion == 'success' && '36a64f(緑)' || 'f26268(赤)' }}のように全ジョブが成功したとみなされる時のみcolorが緑になり、それ以外は全て赤として扱っている。

その他このジョブを実行するためにはSlackチャンネルの作成やSLACK_WEBHOOK_URLの発行が必要になるが、手順については公式ドキュメントが一番分かりやすい。

(英語だがそこまでドキュメントを読み込まなくても、道なりに操作するだけで作成が完了する)

あとSlackは利用者が一人の場合、利用料は月額1,155円(24年4月時点)なので比較的個人でも導入しやすい価格となっている。

🔗 stg.yaml

test.yamlでは多くのバリデーションやテストを実行しているために複雑な定義になっていたが、stg.yamlのワークフローはstaging環境へのプロビジョニングを行うだけとなっているため、かなりシンプルな定義となっている。

🔗 トリガー

このワークフローのトリガーはmainブランチに向けたPRがマージされた際やクローズが行われた場合に発火するといった条件になっている。

on:
  pull_request:
    branches:
      - main
    types: [closed]

これに加えジョブ側でif: github.event.pull_request.merged == trueの条件が定義されていることから、mainブランチに対してPRマージを行なった時のみワークフローが発火するようになっている。

ここから同時実行数制御やパーミッションに関する定義が続くが、これらの設定はtest.yamlと同様の設定(同時実行数1かつデフォルト権限無効)となっている。

ジョブについても、既に説明したsetupアクションやtfcmdアクションを使用して、terraform apply -auto-approveコマンドを実行してstaging環境を更新を行なっているだけとなる。

そして実行が完了したらワークフローの実行結果をSlackに通知している。

🔗 prod.yaml

基本的にはstg.yamlと定義はほぼ同じで、唯一異なるのはトリガーだけである。

on:
  pull_request:
    branches:
      - release
    types: [closed]

stg.yamlはmainブランチへPRをマージすると発火するようになっていたが、本番環境へのリリースはmainブランチからreleaseブランチ対して作成されたPRをマージするという作業を行うことで実行される。

なお運用を始める前にリポジトリにmainブランチから派生したreleaseブランチを事前に作成しておく必要がある点は忘れてはいけない。

あとstg.yamlやprod.yamlではバリデーションやテストコードを実行しないのは、mainブランチは既にそれらをパスした状態であるため再度実行する必要がないのと、destroyが実行されるテストコードの実行をstaging環境や本番環境にアクセス可能な状態で行うことは絶対に避けた方がいいと考えているためである。

Workload Identity 連携

今回追加したCI/CDではWorkload Identity連携を行なっているが、これはgithubモジュールで定義されているリソースをプロビジョニングすることでGCP側の設定を行なっている。

githubモジュールではGoogle公式が提供しているgh-oidcモジュールを使用してWorkload Identity関連のリソースを作成していて、あとは連携に使用するサービスアカウントを別途定義しているだけとなっている。

なおこのサービスアカウントにはownerロールを付与していて、セキュリティ上かなり問題があるような気もする。

しかし一方で厳密にロールを管理しようとすると、インフラ改修のたびにGithub Actions上で行われるリリース(terrsform apply)に必要なロールを事前追加する作業が必要になってくる。

そしてその作業はローカルマシンから直接本番環境やstaging環境に対してapplyを実行することになり、destroyが飛び交うこのプロジェクトではむしろそちらの方が危険であるとの考えから現状はこのようにしている。

この辺りの運用に関して何か良い解決方法があれば本当に教えてほしいが、おそらくはdestroyが実行されるリスクのない別インフラプロジェクトでstateを分離して管理といった解決方法になる気がしている。

CI/CD 環境の冗長化

基本的にCI/CD環境はチームに対して1つだけ用意するのが一般的ではあるとは思うが、これまで過去の記事で頻繁に指摘してきたようにテストコードを実行するインフラプロジェクトは復旧不可能なクラッシュが発生する可能性が十分にあるため、プロジェクト破損によるリリース不可能状態が長期化しないように冗長化を行なっておいた方がいいのではないかと考えている。

やり方としては、your-project-1-test、your-project-2-testのようにCI用のGCPプロジェクトをまず2つ作成する。

次にterraformコマンド実行時に必要な環境変数TF_VAR_serviceに値をセットする際に、リポジトリの設定ページから手動で値を設定出来る${{ vars.TEST_SERVICE }}から値を取得するようにして、たとえばメイン系であるyour-project-1-testプロジェクトが壊れたら、TEST_SERVICEにyour-project-2を設定するといった方法であれば、ワークフローの修正は多少必要があるものの冗長化を実現できると考えている。

(このプロジェクトでは削除再作成を行わないと変更不可能なsecretsにservice名を設定しているが、これはオープンソースの性質上プロジェクト名を特定できる値を公開しないための対応であって、本来のプライベートリポジトリでの運用ならそこまで隠す必要がないのでvariablesを使って良いという話になる)

もしくはcommitメッセージに特定のキーワード入れることで、実行するtest環境を選択できるようにするといった方法でも実現できるかと思う。

未対応

今回追加したCI/CDでは通信が発生するステップにおいてキャッシュの利用は行なっているものの、根本的なflaky対策は行なっていない。

これはそもそも常時再現性のある状況が発生してくれない限り、対応を考えようがないのでいったんは発生するまで保留としている。

(もしかすると余計なリトライ実装を行うくらいなら、大人しくワークフローの再実行を手動で行う方がシンプルかつ安全な運用になるではとも考えている)

この件に関連して、TFLintだけでなくterraform init実行時にダウンロードされるモジュール群についてもcacheアクションの利用を検証してみたが、現時点では未導入となっている。

一応、検証は以下のコードをvalidationジョブに追加することで行なった。

検証コード
- name: cache all .terraform directories and .terraform.lock.hcl
  id: terraform-dir-cache
  uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9
  with:
    path: |
      ./terraform/environments/prod/tier1/.terraform
      ./terraform/environments/prod/tier2/.terraform
      ./terraform/environments/stg/tier1/.terraform
      ./terraform/environments/stg/tier2/.terraform
      ./terraform/environments/test/tier1/.terraform
      ./terraform/environments/test/tier2/.terraform
      ./terraform/environments/sbx/tier1/.terraform
      ./terraform/environments/sbx/tier2/.terraform
      ./terraform/environments/sbx/tier2/.terraform
      ./terraform/modules/db/.terraform
      ./terraform/modules/github/.terraform
      ./terraform/modules/network/.terraform
      ./terraform/environments/prod/tier1/.terraform.lock.hcl
      ./terraform/environments/prod/tier2/.terraform.lock.hcl
      ./terraform/environments/stg/tier1/.terraform.lock.hcl
      ./terraform/environments/stg/tier2/.terraform.lock.hcl
      ./terraform/environments/test/tier1/.terraform.lock.hcl
      ./terraform/environments/test/tier2/.terraform.lock.hcl
      ./terraform/environments/sbx/tier1/.terraform.lock.hcl
      ./terraform/environments/sbx/tier2/.terraform.lock.hcl
      ./terraform/environments/sbx/tier2/.terraform.lock.hcl
      ./terraform/modules/db/.terraform.lock.hcl
      ./terraform/modules/github/.terraform.lock.hcl
      ./terraform/modules/network/.terraform.lock.hcl
    key: terraform-dir-${{ hashFiles('terraform/globals.tf') }}

- name: run terraform init in all directories if the cache doesn't exist
  if: steps.terraform-dir-cache.outputs.cache-hit != 'true'
  run: |
    # environments
    while IFS= read ENV_DIR; do
      for TIER in "tier1" "tier2"; do
        (cd $ENV_DIR/$TIER &&  terraform init -backend=false >/dev/null)
      done
    done <<< "$(find terraform/environments -type d -mindepth 1 -maxdepth 1 | grep -v scripts)"

    # modules
    while IFS= read MODULE_DIR; do
      (cd $MODULE_DIR && terraform init -backend=false >/dev/null)
    done <<< "$(find terraform/modules -type d -mindepth 1 -maxdepth 1 | grep -v scripts)"

このコードをterraform validate実行前に置くことで、キャッシュヒット時に30秒ほどワークフローの実行時間を短縮することが出来る。

一方で、コードが冗長になる、キャッシュを600MB使用する、特にterraformコマンド実行のような重要オペレーションでキャッシュを使用することで予期せぬ挙動を発生させたくないなどの理由で、キャッシュの使用は見送りになった。

おわり

今回はCI/CDの実装の詳細について説明した。

残りは基礎調査が完了しているConftestTrivyといった機能のマージと、その他細かい機能追加が完了するとinfra-testing-google-sampleプロジェクトのプロトタイプ版は完成する。

なおGo+terraform-exec+Google Cloud SDKによるなんでもありのe2eテスト実行環境は贅沢品であるため最後に回して時間のある時に対応する予定となっている。

追記

Github Actionsの公式ドキュメントを確認する限り、Github Actionsの各ジョブはVMインスタンスレベルで分離されているはずという前提でCI/CDを構築している。

続き

https://zenn.dev/erueru_tech/articles/0064d0c9902b2a

Discussion