第4回:TerraformとGitHub Actionsで構築するインフラCI

※本記事は、技術評論社「Software Design」(2023年7月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。

はじめに

前回はTerraformの基本的な概念とステート管理について解説しました。 今回からは 2 回にわたり、Infrastructure as Code(IaC)のCI/CD(継続的インテグレーション/継続的デリバリ)パイプラインについて紹介します(図1)。

▼図1 CADDiのスタックにおける今回の位置付け

Google Cloudプロジェクトのインフラ構成をTerraformで定義し、GitHub Actionsでデプロイするまでを目標とし、前半となる今回は事前備とインフラCIについて焦点を当てていきます。後半となる次回では、インフラCDについて触れつつ、運用をよりスケールさせるためにキャディで取り組んでいる事例について紹介予定です。

IaCとCI/CD

本連載第2回(本誌2023年5月号)では、IaC化によってLinterによる自動チェックや再現性の担保など、さまざまなメリットが得られることを解説しました。今回はIaCの管理・デプロイについて考えてみましょう。 一般に、作業者のPCなどローカル環境でTerraformを実行するときには、次のような課題が生じます。

  • 作業者以外が自動チェックの実行結果を確認できない
  • 実行に必要なシークレットが作業者PCに保管されてしまう
  • 誰がいつデプロイしたのか証跡を残しづらい
  • Terraform plan/applyの実行ログが残らない
  • 手動オペレーションのため作業者のリソースに依存してしまう

アプリケーション開発の領域において、CI/CDはすでに一般的なプラクティスとなっています。Pull request(PR)に対してLinterやテストを実行し、問題がなければmainブランチへマージ、アプリケーションコンテナのビルド、デプロイが自動実行されます。 インフラ領域においても、IaC化することで、このプロセスを実現できます。また、GitHub Actionsなどを活用してCI/CDパイプラインを構築することで、先ほどの課題を解消・軽減できます。

GitHub Actions概要

GitHub Actionsは、タスク実行やワークフローを自動化するCI/CDサービスです。GitHubでホストされているリポジトリであれば設定ファイルを設置するだけで利用でき、セットアップも容易です。 ここでは、本連載を理解する上で必要となる知識について簡単に解説します。 理解をより深めたい方は、公式ドキュメント1や本誌のバックナンバーSoftware Design 2022年2月号2を参考にしてください。 ワークフローはGitHubリポジトリ内の.github/workflowsディレクトリ配下にYAML形式のファイルとして定義します。これらが、記述内容に従ってプッシュなど特定のイベントをトリガーに実行されます。 リスト1はPRをトリガーとしてTerraform組み込みのフォーマッタであるterraform fmtを実行する例です。 以下はPRをトリガーとしてTerraform組み込みのフォーマッタである を実行する例です。

▼リスト1 .github/workflows/terraform_ci.yml

name: Terraform CI
on:
  pull_request:
    branches:
      - main
jobs:
  tf_version:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v3
      - uses: hashicorp/setup-terraform@v2
      - run: terraform fmt -check -no-color -recursive
        working-directory: ./terraform

ワークフローは名前(name)、トリガー(on)、ジョブ(job)などから構成されます。 onではワークフローをトリガーするイベントを定義します。ここではmainブランチに対するPR関連のアクティビティをトリガー条件としています。 ジョブは、処理タスクを表現する複数のステップから構成されます。ステップでは、shellのコマンド実行や、「アクション」と呼ばれる再利用可能なコンポーネントが利用できます。 リスト1の例ではactions/checkout3でPR元ブランチのコードをワークフローランナー上に展開し、hashicorp/setup-terraform4でterraformコマンドを利用するためのセットアップを実施しています。 最後にterraform fmtコマンドを実行しフォーマットのチェックを行っています。 このように、GitHub Actionsでは、アクションを活用しつつ、CI/CDパイプラインで実施すべき処理をYAMLファイルに記述します。雰囲気をつかんでいただけたでしょうか。

事前準備

ここからはGoogle Cloudも含めたCI/CDパイプラインの具体的な構築方法を解説していきます。まずはワークフロー実行に必要なリソースを事前に作成します。 はじめに、Terraformのtfstateを保管するためのStorageバケットを作成します(図2)。前回でも紹介したように、ステートが複数環境から参照されるときには、tfstateをローカルではなくリモートのオブジェクトストレージなどに保管する必要があります。

▼図2 tfstate用バケット作成

$ gcloud storage buckets create \
  gs://my-tfstate-dev-${RANDOM} \
  --location=asia-northeast1 \
  --uniform-bucket-level-access

なお、Google Cloud Storageのバケット名は、グローバルに一意でなくてはならないため、バケット名にランダム値を追加しています。 次に、Terraformを実行するためのサービスアカウントを作成し、必要な権限を付与します(図3)。${PROJECT_ID}は対象のGoogle CloudプロジェクトIDに置き換えてください。今回は編集者ロールを付与しますが、実際は必要に応じた最小限の権限とするのが適切です。

▼図3 サービスアカウント作成

$ gcloud iam service-accounts create my-github-actions
$ gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member="serviceAccount:my-github-actions@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/editor"

最後に、サンプルのtfファイル(リスト2、3)を用意します。VPCネットワークを構築済みの既存プロジェクトに対してComputeインスタンスを作成します。のちの説明のために、対象のプロジェクトは便宜上開発(dev)環境として扱います。

▼リスト2 terraform/modules/vm/main.tf

variable "name" {
  type = string
}

resource "google_compute_instance" "default" {
  name         = var.name
  machine_type = "e2-micro"
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
      size  = 10
    }
  }
  network_interface {
    network = "default"
  }
}

