GitHub Actions を使って Google Cloud 環境に Terraform を実行する方法

記事タイトルとURLをコピーする

G-gen の武井です。

当記事では Infrastructure as Code (IaC) を実現する Terraform を Google Cloud (旧称 GCP) で実行する際、GitHub の CI/CD 機能である GitHub Actions を介して実行する方法を紹介します。

GitHub Actions

GitHub Actions とは

概要

GitHub Actions とは、ソースコード管理ツールである GitHub に包含される機能の1つで、GitHub 上で管理されるソースをもとに CI/CD (継続的インティグレーション / 継続的デリバリー) を実現します。

例えば Terraform を GitHub Actions で実行する場合、Pull Request や Merge などのイベントをトリガーにして、plan や apply などの処理を自動化できます。

ワークフロー

GitHub Actions で自動化したい処理とその内容・条件を定義したものを ワークフロー (Workflow) と言います。

ワークフローは以下の条件に従い定義します。

  1. 定義ファイルは YAML 形式 で記述する
  2. 定義ファイルは .github/workflows ディレクトリ に保存する

図説

GitHub Actions で Terraform を実行した際のイメージは以下のとおりです。

  1. main ブランチ へ Pull Request が行われた場合、Google Cloud 環境に terraform plan を実行するワークフローが起動 (図①)
  2. main ブランチ へ Merge が行われた場合、Google Cloud 環境に terraform apply を実行するワークフローが起動 (図②)

GitHub Actions による Terraform デプロイの自動化

Google Cloud との連携

概要

GitHub Actions で Google Cloud 上のリソースを管理する場合、Google Cloud への 認証 を通す必要があります。

従来の認証

従来は サービスアカウントキーによる認証 が必要でした。キーによる認証では以下のような制約や懸念が考えられます。

  1. キーの エクスポート / 保存 といった管理工数の発生
  2. キー漏洩時のセキュリティリスクの懸念
  3. 組織のポリシー (constraints/iam.disableServiceAccountKeyCreation) で統制された環境下での利用制限

Workload Identity 連携による認証

2021年10月、GitHub Actions で OIDC (OpenID Connect) トークンを使った認証 がサポートされたことで、Workload Identity 連携を使用したキーレスな認証 が実現可能となりました。

Workload Identity 連携

仕組み

Workload Identity 連携 は、GitHub 等の外部 ID プロバイダ (IdP) と連携し、Google Cloud のリソースを呼び出します。

GitHub のワークロードはセキュリティトークンサービス (STS) エンドポイントを呼び出し、IdP から取得した認証トークンを有効期限が短い Google Cloud アクセストークン と交換します。

メリット

ワークロード (GitHub Actions) はトークンの交換でサービスアカウントの権限を借用できます。そのため、従来のようなキー発行は不要となり、それに伴う制約や懸念が解消されます。

図説

Workload Identity 連携を図に表すと以下のイメージとなります。

Workload Identity 連携による認証イメージ

設定方法

概要

以下の流れで GitHub Actions を設定します。

  1. Terraform のセットアップ
  2. GitHub Actions の設定

Terraform のセットアップ

GitHub Actions の実行に必要なリソースを Terraform でデプロイするため、以下のリソースを事前に準備します。

  1. プロジェクト
  2. tfstate ファイル格納用バケット
  3. Terraform 用サービスアカウント

gcloud コマンドで準備する場合、以下の記事の 「3章 事前準備」 にて説明しています。

blog.g-gen.co.jp

GitHub Actions の設定

Terraform のセットアップが完了したら、GitHub Actions を設定します。

ディレクトリ構成

GitHub 上で管理するソースのディレクトリ構成は以下の通りです。この章では main.tfterraform.yml について説明します。

ディレクトリ構成

Workload Identity 連携の設定

main.tf に Workload Identity 連携に関する設定を定義し、Cloud Shell や Local 環境などから Terraform を実行してリソースをデプロイします。

コードは以下の通りで、詳細については 弊社ブログ で解説しています。

# local 定義
locals {
    github_repository           = "myuser/myrepository"
    project_id                  = "myproject"
    region                      = "asia-northeast1"
    terraform_service_account   = "tf-exec@myproject.iam.gserviceaccount.com"
    
    # api 有効化用
    services = toset([                         # Workload Identity 連携用
        "iam.googleapis.com",                  # IAM
        "cloudresourcemanager.googleapis.com", # Resource Manager
        "iamcredentials.googleapis.com",       # Service Account Credentials
        "sts.googleapis.com"                   # Security Token Service API
    ])
}
  
# provider 設定
terraform {
    required_providers {
        google  = {
            source  = "hashicorp/google"
            version = ">= 4.0.0"
        }
    }
    required_version = ">= 1.3.0"

    backend "gcs" {
        bucket = "myproject_terraform_tfstate"
        prefix = "terraform/state"
    }
}
  

## API の有効化(Workload Identity 用)
resource "google_project_service" "enable_api" {
  for_each                   = local.services
  project                    = local.project_id
  service                    = each.value
  disable_dependent_services = true
}
    
