こんにちは。Tomoyuki(@tomoyuki65)です。
Webサービス開発においては、何らかのクラウドサービスを利用してインフラ環境を構築することになると思います。
その際に実務においては主にTerraformなどのIaC(Infrastructure as Code)ツールを用いてコード管理されることが多いです。
そこでこの記事では、以前にご紹介したGoogle Cloud Artifact Registry(GAR)に保存したDockerコンテナを使いながら、TerraformでCloud Runにデプロイしてインフラ環境を構築する方法について解説します。
関連記事

【Terraform入門】GARのDockerコンテナをCloud Runにデプロイしてインフラ環境を構築する方法
まずはTerraformのコマンドを使えるようにします。
開発用PCがMacの場合は、パッケージ管理にHomebrewを使っていることが多いと思うので、その場合は以下のコマンドでTerraformをインストールします。
$ brew install hashicorp/tap/terraform
インストール後、以下のコマンドを実行し、パスが通っていることを確認します。
$ terraform -v
コマンド実行後、以下のようにログが出力されればOKです。

GARのDockerコンテナをCloud Runにデプロイする
次にGARに保存しておいたDockerコンテナを使う形で、Cloud RunにデプロイするためのTerraformを書くため、以下のコマンドを実行して各種ファイルを作成します。
$ mkdir infra-sample && cd infra-sample
$ mkdir -p deploy/terraform/modules/cloud_run
$ touch deploy/terraform/modules/cloud_run/variables.tf
$ touch deploy/terraform/modules/cloud_run/sa.tf
$ touch deploy/terraform/modules/cloud_run/iam.tf
$ touch deploy/terraform/modules/cloud_run/main.tf
$ touch deploy/terraform/modules/cloud_run/outputs.tf
$ mkdir -p deploy/terraform/environments/dev
$ touch deploy/terraform/environments/dev/variables.tf
$ touch deploy/terraform/environments/dev/main.tf
$ touch deploy/terraform/environments/dev/outputs.tf※今回は実務を想定してモジュールを使ったやり方でファイルを作成します。尚、Terraformではファイル分割しても適切な順序でいい感じに読み込まれる仕様のため、ファイル分割して作成するのが推薦です。
次に作成したファイルをそれぞれ以下のように記述します。
・「deploy/terraform/modules/cloud_run/variables.tf」
variable "deletion_protection" {
  description = "Terraformでの削除不可設定"
  type = bool
}
variable "project_id" {
  description = "プロジェクトID"
  type = string
}
variable "region" {
  description = "リージョン"
  type = string
}
variable "service_name" {
  description = "サービス名"
  type = string
}
variable "image" {
  description = "Artifact RegistryのDockerイメージURL"
  type = string
}
variable "allow_unauth" {
  description = "未認証の呼び出しを許可する設定"
  type = bool
}
variable "min_instance_count" {
  description = "最小インスタンス数"
  type = number
}
variable "max_instance_count" {
  description = "最大インスタンス数"
  type = number
}
variable "ingress" {
  description = "Ingress(Cloud Runサービスへのネットワークアクセスを制限)"
  type = string
}
variable "container_port" {
  description = "コンテナのポート番号"
  type = number
}
variable "memory" {
  description = "メモリ"
  type = string
}
variable "cpu" {
  description = "CPU"
  type = string
}
variable "cpu_idle" {
  description = "CPUをリクエスト時のみ割り当てる設定"
  type = bool
}
variable "startup_cpu_boost" {
  description = "起動時のCPUブースト"
  type = bool
}
variable "timeout" {
  description = "リクエストのタイムアウト"
  type = string
}
variable "max_instance_request_concurrency" {
  description = "インスタンスあたりの最大同時リクエスト数"
  type = number
}
variable "env_vars" {
  description = "環境変数リスト"
  type = list(object({
    name = string
    value = string
    secret_id = string
    secret_version = string
  }))
  default = []
}※これはCloud Runのモジュール用の変数設定ファイルです。
・「deploy/terraform/modules/cloud_run/sa.tf」
# サービスアカウントの追加
resource "google_service_account" "cloud_run_sa" {
  account_id = "${var.service_name}-sa"
  display_name = "Service account for ${var.service_name}"
}※これはCloud Runのモジュール用のサービスアカウントを追加する設定ファイルです。
・「deploy/terraform/modules/cloud_run/iam.tf」
# Artifact Registryの読み取り権限付与
resource "google_project_iam_member" "artifact_registry_reader" {
  project = var.project_id
  role = "roles/artifactregistry.reader"
  member = "serviceAccount:${google_service_account.cloud_run_sa.email}"
}
# 未認証の呼び出しを許可した場合のみ追加(allow_unauth = trueの場合のみ)
resource "google_cloud_run_service_iam_member" "unauthenticated_invoker" {
  count = var.allow_unauth ? 1 : 0
  project = google_cloud_run_v2_service.default.project
  service = google_cloud_run_v2_service.default.name
  location = google_cloud_run_v2_service.default.location
  role = "roles/run.invoker"
  member = "allUsers"
}※これはCloud Runのモジュール用のサービスアカウント等にIAMの各種権限を追加する設定ファイルです。
・「deploy/terraform/modules/cloud_run/main.tf」
resource "google_cloud_run_v2_service" "default" {
  # Terraformでの削除不可設定
  deletion_protection = var.deletion_protection
  # サービス名
  name = var.service_name
  # リージョン
  location = var.region
  # Ingress(Cloud Runサービスへのネットワークアクセスを制限)
  ingress = var.ingress
  # 自動スケーリング設定
  scaling {
    min_instance_count = var.min_instance_count
    max_instance_count = var.max_instance_count
  }
  template {
    containers {
      # イメージURL
      image = var.image
      # ポート
      ports {
        container_port = var.container_port
      }
      # リソース
      resources {
        limits = {
          cpu = var.cpu
          memory = var.memory
        }
        # CPUをリクエスト時のみ割り当てる設定
        # resourcesを設定したら明示的に指定すべき項目
        cpu_idle = var.cpu_idle
        # 起動時のCPUブースト
        startup_cpu_boost = var.startup_cpu_boost
      }
      # 環境変数を設定するためのdynamicブロック
      dynamic "env" {
        for_each = var.env_vars
        content {
          name = env.value.name
          # 通常の環境変数の場合に値を設定
          value = env.value.value != "" ? env.value.value : null
          # Secret参照の場合にvalue_sourceブロックを作る
          dynamic "value_source" {
            for_each = env.value.value == "" ? [1] : []
            content {
              secret_key_ref {
                secret = env.value.secret_id
                version = env.value.secret_version
              }
            }
          }
        }
      }
    }
    # リクエストのタイムアウト
    timeout = var.timeout
    # インスタンスあたりの最大同時リクエスト数
    max_instance_request_concurrency = var.max_instance_request_concurrency
    # サービスアカウントの設定
    service_account = google_service_account.cloud_run_sa.email
  }
}※これはCloud Runのモジュールのメインファイルです。設定できる項目などについては、Terraform公式サイトのCloud Run V2のドキュメントを確認して下さい。
・「deploy/terraform/modules/cloud_run/outputs.tf」
output "service_url" {
  description = "Cloud RunサービスのURL"
  value = google_cloud_run_v2_service.default.uri
}※これはCloud Runのモジュール用の出力値を設定するためのファイルです。
・「deploy/terraform/environments/dev/variables.tf」
variable "project_id" {
  description = "プロジェクトID"
  type = string
  default = "<対象のプロジェクトID>"
}
variable "image_tag" {
  description = "Artifact RegistryのDockerイメージのタグ"
  type = string
}※これはdev環境用の変数設定ファイルです。project_idのdefaultの<対象のプロジェクトID>は各自の環境に合わせて修正して下さい。
・「deploy/terraform/environments/dev/main.tf」
# Terraformの設定
terraform {
  required_version = ">= 1.13.0"
  required_providers {
    google = {
      source = "hashicorp/google"
      version = "~> 7.0"
    }
  }
}
# プロバイダー設定
provider "google" {
  project = var.project_id
}
# Cloud Runのモジュール設定
module "cloud_run" {
  source = "../../modules/cloud_run"
  deletion_protection = false
  project_id = var.project_id
  region = "us-central1"
  service_name = "go-api"
  image = "us-central1-docker.pkg.dev/${var.project_id}/go-api/go-api:${var.image_tag}"
  allow_unauth = true
  min_instance_count = 0
  max_instance_count = 1
  ingress = "INGRESS_TRAFFIC_ALL"
  # コンテナ設定
  container_port = 8080
  cpu = "1"
  memory = "256Mi"
  cpu_idle = true
  startup_cpu_boost = true
  timeout = "300s"
  max_instance_request_concurrency = 80
  # 環境変数設定
  env_vars = [
    { name = "ENV", value = "dev", secret_id = "", secret_version = "" }
  ]
}※これはdev環境用のメインファイルです。Cloud Runのモジュールを使って各種設定を決めています。
・「deploy/terraform/environments/dev/outputs.tf」
output "cloud_run_url" {
  description = "Cloud RunのURL"
  value = module.cloud_run.service_url
}※これはdev環境用の出力値を設定するためのファイルです。
ファイル作成後、以下のコマンドでフォーマット修正が可能です。
$ terraform fmt -recursive
gcloudコマンドでApplication Default Credentials (ADC)を設定
TerraformからGoogle Cloudに接続するには何らかの認証設定が必要になりますが、簡単に認証したい場合は、以下のコマンドを実行してApplication Default Credentials (ADC) を設定して下さい。
※もしCI/CDなどを構築する場合は、別途Terraform用のサービスアカウントを作り、その認証情報のJSONファイルを取得して利用して下さい。
$ gcloud auth application-default login
ブラウザが起動してアカウント選択画面が表示されるので、対象のアカウントを選択します。

