Retty の Terraform CI/CD 解体新書

Retty インフラチームの幸田です。

6月に実施したマイクロサービス強化月間で公開した記事では、マイクロサービス環境を Terraform を利用して刷新した話を書きました。 engineer.retty.me

この記事では前回と重複する箇所もありますが、Terraform の CI/CD にフォーカスした内容を書こうと思います。

CI を整備するにあたって意識したこと

既に別の Terraform リポジトリで取り入れているものもありますが、今回の CI/CD を設計するにあたって下記のようなポイントを意識しました。

「誰でも」かつ「安全に」利用できるように

今回の Terraform リポジトリは、Retty のマイクロサービス全体のインフラ構成を管理するもので、インフラチーム以外のメンバーにも広く触ってほしいと考えていました。

以前までマイクロサービスの新規追加などインフラ関連の作業は、開発メンバーからインフラチームに依頼し、インフラチームが追加作業を行うといったフローでした。
これは主にインフラ関連の変更作業には比較的強い権限が必要になるためです。

この課題は CI に権限を持たせて、個人に紐付かない権限で作業が可能になることである程度解決できると考えました。ただし CI には比較的強い権限を持たせる必要があるため、「誰でも」触れるようにするだけでなく「安全に」触れるような仕組みを検討しました。

CI 上ですべての作業を完結させる

Terraform で代表的なコマンドといえば、実行計画を表示する terraform plan とその実行計画を反映する terraform apply ですが、実際のオペレーションではこれ以外のコマンドも必要になるかと思います。

  • ステートファイル上から削除を実施する terraform rm
  • ステートファイル上のリネームを行う terraform mv
  • ステートファイルにリソースを取り込む terraform import

これらのコマンドは plan や apply と比べると頻繁に利用するものではないですが、長期的にコードを運用していくにあたっては登場する機会もそれなりにあると思います。
またステート操作だけでなく「先にステージング環境にだけ変更を適用したい」「apply が失敗したので再実行したい」といった特殊なオペレーションも発生すると思います。

こうした特殊なオペレーションも例外なく CI 上で実行させたいと考えていました。特殊なオペレーションだからこそレビューを通してから実行し、変更の履歴を残したいためです。

Pull Request によるレビュー環境の整備

インフラ設定をコード化することのメリットは様々ですが、個人的には通常のアプリケーション開発と同様に Pull Request を通してレビューが可能になる、更に linter の適用などアプリケーション開発におけるプラクティスをインフラの設定にも導入できる点が最大のメリットだと考えています。

ただ tf ファイルの差分を表示させるだけでなく、より快適にレビューができるように整備しました。

バージョンアップ作業の完全自動化

アプリケーション開発においてライブラリやミドルウェアのバージョンアップ作業は避けて通れないものだと思いますが Terraform も同様です。
長期に渡ってバージョンアップを放置すると、必要な機能が使えなかったり、プラットフォームの Deprecated などにより使えていたものが使えなくなったりする可能性があります。

滞りがちなバージョンアップ作業は、できる限り最新のバージョンに追従できるようにし、アップデートの作業も完全に自動化したいと考えていました。
ただ自動化するだけでなく、意図しない変更が加わらないように安全性もある程度担保した仕組みを取り入れました。

Terraform のディレクトリ構成について

CI の説明に入る前に採用している Terraform のディレクトリ構成を紹介します。module 構成などは、前回の記事で紹介したためここでは省略します。

ディレクトリ構成は下記のようになっており、terraform コマンドは環境ごとに作られた /envs 配下のディレクトリで実行します。

├── envs
│   ├── development
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   └── versions.tf
│   ├── production
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   └── ...
│   └── staging
│       ├── backend.tf
│       ├── main.tf
│       └── ...
├── modules
│   ├── alb
│   │   ├── alb.tf
│   │   └── ...
│   ├── ci
│   │   ├── circleci.tf
│   │   └── ...
│   ├── ecs-service
│   │   ├── ecs.tf
│   │   ├── ...

リポジトリの運用フロー

ブランチ戦略としては、default branch である main を用意し、その他のブランチは feature branch のみ分岐させる GitHub Flow を採用しています。具体的には下記のようなフローです。

  1. main から feature branch を作成する
  2. main に向けて Pull Request を作成しレビューをもらう
  3. Pull Request をマージする

リポジトリについては、複数のプロジェクトを1つにまとめる Monorepo 構成は取らず、プロジェクト毎に別のリポジトリを作成しています。深い理由はなく、今まで個別にリポジトリを作成してきたというのが経緯です。*1

