はじめに
Github Actionsの正式版がリリースされて1ヶ月ほど経ちました。中々さわれてなかったのですが、TerraformのCI/CD環境構築の調査で動かしたのでその時のまとめです。
サンプルプロジェクト
とりあえず試すだけであれば、Terraform公式が用意しているTerraform GitHub Actionsを使えばすぐに終わってしまいます。
それだけだと面白くないのでなるべく実際の運用を想定して動かしていきます。とはいえあらゆるパターンを試すのは難しいので以下のようなサンプルプロジェクトを仮定します。
- providerはAWS
- moduleを使う
- workspaceを使う
- リポジトリ内に複数のmain.tfがある
ディレクトリ構成は以下。(ファイルの中身は本題ではないので割愛)
├── modules
│ ├── ec2
│ │ ├── main.tf
│ │ └── variables.tf
│ └── iam-role
│ ├── main.tf
│ └── variables.tf
├── service1
│ ├── ec2
│ │ ├── main.tf
│ │ └── variable.tf
│ └── iam-role
│ ├── main.tf
│ └── variable.tf
└── service2
├── ec2
│ ├── main.tf
│ └── variable.tf
└── iam-role
├── main.tf
└── variable.tf
ゴール
上に書いた構成のサンプルに対して以下の1〜4を行う。
- masterブランチへのプルリクエスト作成をトリガーに以下の3つ(以降、自動テストと呼ぶ)を実行する。
- terraform fmt
- terraform validate
- terraform plan
- 一度のプルリクエストで複数のworkspaceに対してまとめて自動テストを実行する。
- masterブランチへのプッシュ(マージ)をトリガーに自動テストと
terraform apply
を実行する。自動テストのwrokflowが成功した場合のみapplyを実行する。 - 自動テストおよびapplyは毎回リポジトリ内の全てのmain.tfに対して実行するのではなく、変更があったファイルに関係するmain.tfに対して実行する。
1. masterブランチへのPR作成をトリガーに自動テストを実行
workflowの作成
早速workflowを作成していきますが、Terraformの公式が用意してくれているTerraform GitHub Actionsを使っていきます。
以下の公式のサンプルを修正していく形で進めます。
name: 'Terraform GitHub Actions'
on:
- pull_request
jobs:
terraform:
name: 'Terraform'
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: actions/checkout@master
- name: 'Terraform Format'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'fmt'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 'Terraform Init'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'init'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 'Terraform Validate'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'validate'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: 'Terraform Plan'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'plan'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AWSのcredential
AWSのリソースを扱うためのcredentialをsecretsから取得するようにします。secrets.xxx
でGitHub上で設定したSecretsの情報できます。
GitHub側の設定はリポジトリの Settings > Secrets で設定してあげればOKです。
stepsの中にあったenvを一つ上の階層に移動させ、そこにAWSのクレデンシャルの情報を設定しています。Checkoutのstepでは使用しませんが、何度も書くのも冗長なので上に持ってきています。
name: 'Terraform GitHub Actions'
on:
- pull_request
jobs:
terraform:
name: 'Terraform'
runs-on: ubuntu-latest
env: # 追加
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 移動
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} # 追加
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # 追加
steps:
- name: 'Checkout'
uses: actions/checkout@master
- name: 'Terraform Format'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'fmt'
- name: 'Terraform Init'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'init'
- name: 'Terraform Validate'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'validate'
- name: 'Terraform Plan'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'plan'
workspaceを指定する
まずはdevというworkspaceで動くようにしてみます。 Terraform GitHub ActionsではTF_WORKSPACE
という環境変数でworspaceを指定できるのでenvに追加します。
env:
TF_WORKSPACE: dev # 追加
実行するディレクトリを指定する
main.tfが複数に別れているので、どのmain.tfがあるディレクトリで実行するかを指定します。
Github Actionsに準備されているstrategy.matrixを指定してあげれば簡単に複数の設定に対してjobを実行することができます。
今回のサンプルだと4つmain.tfがあるのでそれぞれstrategy.matrix
を指定してそれをtf_actions_working_dir
で渡します。
jobs:
terraform:
name: 'Terraform'
runs-on: ubuntu-latest
strategy: # 追加 strategyのmatrixでworkspace設定。この組み合わせの数だけjobが実行される
matrix:
workdir: [./service1/ec2, ./service2/ec2, ./service1/iam-role, ./service2/iam-role]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
TF_WORKSPACE: dev
steps:
- name: 'Checkout'
uses: actions/checkout@master
- name: 'Terraform Format'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'fmt'
tf_actions_working_dir: ${{ matrix.workdir }} # 追加
- name: 'Terraform Init'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'init'
tf_actions_working_dir: ${{ matrix.workdir }} # 追加
- name: 'Terraform Validate'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'validate'
tf_actions_working_dir: ${{ matrix.workdir }} # 追加
- name: 'Terraform Plan'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'plan'
tf_actions_working_dir: ${{ matrix.workdir }} # 追加
masterへのプルリクエストをトリガーにする
以下のようにbranchesのフィルターを設定すればOK。(以下の設定の場合はPR作成時以外にもプルリクエストを作成したブランチにプッシュした場合などいくつかのイベントでトリガーされます。)
name: 'Terraform GitHub Actions'
on:
pull_request:
branches:
- master
動作確認
ここまでの設定で動作を確認してみます。masterブランチに対してプルリクエストを作成すると、無事にActionsが実行されるはずです。strategy.matrix
で指定したディレクトリの数だけjobが実行されているのがわかります。
2. 一度のプルリクエストで複数のworkspaceに対してまとめて自動テストを実行する
実行ディレクトリを指定した時と同じようにstrategy.matrix
を使えば簡単に実現できます。
devとprodの二つのworkspaceがあると想定して以下のように設定します。
terraform:
name: 'Terraform'
runs-on: ubuntu-latest
strategy:
matrix:
env: [dev, prod] # 追加
workdir: [./service1/ec2, ./service2/ec2, ./service1/iam-role, ./service2/iam-role]
env:
TF_WORKSPACE: ${{ matrix.env }} # matrixから値を取得するように修正
動作確認
ここまでの設定で動作確認します。masterブランチに対してプルリクエストを作成すると今度は 4(workdir) × 2(env) = 8
のjobが動きます。
3. masterブランチへのプッシュ(マージ)をトリガーに自動テストとapplyを実行する
masterブランチへのプッシュ(マージ)をトリガーに実行
pull_requestと同じ階層にpushを追加してbranchesにmasterブランチを指定します。
これによりmasterブランチへのプルリクエスト or masterブランチへのプッシュの条件でjobが実行されるようになります。
name: 'Terraform GitHub Actions'
on:
pull_request:
branches:
- master
push: # 追加
branches:
- master
今回は一つのymlで定義しましたが、プルリクエストの場合とプッシュの場合でymlを複数に分けてしまうのもありだと思います。
apply用のstepを追加
applyも他と同様にTerraform GitHub Actionsに用意されているのでそれを使います。
ただしapplyはmasterへ修正が反映された時のみ動いてほしいので if
で条件を追加しています。github contextでjobを実行のトリガーとなったイベント情報などを取得できるので、masterブランチへのイベントの場合のみ実行されるようにします。
- name: 'Terraform Plan'
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'plan'
tf_actions_working_dir: ${{ matrix.workdir }}
- name: 'Terraform Apply' # ここから下を追加
if: github.ref == 'refs/heads/master' # プルリクエストの作成では動かないように
uses: hashicorp/terraform-github-actions@master
with:
tf_actions_version: 0.12.17
tf_actions_subcommand: 'apply'
tf_actions_working_dir: ${{ matrix.workdir }}
planまでのstepが成功した場合のみapplyを実行
デフォルトで前のstepが失敗したは次のstepは実行されないため特に設定しなくてもplanが成功した時のみapplyが実行されます。
動作確認
ここまでの設定で動作確認します。プルリクエストを作成してマージするとapplyまで実行されています。
4. ディレクトリ内のファイルが修正されたmain.tfに対してのみjobを実行する
ここまででプルエスト作成でplanを実行してmasterブランチをマージしてapplyするという流れができましたが、実際の運用を想定した時に以下のような課題で出てきそうです。
- main.tfの数やworkspaceのが増えると一度の修正で実行されるjobの数が増え、jobの並列実行数が増加し続けGitHub Actionsの上限までいってしまう。
- 修正と関係ないmain.tfに対してもplan、applyが実行されるので、修正内容に関わらず一番実行時間が長いjobの分だけ時間がかかってしまう。
これらを解決するためにディレクトリ内の修正されたファイルに関連するjobのみ実行するような設定をしていきます。
pathsフィルタの設定
pathsフィルタを使うことで特定のディレクトリやファイルに変更が合った場合にjobを実行することができます。例えば以下のように設定するとmasterブランチへのプルリクエスト かつ service1ディレクトリとmodulesディレクトリ内に変更が合った場合にjobが実行されます。
name: 'Terraform GitHub Actions'
on:
pull_request:
branches:
- master
paths: # 追加
- service1/**
- modules/**
ちなみに差分をどのように検知しているかは、プルリクエストやすでにあるブランチへのプッシュ、新規ブランチへのプッシュによって以下のように動作するようです。
Pull requests: Three-dot diffs are a comparison between the most recent version of the topic branch and the commit where the topic branch was last synced with the base branch.
Pushes to existing branches: A two-dot diff compares the head and base SHAs directly with each other.
Pushes to new branches: A two-dot diff against the parent of the ancestor of the deepest commit pushed.
pathsフィルタを利用して
-
./service1
ディレクトリ以下のファイルと./modules
ディレクトリ以下のファイルに変更がある場合、./service1
以下のmain.tfを実行する -
./service2
ディレクトリ以下のファイルと./modules
ディレクトリ以下のファイルに変更がある場合、./service2
以下のmain.tfを実行する
のように動作させるため元のterrafrom.ymlを以下のように二つに分割していきます。
name: 'Terraform Service1'
on:
pull_request:
branches:
- master
paths: # service1に関係する変更のみを検知
- service1/**
- modules/**
push:
branches:
- master
paths: # service1に関係する変更のみを検知
- service1/**
- modules/**
jobs:
terraform:
name: 'Terraform'
runs-on: ubuntu-latest
strategy:
matrix:
env: [dev, prod]
workdir: [./service1/ec2, ./service1/iam-role] # service1のディレクトリのみを指定
# env以下は元のterraform.ymlと同じ
name: 'Terraform Service2'
on:
pull_request:
branches:
- master
paths: # service2に関係する変更のみを検知
- service2/**
- modules/**
push:
branches:
- master
paths: # service2に関係する変更のみを検知
- service1/**
- modules/**
jobs:
terraform:
name: 'Terraform'
runs-on: ubuntu-latest
strategy:
matrix:
env: [dev, prod]
workdir: [./service2/ec2, ./service2/iam-role] # service1のディレクトリのみを指定
# env以下は元のterraform.ymlと同じ
各main.tfの依存関係などを考慮し出すと複雑になりそうなど課題はありますが、pathsフィルタを使えば修正ファイルに応じたjobを実行できそうです。
動作確認
修正後の状態(service1.yml、service2.yml)で試しにservice1/ec2/variable.tf
のみ修正してプルリクエストを作成すると./service1
ディレクトリ内のmain.tfに関するjobが実行されることが確認できました。
まとめ
簡単なTerraformリポジトリを想定してGitHub Actions上でCI/CDを作り、どんな設定ができるか試していきました。
初めてGitHub Actionsを使いましたがドキュメントもしっかり書かれていてつまることも少なく進めらかなり使いやすいと感じました。他にも以下のあたりがいい感じでした。
- GitHub上のサービスだけあって、GitHubのコンテキストの情報を簡単に取得できる
- 無料で並列実行までできるのすごい
- maxrixによる並列実行がかなり便利
- pathsフィルタがかなり便利
個人的にはapplyする時はCircle CIで用意されているManual Approvalのような手動で実行を承認する機能が欲しいところですが、要望も多そうなのでそのうち追加されるのではと思っています。
今回想定したTerraformリポジトリはシンプルなので、実運用にそのまま使えるかという難しいかもしれませんが、どんなことができそうかイメージをつけられたのがよかったです。
参考
GitHub Actionsドキュメント
terraform-github-actionsドキュメント
terraform-github-actions