🚀

Terraform+GitHubActionsでGoogleCloudのCI/CD構築入門

2024/08/15に公開

はじめに

クラウドインフラの構築・管理において、冪等性の担保や効率性の向上のために、TerraformやGitHub Actionsなどがよく利用されます。Terraformによるインフラのコード化は、手作業によるミスを減らし、設定変更のたびに発生する手間を削減します。さらに、GitHub Actionsとの連携により、コードの変更をトリガーとした自動テストやデプロイが可能になり、インフラ管理のライフサイクル全体を効率化できます。

本記事では、TerraformとGitHub Actionsを利用して、Google CloudインフラのCI/CDパイプラインを構築する具体的な手順を解説します。1時間程度でサクッと完了できるように具体的な手順を記載します。

なお、GitHubからGoogle Cloudへのアクセス許可には、Workload Identity連携を採用します。Workload Identity連携ではサービスアカウントの権限借用よりも、フェデレーションIDを利用してリソースへ直接アクセス指定する方が推奨されていることを執筆中に知り、本記事でもこの方法を採用します。執筆時点では、この設定に関してTerraformを使用するドキュメントは見当たりませんでしたので、その部分に関しても参考になれば幸いです。

作成したサンプルコードは以下にあります。

https://github.com/shunsuke-tsumori/googlecloud_terraform_template_2024

対象読者

  • Terraformを使ってGoogle Cloudインフラを構築してみたい方
  • Terraformによるインフラ構築をGitHub ActionsでCI/CD化したい方
  • TerraformやGoogle Cloudの初心者で、コードをみながら学習したい方

前提

フォルダ構成

フォルダ構成には様々なパターンがありますが、今回はdev/stg/prdの環境ごとにフォルダを分け、各環境で必要なモジュールを選択して使用する構成を採用します。

.
├── env
│   ├── dev
│   │   ├── locals.tf         // dev環境のローカル環境を設定する
│   │   ├── main.tf           // backend、moduleなどの設定を行う
│   │   ├── providers.tf      // プロバイダ情報を記載する
│   │   ├── services.tf       // 有効化するGoogle Cloud APIを列挙する
│   │   └── state.tf          // stateファイルを格納するバックエンドリソースを記載する
│   ├── prd           // devと同様の構成にする
│   └── stg           // devと同様の構成にする
└── module
    ├── (module1)        // 構築するGoogle Cloudリソースの集合(本記事ではVMとネットワークリソースをサンプルとして作成する)
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── wif              // GitHub ActionsからGoogle Cloudへのアクセス許可を行うために使用するWorkload Identity連携の関連リソース
        ├── github.tf
        ├── pool.tf
        └── variables.tf

この構成は各環境の分離を重視する場合に有効で、モジュールの再利用性も高くなりやすい構成です。しかし、環境間で共有するモジュールがあったり依存関係があるような場合では、より適したフォルダ構成が考えられるかと思います。

事前準備

構築手順

以下では構築の手順を順を追って説明していきます。

1. Terraformによるリソース構築

dev/stg/prdのように環境ごとにGoogle Cloudプロジェクトが分かれていると仮定し、まずはdev環境の構築から始めることにします。

1-1. プロバイダの設定

まず、Google Cloudリソースを管理・操作するためのgoogleプロバイダの設定を行います。google-betaというプロバイダも存在するのですが、こちらはプレビューリソースに対する管理・操作を行うプロバイダなので、本番環境での使用は避けた方が無難でしょう。

env/dev/providers.tf
provider "google" {
  project = "<project-id>"
  region  = "asia-northeast1"
}
env/dev/main.tf
terraform {
  required_version = ">= 1.9.4, < 2.0.0"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.41.0"
    }
  }
}

ここでは、Terraformのバージョンやgoogleプロバイダのバージョンを指定しています。

この時点でバケットなど適当なリソースを作成して Hello World を実行するのが良いと思います。以下のようなコード[2]を書いて、コマンドを実行し、Apply complete! Resources: 1 added, 0 changed, 0 destroyed.と表示されたらOKです。

env/dev/main.tf
resource "google_storage_bucket" "random5301270235081" {
  location = "asia-northeast1"
  name     = "random5301270235081"
}

実行するコマンドは以下です。

cd env/dev
terraform init
terraform plan
terraform apply

各コマンドの概要は以下の通りです。詳細はリンクのドキュメントを参照してください。

terraform init

作業ディレクトリを初期化し、必要プロバイダやモジュールをインストールする。また、バックエンドの初期化なども行う。

terraform plan

ステートファイルを読み込み、インフラの現在の状態とTerraformファイルの定義を比較し、変更をプレビューする。

terraform apply

terraform planで表示された変更差分を実際に適用する。

1-2. Terraformバックエンドの作成

次に、Terraformのバックエンドを設定します。Terraformでは、管理対象のリソースの状態をステートファイルで管理し、開発チームで共有することで、競合を避けることができます。このステートファイルを格納する場所をバックエンドと呼び、Google CloudではGCSバケットをバックエンドとして利用するのが一般的です。