次に「次へ」をクリックします。

次にアクセス情報の選択のチェックボックスが表示されるので、問題なければ「すべて選択」のチェックを付けて、「続行」をクリックします。

その後に「gcloud CLIの認証が完了しました。」の画面が表示されればOKです。

設定完了後、パス「/Users/<ユーザー名>/.config/gcloud/application_default_credentials.json」に認証用のファイルが作成され、これで認証できるようになります。
※事前に行っていたgcloudコマンドの認証設定で設定したプロジェクトIDで設定されているようなので、別のプロジェクトIDに変更した場合などは再認証が必要になると思われます。
Terraformを試す
次に作成したファイルを使ってTerraformを試します。
まずは以下のコマンドを実行し、対象の環境のディレクトリに移動してTerraformの初期化を行います。
$ cd deploy/terraform/environments/dev
$ terraform init
次に以下のコマンドを実行し、「plan」コマンドを使って計画のチェックをします。
$ terraform plan -var="image_tag=1.0.0"※今回は対象のDockerコンテナイメージのタグの部分はパラメータで渡せるようにしているため、「-var=”image_tag=1.0.0″」のように指定しています。
コマンド実行後、以下のように計画が表示されるので、間違いがないかはチェックして下さい。
※構文にエラーがあったりする場合、ここでエラーになります。


