コネヒト開発者ブログ

コネヒト開発者ブログ

(Google Cloud 向け)Terramate と Workload Identity 連携で始める楽でセキュアな GitHub Actions の構築

本エントリは「コネヒト Advent Calendar 2024」9日目の記事です!

adventar.org

こんにちは。ついこの前引っ越しをしたのですが、自宅から富士山が眺められることに最近気づきテンションが上がっている @sasashuuu です。 本日は以前行った CI/CD 構築 についてのブログを発信します。

背景

弊社のインフラに関しては主に AWS を使用していますが、データ基盤等のシステムは Google Cloud で管理するなどマルチクラウドの構成を取っています。

AWS に関しては IaC 管理のための基盤が存在しているものの、Google Cloud には手が回っておらずほぼ管理できていない状態でした。

そこで IaC のオーケストレーションツールである Terramate や OIDC などの認証を実現するための Google Cloud の Workload Identity 連携を使用し、Google Cloud 側の IaC 管理基盤および CI/CD を GitHub Actions にてゼロベースで構築しました。

本エントリでは、特に CI/CD にフォーカスする形で内容を発信しようと思います。構築方法や使って見た所感などをつらつらとまとめる形にはなりますが、技術スタックとしてこういったツールやサービスでの構成があるという引き出しにつながる一助になれば幸いです。

Terramate とは

IaC 管理のためのオーケストレーションツールです。Terraform、OpenTofu、Terragrunt を管理対象に「スタック」と呼ばれる独自の単位(Terraform であればリソースそのものや設定を管理する *.tf ファイルや state などの組み合わせもとに構成される単位)をもとに、それらをオーケストレーションすることができます。具体のユースケースについては後述しますが、例として以下のようなことが可能です。

  • スタックに対して一括でのコマンド実行
  • ファイルの変更があったスタックのみを対象とするコマンド実行

terramate-io/terramate

Workload Identity 連携とは

従来のやり方であるサービスアカウント(サービスアカウントキー)を使った認証と異なり、ID フェデレーションを使用したセキュアな認証方法です。 この方法により、クレデンシャル情報の「管理コスト」や「流出によるセキュリティリスク」を減らすことが可能です。本エントリでは GitHub Actions から Google Cloud へアクセスする際の OIDC 認証のために使用します。

Workload Identity 連携

Terramate の導入

始めにCI/CD を組み込んだ対象リポジトリの全体像を書いておくと以下のようなイメージです。

GitHub Actions 用ワークフローファイルは .github/workflows に、config.tm.hcl を除く Terramate や Terraform 関連のファイルはプロジェクトごとのディレクトリ(例:projectA)に作成しています。

.
├── .github
│   └── workflows
│       ├── projectA-pull-request.yaml
│       ├── projectA-push-tag.yaml
│       ├── template-pull-request.yaml
│       └── template-push-tag.yaml
├── config.tm.hcl
├── projectA
│   └── terraform
│       ├── backend.tf
│       ├── iam.tf
│       ├── monitoring.tf
│       ├── provider.tf
│       ├── stack.tm.hcl
│       └── version.tf
└── projectB
...

Terramate においてポイントとなるのは、次の2ファイルです。

config.tm.hcl - Terramate で自動的に生成するための各種 tf ファイル(backend.tf、provider.tf、version.tf 等)の内容を定義している。

stack.tm.hcl - Terramate によって自動生成されるもので、スタックの管理に利用されます。基本的に手動で編集することはないファイル。

config.tm.hcl の内容

globals {
  terraform_version = "x.x.x"
  provider_version = "x.x.x"
}

generate_hcl "backend.tf" {
  content {
    terraform {
      backend "gcs" {
        bucket = "projectA-tfstate"
          prefix = "hoge/terraform/${terramate.stack.tags[0]}"
      }
    }
  }
}

generate_hcl "provider.tf" {
  content {
    provider "google" {
      project = terramate.stack.tags[0]
      }
  }
}

generate_hcl "version.tf" {
  content {
    terraform {
      required_version = global.terraform_version
        required_providers {
          google = {
            source  = "hashicorp/google"
            version = global.provider_version
          }
        }
    }
  }
}

globals は変数、generate_hcl はファイル生成のための block といった具合です。

stack.tm.hcl は Terramate によって自動生成されるもので、スタックの管理に利用されます。基本的に手動で編集することはないファイルです。

stack.tm.hcl の内容