Terraform によるリソースの追加、変更、削除

新規リソースの追加や既存リソースの変更、削除を行う際のフローです。

  1. main から feature branch を作成する
  2. target ラベルを付与して PR を作成する
    • terraform validate / terraform plan / tflint などが実行される
  3. レビューを経て PR をマージする
    • terraform apply が実行される

PR が作成されたら terraform plan を実行して、マージされたら terraform apply を実行するのはよくあるパターンだと思いますが、Retty では PR に付与するラベルに応じて terraform コマンドを実行するディレクトリや処理内容を分岐できるようにしています。詳細は後述します。

tfmigrate によるステートファイルの操作

terraform stateterraform import コマンドを実行する際のフローです。

  1. main から feature branch を作成する
  2. tfmigrate ラベルを付与して PR を作成する
    • tfmigrate plan が実行される
  3. レビューを経て PR をマージする
    • tfmigrate apply が実行される

ステートファイルの操作については tfmigrate というツールを利用して CI 上で行えるようにしています。

CI で実行される job について

ここからは CI で実行される job の詳細について書きます。

Pull Request をオープンした時

ワークフローの全体像はスクリーンショットの通りです。

Pull Request 作成時にトリガーされるワークフロー

このワークフローは PR オープン時、もしくは PR が変更されるたびに実行されます。

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

PR に付与されているラベルの取得

一番最初に実行されるこの job では、PR に付与されている GitHub Label の情報を取得して、後続の job に渡します。
(ラベルを取得するための job を単体で切り出している理由については後述します。)

付与するのは次のようなラベルです。skip citfmigrate については後ほど紹介します。

  • target ラベル
    • target:develop
    • target:staging
    • target:production
    • target:all
  • skip ci ラベル
  • tfmigrate ラベル

target ラベルと呼ばれるラベルを付与することで、terraform コマンドを実行するディレクトリを選択できるようにしています。

特定のディレクトリにだけ terraform コマンドを実行するアプローチとして、変更のあったファイルを検出して、そのファイルが含まれるディレクトリにコマンドを実行するやり方があると思いますが、今回のようなディレクトリ構成の場合、各環境から共通で利用される /modules ディレクトリにだけ変更があった際に対応できないためこのような方法を取っています。

fmt の実行

tf ファイルのフォーマットを行う terraform fmt を実行するための job です。
Retty では CI 上で terraform fmt を実行し、差分があった場合にはその修正を自動的に commit & push するようにしています。これはリポジトリを初めて触る人の負荷を軽減するためです。

CI によってフォーマットが自動修正されている様子

terraform fmt は、-check というオプションを付与することで、差分があった場合にはファイルの修正を行わずに、フォーマット対象のファイルを表示して non-zero で終了させることができます。

-check - Check if the input is formatted. Exit status will be 0 if all input is properly formatted. If not, exit status will be non-zero and the command will output a list of filenames whose files are not properly formatted. - Command: fmt | Terraform

これを利用して terraform fmt -check -recursiveterraform fmt -recursive を別の step で実行し、修正対象のファイルがあった時には即座に CI を落とすようにしています。

      # terraform fmt で修正が必要になるファイルがあるかをチェック
      # もしも修正の必要があるファイルが存在する場合には、終了コードがエラーになるので CI が落ちる
      - name: Check terraform fmt
        run: terraform fmt -check -recursive

      # if: failure() によって手前のステップがエラーの時のみ terraform fmt を実行しファイルをフォーマットする
      - name: Terraform fmt
        if: failure()
        run: terraform fmt -recursive

      # if: always() によって常に実行され、変更があれば commit & push して、変更が無ければ何もしない
      # ファイルをフォーマットした際にはここの push をトリガーに新しい CI が実行される
      - name: Auto commit
        if: always()
        uses: EndBug/add-and-commit@v9
        with:
          author_name: "github-actions[bot]"
          author_email: "github-actions[bot]@users.noreply.github.com"
          committer_name: "github-actions[bot]"
          committer_email: "github-actions[bot]@users.noreply.github.com"
          message: "chore: terraform fmt"

terraform fmt -recursive ではファイルのフォーマットが実行された後、修正が commit & push されて、その commit をトリガーに新しい job が起動しますが、その job の実行をなるべく早く行いたいためです。

ドキュメントの生成

terraform-docs は Terraform の module に関するドキュメントを自動生成するためのツールです。公式の module registry ページでも利用されています。(AWS VPC module の例)

