Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

tfaction を導入したら便利だった話

この記事は Gunosy Advent Calendar 2023 の 14 日目の記事です。昨日の記事は上村さんの「ChatGPTを活用した業務支援ツール「ウデキキ」のチャット実装」でした。

こんにちは、プロダクト開発部 Ads チームの fujishiro です。最近は年末に行われる RIZIN.45 が楽しみで YouTube で試合予想動画を見ていることが多いです。

さて、今回は弊チームが管理している Terraform のリポジトリの CI/CD 環境を CircleCI から GitHub Actions に移行した際に tfaction という Action を導入したのですが、これが便利だったので紹介したいと思います。

tfaction とは

tfaction とは GitHub Actions で Terraform Workflow を実行するための Action で、中身は複数の Action の集合体になっています。tfaction に含まれる Action を組み合わせて使用することで簡単に Terraform Workflow を実現することができます。

例えば、下記のような AWS のリソースが記述されている作業ディレクトリが 2 つあるモノレポ構成の Terraform リポジトリを管理しているとします。

├── shared
│   ├── main.tf
│   └── .terraform.lock.hcl
└── components
    ├── main.tf
    └── .terraform.lock.hcl

この時に以下のようなワークフローを実現したい場合、tfaction を使って以下のような設定を行うことで実現することができます。

実現したいワークフロー

  1. main ブランチに向けた feature ブランチの PR の作成時に terraform plan を実行する
  2. main ブランチに PR がマージされた際に terraform apply を実行する

必要な設定*1

  1. tfaction-root.yaml を追加する
  2. tfaction.yaml を作業ディレクトリごとに追加する
  3. aqua を使って必要な CLI ツールをインストールできるように aqua.yaml を追加する
  4. terraform plan を実行する workflow と terraform apply を実行する workflow を記述する

ディレクトリ構成は以下のようになります。

├── .github
│   └── workflows
│       ├── plan.yaml # 追加
│       └── apply.yaml # 追加
├── shared
│   ├── main.tf
│   ├──  .terraform.lock.hcl
│   └── tfaction.yaml # 追加
├── components
│   ├── main.tf
│   ├──  .terraform.lock.hcl
│   └── tfaction.yaml # 追加
├── aqua.yaml # 追加
└── tfaction-root.yaml # 追加

1. tfaction-root.yaml を追加する

tfaction-root.yaml は tfaction の全体の設定ファイルになるので、ベースとなる tfaction の設定をここで記述していきます。

tfaction-root.yaml

plan_workflow_name: terraform-plan

target_groups:
  - working_directory: shared
    target: shared
    aws_region: ap-northeast-1
    terraform_plan_config:
      aws_assume_role_arn: arn:aws:iam::xxxxx:role/xxxxx
    terraform_apply_config:
      aws_assume_role_arn: arn:aws:iam::xxxxx:role/xxxxx
    # ... その他の設定

  - working_directory: components
    target: components
    aws_region: ap-northeast-1
    terraform_plan_config:
      aws_assume_role_arn: arn:aws:iam::xxxxx:role/xxxxx
    terraform_apply_config:
      aws_assume_role_arn: arn:aws:iam::xxxxx:role/xxxxx
    # ... その他の設定

2. tfaction.yaml を作業ディレクトリごとに追加する

tfaction.yaml は各作業ディレクトリごとに配置する tfaction の設定ファイルで、tfaction-root.yaml で記述した設定を上書きすることができます。

tfaction は tfaction.yaml を見て、作業ディレクトリを検出するので上書きする必要がない場合でも配置する必要があります。

tfaction.yaml

{}

3. aqua を使って必要な CLI ツールをインストールできるように aqua.yaml を追加する

aqua は宣言的に CLI ツールを管理することができる CLI ツールです。 tfaction は CLI ツールの管理に aqua を利用しているため、設定ファイルである aqua.yaml を追加する必要があります。

GitHub Actions では aquaproj/aqua-installer を使って aqua 経由で必要な CLI ツールをインストールします。

