Timee Product Team Blog

タイミー開発者ブログ

terraformでPRごとにテスト環境を用意する

この記事はTimee Advent Calendar 2023シリーズ 2の5日目の記事です。

はじめに

DREグループの石井です。

今回はDREグループの管理するデータ基盤に関するインフラのterraformのテスト環境の話をしようと思います。

導入前の課題感

我々のチームではデータ基盤として複数のGCP Projectを管理していますが、その全てをterraformで管理しています。

この時点でGithubActionsによる自動テスト(validate, plan) 及び 自動デプロイは導入されていたため、レビューさえ通れば誰でもインフラの変更を反映できる状態になっています。

しかし、この時点でよく起こっていた問題として以下のようなものがありました。

  • validationが実装されていないリソースの命名規則などでデプロイ時に落ちる
  • 実際にapplyして試してみたいけど、デプロイ先が本番しかない

そこでPRごとにGCP Projectを作成しその中でapplyを実際に試せる仕組みを実装して、しばらく運用してみたので実装からその所感までをまとめてみようと思います。

やったこと

PR時にテスト環境が作成するために、ざっくりいうと以下のようなことをやっていますので、それぞれ詳細に記載しようと思います。

  • リポジトリ構成の変更
  • CIで実際にapplyされるときのリソース名の調整
  • GithubActionsの実装

リポジトリ構成の変更

元々、1つのGithub Repositoryで全てのGCP Projectを取り扱っていたのですが、元々は以下のような構造でした。

(必要なところのみ抜粋)

envs/
  GCP_ProjectA/
    各種terraform
    ..
  GCP_ProjectB/
modules/
  module_A/
  module_B/
global.tfvars ... ユーザの管理等

envs配下にproject単位で切られており、modulesは共通の部品だけ切り出しておくという構成です。

これを以下の様に変更しています。

envs/
  GCP_ProjectA/
    environment/
      prod/
        main.tf
        backend.tf
        ...
           test/
        main.tf
        backend.tf
    modules/
      module_X/
         各種terraform
    ..
  GCP_ProjectB/
modules/
  module_A/
  module_B/
global.tfvars ... ユーザの管理等

プロジェクトを跨いでグローバルに使用していたモジュールはそのままとして、モジュール化されていなかったプロジェクト固有のterraformファイルをモジュールとしてまとめています。

そして、main.tf内でモジュールを呼び出すという一般的なモジュール構成に似た形になっています。

リソース名の修正

リソース名もvalidation対象ではあると思うので本来はそのまま適用していきたいのですが、GCSのようなグローバルに一意にしないといけないリソースはこのまま実行するとテスト環境を壊すのと本番環境を作るときのラグ等でリソース名が利用できなかったりして困るケースがあります。

そのため gcs_suffix というvariableを用意しておいて、module側で例えば以下のようにしています。

resource "google_storage_bucket" "gcs" {
  name = "test${var.gcs_suffix}"
..
}

default値を ""(空文字) としておくことで、本番環境には影響を与えないようにしています。

Github Actionsの実装

それでは上で整理したモジュールを使ってテスト環境を生成する部分の話に移ります。

基本的にはPJを作るテンプレートを別途用意しておき、それをコピーしてきてapplyしてプロジェクトを作成、その後、environment/test内をapplyする、という流れになっています。

project.tf (Projectの作成)

resource "google_project" "test-environment" {
  billing_account = var.BILLING_ACCOUNT_ID
  name            = local.project_id
  project_id      = local.project_id
  org_id          = var.ORGANIZATION_ID
}

resource "google_billing_budget" "budget" {
  depends_on = [
    google_project.test-environment
  ]
  provider        = google-beta
  billing_account = var.BILLING_ACCOUNT_ID

  # 消し忘れ対策にバジェットを指定
  amount {
    specified_amount {
      currency_code = "JPY"
      units         = 10000
    }
  }

  budget_filter {
    projects = ["projects/${google_project.test-environment.number}"]
  }

  threshold_rules {
    threshold_percent = 0.5
...
}

services.tf (サービスの有効化)

resource "google_project_service" "service" {
  depends_on = [
    google_project.test-environment
  ]
  for_each                   = local.services
  project                    = local.project_id
  service                    = each.value
  disable_dependent_services = true
}

(variable等は割愛します)

これをapplyした後に実際のtest配下をapplyするのですが、backend設定だけ差し替えないといけないため、以下のようなテンプレートを用意して書き換えています。

backedn.tf.template

terraform {
  backend \"gcs\" {
    bucket = \"terraform-backend-bucket-name\"
    prefix = \"ci_projects/${PROJECT_ID}/projects.tfstate\"
  }
}

