Terraform+GitHubActionsでGoogleCloudのCI/CD構築入門
はじめに
クラウドインフラの構築・管理において、冪等性の担保や効率性の向上のために、TerraformやGitHub Actionsなどがよく利用されます。Terraformによるインフラのコード化は、手作業によるミスを減らし、設定変更のたびに発生する手間を削減します。さらに、GitHub Actionsとの連携により、コードの変更をトリガーとした自動テストやデプロイが可能になり、インフラ管理のライフサイクル全体を効率化できます。
本記事では、TerraformとGitHub Actionsを利用して、Google CloudインフラのCI/CDパイプラインを構築する具体的な手順を解説します。1時間程度でサクッと完了できるように具体的な手順を記載します。
なお、GitHubからGoogle Cloudへのアクセス許可には、Workload Identity連携を採用します。Workload Identity連携ではサービスアカウントの権限借用よりも、フェデレーションIDを利用してリソースへ直接アクセス指定する方が推奨されていることを執筆中に知り、本記事でもこの方法を採用します。執筆時点では、この設定に関してTerraformを使用するドキュメントは見当たりませんでしたので、その部分に関しても参考になれば幸いです。
作成したサンプルコードは以下にあります。
対象読者
- 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
この構成は各環境の分離を重視する場合に有効で、モジュールの再利用性も高くなりやすい構成です。しかし、環境間で共有するモジュールがあったり依存関係があるような場合では、より適したフォルダ構成が考えられるかと思います。
事前準備
- Google Cloudプロジェクトが作成されていること
- gcloud CLIがインストールされていること
https://cloud.google.com/sdk/docs/install-sdk?hl=ja#installing_the_latest_version
https://cloud.google.com/sdk/docs/downloads-docker?hl=ja - terraformがインストールされていること[1]
https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli
https://hub.docker.com/r/hashicorp/terraform
構築手順
以下では構築の手順を順を追って説明していきます。
1. Terraformによるリソース構築
dev/stg/prdのように環境ごとにGoogle Cloudプロジェクトが分かれていると仮定し、まずはdev環境の構築から始めることにします。
1-1. プロバイダの設定
まず、Google Cloudリソースを管理・操作するためのgoogle
プロバイダの設定を行います。google-beta
というプロバイダも存在するのですが、こちらはプレビューリソースに対する管理・操作を行うプロバイダなので、本番環境での使用は避けた方が無難でしょう。
provider "google" {
project = "<project-id>"
region = "asia-northeast1"
}
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です。
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バケットをバックエンドとして利用するのが一般的です。
resource "google_storage_bucket" "tfstate_backend" {
location = "asia-northeast1"
name = "tfstate-backend-dev"
}
このコードを追加したら、再度 terraform apply
を実行します。バケットが作成されたことを確認したら、作成したバケット名を使ってterraform
ブロックにbackend
を追記します[3]。
terraform {
backend "gcs" {
bucket = "tfstate-backend-dev"
prefix = "dev"
}
# 以下同じ
}
1-3. 必要なAPIを有効化する
以下の2つのAPIを有効化します。
- Compute Engine API: サンプルmoduleを作成するために使用する
- IAM API: GitHub Actionsから使用するプリンシパルに権限を付与するために使用する
locals {
services = toset([
"compute.googleapis.com",
"iam.googleapis.com",
])
}
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環境で使用するために、以下のようなコードを追加します。
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プロバイダ)を作成します。
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_provider
のattribute_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
この後
stg
とprd
にも同様の手順を踏んでリソースやワークフローを作って、各環境の構築が完了となります。次のステップとして、Renovateを追加して依存モジュールの更新を自動化するなどが挙げられます。
ここから、「1-4. サンプルmoduleを作成する」で記載のようなモジュールを追加していくことで、Google Cloudのリソースを追加していくことができます。
Terraformコーディングの仕方については、以下のようなドキュメントが参考になります。
- https://cloud.google.com/docs/terraform/best-practices-for-terraform?hl=ja
- https://www.terraform-best-practices.com/
まとめ
この記事では、TerraformとGitHub Actionsを用いたGoogle CloudインフラのCI/CDパイプライン構築について解説しました。環境分離を重視したフォルダ構成、Workload Identity連携による安全なアクセス許可、GitHub ActionsワークフローによるCI/CDなど、実践的な手順を説明しました。
特に、Workload Identity連携では、直近の推奨事項であるフェデレーションIDを利用したリソースへの直接アクセス指定方法を採用しました。
この記事が何かしら参考になれば幸いです。
-
Terraformの複数バージョンをインストールして管理するツールとして、tfenvというツールが最もメジャーですが、個人的には最近tenvに注目しています。
https://github.com/tfutils/tfenv
https://github.com/tofuutils/tenv ↩︎ -
バケット名はグローバルに一意である必要があるので、「5301270235081」の部分は適当な文字列に置き換えてください。 ↩︎
-
backendには変数を使えないので、バケット名は直接指定します ↩︎
世界のラストワンマイルを最適化する、OPTIMINDのテックブログです。「どの車両が、どの訪問先を、どの順に、どういうルートで回ると最適か」というラストワンマイルの配車最適化サービス、Loogiaを展開しています。recruit.optimind.tech/
Discussion