stack {
  name        = "terraform"
  description = "terraform"
  tags        = ["projectA"]
  id          = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

tags は 後に解説しますが、terramate コマンドで特定のスタックを対象に実行する際に便利な機能です。

ここからは実際の導入方法について解説します。今回は何もない状態のリポジトリで一から作成するパターンとしています。

まずは対象のディレクトリで git init を実行します。

git init

前述した config.tm.hcl を作成します(※内容は前述した定義を参照)。

.
└── config.tm.hcl

この状態で terramate create を実行します。

terramate create projectA/terraform --tags=projectA
Created stack /projectA/terraform

以下のように stack.tm.hcl 含め関連ファイルが自動生成されます。

.
├── projectA
│   └── terraform
│       ├── backend.tf
│       ├── provider.tf
│       ├── stack.tm.hcl
│       └── version.tf
└── config.tm.hcl

それぞれ出来上がったファイルの中身は以下のような内容です。補足ですが、state ファイルの保存場所 は Cloud Strage を指定しています。terraform の実行前にはあらかじめ Cloud Storage に state ファイルを管理するためのバケットは作成しておいてください。

backend.tf

// TERRAMATE: GENERATED AUTOMATICALLY DO NOT EDIT

terraform {
  backend "gcs" {
    bucket = "projectA-tfstate"
    prefix = "hoge/terraform/projectA"
  }
}

provider.tf

// TERRAMATE: GENERATED AUTOMATICALLY DO NOT EDIT

provider "google" {
  project = "projectA"
}

version.tf

// TERRAMATE: GENERATED AUTOMATICALLY DO NOT EDIT

terraform {
  required_version = "x.x.x"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "x.x.x"
    }
  }
}

上記で生成されたファイルは基本的には手動で編集することはなく、試しにファイル内を見てもらうとわかるように「DO NOT EDIT」の定義が存在しています。

// TERRAMATE: GENERATED AUTOMATICALLY DO NOT EDIT

...

あらかじめ Terraform の lock ファイルの生成も行っておきます。stack を作成したディレクトリに作られるよう実行します。

terraform -chdir=projectA/terraform providers lock \
    -platform=linux_amd64 \
    -platform=linux_arm64 \
    -platform=darwin_amd64 \
    -platform=darwin_arm64 \
    -platform=windows_amd64

後に実行する terramate run について、git 上でコミットしていないファイルの変更があると実行できないという制約があるためこの対応を行なっています。GitHub Actions 上のホステッドランナーで実行した terraform init により lock ファイルに変更がかかり、ファイル差分が発生したまま後続する terraform plan が実行できないという事態を防ぐ目的で行います(また、幅広いプラットフォームへの対応も兼ね上記のように実行)。

ここまでで生成された各種ファイルは一度コミットしておいてください(前述したようにコミットしていないファイルがあると terramate run が実行できないためです)。

その後はスタックがある階層(ここでは projectA/terraform 配下)に ec2.tf 等などのリソース作成用の terraform 定義を記載した tf ファイルを配置していけば、terraform 側の準備は完了です。

続いて GitHub Actions 側の実装を見ていきます。

再度 GitHub Actions に関するファイルが置かれているディレクトリ構成を見ておくと以下のようになっております。

├── .github
│   └── workflows
│       ├── projectA-pull-request.yaml
│       ├── projectA-push-tag.yaml
│       ├── template-pull-request.yaml
│       └── template-push-tag.yaml

ざっくりとしたファイルの役割について触れておくと以下のようになっています。

  • template-pull-request.yaml
  • projectA-pull-request.yaml
    • 再利用可能なワークフローのテンプレート(template-pull-request.yaml)を呼び出すファイル(※今回の例ではプロジェクトごとに作成)
  • template-push-tag.yaml
    • template-pull-request.yaml 同様に再利用可能なワークフローのテンプレートファイル
    • tags のイベントで terraform apply が実行される
  • projectA-push-tag.yaml
    • 再利用可能なワークフローのテンプレート(template-push-tag.yaml)を呼び出すファイル(※今回の例ではプロジェクトごとに作成)

中身を見ていきます。まずはテンプレートとなる template-pull-request.yaml です。

on:
  workflow_call:
    inputs:
      workload_identity_provider:
        description: '使用する Workload Identity Provider'
        required: true
        type: string
      service_account:
        description: 'Workload Identity 経由で認証するサービスアカウント'
        required: true
        type: string
      terraform_version:
        description: 'Terraform のバージョン'
        required: true
        type: string
      tag_name:
        description: 'Terramate で使うタグ名(Google Cloud のプロジェクト名)'
        required: true
        type: string

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

