MonorepoとGitHub Actionsで実現するTerraform - 副作用を活用したCI/CD最適化の取り組み -

2024-11-28

MonorepoとGitHub Actionsで実現するTerraform - 副作用を活用したCI/CD最適化の取り組み -

こんにちは、SREチームのkekeです。

株式会社Eukaryaは、Google Cloudなどのリソース管理にTerraformを使用しています。Re:Earthは汎用的WebGISプラットフォームで、マネージドサービスのRe:Earth Cloudだけでなく、国土交通省のPLATEAU(プラトー)で使われているシステムのホスティングをしています。さらに、本番・開発・テストなど複数の環境にデプロイされ、Re:Earth CMSなど他のコンポーネントも含むため、再利用するTerraformファイルが多いです。

そこで、再現性と保守性を向上させるため、Terraform Moduleを活用して設定を共通化し、すべてのTerraformファイルを1つのリポジトリ(Monorepo)にまとめて管理しています。

最近になってGitHub Actionsによって自動的にterraform planやapplyを実行するCI/CDが実装され、開発者が徐々にTerraformに触れ始め、リソース管理できるようになりました。現在も、少人数のSREメンバーで整備を進めながら、試行錯誤を繰り返しています。

この記事では試行錯誤の過程で生じた、Terraform Module変更時にterraform applyがうまく行われない問題と、その解決方法について解説します。

Monorepoにおけるディレクトリ構成

問題の説明に入る前に、現在のTerraformのリポジトリのディレクトリ構成について説明します。

Monorepo構成は、実際に運用しているものを大きく簡略化していますが、以下のような構成になっています。terraform/プロダクト/デプロイメント環境ディレクトリの一つ一つがGoogle Cloudプロジェクトに対応しています。

.
├─ modules
│  ├─ reearth
│  └─ reearth-cms
└─ terraform
   ├─ cloud
   │  ├─ dev
   │  │  ├─ reearth.tf
   │  │  └─ reearth_cms.tf
   │  └─ prod
   │     ├─ reearth.tf
   │     └─ reearth_cms.tf
   ├─ plateau
   │  ├─ dev
   │  │  ├─ reearth.tf
   │  │  └─ reearth_cms.tf
   │  └─ prod
   │     ├─ reearth.tf
   │     └─ reearth_cms.tf
   └─ ...

例えば、reearth.tfでは、以下のようにTerraform Moduleを指定しています。本番環境ではタグを使ってバージョンを固定し、開発環境ではModuleに入れた変更を任意のタイミングで適用できるように調整しています。

# 開発環境
module "reearth" {
     source = "../../modules/reearth"
     ....
}

# 本番環境
module "reearth" {
     source = "<GITHUB_REPOSITORY_URL>//modules/reearth?ref=v0.1.0"
     ....
}

この構成により、Terraform Moduleを保守することで、さまざまな環境にデプロイしているリソースに同じ変更を適用でき、運用負荷を軽減しています。また、セキュリティやベストプラクティスのために必ず適用すべき実装や命名規則などを統一的に管理することができています。

GitHub Environmentsとchanged-filesを使ったCI/CD

次に、TerraformのCI/CDに付いて説明します。GitHub Actionsを使用して、以下のようにPull Request(PR)の状態でterraform planを実行し、その結果をPRにコメントしています。

name: Terraform Plan
on:
  pull_request:

jobs:
  changes:
    runs-on: ubuntu-latest
    permissions:
      contents: read # To checkout
    outputs:
      terraform-modified: ${{ steps.terraform-changed-files.outputs.changed }}
      terraform-modified-files: ${{ steps.terraform-changed-files.outputs.changed_files }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - id: changed-files
        uses: tj-actions/changed-files@v44
        with:
            dir_names_max_depth: 3
            files: terraform/**/*.{hcl,tf,yaml}
            matrix: true

  plan:
    if: needs.changes.outputs.terraform-modified == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 30 # mins. Default is 360 mins.
    needs:
      - changes
    permissions:
      contents: read # To checkout
      id-token: write # To authenticate with GCP using OIDC
      pull-requests: write # To comment on PRs
    defaults:
      run:
        working-directory: ${{ matrix.working-directory }}
    environment: ${{ matrix.working-directory }}-plan
    strategy:
      fail-fast: false
      matrix:
        working-directory: ${{ fromJson(needs.changes.outputs.terraform-modified-files) }}
    steps:
      - uses: actions/checkout@v4

      - uses: google-github-actions/auth@v2
        with:
          service_account: ${{ secrets.GOOGLE_CLOUD_SERVICE_ACCOUNT_EMAIL }}
          workload_identity_provider: ${{ secrets.GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER }}
      
      - run: tfcmt -var "target:${{ matrix.working-directory }}" plan -- terraform plan
terraform planの結果がコメントされるため、すぐに差分を確認できる。
terraform planの結果がコメントされるため、すぐに差分を確認できる。

PRがmainブランチにマージされた後は、同じようなワークフローでterraform applyを実行しています。

こうしたCIではtj-actions/changed-files GitHub Actionを活用し、変更されたディレクトリ(環境)を検知しています。そして、検知した変更内容とGitHub Environmentsと組み合わせることで、各環境ごとの認証情報(Google Cloudサービスアカウントなど)を用いてTerraformを実行しています。

この方法により、変更された環境のみでTerraformが実行されるため、GitHub Actionsの使用量と待機時間が最適化され、各環境ごとの認証情報を分離することができています。わずか100行程度のYAMLで、ディレクトリさえ追加すればYAML(ワークフロー)の書き換えなしに、事実上無限の環境に対応できる柔軟性を実現しています。この仕組みにより、柔軟かつ効率的な運用が可能になります。

しかし、ここで問題が発生します。

Moduleを変更してもapplyされない問題

すでに述べたように、私たちはTerraform Moduleを多用しており、 source でmoduleの相対パス指定しています。しかし、GitHub Actionsは各環境の変更のみを検知してTerraformを実行するため、Moduleを変更しても、各環境のtfファイルに変更がない場合はapplyが実行されません。

├─ modules
│  ├─ reearth <- ここを変更しても...
│  └─ ...
└─ terraform
   ├─ cloud
   │  └─ dev
   │     └─ reearth.tf <- 依存しているこの環境でTerraformは実行してくれない...
   └─ ...

これは開発体験に関わる大きな問題で、1つのPR内でTerraform Moduleを変更し、開発環境に適用しながら開発をしたい場合が多いでしょう。このままでは、Terraform Moduleを変更するために1PR、Moduleを適用するために1PRと、二段階必要になります。Moduleにバグなどがあれば、再度二段階で変更しなければならず非常に煩わしいです。また、以下のように相対パスを使っているため、空行を追加するなど何らかの方法で変更を起こさないとGitHub Actionsが実行されません。

module "reearth" {
     source = "../../modules/reearth"
     ....
}
+ # 無理やり変更を追加してGitHub ActionsでTerraformを実行するしか...

最適化を追求したがゆえに、GitHub Actionsを実行させるためだけに空行やコメントを追加するという方法は避けたいと考えました。そのため、Terraform Moduleの変更を各環境に伝える仕組みが必要でした。

副作用としての依存関係を定義する

Moduleの変更による「副作用」として、あたかもそのModuleに依存する環境(ディレクトリ)で変更があったように見せるために、GitHub Action KeisukeYamashita/side-effect-changed-files を実装しました。

このActionでは、外部YAMLファイルまたはインラインでどの環境がどのModuleに依存するのかを定義します。

- uses: tj-actions/changed-files@v44
  id: raw-changed-files
  files: **/*.{hcl,tf,yaml}

- name: Get changed files
  id: changed-files
  uses: KeisukeYamashita/side-effect-changed-files@v1
  with: 
    bypass: terraform/**/*.{hcl,tf,yaml} # 各環境の変更はいつも通りで、何もしない。
    dir_names: true # ディレクトリごとにグルーピングする
    files: ${{ steps.raw-changed-files.outputs.files }}
    mapping: |
      # reearthおよびreearth-cms Moduleに変更が入ると
      # cloudおよびplateauプロジェクトの開発環境のTerraformが実行される
      terraform/{cloud,plateau}/dev/*:
        - modules/reearth/*.tf
     matrix: true

この設定により、reearthに変更があると副作用が発生し、cloudおよびplateauで変更があったとみなされ、開発環境でGitHub Actionsが実行されます。

.
├─ modules
│  ├─ reearth <- 変更があると
│  └─ reearth-cms
└─ terraform
   ├─ cloud
   │  └─ dev <- ここに副作用として変更があったように実行される
   │     ├─ reearth.tf
   │     └─ reearth_cms.tf
   ├─ plateau
   │  └─ dev <- ここに副作用として変更があったように実行される
   │     ├─ reearth.tf
   │     └─ reearth_cms.tf
   └─ ...

個別に定義するのは少し手間ですが、Moduleや環境数はそこまで頻繁に増減しないので、現時点では煩雑さを感じていません。仮に設定し忘れたとしても、GitHub ActionsがTerraformを実行しないだけなので、安全であり、開発者も気づきやすいです。

1つのPR内でModuleの変更と開発環境への適用を行うことで、「Moduleを変更したのに適用されていない」というConfiguration Driftの発生を防ぎ、GitOpsの原則(main ブランチが常に最新のリソース状態を反映する)を維持できます。また、Planの失敗などをより速く開発者にフィードバックをすることができ、より高速な開発サイクルを提供できています。

おわりに

本記事では、MonorepoとGitHub Actionsを活用したTerraformの管理方法について紹介しました。特に、Moduleの変更を効果的に各環境に反映させる仕組みを実装することで、開発効率の向上とConfiguration Driftsの防止を実現しました。この取り組みにより、より安全で効率的なインフラストラクチャ管理が可能となり、開発チームの生産性向上につながっています。今後も継続的に改善を重ね、より良いDevOps実践を目指していきます。

Japanese

Eukaryaでは様々な職種で積極的にエンジニア採用を行っています!OSSにコントリビュートしていただける皆様からの応募をお待ちしております!

Eukarya 採用ページ

Eukarya is hiring for various positions! We are looking forward to your application from everyone who can contribute to OSS!

Eukarya Careers

Eukaryaは、Re:Earthと呼ばれるWebGISのSaaSの開発運営・研究開発を行っています。Web上で3Dを含むGIS(地図アプリの公開、データ管理、データ変換等)に関するあらゆる業務を完結できることを目指しています。ソースコードはほとんどOSSとしてGitHubで公開されています。

Eukarya Webサイト / ➔ note / ➔ GitHub

Eukarya is developing and operating a WebGIS SaaS called Re:Earth. We aim to complete all GIS-related tasks including 3D (such as publishing map applications, data management, and data conversion) on the web. Most of the source code is published on GitHub as OSS.

Eukarya Official Page / ➔ Medium / ➔ GitHub