env/dev/state.tf
resource "google_storage_bucket" "tfstate_backend" {
  location = "asia-northeast1"
  name     = "tfstate-backend-dev"
}

このコードを追加したら、再度 terraform apply を実行します。バケットが作成されたことを確認したら、作成したバケット名を使ってterraformブロックにbackendを追記します[3]

env/dev/main.tf
terraform {
  backend "gcs" {
    bucket = "tfstate-backend-dev"
    prefix = "dev"
  }
  # 以下同じ
}

1-3. 必要なAPIを有効化する

以下の2つのAPIを有効化します。

  • Compute Engine API: サンプルmoduleを作成するために使用する
  • IAM API: GitHub Actionsから使用するプリンシパルに権限を付与するために使用する
env/dev/services.tf
locals {
  services = toset([
    "compute.googleapis.com",
    "iam.googleapis.com",
  ])
}
env/dev/main.tf
resource "google_project_service" "service" {
  for_each           = local.services
  service            = each.value
  disable_on_destroy = false
}

ここでも terraform apply を実行して、APIを有効化します。

1-4. サンプルmoduleを作成する

本記事では、Google Cloudリソースを作成するためのリソース作りにフォーカスしていますが、その目的はコンピューティングなどのGoogleCloudリソースを効率よく作成・管理することにあります。

いま、Google Cloudのリソース群でmoduleを作成して確認することにします。

作成モジュールはなんでも良いのですが、一旦VMとネットワークを作成してみました(サンプルコード)。これをdev環境で使用するために、以下のようなコードを追加します。

env/dev/main.tf
module "vm" {
  source = "../../module/vm"

  env = local.env
}

env = local.env の左辺のenvは、モジュールvmのvariables.tfで指定する変数で、これにenv/dev/locals.tfで指定したローカル変数を代入しています。

moduleを追加したら、再度terraform initを実行する必要があります。

moduleが問題なく作成されることを確認したら、余計なコストがかからないように削除して良いでしょう。env/dev/main.tfからmoduleを削除してterraform applyを実行することで、module内のリソースがまとめて削除されます。

1-5. Workload Identity連携関連リソースを作成する

次に、GitHub ActionsからGoogle Cloudに対してアクセス許可を行うためのWorkload Identity関連リソースを作成します。

Workload Identityは外部のアイデンティティプロバイダと連携して、Google Cloudへの安全なアクセス許可を提供する仕組みです。これを使うことによって、サービスアカウントキーを使用することなくOIDCトークンを使用することでアクセス許可を行うことができます。

GitHubとの連携においては、Workload Identityプールを作成したのち、GitHubをアイデンティティプロバイダとして設定します。

まずはこのリソース(Workload Identityプールと、GitHubプロバイダ)を作成します。

module/wif/pool.tf
data "google_project" "current" {}

resource "google_iam_workload_identity_pool" "pool" {
  workload_identity_pool_id = "github-pool"
  display_name              = "github-pool"
  description               = "for github actions workflows"
}

locals {
  my_github_repository_owner = "<githubのorganization>"
}

resource "google_iam_workload_identity_pool_provider" "provider" {
  workload_identity_pool_id = google_iam_workload_identity_pool.pool.workload_identity_pool_id

  workload_identity_pool_provider_id = "github-provider"
  display_name                       = "github-provider"
  description                        = "for github actions workflows"

  attribute_mapping = {
    "google.subject"             = "assertion.sub"
    "attribute.repository"       = "assertion.repository"
    "attribute.repository_owner" = "assertion.repository_owner"
  }
  attribute_condition = "assertion.repository_owner == '${local.my_github_repository_owner}'"

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

この時の注意点として、google_iam_workload_identity_pool_providerattribute_conditionでアクセス制限をかけないと、誰でもpoolを使用できる状態になってしまうので、アクセス元のGitHubのorganizationなどを必ず指定するようにしてください。

また、attribute_mappingでは、Google Cloudの属性とGitHubのOIDCトークンに含まれるクレームのマッピングを指定しています。例えば、Google Cloudの属性attribute.repositoryを、OIDCトークンのrepositoryクレームにマッピングしています。

1-6. リソースへのアクセス権の付与

次に、リソースへのアクセス権の付与を行います。本記事の冒頭で述べた通り、フェデレーションIDを使用してアクセス権を付与する方法とサービスアカウントの権限借用を使用してアクセス権を付与する方法がありますが、前者が推奨されています。

2024/08/14現在、GitHub向けのフェデレーションIDを使用した設定に関する公式/非公式ドキュメントはまだ無いようですが、以下のようにGitHubからのアクセスを許可するプリンシパルを指定することで、権限を付与できます。

以下ではそのプリンシパルに対して、1-4で作成したモジュールを構築したり、バックエンドのオブジェクトを参照したりする権限を与えています。

locals {
  my_github_repository = "<githubのrepository名>"
}

resource "google_project_iam_member" "wif_principal" {
  for_each = toset([
    "roles/compute.networkAdmin",
    "roles/compute.instanceAdmin.v1",
    "roles/editor",
  ])
  project = data.google_project.current.project_id
  role    = each.value
  member  = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.pool.name}/attribute.repository/${local.my_github_repository_owner}/${local.my_github_repository}"
}