▼リスト3 terraform/environments/dev/main.tf

terraform {
  backend "gcs" {
    bucket = "my-tfstate-dev-<<SUFFIX>>"
  }
}

provider "google" {
  project = "<<プロジェクトID>>"
  region  = "asia-northeast1"
  zone    = "asia-northeast1-b"
}

module "vm" {
  source = "../../modules/vm"  # ①
  name   = "my-vm"
}

再利用しやすくするためComputeインスタンスのリソース定義はモジュール化しています。 モジュールは複数のtfファイルを含んだディレクトリで、1のようにモジュールディレクトリのパスを指定して利用します。 このtfファイルを利用してGitHub Actions上でTerraformを実行してみましょう。

[Column] IaC するもの/しないもの

IaCは再現性や監査など多く点でメリットがあります。しかし、手動オペレーションをすべて禁止してしまうと、かえって作業が煩雑になったりセキュリティリスクが高まったりするケースもあります。そのような場合には、柔軟に手動オペレーションを許容することも選択肢の1つです。 キャディでは、実施頻度が低く強い権限を要する一部の作業は手動で行っています。Google CloudではプロジェクトやCloud APIについて、IaCの可否を事前に検討しておけると後の管理がスムーズになります。 IaC化しているリソース、IaC化していないリソースはドキュメントで明文化しておくことをお勧めします。明文化しておくことで開発者が意図せずリソースを手動で編集してしまい、IaCで定義した状態から乖かい離してしまうといった事故の予防につながります。

認証方法

TerraformからGoogle Cloud上のリソースを操作するには、認証が必要です。GitHub Actionsではgoogle-github-actions/auth5アクションが次の2種類の認証方法を提供しています。

・サービスアカウントキーを利用する方法 ・Workload Identity連携を利用する方法

サービスアカウントキーを利用する方法

サービスアカウントのキーファイルを生成し、GitHub Actionsのシークレットへ登録します(図4)。シークレットは機密性の高いデータを管理するための機能で、ワークフローからは環境変数として参照できます(図5)。

▼図4 サービスアカウントキー作成

$ gcloud iam service-accounts keys create gsa-key.json \
  --iam-account=my-github-actions@${PROJECT_ID}.iam.gserviceaccount.com

▼図5 Actionsシークレット

ワークフローでは、 credentials_json でアクションに対してサービスアカウントキーを与え、認証します(リスト4)。${{ secrets.DEV_GCP_SA_KEY }}が、DEV_GCP_SA_KEYという名前で登録したシークレットの内容を参照する部分です。

▼リスト4 サービスアカウントキーでの認証

- uses: google-github-actions/auth@v1
  with:
    credentials_json: '${{ secrets.DEV_GCP_SA_KEY }}'

この方法はシンプルですが、セキュリティ面で好ましい方法ではありません。このキーは有効期限がなく、漏洩した際にはキーを使用している全環境でローテーション作業が発生します。 そこで、よりセキュアな手法として、Workload Identity連携を利用した方式が推奨されています。

Workload Identity連携 を利用する方法

Google Cloud の Workload Identity 連携は、GitHubなど外部IDプロバイダ(IdP)の認証情報をもとに、Google Cloudリソースへのアクセス制御を行うサービスです。外部IdPで発行されたトークンを検証し、対象サービスアカウントの権限を借用することで、サービスアカウントキーを使用することなく認証ができます(図6)。

▼図6 Workload Identity連携イメージ