jobs:
  terraform-plan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Terramate
        uses: terramate-io/terramate-action@v2

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          # terraform の action を利用する場合は wrapper を無効化する必要があるため false に設定
          # ref. https://terramate.io/docs/cli/automation/github-actions/#:~:text=To%20install%20Terraform%20using%20the%20hashicorp/setup%2Dterraform%20GitHub%20Action%2C%20you%20must%20disable%20the%20included%20wrapper.
          terraform_wrapper: false  # Terramate 経由 で Terraform を実行するためラッパーを無効化
          terraform_version: ${{ inputs.terraform_version }}

      - name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v1'
        with:
          workload_identity_provider: ${{ inputs.workload_identity_provider }}
          service_account: ${{ inputs.service_account }}

      - name: Initialize Terraform
        run: terramate run --tags=${{ inputs.tag_name }} -- terraform init

      - name: Plan Terraform
        run: terramate run --tags=${{ inputs.tag_name }} --changed -- terraform plan

ポイントは以下です。

  • terraform_wrapper の無効化
  • google-github-actions/auth の利用
  • terramate run の実行
  • terraform_wrapper の無効化

Terramate のドキュメントにも記載がありますが、HashiCorp の Terraform Setup GitHub Action を使う場合は terraform_wrapper を無効化する必要があります。

terraform_wrapper: false  # Terramate 経由 で Terraform を実行するためラッパーを無効化

Terramate - Automating Terramate in GitHub Actions

  • google-github-actions/auth の利用

google-github-actions/auth という専用のアクションを使っています。 workload_identity_provider と service_account は inputs 経由で渡していますが、後述する Workload Identity 連携のために作成したプロバイダーとサービスアカウントを指定する必要があります。

workload_identity_provider: ${{ inputs.workload_identity_provider }}
service_account: ${{ inputs.service_account }}
  • terramate run の実行

terramate run は Terramate を利用する上でキモとなるコマンド実行です。

- name: Plan Terraform
  run: terramate run --tags=${{ inputs.tag_name }} --changed -- terraform plan

基本的には terramate run -- <実行したいコマンド> で管理している全スタックに対して、一括で<実行したいコマンド>が実行されるという仕様です。ここでは併せて --tags と --changed のオプションをつけています。--tags は実行対象となるスタックを制限するためのオプション、--changed は git をもとにファイルの変更のあったスタックのみを対象に実行してくれるオプションです。

ここで紹介しているオプションや terramate コマンドの使い方はほんの一部に過ぎず他にも便利な機能(parallel 等)があるので、詳しくは公式のドキュメントをご参照ください。

Terramate - Orchestration

そして再利用可能なワークフローの呼び出し側である projectA-pull-request.yaml は以下のような内容です。

name: Create Pull Request

on:
  pull_request:

jobs:
  terraform-plan:
    uses: ./.github/workflows/template-pull-request.yaml
    with:
      workload_identity_provider: '<後ほど作成する Workload Identity プロバイダー名>'
      service_account: '<後ほど作成するサービスアカウント名>'
      terraform_version: 'x.x.x'
      tag_name: 'projectA'

基本的にはテンプレートの呼び出しやテンプレートへの変数受け渡しを定義しているような内容です。

template-push-tag.yaml や projectA-push-tag.yaml の内容も一応記載しておきますが、基本的には同じ要領で実装しているため解説は割愛します。

template-push-tag.yaml

on:
  workflow_call:
    inputs:
      workload_identity_provider:
        description: '使用する Workload Identity Provider'
        required: true
        type: string
      service_account:
        description: 'Workload Identity 経由で認証するサービスアカウント'
        required: true
        type: string
      terraform_version:
        description: 'Terraform のバージョン'
        required: true
        type: string
      tag_name:
        description: 'Terramate で使うタグ名(Google Cloud のプロジェクト名)'
        required: true
        type: string

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

jobs:
  terraform-apply:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Terramate
        uses: terramate-io/terramate-action@v2

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          # terraform の action を利用する場合は wrapper を無効化する必要があるため false に設定
          # ref. https://terramate.io/docs/cli/automation/github-actions/#:~:text=To%20install%20Terraform%20using%20the%20hashicorp/setup%2Dterraform%20GitHub%20Action%2C%20you%20must%20disable%20the%20included%20wrapper.
          terraform_wrapper: false
          terraform_version: ${{ inputs.terraform_version }}

      - name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v1'
        with:
          workload_identity_provider: ${{ inputs.workload_identity_provider }}
          service_account: ${{ inputs.service_account }}

      - name: Initialize Terraform
        run: terramate run --tags=${{ inputs.tag_name }} -- terraform init

      - name: Apply Terraform
        run: terramate run --tags=${{ inputs.tag_name }} --changed -- terraform apply -auto-approve

