はじめに
ソフトウェアシステムを作成する際に、インフラの構築の再現性を確保するためにIaCを行うケースがあります。
さらに、インフラ構築を自動化するためにこのIaCに対してCI/CDを行うこともよく行われるかと思います。
今回は以下のような条件でIaCのCI/CDを行う際に、どのようにしたかについて述べます。
環境は次の通りです。
- インフラはGoogle Cloud
- IaCにTerraformを用いる
- CI/CDは Github Actionsで実施する
またCI/CDでは次のようなことの実現を目指します。
- Terraformのvalidationを行う
- Terraformのlintを行う
- Terraformの単体テストを実施する
- 各種チェックが問題なければapplyしてインフラ構築を実行する
Terraformのフォルダ構成はスタイルガイドのような構成を想定します。
.
├── modules
│ ├── function
│ │ ├── main.tf # contains aws_iam_role, aws_lambda_function
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── queue
│ │ ├── main.tf # contains aws_sqs_queue
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── vpc
│ ├── main.tf # contains aws_vpc, aws_subnet
│ ├── outputs.tf
│ └── variables.tf
├── main.tf
├── outputs.tf
└── variables.tf
ワークフロー構成
今回はGitHub Actionsのワークフローを以下のような構成にしています。
具体的な処理を記述したワークフロー_ci_cd.yaml
と環境ごとの実行に対応するためのci_cd_dev.yaml
から成ります。
.
├── .github
│ ├── workflows
│ │ ├── _ci_cd.yaml
│ │ └── ci_cd_dev.yaml
~~~~~~~~~~~~~~~~~~~~略~~~~~~~~~~~~~~~~
ci_cd_dev.yaml
name: ci_cd(dev)
on:
pull_request:
branches:
- dev
push:
branches:
- dev
workflow_dispatch:
jobs:
call_ci_cd:
uses: ./.github/workflows/_ci_cd.yaml
with:
environment: github-gcp-dev
env_name: dev
secrets: inherit
トリガー条件
このワークフローは特定の環境に対応して具体的な処理を起動するためのゲートとなるようなフローです。
on: ~
で記述されるトリガー条件を見ると、「dev」ブランチに対するプルリクエストの場合と、「dev」ブランチに対するプッシュがあった場合です。
pull_request:
におけるトリガーは正確には「dev」ブランチへのプルリクエスト作成時、プルリクエストが開かれた状態でソースブランチが更新されたとき、プルリクエストが再オープンされたときです。
type:
によってその他のタイミングを指定することも可能です。
また、workflow_dispatch:
もトリガー条件となっています。これによりGitHubのweb画面上でワークフローを実行できるようになります。
ジョブ
次に、このワークフローのjobs
を確認します。
ジョブでは、./.github/workflows/_ci_cd.yaml
つまり具体的な処理を行うフローを呼び出しており、その際にenvironment
とenv_name
という値を引き渡しています。environment
はGitHubリボリトリに対して作成できる環境の名前です。GitHubでは、リポジトリごとに複数の環境を作成することができ、その環境ごとに「Environment secrets」、や「Environment variables」というシークレット値や環境変数の一覧を作成する事ができます。
environment
は各環境ごとに設定されたシークレット値や環境変数をワークフローで用いる際に、どの環境を参照すればよいのかを指定するために渡す必要があります。
env_name
は環境の名前でどの環境か区別して命名する際に、参照するために渡す値です。
また、secrets: inherit
の設定をすることで呼び出し先のワークフローでも呼び出し元で参照可能なシークレットを参照させる事ができるようになります。
_ci_cd.yaml
name: ci_cd execution
on:
workflow_call:
inputs:
environment:
description: 'github environment name'
required: true
type: string
env_name:
description: 'environment name(dev/stg/prd)'
required: true
type: string
workflow_dispatch:
inputs:
environment:
description: 'github environment name'
required: true
type: string
env_name:
description: 'environment name(dev/stg/prd)'
required: true
type: string
jobs:
ci_job:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
permissions:
# workload identity連携によるToken発行には以下権限が必要
id-token: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: auth-login-gcp
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- name: Set up mise
uses: jdx/mise-action@v2
with:
Install: true
cache: true
- name: Terraform Format
run: terraform fmt -check -recursive
- name: Init TFLint
run: tflint --init
- name: Run TFLint
run: tflint --recursive -c "$(pwd)/.tflint.hcl"
- name: Terraform Init
run: find . -name "*.tf" -exec dirname {} \; | sort -u | xargs -I {} sh -c 'cd "{}" && terraform init'
- name: Terraform Validation
run: find . -name "*.tf" -exec dirname {} \; | sort -u | xargs -I {} sh -c 'cd "{}" && terraform validate'
- name: Terraform unit test
run: find . -name "*.tftest.hcl" -exec dirname {} \; | sort -u | xargs -I {} sh -c 'cd "{}" && terraform test'
- name: Terraform Plan
run: |
cd ${{inputs.env_name}}
terraform plan
cd_job:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
if : ${{ github.event_name == 'push'}}
needs: ci_job
permissions:
# workload identity連携によるToken発行には以下権限が必要
id-token: write
contents: read
# PR画面でterraform plan結果を投稿させるために以下権限が必要
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: auth-login-gcp
uses: google-github-actions/auth@v2
with:
project_id: ${{ secrets.GCP_PROJECT_ID }}
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- name: Set up mise
uses: jdx/mise-action@v2
with:
Install: true
cache: true
- name: Terraform Init
run: |
cd ${{inputs.env_name}}
terraform init
- name: Terraform Plan
run: |
cd ${{inputs.env_name}}
terraform plan
- name: Terraform Apply
run: |
cd ${{inputs.env_name}}
terraform apply -auto-approve
トリガー条件
このワークフローはworkflow_call:
で他のワークフローで呼び出された場合と、workflow_dispatch:
で画面上から呼び出すことで実行できます。この際には、environment
とenv_name
の値が与えられている必要があります。
ジョブ
ジョブはci_job
とcd_job
の2️つからなっています
ci_job
ci_job
はTerraformのテストやチェックを行うCIのためのジョブです。cd_job
と共通の準備として、リポジトリのチェックアウト、Google Cloudへのログイン、miseによるTerraformとTflint環境のセットアップを行っています。
CIのジョブで行っていることは以下です。
- terraformのフォーマットチェック
- tflintの初期化
- tflintの実行
- terraform initの実行
- terraform validationの実行
- terraform testの実行
- terraform planの実行
terraformのフォーマットチェックでは、-recursive
オプションを付けて再帰的にチェックを行っています。
tflintによるlintのチェックでも-recursive
オプションで再帰的に実行ができます。その際に参照する設定ファイル.tflint.hcl
が正しく読み込めるように指定する必要があります。
この記事で述べたように、terraform init
、terraform validate
、terraform test
は再帰的に実行する方法がないため例のようにTerraform のフォルダ構成によっては再帰的なコマンドをこちらで作り実行する必要があります。
最後に、実際に作りたい環境のディレクトリでterraform plan
を実行してエラーが無いかを確認しています。
cd_job
このジョブは実行にあたり以下のような条件が指定されています。
CDはdevブランチにpushがあったときのみ動かしたい(例えば、プルリクが開かれた直後のCI成功後にはCDは動かしたくない)ので、このような条件と、CIのジョブが成功した場合のみ実行するような制限を設けています。
if : ${{ github.event_name == 'push'}}
needs: ci_job
また、Terraformのスタイルガイドのようなフォルダ構成ではある環境に対してインフラを構築したい場合、その環境名のディレクトリでterraform plan
、terraform apply
をする必要があります。
そのために、このジョブではワークフローが事前に受け取ったenv_name
の値を参照して該当する環境のディレクトリに移動してコマンドを実行するようにしています。
CIについては、前述のジョブで成功しているため、ここでは単純にinit、plan、applyのみを行っています。
所感
CIとCDそれぞれでジョブを分けて記載することで、利便性の高いワークフローになるかと思います。
また、実際の処理の部分と環境ごとのゲート部分のワークフローを分けることで、環境ごとの設定がやりやすくなっているかと感じます。(この方法だと1環境ごとにワークフローが1つ増えてしまう課題がありますが...)
詳細なトリガー条件については他にもっと良いやり方があると思うので、より良い方法をご存知の方はご教示くださればありがたいです!