VisasQ Dev Blog

ビザスク開発ブログ

エンジニア全員が Terraform を安心・安全に触れるような仕組みを整えています

はじめに

こんにちは!DPE(Developer Productivity Engineering)チームの高畑です。

ちょっと前に iPhone 15 Pro に変えてようやく USB-C ケーブルに統一できる!と思っていたら、手元にある Magic Trackpad が Lightning ケーブルでしょんぼりしました。

さて今回は、ビザスクのインフラ周りで利用している Terraform をエンジニア全員が安心・安全に利用できる仕組みづくりを行なっている話をしていきます!

これまで

ビザスクではインフラの構築・運用に Terraform を利用しており、依頼ベースで DPE のメンバーが Terraform の修正を行なってレビュー&リリースをしていました。

開発メンバーから Terraform の PR をあげてもらうこともありますが、plan / apply の権限を持っていないため権限を持っている DPE メンバーで plan / apply を行わなければならず、かなりリードタイムが長い状態となっていました。

そのため、DPE チームとしてはエンジニア全員に Terraform を触ってもらいたいという気持ちはあるものの、結局 DPE チームに依頼した方がリードタイムも短くて済むのであまり触ってもらえないという課題感を持っていたという状態です。

また Terraform の plan / apply を行うにはローカルの環境でソースコードの最新を取ってきて、ターミナルを起動して手動で plan / apply を行う必要があるため、ダブオペ必須にしているもののオペレーションミスは発生しやすかったり、plan の結果をエビデンスとして GitHub のコメントにコピーして貼り付けたりと結構手間もかかるといった状況でした。

terraform plan のエビデンス

チーム内でもエンジニア全員が安心して Terraform を実行できる環境を整えたいという声が上がっていたこともあり、CI / CD での Terraform 実行環境を整えていくことになりました。

(↓ Terraform に関する他の記事も是非ご覧ください😉)

tech.visasq.com

tech.visasq.com

tech.visasq.com

利用技術検証

Terraform の自動化を図るにあたり Terraform Cloud を活用できないかということで検証を進めていきましたが、以下の理由から今回は GitHub Actions を利用することになりました。

  • VCS ドリブンのワークフローの場合、plan の結果は Terrafrom Cloud の画面を見に行く必要がある
  • PR のコメントに plan の結果を貼るには、 API ドリブンのワークフローにして GitHub Actions から Terraform Cloud の API を叩きに行く構成にする必要がある
  • ステージング環境や本番環境で Terraform の実行ディレクトリが異なる場合は、環境分のワークスペースを作成する必要がある(state ファイル単位で分かれる)
    • ビザスクの場合、6 リポジトリ(それぞれステージングと本番の 2 環境)分を作成する必要があるため、ワークスペースは最低 12 個必要となる

GitHub Actions から OIDC を利用して GCP の認証を行う

GitHub Actions から GCP 環境へ Terraform を実行するためには、サービスアカウントの秘密鍵GitHub 側に登録するのが一番簡単ではありますが、セキュリティの懸念から OIDC で GCP の認証を行うことにしました。

OIDC で認証をするためには GCP 側へ Terraform 実行用のサービスアカウントを払い出すための Workload Identiry Pool を作成する必要があります。

resource "google_iam_workload_identity_pool" "terraform_pool" {
  provider                  = google-beta
  project                   = var.project
  workload_identity_pool_id = "terraform-identity-pool"
}

resource "google_iam_workload_identity_pool_provider" "terraform_provider" {
  provider                           = google-beta
  project                            = var.project
  workload_identity_pool_id          = google_iam_workload_identity_pool.terraform_pool[0].workload_identity_pool_id
  workload_identity_pool_provider_id = "terraform-provider-id"
  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.actor"      = "assertion.actor"
    "attribute.repository" = "assertion.repository"
  }
  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
  attribute_condition = "..." #  セキュリティ方針に則って適宜設定すること
}

resource "google_service_account" "terraform_service_account" {
  project      = var.project
  account_id   = "terraform-service-account"
  display_name = "Terraform Service Account"
}