projectA-push-tag.yaml

name: Push Tag

on:
  push:
    tags:
      - "*"

jobs:
  terraform-apply:
    uses: ./.github/workflows/template-push-tag.yaml
    with:
      workload_identity_provider: '<後ほど作成する Workload Identity プロバイダー名>'
      service_account: '<後ほど作成するサービスアカウント名>'
      terraform_version: 'x.x.x'
      tag_name: 'projectA'

Workload Identity 連携用リソースの構築

続いて Workload Identity 連携用リソースを構築していきます。

まず Workload Identity 連携で使用するサービスアカウントを作成しておいてください。 サービスアカウントキーは使用しませんが、Workload Identity 連携を経由して Google Cloud リソースを操作するための IAM そのものは必要となります(また、1つ注意として roles/iam.workloadIdentityUser のロールを持つサービスアカウントが必要となりますで必要権限も付与しておいてください)。

Workload Identity 連携のコアとなるリソースを作成していきます。

Google Cloud Console にログインし、「IAM と管理」>「Workload Identity 連携」へ移動します。

プロバイダの追加・プールの作成をしていきます。

設定項目を見ていきます。

名前やプールについてはなんでも良いです。ここでは「github」としておきます。

プロバイダ設定は以下のように行います。

次に OIDC プロバイダーから送られるトークンを Google Cloud 内で扱えるようにするためのマッピングを行います。今回は特定のリポジトリからのみのアクセスに限定するため、repository 情報が扱えるようなマッピングにします。

Google側 OIDC側(GitHub)
google.subject assertion.sub
attribute.repository assertion.repository

google.subject は必須設定という仕様のため、assertion.sub でマッピングしています。

attribute.repository は assertion.repository でマッピングし、後述する特定のリポジトリアクセスの制御に使います。

条件 CEL は マッピングした属性値を使う形で以下のようにします(OWNER や REPO の値は書き換えてください)。CEL という 言語を使用します。

assertion.repository == "<OWNER>/<REPO>"

マッピングをしているので、CEL では attribute を使用すると思いきやここでは assertion を使用することになるため注意が必要です(と言いつつ attribute でも問題ないのかは試していません)。

マッピングに関するドキュメントは以下が参考になります。

Google Cloud - Workload Identity 連携

OpenID Connect を使ったセキュリティ強化について

上記設定でプロバイダの追加とプールの作成を進めてください。

その後、プールで使用するサービスアカウントの紐付けを行います(プールの画面に戻ると「アクセスを許可」があるのでそちらから設定します)。

使用するサービスアカウントを選択し、マッピングした属性を使う形でサービスアカウントへの制御もかけます。ここでは attribute を使用しています(こちらでも OWNER や REPO の値は書き換えてください)。

ここまでの手順を終えたら、Pull Request の更新や tag の push で GitHub Actions の CI/CD が動くようになっているはずです。

感想

Terramate は簡単にコマンドの一括実行や差分検出によるフィルターなど Terraform のオーケストレーションが行えるため重宝しそうです(前述したように OpenTofu、Terragrunt なども)。変更があったファイルを対象に Terraform を実行するという制御のために独自のスクリプトなどを実装する必要もなく、オプション1つで制御できるのは大きな魅力です。導入も簡単で CI/CD もシンプルな構成になるので、とても良いなと思っています。

弊社においては正直なところ Google Cloud 向けの IaC 管理基盤は導入したばかりで、まだまだ IaC 化自体が推進できていないため Terramate の大きな恩恵はそれほど実感できていませんが、今後の IaC 化の推進でさらに役立ってくれそうな気配を感じています(まだ利用していない parallel やその他機能など)。

また、Workload Identity 連携についてもサービスアカウントキーの管理が発生しないというのはセキュリティ面で大きな魅力です。専用の action も存在するので CI/CD への組み込みも容易でした。

おわりに

今回は Terramate と Workload Identity 連携を使った GitHub Actions の CI/CD の構築方法について書きました。とても便利でセキュアですので良ければみなさんも候補の1つに入れてみてください。