github.com

terraform-docs の詳細はここでは触れませんが /modules 配下に存在する README を自動更新するようにしています。更新の処理は、terraform-docs の公式から用意されているも Actions を利用しています。

github.com

ラベルに応じた matrix の生成

GitHub Actions では matrix と呼ばれる機能を利用することで、変数に基づいて複数の job を並列で実行させることができます。この job では matrix で利用するための変数を生成しています。

運用フローで触れたように、Retty では PR に付与されたラベルで terraform コマンドを実行するディレクトリを決定しています。標準でそのような機能はないので、これを実現するための Action を作成しました。

github.com

select-target-action は PR に付与されたラベルに応じて、予め定義しておいたディレクトリのリストを返すためのカスタムアクションです。以前は set-terraform-matrix という名前で同じようなアクションを作成していましたが、より汎用的に利用できるように TypeScript で書き直して名前も変更しました。

使い方は README にある通りですが、GitHub Label とそれに紐づくディレクトリを定義しておきます。デフォルトではリポジトリルートに配置した .deploy_target.json が利用されます(変更可能)。

{
    "target:develop": [
        "envs/development"
    ],
    "target:staging": [
        "envs/staging"
    ],
    "target:production": [
        "envs/production"
    ],
    "target:all" : [
        "envs/development",
        "envs/staging",
        "envs/production"
    ]
}

このアクションが実行されると、PR に付与されたラベルを取得し、ラベルに対応したディレクトリのリストを output として出力します。

例えば target:developtarget:staging を選択すると、それぞれの配列がマージされて、次のような出力になります。

["envs/development","envs/staging"]

この出力を matrix の変数として利用することで、PR に付与されたラベルによってコマンドを実行するディレクトリを変更することができます。

matrix を利用する際は下記のように select-target-action の output を入力に利用して ${{fromJson(needs.set-matrix.outputs.workdir)}} な形で matrix を組み立てます。

...
    strategy:
      matrix:
        workdir: ${{fromJson(needs.set-matrix.outputs.workdir)}}

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup terraform
        uses: hashicorp/setup-terraform@v1

      - name: Terraform plan
        working-directory: ${{ matrix.workdir }}
        run: terraform plan -input=false -no-color

こうすることで特定の環境にだけ terraform コマンドを実行することができます。もちろん別のコマンドでも応用可能です。

job が動的に生成されている様子

validate と plan の実行

terraform validateterraform plan を実行するための job です。先ほどの select-target-action にて決定されたディレクトリに対してのみ実行されます。

またそれぞれのコマンドの実行結果は、レビューしやすいように PR のコメントとして投稿しています。PR へのコメントを実現するためのツールはいくつか存在しますが、github-comment と tfcmt を利用しています。

github.com

github.com

terraform validate でエラーが見つかった際には、github-comment を利用して Terraform を実行したディレクトリと対象の CI へのリンク、実際のエラー内容をコメントとして投稿しています。
ここで表示される内容はテンプレートを利用してカスタマイズ可能です。詳しくは作者様の記事を参照してください。

terraform validate の結果が表示されている様子

terraform plan の結果は、単純にコマンドの出力が表示されるだけでなく、Create / Update / Delete などにリソースが分類して分かりやすく表示してくれます。

terraform plan の結果を表示する tfcmt の様子

tfcmt には変更内容を示すラベルが付与されるなど、他にも様々な機能が備わっているので詳しくは作者様の記事を参照してください。

tflint の実行

tflint は Terraform のための Linter ツールです。

github.com

本体とは別に ruleset と呼ばれるプラグインを利用することで、様々なサービスに対応することができます。Retty では tflint-ruleset-terraformtflint-ruleset-aws を使用しています。

Linter の結果は、reviewdog を利用して PR で表示させるようにしています。tflint のアウトプットは --format で変更することができるため、reviewdog にも簡単に対応できます。

reviewdog によって指摘されている様子