resource "google_storage_bucket_iam_member" "backend_viewer" {
  bucket = var.backend_bucket_name
  role   = "roles/storage.objectViewer"
  member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.pool.name}/attribute.repository/${local.my_github_repository_owner}/${local.my_github_repository}"
}

今後Terraformで作成するリソースに応じて、この権限付与を適切に修正してください。

2. GitHub Actionsワークフローの追加

次に、dev環境に対して実行するCI/CDとしてのGitHub Actionsワークフローを作成します。

2-1. CIの追加

まずはCIを実装します。トリガーはmainブランチへのPR起票時とします。CIでは以下のようなことを実行するようにします。

  • フォーマットチェック
  • terraform validate
  • terraform plan
name: "1.1. infra CI for dev"

on:
  pull_request:
    branches:
      - "main"

jobs:
  ci_dev:
    name: "ci_dev"
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
    permissions:
      id-token: write
      contents: read
    env:
      WORKLOAD_IDENTITY_PROVIDER: "projects/<project番号>/locations/global/workloadIdentityPools/github-pool/providers/github-provider"
      PROJECT_ID: "<project ID>"

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
          project_id: ${{ env.PROJECT_ID }}

      - name: Detect Terraform version
        run: |
          printf "TF_VERSION=%s" $(cat .terraform-version) >> $GITHUB_ENV

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
          terraform_wrapper: false

      - name: Terraform Format
        run: terraform fmt -recursive -check

      - name: Terraform Init
        run: terraform init
        working-directory: ./env/dev

      - name: Terraform Validate
        run: terraform validate -no-color
        working-directory: ./env/dev

      - name: Terraform Plan
        run: terraform plan -no-color
        working-directory: ./env/dev

改善ポイント

  • TerraformののVer.1.5より導入されたChecksを導入すると、terraform plan実行時にリソースの検証が行えます。
  • trivyなどによる脆弱性の静的スキャンを入れても良いと思います。

2-2. CDの実装

次にCDを実装します。トリガーはmainブランチへのpush、または手動とします。CDではdev環境にリソースをデプロイするために、シンプルにterraform applyを実行するようにします。

name: '1.2. infra CD for dev'

on:
  push:
    branches:
      - "main"
  workflow_dispatch:

jobs:
  cd_dev:
    name: 'cd_dev'
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
    permissions:
      id-token: write
      contents: read
    env:
      WORKLOAD_IDENTITY_PROVIDER: "projects/<project番号>/locations/global/workloadIdentityPools/github-pool/providers/github-provider"
      PROJECT_ID: "<project ID>"

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
          project_id: ${{ env.PROJECT_ID }}

      - name: Detect Terraform version
        run: |
          printf "TF_VERSION=%s" $(cat .terraform-version) >> $GITHUB_ENV

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
          terraform_wrapper: false

      - name: Terraform Init
        run: terraform init
        working-directory: ./env/dev

      - name: Terraform Apply
        run: terraform apply -auto-approve
        working-directory: ./env/dev

この後

stgprdにも同様の手順を踏んでリソースやワークフローを作って、各環境の構築が完了となります。次のステップとして、Renovateを追加して依存モジュールの更新を自動化するなどが挙げられます。

ここから、「1-4. サンプルmoduleを作成する」で記載のようなモジュールを追加していくことで、Google Cloudのリソースを追加していくことができます。

Terraformコーディングの仕方については、以下のようなドキュメントが参考になります。

まとめ

この記事では、TerraformとGitHub Actionsを用いたGoogle CloudインフラのCI/CDパイプライン構築について解説しました。環境分離を重視したフォルダ構成、Workload Identity連携による安全なアクセス許可、GitHub ActionsワークフローによるCI/CDなど、実践的な手順を説明しました。

特に、Workload Identity連携では、直近の推奨事項であるフェデレーションIDを利用したリソースへの直接アクセス指定方法を採用しました。

この記事が何かしら参考になれば幸いです。

脚注
  1. Terraformの複数バージョンをインストールして管理するツールとして、tfenvというツールが最もメジャーですが、個人的には最近tenvに注目しています。
    https://github.com/tfutils/tfenv
    https://github.com/tofuutils/tenv ↩︎

  2. バケット名はグローバルに一意である必要があるので、「5301270235081」の部分は適当な文字列に置き換えてください。 ↩︎

  3. backendには変数を使えないので、バケット名は直接指定します ↩︎

OPTIMINDテックブログ

Discussion