bon now

ありのままの現実を書き殴る吐き溜め。底辺SEの備忘録。
Written by bon who just a foolish IT Engineer.

完全自動化:Terraformを使用したCloud RunとCloud DNSのドメインマッピング

Created Date: 2023/07/01 18:35
Updated Date: 2023/11/05 03:00

以前の職場では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を使うことが多いと思う。 そういう意味では、今回のコードはニッチなノウハウになるかもしれない。
個人的には手作業の範囲を限りなく減らすことができて満足。

今回のコードもまた誰かの参考になれば幸いである。

local_offer
gcp
folder work