aqua.yaml

---
# aqua - Declarative CLI Version Manager
# https://aquaproj.github.io/
registries:
  - type: standard
    ref: v4.100.0 # renovate: depName=aquaproj/aqua-registry
packages:
  - name: suzuki-shunsuke/github-comment@v6.0.3
  - name: suzuki-shunsuke/ci-info@v2.1.3
  - name: int128/ghcp@v1.13.2
  - name: suzuki-shunsuke/tfcmt@v4.7.2
  # ... その他のツール

4. terraform plan を実行する workflow と terraform apply を実行する workflow を記述する

workflow の中身は基本的に setup と plan もしくは apply の 2 段階に分かれており、setup で変更のあった作業ディレクトリを取得し、plan や apply で取得した作業ディレクトリを並列に実行するという流れになっています。

plan.yaml

name: terraform-plan

on:
  pull_request:
    branches:
      - main

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  # setup で変更のあった作業ディレクトリを取得する
  setup:
    runs-on: ubuntu-latest
    outputs:
      targets: ${{ steps.list-targets.outputs.targets }}
    steps:
      - uses: actions/checkout@v4

      # aqua を使って必要なツールをインストールする
      - uses: aquaproj/aqua-installer@v2.2.0
        with:
          aqua_version: v2.21.0

      # 変更のあった作業ディレクトリを取得する Action
      - uses: suzuki-shunsuke/tfaction/list-targets@v0.7.3
        id: list-targets

  # plan で setup で取得した変更のある作業ディレクトリを並列に実行する
  plan:
    name: "terraform plan (${{ matrix.target.target }})"
    runs-on: ${{ matrix.target.runs_on }}
    needs: setup

    # setup で取得した変更のある作業ディレクトリが空の場合は実行しない
    if: join(fromJSON(needs.setup.outputs.targets), '') != ''

    strategy:
      fail-fast: false
      matrix:
        target: ${{ fromJSON(needs.setup.outputs.targets) }}
    env:
      TFACTION_TARGET: ${{ matrix.target.target }}
      TFACTION_JOB_TYPE: terraform
    steps:
      - uses: actions/checkout@v4
      - uses: aquaproj/aqua-installer@v2.2.0
        with:
          aqua_version: v2.21.0

      - id: github_app_token
        uses: tibdex/github-app-token@v2
        with:
          app_id: ${{ secrets.APP_ID }}
          private_key: ${{ secrets.PRIVATE_KEY }}

      # terraform init などの準備を行う Action
      - uses: suzuki-shunsuke/tfaction/setup@v0.7.3
        with:
          github_app_token: ${{ steps.github_app_token.outputs.token }}

      # terraform plan を実行する Action
      - uses: suzuki-shunsuke/tfaction/plan@v0.7.3
        with:
          github_app_token: ${{ steps.github_app_token.outputs.token }}

apply.yaml

name: terraform-apply

on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read
  pull-requests: write
  actions: read # artifact を取得するために必要