これらを用いて、以下のようなActionsになりました。なお、PJがかなり多いため実際によく変更されるプロジェクトのみを対象としたかったのでそうなるようにしています。

name: terraform-test-apply

on: pull_request

env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  GOOGLE_BACKEND_CREDENTIALS: ${{ secrets.GOOGLE_BACKEND_CREDENTIALS }}
  GOOGLE_CLOUD_KEYFILE_JSON: ${{ secrets.TIMEE_CORE__GOOGLE_CREDENTIALS }}

jobs:
  set-matrix:
    runs-on: ubuntu-latest
    outputs:
      target_project: ${{ steps.get-diff.outputs.value }}
    steps:
      - uses: actions/checkout@v3
      - name: Fetch changes
        run: git fetch origin ${{ github.base_ref }}
      - name:
        id: get-diff
        run: |
          diff=$(
            echo "$(git diff --name-only origin/master..HEAD | \
            cut -d'/' -f2 | \
            grep -e 'Project1' -e 'Project2' -e 'Project3\' | \
            jq -R . | jq -s .  | jq -c '.|unique')"
          )
          echo $diff
          echo "value=${diff}" >> $GITHUB_OUTPUT

  apply:
    needs: set-matrix
    if: ${{ needs.set-matrix.outputs.target_project != '[]' }}
    strategy:
      fail-fast: false
      matrix:
        PROJECT_NAME: ${{fromJson(needs.set-matrix.outputs.target_project)}}
    runs-on: ubuntu-latest
    env:
      PROJECT_ID: "tf-test-${{ matrix.PROJECT_NAME }}-${{ github.event.number }}"
      PR_NUMBER: ${{ github.event.number }}
    steps:
      - uses: actions/checkout@v3

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2

      - name: create project tf and apply
        run: |
          pushd tests/project
          eval "echo \"$(cat ../templates/backend.tf.template )\"" > backend.tf
          terraform init -lock=true -lock-timeout=60s
          terraform apply -var="PROJECT_ID=${PROJECT_ID}" -auto-approve -var-file=../../global.tfvars -lock=true -lock-timeout=60s
          project_number=$(terraform output google_project_number | head -n 2 | tail -1)
          echo "project_number=$project_number" >> $GITHUB_ENV
          popd

      - name: apply `iam/`
        run: |
          pushd envs/${{ matrix.PROJECT_NAME }}/environment/test/
          terraform init -backend-config="prefix=${{ matrix.PROJECT_NAME }}/${PROJECT_ID}/test.tfstate"
          terraform apply -var="project=${PROJECT_ID}"  -auto-approve -var-file=../../../../../global.tfvars -lock=true -lock-timeout=60s -parallelism=20

      - name: Terraform apply Link
        uses: actions/github-script@v6
        with:
          result-encoding: string
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: "### Test Apply of `${{ matrix.PROJECT_NAME }}` :rocket:" + "\n" + "Link: https://console.cloud.google.com/welcome?project=" + process.env.PROJECT_ID
            });

やってみた感想と課題

正直1PRごとに1GCP Projectを立てるのはやりすぎかなと思っていましたが、実際立っていると確認はしやすく、かつ他の影響も受けないため特に新しい機能を開発するようなタイミングでは大変良かったように思います。

ただ、現実問題としてapplyにかかる時間がだいぶ長いという問題はあり、環境によっては4,50分かかっていたこともありました。

検証する必要性の薄いリソースを対象外とするなど色々改善しましたが、それでも軽微な変更をするのにもこれを回さないといけないというのはやりすぎでは?という側面も正直あるかなと思っています。

このあたりは程度問題な気もするので、今後も見極めていければとは思っています。 

個人的には初めて行う設定などでterraformの書き方にやや自信がないものをある程度自信を持ってレビューを出せるようになったことに最も価値を感じているところではあります。

We’re Hiring

DREグループではまだまだやっていきたいことがたくさんあるのですが、まだまだ手が足りておら

ず、ともに働くメンバーを募集しています!!

データに係る他のポジションやプロダクト開発などのポジションも絶賛募集中なのでこちらからご覧ください