完全自動化:Terraformを使用したCloud RunとCloud DNSのドメインマッピング
Updated Date: 2024/01/01 02:55
以前の職場ではCloud RunのドメインはLoad Balancer経由だったのだが、
今回はCloud Runを直接Cloud DNSにマッピングする「ドメインマッピング」を利用してみた。
これらの作業を「完全に」Terraformで自動化することに(多分)成功したのでここに記録しておく。
はじめに
Cloud RunとはGoogle Cloudのコンテナベースのサーバーレスなマネージドプラットフォームである。 そしてCloud DNSは先日Google Domainが管理移譲を発表したところではあるが、 ドメインに設定するDNSをGoogle Cloud上で管理できるDNSゾーンサービスである。
Cloud Runはデフォルトでは*.run.app
のドメインが割り当てられるが、
これを本番で使うには心もとない。
そこでCloud DNSとCloud Runのドメインマッピングを利用することで独自ドメインでCloud Runを公開することができる。
どうせならこの一連の手続きを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の詳細は各自の環境で埋めてほしい。(この記事を理解できる方であれば問題ないはず)
ディレクトリ構成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── environments
│ ├── dev
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── variables.tf
│ └── prod
├── main.tf
└── modules
├── network
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── cloudrun
├── main.tf
├── outputs.tf
└── variables.tf
Network(Cloud DNS)の設定
1
2
3
4
5
6
7
8
9
10
11
12
# 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の全体は以下のとおり。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# 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
が失敗してしまう。
初回デプロイ時はイメージを先にArtifacts Registryに手作業で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ゾーンに登録するようにしている。
参考にしたサイト: How to access Cloud Run service IPs from Terraform / Pulumi to dynamically create A records? - stackoverflow
参考サイトの仕組みでは、 rrdatas
が空の場合でもレコードが生成されてしまうため、ドメインとサブドメインをもつCloud Runを同時にマッピングすることができなかった。
そこでTerraformの count
を使って場合分けする仕組みを使っている。
このコードはなんと ChatGPTが生成してくれている。さすがGPT-4。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# ドメインマッピングで発生する各種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のサービスをドメインマッピングするためのTerraformコードを紹介した。
Cloud Runのドメインマッピングはpre-GAの機能であるため、今後仕様が変更される可能性があるので恒久的にこの仕組みが使えるわけではない。
また、それなりに規模が大きく信頼性の求められるプロダクトでは、ドメインマッピングを使わずにCloud Load Balancingを使うことが多いと思う。
そういう意味では、今回のコードはニッチなノウハウになるかもしれない。
個人的には手作業の範囲を限りなく減らすことができて満足。
今回のコードもまた誰かの参考になれば幸いである。