jobs:
  # plan.yaml と同様に setup で変更のあった作業ディレクトリを取得し、outputs で出力する
  setup:
    runs-on: ubuntu-latest
    outputs:
      targets: ${{ steps.list-targets.outputs.targets }}
    steps:
      - uses: actions/checkout@v4

      # aqua を使って必要なツールをインストールする
      - uses: aquaproj/aqua-installer@v2.2.0
        with:
          aqua_version: v2.21.0

      # 変更のあった作業ディレクトリを取得する Action
      - uses: suzuki-shunsuke/tfaction/list-targets@v0.7.3
        id: list-targets

  # apply で setup で取得した変更のある作業ディレクトリを並列に実行する
  apply:
    name: "terraform apply (${{ matrix.target.target }})"
    runs-on: ${{ matrix.target.runs_on }}
    needs: setup

    # setup で取得した変更のある作業ディレクトリが空の場合は実行しない
    if: join(fromJSON(needs.setup.outputs.targets), '') != ''

    strategy:
      fail-fast: false
      matrix:
        target: ${{ fromJSON(needs.setup.outputs.targets) }}
    env:
      TFACTION_IS_APPLY: "true" # apply する場合は TFACTION_IS_APPLY を "true" に指定
      TFACTION_TARGET: ${{ matrix.target.target }}
      TFACTION_JOB_TYPE: terraform
    steps:
      - uses: actions/checkout@v4
      - uses: aquaproj/aqua-installer@v2.2.0
        with:
          aqua_version: v2.21.0

      - id: github_app_token
        uses: tibdex/github-app-token@v2
        with:
          app_id: ${{ secrets.APP_ID }}
          private_key: ${{ secrets.PRIVATE_KEY }}

      # terraform init などの準備を行う Action
      - uses: suzuki-shunsuke/tfaction/setup@v0.7.3
        with:
          github_app_token: ${{ steps.github_app_token.outputs.token }}

      # terraform apply を実行する Action
      - uses: suzuki-shunsuke/tfaction/apply@v0.7.3
        with:
          github_app_token: ${{ steps.github_app_token.outputs.token }}

詳しくはドキュメントを参照していただくのが良いかと思いますが、上記のように tfaction を使うことで Terraform の CI/CD を GitHub Actions で実現することができます。

設定ファイルの配置など多少のルールはありますが、Terraform のコマンドの実行部分をすべて tfaction 側に任せることができるようになり、全体の workflow を簡潔に記述することができるようになっています。

以前は Terraform のコマンドを直接記述していたので、このように tfaction に任せてしまえるのは管理する上でとても楽になりました。

さらに tfaction は Terraform のコマンドの実行に加えて、以下のような機能もサポートされています。 ここからは tfaction がサポートしている機能のうち、便利に感じているいくつかの機能を紹介したいと思います。

tfaction の便利な機能

複数の作業ディレクトリの並列処理

これは先ほどの話にも少しありましたが、tfaction は GitHub Actions の機能である build matrix を使うことで複数の作業ディレクトリの plan や apply を並列に実行することに対応しています*2

弊チームで管理している Terraform のリポジトリは AWS で 2 つと k8s で 1 つの合計 3 つの作業ディレクトリでそれぞれ構成されているため、この機能によって CI/CD の実行時間を短縮することができています。

また、先ほどのサンプルコードでも使っている suzuki-shunsuke/tfaction/list-targets という Action は変更された作業ディレクトリを取得し outputs として吐き出すための Action なので、これを使うことで変更された作業ディレクトリのみを並列で処理することができます。

変更のあった作業ディレクトリのみを並列に処理させることができる

plan, apply の実行結果を tfcmt を使って PR にコメントしてくれる

tfcmt とは Terraform の plan や apply の実行結果を PR にコメントしてくれる CLI ツールです。このコマンドを使うことで、以下のように PR に plan や apply の実行結果をコメントしてくれます。

tfcmt でのコメント

これにより、毎回 CI/CD のログを確認をしに行かなくとも PR のコメントから実行結果を確認することができるようになります。 また、tfcmt は aqua 経由でインストールできるので、導入も比較的簡単です。

弊チームでは CircleCI を使っていた時から、上記のようなメリットもあり tfcmt を導入し利用していたのですが GitHub Actions に移行時に tfaction を導入したことで tfcmt のコマンドを直接記述する必要がなくなり aqua で tfcmt をインストールするだけで済むようになりました。

このあたりもまとめて tfaction 側に任せることができるのは非常に便利な点です。

plan の実行結果を元に apply を実行する

これは tfaction の大きな特徴の 1 つだと思っていますが、tfaction では plan の実行結果を S3 や GCS、v0.7.0 からは GitHub Actions の artifact に保存し、apply の実行時には plan の実行結果を取得して apply を実行するようになっています*3

これにより plan から apply までの間に何かしらの変更があった場合には apply が失敗するようになるので意図しない変更が反映されてしまうことを防ぎ、安心して運用することができます。