GitHub Actions は OpenID Connect(OIDC)トークンを利用した認証をサポートしているので、ワークフローで短命なOIDCトークンを生成し、ID連携に利用できます。 GitHub ActionsはOpenID Connect(OIDC)トークンを利用した認証をサポートしているので、ワークフローで短命なOIDCトークンを生成し、ID連携に利用できます。6 先ほどのサービスアカウントキーと異なりOIDCトークンは数時間程度で失効するため、セキュリティリスクを軽減できます。 Workload Identity連携はプールとプロバイダから構成されます。プールは外部IdPにより発行されたIDを管理し、プロバイダはGoogle Cloudと外部IdPにおける属性情報の対応を管理します。 まず、GitHub Actionsで利用するプールとプロバイダを作成します(図7、8)。

▼図7 プールの作成

$ gcloud iam workload-identity-pools \
  create my-github-actions \
  --location=global

▼図8 プロバイダの作成

$ gcloud iam workload-identity-pools providers create-oidc github-provider \
  --location=global \
  --workload-identity-pool=my-github-actions \
  --issuer-uri=https://token.actions.githubusercontent.com \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.aud=assertion.aud,attribute.repository=assertion.repository"

これでOIDCトークンを検証する準備ができました。

次に、my-github-actions Workload Identityユーザーロールをmy-github-actionsアカウントに付与し、外部からのアカウント権限借用を許可します(図9)。

▼図9 サービスアカウントの権限借用許可

$ gcloud iam service-accounts add-iam-policy-binding my-github-actions@${PROJECT_ID}.iam.gserviceaccount.com \
    --role=roles/iam.workloadIdentityUser \
    --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUM}/locations/global/workloadIdentityPools/my-github-actions/*"

これで、ワークフローからmy-github-actions サービスアカウントを使用してGoogle Cloudリソースを操作できます。 ワークフローではリスト5のように記述します。

▼リスト5 Workload Identity連携での認証

- uses: google-github-actions/auth@v1
  with:
    workload_identity_provider: projects/(…略…)/providers/github-provider
    service_account: my-github-actions@(…略…).com

Terraform planの実行

GitHub ActionsでTerraformを実行するための準備が整いました。PRに対してterraform planを実行し、どのようなリソース変更が予定されているのかチェックしてみましょう。 plan結果はActionsの実行ログから参照できますが、PRコメントとして投稿されるとレビュー体験がより良くなります。リスト6はPRに対してterraform planを実行し、plan結果をPRコメントとして投稿するワークフロー定義です。

▼リスト6 .github/workflows/terraform_ci.yml

name: Terraform CI
on:
  pull_request:
    branches:
      - main

jobs:
  terraform_ci:
    runs-on: ubuntu-latest
    permissions:  # ①
      contents: read
      id-token: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v3
      - uses: hashicorp/setup-terraform@v2
      - uses: google-github-actions/auth@v1
        with:
          workload_identity_provider: projects/(…略…)/providers/github-provider
          service_account: my-github-actions@(…略…).com
      - run: terraform init
        working-directory: ./terraform/environments/dev
      - id: plan
        run: terraform plan -no-color
        working-directory: ./terraform/environments/dev
        continue-on-error: true
      - uses: actions/github-script@v6  # ②
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `Terraform Plan: \`${{ steps.plan.outcome }}\`
            <details><summary>Show plan</summary>

            \`\`\`
            ${{ steps.plan.outputs.stdout }}
            \`\`\`
            </details>`

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

①:このワークフローではOIDCトークンの生成やコメント投稿を行うため、permissionで対応する権限を付与します。 ②:plan 結果のコメント投稿にはactions/github-script7を利用します。これはGitHub APIを用いた処理をJavaScriptで記述できるアクションです。hashicorp/setup-terraform8アクションのREADMEにはgithub-scriptのサンプルも記載されていますので、参考にしてください。 サンプルのtfファイルを含め、現状GitHubリポジトリは図10のファイル構成となっています。

▼図10 プロジェクトIaCリポジトリの構成

|-- .github
|   `-- workflows
|       `-- terraform_ci.yml
`-- terraform
    |-- environments
    |   `-- dev
    |       `-- main.tf
    `-- modules
        `-- vm
            `-- maint.tf

これらのファイルをコミットし、PRを作成するとワークフローが実行され、terraform planの結果がコメントに投稿されます。

▼図11 plan結果のコメント投稿

今回はterraform planのみですが、コードの品質を高めるためにバリデーション(terraform validate)やフォーマット(terraform fmt)も実行すると良いでしょうtfsec9などtfファイルを静的解析し、セキュリティリスクのあるインフラ構成を検知できるOSSもあります。 なお、コメント投稿は actions/github-scriptを利用しましたが、より見やすい形でコメントするアクションもOSSで公開されています10

おわりに

今回はTerraformとGitHub Actionsを組み合わせたIaCのCIパイプラインについて紹介しました。実運用する際には開発用・商用など複数環境対応や複数プロジェクト管理についても考慮が必要です。 次回はIaCのCDパイプラインに触れつつ、運用をスケールさせるキャディでの取り組みを紹介します。