# Workload Identity Pool 設定
resource "google_iam_workload_identity_pool" "mypool" {
    provider                  = google-beta
    project                   = local.project_id
    workload_identity_pool_id = "mypool"
    display_name              = "mypool"
    description               = "GitHub Actions で使用"
}
  
# Workload Identity Provider 設定
resource "google_iam_workload_identity_pool_provider" "myprovider" {
    provider                           = google-beta
    project                            = local.project_id
    workload_identity_pool_id          = google_iam_workload_identity_pool.mypool.workload_identity_pool_id
    workload_identity_pool_provider_id = "myprovider"
    display_name                       = "myprovider"
    description                        = "GitHub Actions で使用"
    
    attribute_mapping = {
        "google.subject"       = "assertion.sub"
        "attribute.repository" = "assertion.repository"
    }
    
    oidc {
        issuer_uri = "https://token.actions.githubusercontent.com"
    }
}
  

# GitHub Actions が借用するサービスアカウント
data "google_service_account" "terraform_sa" {
    account_id = local.terraform_service_account
}
  

# サービスアカウントの IAM Policy 設定と GitHub リポジトリの指定
resource "google_service_account_iam_member" "terraform_sa" {
    service_account_id = data.google_service_account.terraform_sa.id
    role               = "roles/iam.workloadIdentityUser"
    member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.mypool.name}/attribute.repository/${local.github_repository}"
}

ワークフローの定義

次に、ワークフローの定義ファイル (terraform.yml) を準備します。
.github/workflows/ ディレクトリに格納してください。

コードは HashiCorp 公式サイトGoogle Cloud 公式ガイド を参考にしています。
※ 32行目の ${PROJECT_NUMBER} は適宜置き換えてください。

name: terraform
  

# main ブランチへのPull Request と Merge をトリガーに指定
on:
  push:
    branches:
      - main
  pull_request:
  

# 作業ディレクトリの指定
defaults:
  run:
    working-directory: ./
  
# ジョブ / ステップ / アクションの定義
jobs:
  terraform-workflow:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      pull-requests: write
  
    # Workload Identity 連携
    steps:
      # https://cloud.google.com/iam/docs/using-workload-identity-federation#generate-automatic
      - uses: actions/checkout@v3
      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v1'
        with:
          workload_identity_provider: 'projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/mypool/providers/myprovider'
          service_account: 'tf-exec@myproject.iam.gserviceaccount.com'
  

      # https://github.com/hashicorp/setup-terraform
      - uses: hashicorp/setup-terraform@v2
  

      - name: Terraform fmt
        id: fmt
        run: terraform fmt -check -recursive
        continue-on-error: true
  

      - name: Terraform Init
        id: init
        run: terraform init
  

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color
  

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color
        continue-on-error: true
  

      - name: Comment Terraform Plan
        uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
            #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
            <details><summary>Validation Output</summary>

            \`\`\`\n
            ${{ steps.validate.outputs.stdout }}
            \`\`\`

            </details>

            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

            <details><summary>Show Plan</summary>

            \`\`\`\n
            ${process.env.PLAN}
            \`\`\`

            </details>

            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
  

      - name: Terraform Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1
  

      # main ブランチに push した場合にだけ terraform apply も実行される
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve -input=false

動作検証

概要

以下の流れで GitHub Actions の動作検証を行います。

  1. 動作検証用リソースの定義
  2. Pull Request 実行
  3. Merge 実行

動作検証用リソースの定義

test.tf に動作検証用に作成する Cloud Storage バケットを定義します。

resource "google_storage_bucket" "test001" {
    name            = "${local.project_id}-bucket-test001"
    project         = local.project_id
    location        = local.region
    force_destroy   = true
    uniform_bucket_level_access = true
}

Pull Request 実行

GitHub にログインし、main ブランチに Pull Request を実行します。

Pull Request の実行

Actions タブから画面を確認すると、ワークフローが実行されたことがわかります。

Pull Request をトリガーにしたワークフローの実行

ワークフロー名をクリックすると詳細が確認できます。

今回のトリガーは main ブランチへの Pull Request ですので、terrafrom plan が実行されています。

Pull Request をトリガーにしたワークフローの実行

terraform plan の詳細

Merge 実行

Pull Request の結果が問題ない事を確認したら main ブランチに Merge を実行します。

Merge の実行

Merge の実行

再度 Actions タブから画面を確認すると、さらにワークフローが自動実行されたことがわかります。

Merge をトリガーにしたワークフローの実行

今回のトリガーは main ブランチへの Merge ですので、先程とは異なり terrafrom apply が実行されています。

Merge をトリガーにしたワークフローの実行

terraform apply の詳細

Terraform (GitHub Actions) でデプロイされたバケット

武井 祐介 (記事一覧)

2022年4月にジョイン。クラウドソリューション部所属。G-gen唯一の山梨県在住エンジニア

Google Cloud Partner Top Engineer 2024 に選出。IaC や CI/CD 周りのサービスやプロダクトが興味分野です。

趣味はロードバイク、ロードレースやサッカー観戦です。