2023/11/29 追記
以下の方法は正しく動作しますが、Cloud Runを新たに追加したり初回の実行時には以下のエラーが発生すると思われます。
│ The "count" value depends on resource attributes that cannot be determined until apply, so
│ Terraform cannot predict how many instances will be created. To work around this, use the -target
│ argument to first apply only the resources that the count depends on.
その場合、まずはCloud Runだけデプロイを先に実施し、その後DNSの設定を実行するという感じにTerraformコマンドを target
オプションを使って分割する必要があります。
以下はmoduleを使ってTerraformの各種サービスを管理している場合のコマンド例です。
terraform plan -target=module.example.google_cloud_run_service.default
terraform apply -target=module.example.google_cloud_run_service.default
terraform plan -target=module.example.google_cloud_run_domain_mapping.default
terraform apply -target=module.example.google_cloud_run_domain_mapping.default
はじめに
Cloud Runはデフォルトでは*.run.app
のドメインが割り当てられるが、
これを本番で使うには心もとない。
Google Cloudには独自ドメインをCloud Runに適用する方法として大きく2つある
今回の記事では「ドメインマッピング」を利用し、Terraformで自動化してしまおうというもの。
なお、2023/6現在Cloud DNSとCloud Runのドメインマッピング機能はpre-GAなので制限付きであり、今後仕様変更される可能性もあることに注意。
前提条件
今回はCloud DNSを使うにあたりドメインをGoogle Domainで管理することを前提としている。
Cloud DomainのGoogle Domainsからの管理移譲や他ドメインサービスからの移行に関してはTerraformでは実現できないので、
https://support.google.com/domains/answer/10050215?hl=jaを参考に手作業する。
他社ドメインサービスをそのまま利用するにしてもCloud DNSのNSレコードを手動で設定する必要があるので、このあたりは結局Terraformでは対応不可であることに違いはない。
Terraformを使用したCloud RunとCloud DNSの統合
Terraformの構成をすべて説明すると長くなるので、
ここではCloud RunとCloud DNSのドメインマッピングに関連するTerraformのコードのみを説明・記述する。
variables.tfやoutputs.tfの詳細と都度出てくる変数は各自の環境で埋めてほしい。
ディレクトリ構成
.
├── environments
│ ├── dev
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── variables.tf
│ └── prod
└── modules
├── network
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── cloudrun
├── main.tf
├── outputs.tf
└── variables.tf
Network(Cloud DNS)の設定
# Cloud DNSのゾーン設定
resource "google_dns_managed_zone" "myapp" {
name = "zone-myapp"
dns_name = "mydomain.com."
visibility = "public"
dnssec_config {
state = "on"
}
cloud_logging_config {
enable_logging = true
}
}
Cloud DNSの設定は特に難しいことはなく、DNSSECとCloudLoggingを有効にしているくらい。
Cloud Runの設定
Cloud RunのTerraformの全体は以下のとおり。
# Cloud Runのサービス設定
resource "google_cloud_run_service" "default" {
name = var.cloudrun_service_name
location = var.region
# 重複エラーが起きるため、リビジョン名を自動生成し既存のものと重複しないようにする。
autogenerate_revision_name = true
template {
metadata {
annotations = {
"autoscaling.knative.dev/maxScale" = "1" #最小構成
"autoscaling.knative.dev/minScale" = "1"
"run.googleapis.com/vpc-access-connector" = var.vpc_connector_id
"run.googleapis.com/vpc-access-egress" = var.vpc_connector_egress
}
}
spec {
containers {
# 正しいイメージはCloud Buildでビルドしたものを指定するため、ここでは適当なものを指定
image = "us-docker.pkg.dev/cloudrun/container/hello:latest"
ports {
container_port = var.port
}
}
}
}
lifecycle {
ignore_changes = [
#デプロイするたびに差分が出るため無視
template[0].metadata[0].labels["run.googleapis.com/startupProbeType"]
]
}
}
# 未認証のアクセスを許可する設定
data "google_iam_policy" "noauth" {
binding {
role = "roles/run.invoker"
members = ["allUsers"]
}
}
resource "google_cloud_run_v2_service_iam_policy" "noauth" {
location = google_cloud_run_service.default.location
project = google_cloud_run_service.default.project
name = google_cloud_run_service.default.name
policy_data = data.google_iam_policy.noauth.policy_data
}
# Cloud DNSとCloud Runのドメインマッピング
resource "google_cloud_run_domain_mapping" "default" {
location = var.region
name = var.domain_name
metadata {
namespace = var.project
}
spec {
route_name = google_cloud_run_service.default.name
}
}
# ドメインマッピングで発生する各種DNSレコード情報を動的に収集
locals {
dns_records_A = [for rr in google_cloud_run_domain_mapping.default.status[0].resource_records : rr.rrdata if rr.type == "A"]
dns_records_AAAA = [for rr in google_cloud_run_domain_mapping.default.status[0].resource_records : rr.rrdata if rr.type == "AAAA"]
dns_record_WWW = [for rr in google_cloud_run_domain_mapping.default.status[0].resource_records : rr.rrdata if rr.type == "CNAME"]
}
# A、AAAAレコードがない場合はCNAMEレコードを生成
resource "google_dns_record_set" "www" {
count = length(local.dns_records_A) > 0 || length(local.dns_records_AAAA) > 0 ? 0 : 1
name = "${var.domain_name}."
type = "CNAME"
ttl = 3600
managed_zone = var.google_dns_managed_zone_name
rrdatas = local.dns_record_WWW
}
# Aレコードがある場合はAレコードを生成
resource "google_dns_record_set" "default_A" {
count = length(local.dns_records_A) > 0 ? 1 : 0
managed_zone = var.google_dns_managed_zone_name
name = "${var.domain_name}."
type = "A"
ttl = 3600
rrdatas = local.dns_records_A
}
# AAAAレコードがある場合はAAAAレコードを生成
resource "google_dns_record_set" "default_AAAA" {
count = length(local.dns_records_AAAA) > 0 ? 1 : 0
managed_zone = var.google_dns_managed_zone_name
name = "${var.domain_name}."
type = "AAAA"
ttl = 3600
rrdatas = local.dns_records_AAAA
}
簡単に解説する。
Cloud Runのサービス設定
まず google_cloud_run_v2_service
ではなく google_cloud_run_service
リソースを使っている理由は、terraform-provider-google/issues/14569にて言及されているとおり、
v2だとリビジョン名を無視すると terraform apply
時に毎回差分が見つかってデプロイ対象になってしまうし、
無視しないとリビジョン名が重複するためデプロイに失敗するというデッドロックに陥るためである。
image
にてコンテナイメージをダミーではなく実際のアプリのコンテナイメージに変更することで回避できるが、
初回のデプロイ時に image
に指定したコンテナイメージが存在しないと terraform apply
が失敗してしまう。
そのため、初回デプロイ時はイメージを先にArtifact Registry、DockerHub、その他指定のURLに対してpushしておくこと。
未認証のアクセスを許可する設定
data "google_iam_policy" "noauth"
、resource "google_cloud_run_v2_service_iam_policy" "noauth"
は、
Cloud Runを一般公開するための設定である。
Cloud DNSとCloud Runのドメインマッピング
ここが本題。
ドメインマッピング機能を利用すると、サブドメインを指定しなかったCloud Runのサービスに対してはAレコードやAAAAレコードが生成され、CNAMEが存在しない。
対して、サブドメインを指定したCloud Runのサービスに対しては、CNAMEレコードが生成され、A/AAAAレコードが存在しない。
この仕様を利用して locals
でDNSレコード情報を収集し、自動的にAレコードやAAAAレコード、CNAMEレコードを対象のDNSゾーンに登録するようにしている。
参考サイトの仕組みでは rrdatas
が空の場合でもレコードが生成されてしまうため、ドメインとサブドメインをもつCloud Runを同時にマッピングすることができなかった。
そこでTerraformの count
を使って場合分けする仕組みを使っている。
このコードはなんと ChatGPTが生成してくれた。さすがGPT-4。
#ドメインマッピングで発生する各種DNSレコード情報を動的に収集
locals {
dns_records_A = [for rr in google_cloud_run_domain_mapping.default.status[0].resource_records : rr.rrdata if rr.type == "A"]
dns_records_AAAA = [for rr in google_cloud_run_domain_mapping.default.status[0].resource_records : rr.rrdata if rr.type == "AAAA"]
dns_record_WWW = [for rr in google_cloud_run_domain_mapping.default.status[0].resource_records : rr.rrdata if rr.type == "CNAME"]
}
# A、AAAAレコードがない場合はCNAMEレコードを生成
resource "google_dns_record_set" "default_WWW" {
count = length(local.dns_records_A) > 0 || length(local.dns_records_AAAA) > 0 ? 0 : 1
name = "${var.domain_name}."
type = "CNAME"
ttl = 3600
managed_zone = var.google_dns_managed_zone_name
rrdatas = local.dns_record_WWW
}
# Aレコードがある場合はAレコードを生成
resource "google_dns_record_set" "default_A" {
count = length(local.dns_records_A) > 0 ? 1 : 0
managed_zone = var.google_dns_managed_zone_name
name = "${var.domain_name}."
type = "A"
ttl = 3600
rrdatas = local.dns_records_A
}
# AAAAレコードがある場合はAAAAレコードを生成
resource "google_dns_record_set" "default_AAAA" {
count = length(local.dns_records_AAAA) > 0 ? 1 : 0
managed_zone = var.google_dns_managed_zone_name
name = "${var.domain_name}."
type = "AAAA"
ttl = 3600
rrdatas = local.dns_records_AAAA
}
おわりに
今回はCloud RunのサービスをドメインマッピングするためのTerraformコードを紹介した。
Cloud Runのドメインマッピングはpre-GAの機能であるため、今後仕様が変更される可能性があるので恒久的にこの仕組みが使えるわけではない。
また、それなりに規模が大きく信頼性の求められるプロダクトではドメインマッピングを使わずにCloud Load Balancingを使うことが多いと思う。
そういう意味では今回のコードはニッチなノウハウになるかもしれない。
ニッチな誰かの参考になれば幸いである。
参考
DNSにCloud DNSではなくCloudflareを使う場合:
https://alpacat.com/blog/cloud-run-with-cloudflare/