また今回のようなディレクトリ構成で tflint を実行しても各 module のディレクトリは見てくれないので .terraform/modules/modules.json の中身を見て、/modules 配下でもコマンドを実行するようにしています。

      - name: tflint
        working-directory: ${{ matrix.workdir }}
        run: |
          tflint --config ${GITHUB_WORKSPACE}/.tflint.hcl --init
          # module を利用している場合には .terraform/modules/modules.json を見て各ディレクトリにも実行する
          if [ -e .terraform/modules ]; then
              cat .terraform/modules/modules.json | jq -r '[.Modules[].Dir] | unique | .[]' | while read line; do \
              tflint --config $GITHUB_WORKSPACE/.tflint.hcl --format=checkstyle $line | \
              reviewdog -f=checkstyle \
                        -name="tflint" \
                        -reporter=github-pr-review \
                        -level=error \
                        -filter-mode=nofilter \
                        -fail-on-error=true; done
          else
              tflint --config $GITHUB_WORKSPACE/.tflint.hcl --format=checkstyle | \
              reviewdog -f=checkstyle \
                        -name="tflint" \
                        -reporter=github-pr-review \
                        -level=error \
                        -filter-mode=nofilter \
                        -fail-on-error=true
          fi

tfmigrate plan の実行

tfmigrate は Terraform のステート操作を CI 上で実現可能にするためのツールです。この job は tfmigrate ラベルが付与された時のみ実行されます。

github.com

README に説明がありますが、マイグレーションファイルを用意することでステートファイルの操作を実現することができます。詳しい使い方などは作者様の記事を参照してください。

tfmigrate は tfmigrate plan というコマンドを備えており、ステートファイルを書き換える前に実行計画を表示させることができるため、こちらも github-comment を利用して、PR にコメントを残すようにしています。

tfmigrate plan の結果がコメントされている様子

失敗した場合には次のようなコメントが投稿されます。

tfmigrate plan が失敗した時のコメント

Pull Request をマージした時

ワークフローの全体像はスクリーンショットの通りです。

Pull Request マージ時にトリガーされるワークフロー

このワークフローは main ブランチへの push をトリガーに実行されます。

on:
  push:
    branches:
      - main

PR に付与されているラベルの取得 と matrix の生成は、Pull Request 作成時に実行されるものと同様なので、説明は省略します。

apply の実行

ここでは設定の変更を反映するために terraform apply を実行します。tfcmt はマージコミットを元に対象の PR に対して apply の結果もコメントとして投稿してくれます。

terraform apply の結果がコメントされている様子

また plan は成功したものの apply は失敗するケースがありますが、その場合にもエラーの内容をコメントしてくれます。

terraform apply が失敗した時のコメント

任意の環境で Terraform を実行したい時

target ラベルを用いて適用する環境を選ぶことができますが、terraform apply が実行されるのは PR をマージしたタイミングのみです。
そのため先に target:develop / target:staging をつけてマージした場合だと、既にマージされた main ブランチのコードで target:production を付与して terraform apply を実行する必要があります。

これを実現するためには空コミットを積んで PR を出す必要がありますが、1クリックで実現できるように空コミットの作成→PR の作成を行うためのワークフローを用意しました。

空コミットで PR を作るワークフロー

タイトルを入力して Run workflow すると Draft PR が自動的に作成されるため、あとは目的のラベルを付与して Open するだけで実行できるようになります。

再実行したい時

「何故か terraform plan が失敗した」「付与するラベルを間違えた」など PR が Open している状態で CI を再実行したいケースがあります。
Actions の画面から Re-run することもできますが、目的のワークフローを探すのが手間だったり、Re-run するワークフローの選択ミスを防ぐために PR 上のコメントで Re-run できるようにしました。

CI を再実行するための /rerun コマンド

CI を実行したくない時

README だけの更新など、いくつかの場面で CI を実行したくないケースがありますが、その場合は skip ci というラベルを付与することで各処理が実行されないようにしています。

これは素朴に GitHub Actions 側の job レベルの if を用いて、ラベルが含まれている場合には実行しないようにしています。

if: !contains(needs.get-labels.outputs.labels, 'skip ci')

Tips

apply のトリガーに push を利用する

PR がマージされたタイミングで apply を実行するためには、下記のようにトリガーの選択肢がいくつかあります。

  • PR の close をトリガーにする
  • デフォルトブランチへの push をトリガーにする

最初は PR の close をトリガーに利用していたのですが、下記のような理由からトリガーをデフォルトブランチに対する push に変更しました。

PR の close イベントはたまに発火しない時がある

PR の close をトリガーにする場合には、次のようにトリガーを設定します。

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

しかし下記の Discussions にあるように、この書き方はたまにトリガーされないことがあるそうです。

github.com

発生頻度は稀ではありますが、実際に何度かトリガーされない場面がありました。

OIDC のトークンにブランチの情報が含まれないため