resource "google_service_account_iam_member" "terraform_service_account_member" {
  service_account_id = google_service_account.terraform_service_account[0].name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.terraform_pool[0].name}/*"
}

resource "google_project_iam_member" "terraform_project_member_editor" {
  project = var.project
  role    = "roles/editor"
  member  = "serviceAccount:${google_service_account.terraform_service_account[0].email}"
}

... 都度必要な権限をサービスアカウントにアタッチする

上記の Terraform を実行することで、Workload Identiry Pool と Terraform 実行用のサービスアカウントが作成されます。

google_iam_workload_identity_pool_providerattribute_condition についてはセキュリティポリシーに従って適宜設定を行ってください。

GCP Workload Identity Pool

サービスアカウント

必要なリソースが作成されたら、GitHub Actions 側で Provider とサービスアカウントを指定して認証を通すことで、一時的に指定されたサービスアカウントを借用することができるようになります。

jobs:
  terraform:
    ...

    steps:
      ...
      - id: auth
        name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1
        with:
          workload_identity_provider: "projects/xxxxx/locations/global/workloadIdentityPools/terraform-identity-pool/providers/terraform-provider-id"
          service_account: "terraform-service-account@xxxxxx.iam.gserviceaccount.com"

aqua で依存パッケージをインストールする

ビザスクの Terraform 環境周りではパッケージマネージャーとして aqua を利用しています。

aquaproj.github.io

GitHub Actions から利用する terraform コマンドも、後々バージョンアップ対応に手間がかからないよう aqua でインストールをしました。

aquaproj/aqua-installer を利用することにより、ワーキングディレクトリにある aqua.yml を読み込んで GitHub Actions の実行環境にパッケージをインストールすることができます。

---
# aqua - Declarative CLI Version Manager
# https://aquaproj.github.io/
# checksum:
#   # https://aquaproj.github.io/docs/reference/checksum/
#   enabled: true
#   require_checksum: true
registries:
  - type: standard
    ref: v3.103.0 # renovate: depName=aquaproj/aqua-registry
packages:
  - name: hashicorp/terraform@v1.6.3
  ... インストールするパッケージを記載する
jobs:
  terraform:
    ...

    steps:
      ...
      - uses: aquaproj/aqua-installer@v1.1.2
        with:
          aqua_version: v1.25.0

GitHub Actions から Terraform を実行する

事前準備が整ったら、実際に GitHub Actions から Terraform を実行するためのワークフローを作っていきます。

name: terraform-plan-and-apply-production

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

defaults:
  run:
    working-directory: ./

env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
  terraform:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      pull-requests: write

    steps:
      - uses: actions/checkout@v4

      - uses: aquaproj/aqua-installer@v1.1.2
        with:
          aqua_version: v1.25.0

      - name: hide comment
        id: hide-comment
        run: github-comment hide

      - id: auth
        name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1
        with:
          workload_identity_provider: "projects/xxxxx/locations/global/workloadIdentityPools/terraform-identity-pool/providers/terraform-provider-id"
          service_account: "terraform-service-account@xxxxx.iam.gserviceaccount.com"

      - name: terraform init
        id: init
        working-directory: ./terraform/environments/prod
        run: terraform init -reconfigure -backend-config=../../backends/prod.tfvars

      - name: terraform plan
        id: plan
        working-directory: ./terraform/environments/prod
        run: tfcmt plan -- terraform plan
        continue-on-error: true

      - name: terraform plan status
        if: steps.plan.outcome == 'failure'
        run: exit 1

      - name: terraform apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        working-directory: ./terraform/environments/prod
        run: tfcmt apply -- terraform apply -auto-approve

plan / apply の結果を PR のコメントに残すために tfcmt を利用しており、コミットが積まれるたびに terraform plan が実行されこのようにコメントへ投稿されます。

terraform plan

github.com

当初は tfcmt に -patch オプションを渡して plan が実行されるたびに既存のコメントを上書きするようにしていましたが、コード修正前と修正後の plan 結果を比較したかったため github-comment を組み合わせて古い plan の結果を上書きせずに最小化するようにしました。

tfcmt がコメント内に github-comment のメタデータを書き込んでくれるため、github-comment hide が実行されるたびに対象のコメントを最小化してくれます。

古い plan の結果は最小化される

github.com

コードレビューが終わり、リリースするタイミングで PR のマージをすることで apply が自動実行されてコメントに結果が投稿されます。

terraform apply

ステージング環境にも対応する

ここまでで、本番環境に対する plan / apply の自動化はできましたが、ステージング環境に対する plan / apply ができていません。

ビザスクでは、インフラの変更を行う際にまずステージング環境に対して Terraform を適用して動作確認を行なっており、その際手元に最新の staging ブランチを取得してきてローカルマージ & push して apply を実施しています。

そこで、PR のコメントに /pr-staging と投稿することで、GitHub Actions で staging ブランチに向けた PR を作るようにしたのですが、後続の terraform plan する GitHub Actions が発火されないという問題に直面しました。

というのも、当初 gh コマンドを利用して PR を作成する際に secrets.GITHUB_TOKEN を使っていたのですが、どうやら secrets.GITHUB_TOKEN は予期しないワークフローが動かないように制限されているらしく、GitHub App を作るか Personal Access Token を使う必要があるということが分かりました。

docs.github.com

Personal Access Token は個人アカウントに依存してしまい退職などでアカウントが削除された際に GitHub Actions が動かなくなってしまうので、ワークフローを実行できる権限を付与した GitHub App を作成し Organization の環境変数に設定するようにしました。

GitHub App Permission

GitHub App の作成が完了したら、tibdex/github-app-token を利用してトークンを発行することによりワークフローから作成された PR でも GitHub Actions が発火されるようになります。

(公式で GitHub App Token を作成する Actions が出来ていたことを記事執筆中に知ったので今後乗り換えるかもしれません 🙃)

github.com

name: create-staging-pr

on:
  issue_comment:
    types: [created, edited]

jobs:
  create-pr:
    if: (github.event.issue.pull_request != null) && github.event.comment.body == '/pr-staging'
    runs-on: ubuntu-latest

    steps:
      - id: create_token
        uses: tibdex/github-app-token@v2
        with:
          app_id: ${{ vars.TERRAFORM_WORKFLOW_APP_ID }}
          private_key: ${{ secrets.TERRAFORM_WORKFLOW_PRIVATE_KEY }}

      - name: get pull request branch
        id: branch
        run: printf "BRANCH=%s" $(gh pr view --json headRefName --jq .headRefName $NUMBER) >> $GITHUB_ENV
        env:
          GH_TOKEN: ${{ steps.create_token.outputs.token }}
          GH_REPO: ${{ github.repository }}
          NUMBER: ${{ github.event.issue.number }}

      - uses: actions/checkout@v4
        with:
          ref: ${{ env.BRANCH }}

      - name: create pull request for staging
        run: gh pr create --base staging --title "[Can Self Merge] into staging" --body "#$NUMBER" --assignee $ASSIGNEE
        env:
          GH_TOKEN: ${{ steps.create_token.outputs.token }}
          GH_REPO: ${{ github.repository }}
          NUMBER: ${{ github.event.issue.number }}
          ASSIGNEE: ${{ github.event.issue.user.login }}

ワークフローを実行する

ワークフローによって作成された PR

こちらも同様に、PR が作成された時点でステージング環境に対して plan が実行され、マージしたタイミングで apply まで実行されるようになっています。

おわりに

今回、GitHub Actions を活用して Terraform の適用を自動化したことにより、手作業によるオペレーションが無くなったため安心して Terraform を触ることができる環境づくりの第一歩を踏み出せたような気がしています。

実際、社内でもこのようなリアクションをもらったりしたので、結構待ち望んでいた人も多かったのではないでしょうか。

Terraform 自動化のリアクション

他にも linter やセキュリティチェックの導入なども進んでいるので、そちらはまた別の記事にて紹介できたらと思っています!