次に以下のコマンドを実行し、実際に環境構築をしてみます。
$ terraform apply -var="image_tag=1.0.0" -auto-approve※オプション「-auto-approve」で確認プロンプト無しで実行
コマンド実行後、以下のように表示されて正常終了すればOKです。

※cloud_run_urlでAPIのURLも出力するようにしているため、このURLを使ってAPIの検証が可能です。
次にGoogle Cloudの画面からCloud Runの画面を確認してみます。
対象のプロジェクト画面を開き、左のメニューから「Cloud Run」をクリックします。

Cloud Runのサービス画面が開き、Terraformで作成したサービスが表示されていればOKです。
次にサービス名をクリックし、詳細を確認します。

次にメニュー「ログ」をクリックし、想定通りのログ出力がされていればOKです。

次にPostmanでCloud RunのAPIを試してみます。
まずはGETメソッドで「<Cloud RunのURL>/hello」を実行し、以下のように正常終了すればOKです。

次にGETメソッドで「<Cloud RunのURL>/test」を実行し、以下のように正常終了すればOKです。

Terraformを更新し、環境変数を追加してみる
次にTerraformのファイルを更新して環境変数を追加してみます。
まずは対象サービスの「リビジョン」タブを開き、画面右側の環境変数が一つしかないことを確認します。

次にTerraformのファイル「deploy/terraform/environments/dev/main.tf」以下のように修正します。
・「deploy/terraform/environments/dev/main.tf」
# Terraformの設定
terraform {
  required_version = ">= 1.13.0"
  required_providers {
    google = {
      source = "hashicorp/google"
      version = "~> 7.0"
    }
  }
}
# プロバイダー設定
provider "google" {
  project = var.project_id
}
# Cloud Runのモジュール設定
module "cloud_run" {
  source = "../../modules/cloud_run"
  deletion_protection = false
  project_id = var.project_id
  region = "us-central1"
  service_name = "go-api"
  image = "us-central1-docker.pkg.dev/${var.project_id}/go-api/go-api:${var.image_tag}"
  allow_unauth = true
  min_instance_count = 0
  max_instance_count = 1
  ingress = "INGRESS_TRAFFIC_ALL"
  # コンテナ設定
  container_port = 8080
  cpu = "1"
  memory = "256Mi"
  cpu_idle = true
  startup_cpu_boost = true
  timeout = "300s"
  max_instance_request_concurrency = 80
  # 環境変数設定
  env_vars = [
    { name = "ENV", value = "dev", secret_id = "", secret_version = "" },
    { name = "ADD_ENV", value = "add-env", secret_id = "", secret_version = "" }
  ]
}※環境変数「ADD_ENV」を追加
次に以下のコマンドを実行し、計画を確認します。
$ terraform plan -var="image_tag=1.0.0"
コマンド実行後、環境変数「ADD_ENV」が追加されることを確認します。