一度 apply が失敗すると再度 plan を実行して最新の実行結果を保存をしないと apply が実行できないということは運用者側が把握しておく必要がありますが、そこだけ気をつければとても便利な機能だと思っています。

他にも tfaction では Terraform のコマンドの実行に関する様々な機能を提供してくれているので、詳しくは以下のリポジトリやドキュメントを参照してください。

github.com

suzuki-shunsuke.github.io

tfaction の導入時の工夫

最後に tfaction を導入する際に弊チームが行なったことを簡単に紹介したいと思います。

GitHub Flow 以外の開発フローで利用するために (TF_WORKSPACE の切り替え)

tfaction は GitHub Flow を前提にしており*4、基本的に main ブランチと feature ブランチのみを想定しています。 そのため GitHub Flow を前提にしていないリポジトリの場合は tfaction を導入する際には注意が必要です。

弊チームで管理している Terraform のリポジトリは GitHub Flow を採用しておらず master, staging, feature ブランチの 3 つのブランチを想定しており、master と staging で terraform workspace が異なるため、tfaction を導入する際には以下のように plan や apply の実行時に workspace を切り替える必要がありました。

# こちらは plan の workflow での例です。plan の場合、base ブランチを見て workspace を切り替えています。
env:
  TF_WORKSPACE: ${{ fromJSON('{"master":"prd","staging":"stg"}')[github.base_ref] }}

これにより、master ブランチでの plan や apply の実行時には prd workspace が選択され、staging ブランチでの plan や apply の実行時には stg workspace が選択されるようになり、正しい workspace で plan や apply が実行されるようになりました。

apply 後の関連 PR の自動更新の停止

tfaction では PR がマージされ apply によって変更があった場合には関連するそのほかの PR のブランチを更新し plan を実行することで常に最新の状態を保つことができるようになっています*5。 そのため、他の PR が古い plan の実行結果を保持し続け、マージ時に apply が失敗するということを事前に防ぐことができます。

この機能は便利ではありつつ、弊チームでは現在使っておりません。というのも関連する PR の中にすぐにマージできないような PR あった場合についても plan の workflow が他の PR がマージされるごとに都度走ってしまうため GitHub Actions の利用時間を余計に消費してしまうことがあります。

では plan の実行結果を最新にするために弊チームではどうしているかというと GitHub の branch protection rule で代替しています。 branch protection rule で常に最新の base ブランチの変更を取り込んでいないとマージできないようにしているため、plan の実行結果が古い状態のまま apply して失敗する問題を防ぐことができています。

また先ほどのような、すぐにマージできない PR が他の PR のマージにより都度 plan の workflow が走ってしまい GitHub Actions の利用時間が余計に消費されてしまう問題についても、 branch protection rule を使うことでその PR のマージの手前のみ最新の base ブランチの変更を取り込んでからマージすることができるので極力 plan の workflow が走る回数を減らすことができています。

tfaction による関連 PR の自動更新機能を使わないようにするには tfaction の設定ファイルである tfaction-root.yaml に以下のような設定を追加する必要があります。

update_related_pull_requests:
  enabled: false

ただ、tfaction ではこの機能を停止することを推奨していません*6。そのため、この機能を停止する場合は代替手段を検討した上で問題なく運用できれば停止するという形が良いかと思います。

まとめ

今回は tfaction について紹介しました。tfaction は Terraform のコマンドの実行に関する様々な機能を提供しており、Terraform の CI/CD を GitHub Actions で実現する際にとても便利な Action だと思っています。

また、細かく機能を Action として分割しているため必要な機能のみを組み合わせて使って自分のチームの構成に合わせてカスタマイズすることもできます。

そのため弊チームでは今後も tfaction を使ってより良い CI/CD 環境を構築していきたいと思っています。

明日は吉岡さんの「iOSアプリのSWIFT_STRICT_CONCURRENCYをcompleteにした」 です。お楽しみに!