GitHub Actions Terraform

Terraformと他のツールとの間でデータを共有するために設定値をyamlで管理する

GitHub Actions Terraform

こんにちは。技術部プラットフォームグループのそめやポチです。春になるとゴマフアザラシ誕生のニュースが増えるので幸せを感じます。 この記事では、Terraformと他のツールとの間で情報を共有するために、設定値をyamlファイルに切り出して管理する方法を紹介します。

概要

Terraformは、インフラストラクチャの構成をコードとして管理するツールです。Terraformにおいては、開発者はインフラリソースをコードによって宣言的に記述できます。記述されたリソースは他のTerraformコードから参照可能です。しかし、Terraformが管理する値をTerraformの外部、例えばCI/CDワークフローなどの他のツールから直接的に参照することはできません。

そこで、Terraformと他のツールとの間で設定値を共有するために、構造化データファイルを使用することにしました。Terraformにはjsondecodeyamldecodeといった関数があります。これらの関数によって、jsonやyamlなどの形式で書かれたデータをTerraformファイル内で解釈できます。また、jsonやyamlは広く使われている形式であるため、他のツールとの組み合わせが容易です。したがって、共有が必要なデータを構造化データファイルに書き出すことによって、Terraformと他のツールとの間でデータを共有することができます。

この記事ではyamlファイルを使用した例を紹介します。

コード例

Terraformがyamlファイルの内容を読み取るには、yamldecode関数とfile関数を使用します。以下の例では、variables.yamlファイルに記述された値を読み取り、localsブロック内で変数として使用しています。

# variables.yaml
ip_addresses:
  - 1.1.1.1
  - 2.2.2.2
# main.tf
locals {
  ip_addresses = yamldecode(file("variables.yaml")).ip_addresses
}

output "ip_addresses" {
  value = local.ip_addresses
}

このoutputの結果は次のとおりになります。

Changes to Outputs:
  + ip_addresses = [
      + "1.1.1.1",
      + "2.2.2.2",
    ]

実践例: Google CloudのOIDC認証をGitHub Actionsワークフローで利用するために、プロジェクト情報をyamlファイルに書く

この記事ではyamlファイルによる設定値の共有の実践例として、Google CloudのOIDC認証をGitHub Actionsワークフローで利用するために、必要なデータをyamlファイルに書いておく方法を紹介します。

参考: Google Cloud Platform での OpenID Connect の構成 - GitHub Docs

ワークフロー上でOIDC認証をする場面はいくつかあります。たとえばワークフロー上でterraformコマンドを実行するためには、認証を通じて管理対象であるGoogle Cloudリソースへのアクセス権限を取得しておく必要があります。サービスアカウントの秘匿情報によっても認証できますが、OIDC認証を利用する方法の方がセキュアです。

課題

Google Cloudでは「プロジェクト」という単位でリソースが管理されます。Google CloudのOIDC認証を利用するためには、プロジェクトIDなどの情報が必要です。また、TerraformでGoogle Cloudのプロジェクトを管理する際にもプロジェクトの情報が必要になります。

リポジトリ内で管理されるGoogle Cloudリソースがすべて同一のプロジェクトのものであれば、それぞれのコードにプロジェクト情報をハードコードすれば済みます。しかし、単一のリポジトリで複数のGoogle Cloudプロジェクトを管理する場合、ワークフローが動的にプロジェクトを切り替える必要があります。 それぞれのTerraformディレクトリが管理するプロジェクトと、ディレクトリ構造とが綺麗に対応していれば、ディレクトリ名からプロジェクト情報を確定させることもできます。しかし、今回はディレクトリ構造が複雑であり、Terraformのディレクトリ名がプロジェクトに対応していないものとします。

例えば、次の構造のリポジトリを考えます。このリポジトリでは、infra-repository直下にserviceN/environmentNディレクトリが複数あり、それぞれのディレクトリにTerraformコードが配置されています。各Terraformディレクトリが管理するGoogle Cloudプロジェクトには規則性がありません。