次に以下のコマンドを実行し、更新してみます。
$ terraform apply -var="image_tag=1.0.0" -auto-approve
コマンド実行後、以下のように表示されて正常終了すればOKです。

次にもう一度Cloud Runの対象サービスの画面に戻って画面を更新し、最新のリビジョンを選択後、環境変数に「ADD_ENV」が追加されていればOKです。

Terraformで全てを削除する
次にTerraformで作成した全てのリソースを削除してみます。
まずは以下のコマンドを実行し、削除の計画を確認します。
$ terraform plan -var="image_tag=1.0.0" -destroy※もし対象のリソースを指定する場合はオプション「-target」を使います。
コマンド実行後、以下のように計画が表示されるので、間違いないことをチェックします。



次に以下のコマンドを実行し、削除を実行します。
$ terraform destroy -var="image_tag=1.0.0" -auto-approve
コマンド実行後、以下のように表示されて正常終了すればOKです。

次にもう一度Cloud Runのサービス一覧画面に戻り、以下のようにサービスが削除されていればOKです。

Terraformを利用する際の注意点
Terraformの現在の状態について
ローカル環境でTerraformを色々実行させた際の現在の状態については、作業ディレクトリ直下にファイル「terraform.tfstate」が作成され、このファイルと比較して差分管理をしています。
実務でチーム開発をする際などは、この「terraform.tfstate」をCloud Storageなどで管理して共有化しないと差分管理できないので注意しましょう。
例えばGoogle Cloud Storageで管理するようにした場合、ルートディレクトリ直下のmain.tfのTerraform設定で、以下のようにバックエンドの設定を追加すると、対象のストレージのファイルを参照するようにできます。
# Terraformの設定
terraform {
  required_version = ">= 1.13.0"
  required_providers {
    google = {
      source = "hashicorp/google"
      version = "~> 7.0"
    }
  }
  # バックエンド設定(Cloud Storage)が必要な場合
  backend "gcs" {
    bucket = "go-api-terraform-state-bucket"
    prefix = "environments/dev"
  }
}※指定したbucketとprefixの場所に「terraform.tfstate」を保存して下さい。
TerraformのファイルをGitHub管理する場合は「.gitignore」を追加する
Terraformを実行すると各種ファイルが作成されますが、GitHubで管理不要なファイルも作成されます。
そのため、コミットする前に「.gitignore」を追加し、以下のような項目を追加してGitHubで保存されないようにして下さい。
# Terraform .terraform directory
.terraform/
# Terraform state files
*.tfstate
*.tfstate.*backup
# Terraform variable files
*.tfvars
*.tfvars.json
# Terraform CLI configuration files
.terraformrc
terraform.rc
# Crash log files
crash.log
CI/CDで更新したい場合はTerraform専用のサービスアカウントを作成して利用する
GitHub ActionsなどのCI/CDツールでTerraformの更新をしたい場合、別途Terraform専用のサービスアカウントを作成し、最小の権限を付与してから、そのサービスアカウントのJSONキーを取得して認証に利用して下さい。(取得したJSONキーの中身を各種ツールの環境変数に保存して使う)
※サービスアカウントに付ける最小の権限について、例えば「Artifact Registry のイメージ参照」、「Cloud Runサービスの作成・更新」、「Cloud Runに実行用サービスアカウントを指定」、「Cloud RunのIAM ポリシー変更(認証なし公開)」をさせる場合、付与する権限としては「roles/run.admin」、「roles/iam.serviceAccountUser」、「roles/artifactregistry.reader」、「roles/iam.securityAdmin」のようになります。
最後に
今回はTerraformでインフラ環境を構築する方法について解説しました。
実務においてはTerraformなどのIaCツールを用いてインフラ環境をコード管理することが多いと思うので、興味がある方はぜひ参考にしてみて下さい。

 
  
  
  
  
コメント