GitHub Actions から AWS などのクラウドプロバイダーに送られる OIDC トークンの subject claim には、トリガーされたイベントに基づいてブランチの情報などが格納されます。

例えば main ブランチに対する push をトリガーにした場合には refs/heads/main のようなブランチの情報、pull_request をトリガーにした場合には pull_request という文字列が入るようになっています。

PR が main ブランチにマージされた際のトリガーとして、PR の close を利用すると、PR のオープン時とマージ時のどちらに対しても pull_request が入ってしまい、どちらでトリガーされたものなのかが識別できなかったので、デフォルトブランチに対する push をトリガーとして利用しました。
識別したい理由については後の「安全に CI を利用するための取り組み」で紹介します。

ラベル取得用の job を独立させている理由

GitHub Actions で付与されたラベルに応じて処理を分岐させたい場合、各 job や step の if に対して github.event.pull_request.labels.*name のような context を渡し、contains() などを併用して処理のを実行させるかどうかを決定させることが多いと思います。

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

jobs:
  hoge_job:
    if: contains(github.event.pull_request.labels.*.name, 'hoge')
    runs-on: ubuntu-latest
    ...

上記の例だと PR に hoge というラベルが含まれている場合のみ hoge_job が実行されます。このような分岐を各 job に書くことによって、付与されたラベルに応じて特定の job を実行する/しないを制御することができます。

なぜこれを利用せずにラベルを取得するための job を別途設けているかというと、PR に付与されたラベルは on.pull_request をトリガーとするワークフローでしか取得できないためです。
例えば、main に PR がマージされた時に発火するワークフローを on.push.branches["main"] を利用して組み立てた時には github.event.pull_request.labels を利用することができません。

前述のような理由でデフォルトブランチに対する push を利用したかったため、ラベルを取得するための job を設けました。
ちなみにマージコミットに紐づく PR の情報を取得するために、この job では ci-info というツールを利用しています。

github.com

ツールのバージョン管理と継続的なアップデート

Retty では Terraform に限らず、全社的にライブラリのアップデートで Renovate を利用しています。ここでは Renovate を利用した継続的なアップデートに関する取り組みについて紹介します。

Terraform / Terraform Provider のバージョンアップ

Renovate はデフォルトで Terraform にも対応しているので、これを利用して継続的にアップデートを行っています。 単に PR を作るだけでなく、作成された PR で terraform plan を実行して、差分がないかの確認を行っています。差分がある場合にはその旨を PR にコメントして CI を fail させ、差分がない場合には automerge されます。

現在とは構成が異なる部分もありますが、Renovate による Terraform / Terraform Provider のアップデート自動化については以前記事を書いたので良かったら見てください。

engineer.retty.me

aqua を活用したツールのバージョンアップ

バージョンアップの対応を行うべき対象は Terraform / Terraform Provider だけではありません。今まで紹介してきたように、CI 上で下記のようなツールを利用しています。

  • tfcmt
  • github-comment
  • tflint
  • reviewdog
  • tfmigrate
  • tfenv
  • ci-info

Renovate では正規表現を用いて、デフォルトでは対応していないツールにもアップデートを適用することができます。

docs.renovatebot.com

初めはこの機能を利用して、ツール毎にカスタムで正規表現を書き、アップデートの対応を行っていましたが、ツールの数が増えるに従って設定漏れなどが出てくるようになりました。
何か良い方法がないかを探していた所、tfcmt や github-comment でお世話になっている @suzuki-shunsuke 氏が開発した aqua と呼ばれる CLI バージョンマネージャーが今回の要件にフィットしていそうでした。

aquaproj.github.io

aqua は CLI ツールを yaml で宣言的にバージョン管理できるツールです。様々な機能が存在しますが、中でも特徴的なのが Renovate によるツールのアップデートに対応している点です。

細かい説明はここでは端折りますが、aqua を導入したことで CLI ツール毎に記述していた正規表現の設定を全て無くすことができ、また新規ツールを導入した際にも追加の設定なしに Renovate によるアップデートに対応できるようになりました。

Renovate による tflint の更新 PR

aqua.yaml が自動更新されている様子

aqua については書きたいことがたくさんあるので、また別の機会に詳しく紹介できればと思います。

安全に CI を利用するための取り組み

扱うリソースによりますが、Terraform の CI は比較的強い権限を持つことが多いかと思います。例えば AWS の場合には管理者権限に相当する AdministratorAccess Policy が必要なケースが大半です。

ここではなるべく安全に CI が実行できるようにするための取り組みを紹介します。