infra-repository/
├── service1/
│   ├── environment1/
│   │  └── terraform/
│   │      └── code-for-project1.tf
│   ├── environment2/
│   │  └── terraform/
│   │      └── code-for-project2.tf
│   └── environment3/
│      └── terraform/
│          └── code-for-project1.tf
├── service2/
│   ├── environment1/
│   │   ├── terraform/
│   │   │  └── code-for-project3.tf
│   └── environment2/
│       ├── terraform/
│       │   └── code-for-project3.tf
...

実装コード例

上記の課題を解決するため、Terraformディレクトリごとにプロジェクト情報をyamlファイルに書き出すことで、GitHub Actionsのワークフローからも利用できる状態にします。

それぞれのTerraformディレクトリ直下のvariables.yamlにプロジェクト情報を記述します。

# variables.yaml
project_id: "my-project1-id"

tfファイルでは、yamldecode関数を使ってvariables.yamlを読み込み、プロジェクト情報を取得します。

# /path/to/a/terraform-directory/main.tf
locals { project = yamldecode(file("variables.yaml")).project_id }
provider "google" {
  project     = local.project
  region      = "my-region"
}

OIDC認証の過程で使うため、それぞれのプロジェクト情報をまとめたyamlファイルを用意します。

# config/gcloud_projects_data.yaml
projects:
- name: my-project1-display-name
  project_id: my-project1-id
  project_number: my-project1-number
- name: my-project2-display-name
  project_id: my-project2-id
  project_number: my-project2-number
- name: my-project3-display-name
  project_id: my-project3-id
  project_number: my-project3-number

GitHub Actionsのワークフローでは、例えばyqなどのツール使ってyamlファイルを読み込み、OIDC認証に必要なプロジェクトIDとプロジェクトナンバーを取得します。

# .github/workflows/terraform.yaml
name: Terraform workflow
on:
  workflow_dispatch:
    inputs:
      directory:
        description: 'Terraform directory'
        required: true
jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v4
    - name: Install yq
      run: wget https://github.com/mikefarah/yq/releases/download/v4.6.1/yq_linux_amd64 -O yq && chmod +x yq
    - name: Extract Project Data by Directory 
      id: 'extract-project'
      run: |
        DIRECTORY="${{ inputs.directory }}"

        # ディレクトリごとのプロジェクト情報を取得
        GCLOUD_PROJECT_ID=$(yq e '.project_id' $DIRECTORY/variables.yaml)
        GCLOUD_PROJECT_NUMBER=$(yq e ".projects[] | select(.project_id == \"$GCLOUD_PROJECT_ID\").project_number" config/gcloud_projects_data.yaml)
        
        echo "project-id=${GCLOUD_PROJECT_ID}" >> $GITHUB_OUTPUT
        echo "project-number=${GCLOUD_PROJECT_NUMBER}" >> $GITHUB_OUTPUT
    - name: Authenticate to Google Cloud
      id: 'auth'
      uses: google-github-actions/auth@v1
      with:
        workload_identity_provider: projects/${{ steps.extract-project.outputs.project-number }}/locations/global/workloadIdentityPools/gh-oidc-pool/providers/github-actions # プロジェクトごとに同名のOIDCプロバイダを作成しておく
        service_account: github-actions@${{ steps.extract-project.outputs.project-id}}.iam.gserviceaccount.com # プロジェクトごとに同名のサービスアカウントを作成しておく
        project_id: ${{ steps.extract-project.outputs.project-id }}
    - name: Do something with Terraform after authentication
      working-directory: ${{ inputs.directory }}
      run: |
        # 認証が完了した状態なのでTerraformコマンドが実行できる
...

以上のようにすることで、どのディレクトリにおいても、適切なGoogle Cloudプロジェクトに対してOIDC認証をすることができます。

おわりに

以上、yamlファイルで設定値を管理することで、Terraformと他のツールとの間でデータを共有する方法を紹介しました。 私は、ディレクトリ構成が複雑なリポジトリでTerraformのCI/CDを実現しようとしてこの方法に辿り着きました。構造化データを汎用的な形式で管理することによって、ツールを問わず利用できるようにするという手法は、他の事例にも適用できると感じました。 「yaml最高!!1」と主張するつもりはないですが、適切な形式でデータを一元管理すると、手間が減って世界の幸せの総量が増えると思いますのでお試しください。