💡

Terraformでコンテナイメージのビルドからデプロイまでを行う(GCP編)

2024/08/11に公開

概要

背景

  • システム構築を行う際、以下のような手順になることがしばしばあります。
    1. コンテナリポジトリーを作る
    2. コンテナイメージの作成とコンテナリポジトリーへの登録 (docker builddocker push)
    3. コンテナリポジトリーに登録したイメージを起動するインフラを作成する。
  • 1 と 3 の処理は terraform で行い、 2 の処理は docker で行う、など各ステップに使用するツールが別になるため、全体のデプロイ処理のために複数ツールを使用するスクリプトを作ることになります。
  • 加えてコンテナレジストリーへの認証に docker の認証プラグインなどの準備も必要で、スクリプトを実行できる環境を準備するのは割と手間です。
    • ※あとプロジェクトが Windows / Mac / Linux 混在環境の場合も対応が手間。
  • Terraform の kreuzwerker/docker プロバイダーを使うことで、これを terraform apply で一括して行うことができます。
  • さらに Terraform の実行自体も docker で行うことで、開発者がインストールするのは Docker だけという環境も実現できます。

実装例

https://github.com/ikedam/zenn_snippets/tree/dockerimage_terraform_gcp

Google Cloud Run で Web アプリケーションを作成するデモです。

こんな感じに docker compose さえ実行できれば、 terraform apply だけで HTTP エンドポイントを構築できます。

$ export CLOUDSDK_CORE_PROJECT=your_project_name
$ docker compose run --rm terraform init
...
$ docker compose run --rm terraform apply
...
Outputs:

webapp_uri = "https://xxxxxxx.a.run.app"
$ curl https://xxxxxxx.a.run.app
Hello, World!
$ 

設定のポイント

今回の kreuzwerker/docker の利用での設定のポイントを記載します。

TerraformをDockerで起動&TerraformからDockerが使えるようにする

Terraform の Docker イメージを使用することで、プロジェクト参加者が Terraform のインストール手順を踏まなくてもとりあえず Terraform の実行を行えたり、プロジェクトごとに異なる Terraform バージョンを簡単に切り替えて利用できるので便利です。
さらに、今回使用する kreuzwerker/docker プロバイダーは docker デーモンとの通信を行うため、 /var/run/docker.sock をバインドマウントします (docker outside of docker)。

docker-compose.yaml が以下のようになります(関係部分だけ抜粋):

services:
  terraform:
    image: hashicorp/terraform:1.7.5
    environment:
      - CLOUDSDK_CORE_PROJECT
    volumes:
      - .:/workspace
      # docker outside of docker でterraformからdockerデーモンにアクセスさせる
      - /var/run/docker.sock:/var/run/docker.sock
    working_dir: /workspace

dockerプロバイダーでArtifact Registryの認証を行う

Artifact Registryで作ったイメージレジストリーにコンテナイメージを保存するには、認証が必要です。

この認証には gcloud auth configure-docker ... コマンドでセットアップされる gcloud CLI 認証ヘルパーを使用するか、 docker-credential-gcr をインストールして使用するケースが多いでしょう。

今回は代わりに サービスアカウントキー を使った認証を使用します。

Terraform 内でレジストリーにアクセスするためだけのサービスアカウントを作成し、その認証キーを使用します (該当コード):

resource "google_service_account_key" "imagepush" {
  service_account_id = google_service_account.imagepush.name
}

...

provider "docker" {
  registry_auth {
    address  = "https://${google_artifact_registry_repository.image_registry.location}-docker.pkg.dev"
    username = "_json_key_base64"
    password = google_service_account_key.imagepush.private_key
  }
}

認証キーが Terraform のステート(tfstate ファイル)に保存されることに注意してください。このため、ステートファイルは S3 などのバックエンドに保存するようにして、適切なアクセス制限を行う必要があります。

一応、万一認証キーが漏れたとしても、サービスアカウントを専用に作ることで影響範囲を該当のArtifact Registryだけに抑えられます。

docker_imageリソースでコンテナイメージを作成する

docker_image リソース を使うことで、 docker build を実行できます。

扱いが難しい点として、イメージの構築に使用するファイルが使用した際に docker build を再実行するための指定を自前で行う必要があります。

ここでは、ビルドで使用するファイルをarchive_file データソースで zip にして、そのハッシュ値で再ビルドの判定を行うようにしています。この zip ファイルはハッシュ値の計算に使用するだけで、 docker build の処理自体では特に使用しません。

以下のような設定になります :

# イメージのリビルド判定用
data "archive_file" "webapp" {
  type        = "zip"
  output_path = "${path.module}/webapp.zip"
  source_dir  = "${path.module}/webapp"
  # .dockerignore 相当の指定を行う。
  excludes = setunion(
    fileset("${path.module}/webapp", ".dockerignore"),
  )
}

resource "docker_image" "webapp" {
  name         = "${local.registry_uri}/webapp:latest"
  platform     = "linix/amd64"
  keep_locally = true
  build {
    context = "${path.module}/webapp"
  }
  triggers = {
    sha256 = data.archive_file.webapp.output_sha256
  }
}

なお難点として、ローカルにイメージがない場合、 docker build が実行されてしまいます。
CI/CD などで毎回 docker build が実行される結果になります。

docker_registry_imageでコンテナイメージをpushする

docker_registry_imageリソースdocker push を実行できます。

イメージを更新した場合に docker push を再実行するための指定を自前で行う必要があります。
docker_image リソースと同じ zip のハッシュ値を使用します。

以下のような設定になります :

resource "docker_registry_image" "webapp" {
  name          = docker_image.webapp.name
  keep_remotely = true

  triggers = {
    sha256 = data.archive_file.webapp.output_sha256
  }
}

CloudRunで使用するイメージを指定する

イメージを更新したら新しいイメージがデプロイされるように、
以下のフォーマットのダイジェストを使用したイメージ URL を指定します:

ロケーション-docker.pkg.dev/プロジェクトID/レジストリー名/リポジトリー名@sha256:ダイジェスト

ダイジェストの部分は (sha256:ダイジェストのフォーマットで) docker_registry_image リソースの sha256_digest で取得できます:

locals {
  registry_host = "${google_artifact_registry_repository.image_registry.location}-docker.pkg.dev"
  registry_uri  = "${local.registry_host}/${data.google_project.project.project_id}/${google_artifact_registry_repository.image_registry.name}"
}
...
locals {
  repo_image_uri = "${local.registry_uri}/webapp@${docker_registry_image.webapp.sha256_digest}"
}

resource "google_cloud_run_v2_service" "webapp" {
  name     = "${var.basename}-service"
  location = "asia-northeast1"

  template {
    containers {
      image = local.repo_image_uri
    }
  }

  depends_on = [
    google_project_service.run,
  ]
}

Discussion