IAM Role の分割

terraform planterraform apply では必要な権限が異なります。扱うリソース次第ですが、基本的に plan では読み取りの権限だけがあればよく、逆に apply は AdministratorAccess のような権限が必要です。

IAM 公式ドキュメントにも記載されている least-privilege を実現するために、IAM Role は処理に応じて分割し、それぞれの操作に必要最低限のポリシーのみ持たせるようにしました。

OpenIDConnect を利用した認証

以前の記事でも軽く触れましたが、GitHub Actions には OIDC を利用して各クラウドプロバイダーに認証するための仕組みが用意されています。

github.blog

これを利用することで有効期限のない IAM User の認証情報を持たせることなくクラウドプロバイダーへの認証を行うことができます。特に AdministratorAccess のような強いポリシーを利用する上では必須の設定かと思います。

詳しくはドキュメントを参照してください。

IAM Role の利用制限

GitHub Actions の設定ファイルが Terraform のコードと同居しているため、素朴に CI を設定してしまうと誰でも管理者権限を使えるようになってしまいます。これは CI の設定ファイルを書き換えることで、リポジトリへの書き込み権限を持つ全員が管理者権限を使える状態となるためです。

リポジトリに対するアクセス権限を絞ってしまうというのも1つのやり方ですが、今回は強い権限を持たないメンバーでもインフラの設定を変更できるようにしたかったため別の方法を取りました。

OIDC トークンに含まれる subject について

GitHub Actions からクラウドプロバイダーに渡される OIDC トークンには様々な情報が含まれます。中でも Subject (sub) には、GitHub Actions 側のイベントに応じて、ブランチの情報などが含まれます。

公式ドキュメントでは、実際の書式と合わせてどのようなイベントに対して、どのような文字列が入るのかが紹介されています。

例えば on.pull_request をトリガーとするワークフローでは、次のような書式になります。

repo:octo-org/octo-repo:pull_request

信頼ポリシーによるブランチの制限

公式ドキュメントにもあるように OIDC トークンに含まれる subject claim は IAM Role の信頼ポリシーで利用することができます。
これを用いて下記のように強いポリシーを持つ apply 用の IAM Role は main ブランチに対する push のイベントでしか利用できないようにしました。*2

IAM Role の制限のイメージ図

具体的な信頼ポリシーは次の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::123456123456:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:<Org>/<Repo>:ref:refs/heads/main"
                }
            }
        }
    ]
}

plan 用の IAM Role は on.pull_request でトリガーされる job で利用されるため、次のように設定しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::123456123456:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:<Org>/<Repo>:pull_request"
                }
            }
        }
    ]
}

こうすることで、仮に CI の設定ファイルを改変しても強い権限を利用できないようになりました。

今回は許容しましたが、plan 用の IAM Role は引き続き任意のブランチで利用可能なので、この部分は今後の課題としています。*3

まとめ

マイクロサービス基盤の刷新に伴い、今までインフラチームがメインで触っていた Terraform リポジトリの利用が社内でも増える(増えてほしい)ため、マイクロサービスの移行作業と並行して少しずつ CI の改善を行いました。今回の取り組みによって以下のようなことが実現できました。

  • インフラチーム以外のメンバーでもインフラの設定変更が可能になった
  • Terraform に関わる操作をほぼすべて CI 上で実行可能になった
  • 様々な設定変更が PR を通してレビューできるようになった
  • Terraform / Terraform Provider その他様々な CI ツールなどを自動で最新の状態に保つことが可能になった

マイクロサービス基盤の Terraform の CI は今回である程度整備できましたが、取り組む課題はたくさん残っています。興味を持った方はカジュアル面談からでもお話しましょう!

hrmos.co

参考記事

Terraform の CI を整備するにあたって下記の記事が大変参考になりました。

また今回はある程度枠組みができていた関係で採用しませんでしたが、Terraform Workflow をいい感じに作るための Action である suzuki-shunsuke/tfaction の実装やアイディアも大変参考になりました。ありがとうございます。

*1:ただし最近はプロジェクトの数も増えてきて、CI の設定変更を行う際に複数のリポジトリに渡って変更を行う必要があるため、Monorepo 構成に移行するかどうかを考えている段階です。

*2:当然 main に対する直 push は禁止しています

*3:GitHub Actions の設定ファイルを専用の別のリポジトリで管理する gha-trigger というプロジェクトがまさにこの課題を解決してくれそうなので注目しています。