🚧

Terraformを使ってゼロダウンタイムで証明書切替ができるCertificate Managerをデプロイする

2025/02/14に公開

はじめに

普段 Google Cloud でマネージドな証明書サービスを利用する時は Managed SSL を利用していましたが、証明書切替でダウンタイムが発生することもありもっと良いサービスないかなということで Certificate Manager を使ってみました。Certificate Manager は Managed SSL に比べていろいろと細やかな設定ができるのですがそのぶん設定が複雑です。Certificate Manager の導入でいろいろと苦労したので備忘録的に Certificate Manager の使い方を紹介しようと思います。

Managed SSLはお手軽だけど証明書切替でダウンタイムが発生する

Google Cloud のマネージド証明書といえば、Managed SSL でしたが、Managed SSL で作成される証明書は DNS のAレコードが正しく引けないと証明書がアクティベートされない(ロードバランサ認証)という問題がありました。これの何が問題かというと、なんらかの事情でグローバルIPを変えなきゃいけないみたいな時に証明書の切り替えでダウンタイムが発生します。新しい証明書のアクティベートにどのくらいの時間がかかるは神のみぞ知るみたいなところがあり運が良ければ10分で終わるけど運が悪いと1日立ってもアクティベートされないなんてこともあります。24/365のサービスでこのような運ゲーをするわけにはいかないです。

Certificate ManagerのDNS認証を使うと事前に証明書をアクティベートできる

Google Cloud には Managed SSL とは別に Certificate Manager というマネージドな証明書サービスがあります(2022年に登場の比較的新しいサービスです)。Certificate Manager は Managed SSL のようなお手軽さはない代わりにドメインごとに割り当てる証明書とその提供方法をきめ細かく制御することができます。その中の一つに DNS 認証の機能があります。DNS 認証を使うとAレコードが引けない状態でも証明書をアクティベートすることができます(ダウンタイムゼロで証明書の切り替えができるということです)。DNS 認証は証明書の認証のために DNS に CNAME レコードを追加する必要はあるのですが証明書を切り替える前にアクティベートできるというメリットがあります。

Certificate Managerの詳細はこちら

Certificate Manager の詳細はこちらを参照してください(この記事で紹介する使い方以外にも自己証明書の生成やサードパーティの証明書の設定なんかもできます)。

https://cloud.google.com/certificate-manager/docs/certificates?hl=ja

利用するTerraformリソースの説明

Managed SSL の設定は google_compute_managed_ssl_certificate を追加してロードバランサにアタッチするだけでよかったのですが、Certificate Manager の場合は4つのリソースを設定する必要があります(Certificate Manager は Managed SSL に比べて細かくいろいろ設定できる反面、設定しないといけないリソースが多いです)。図にすると以下のようになっています。

google_certificate_manager_certificateで証明書を作成してgoogle_certificate_manager_dns_authorizationを使ってDNS認証の設定を追加します[1]google_certificate_manager_certificate_mapgoogle_certificate_manager_certificate_map_entryを使ってロードバランサに証明書をアタッチします。

設定のポイントはCertificate MapとMap Entry

google_certificate_manager_certificate_mapgoogle_certificate_manager_certificate_map_entryの関係が少々分かりずらいので補足します。ロードバランサに複数の証明書をアタッチする場合、リソースの実態は以下のようになります(map_entryにそれぞれ証明書が紐づいてます)。

図のような構成になっているおかげで証明書を新たに追加したり不要になった証明書を削除する場合は、google_certificate_manager_certificate_map_entryの追加と削除だけで済みます。このようにgoogle_certificate_manager_certificate_mapがワンクッション挿んであることで、証明書の追加削除がロードバランサの設定に影響しないようになっています。

証明書の設定例

example.comabc.example.netの2つのドメイン証明書をロードバランサにアタッチする Terraform のサンプルコードを紹介します。初めに以下のローカル変数を用意します。

# ローカル変数
locals {
  domain_example  = "example.com"
  domain_abc = "abc.example.net"
}

次にexample.comabc.example.netの証明書[2]とDNS認証の設定を追加します。

# example.comのDNS認証
resource "google_certificate_manager_dns_authorization" "example" {
  name   = "dns-authz-example"
  domain = local.domain_example
}
# example.comの証明書
resource "google_certificate_manager_certificate" "example" {
  name = "cert-manager-example"
  # ドメインとDNS認証を関連付ける
  managed {
    domains = [
      google_certificate_manager_dns_authorization.example.domain
    ]
    dns_authorizations = [
      google_certificate_manager_dns_authorization.example.id,
    ]
  }
}
# abc.example.netのDNS認証
resource "google_certificate_manager_dns_authorization" "abc" {
  name   = "dns-authz-abc"
  domain = local.domain_abc
}
# abc.example.netの証明書
resource "google_certificate_manager_certificate" "abc" {
  name = "cert-manager-abc"
  # ドメインとDNS認証を関連付ける
  managed {
    domains = [
      google_certificate_manager_dns_authorization.abc.domain
    ]
    dns_authorizations = [
      google_certificate_manager_dns_authorization.abc.id,
    ]
  }
}

ここまでコードが書けたら一旦証明書とDNS認証の設定をterraform applyします。無事に Apply が成功すると Google Cloud コンソールの Certificate Manager(証明書マネージャ) で DNS に設定すべき CNAME の情報をみることができます。

Google Cloud コンソールで値を確認しながらお使いの DNS サービスにレコードを追加してください。設定する CNAME レコードの形式は以下です。

設定名
レコード名 _acme-challenge.(ドメイン名)
レコードタイプ CNAME
レコード値 (Google が指定する英数字).authorize.certificatemanager.goog.

DNS にレコードを追加するとだいたい10分程度で証明書がアクティブになります。
証明書がアクティブになったら、最後にロードバランサに証明書をアタッチするコードを追加します。

# Certificate Map
resource "google_certificate_manager_certificate_map" "map" {
  name = "cert-map"
}
# example.comのマップエントリー
resource "google_certificate_manager_certificate_map_entry" "example" {
  name = "map-entry-example"
  map  = google_certificate_manager_certificate_map.map.name

  hostname = google_certificate_manager_dns_authorization.example.domain

  certificates = [
    google_certificate_manager_certificate.example.id,
  ]
}
# abc.example.netのマップエントリー
resource "google_certificate_manager_certificate_map_entry" "abc" {
  name = "map-entry-abc"
  map  = google_certificate_manager_certificate_map.map.name

  hostname = google_certificate_manager_dns_authorization.abc.domain

  certificates = [
    google_certificate_manager_certificate.abc.id,
  ]
}
# ロードバランサの設定
resource "google_compute_target_https_proxy" "lb" {
  name = "https-proxy"
  # URLマップ
  url_map = google_compute_url_map.lb.id
  # SSLポリシー
  ssl_policy = google_compute_ssl_policy.lb.name
  # Certificate Mapとの関連付け
  certificate_map = "//certificatemanager.googleapis.com/${google_certificate_manager_certificate_map.map.id}"
}
resource "google_compute_url_map" "lb" {
  ...省略
}
# SSLポリシー
# この設定がないとTLS1.0になっちゃうので必ず設定すること
resource "google_compute_ssl_policy" "lb" {
  name            = "ssl-policy"
  profile         = "MODERN"
  min_tls_version = "TLS_1_2"
}

再度terraform applyするとアクティブな証明書がロードバランサにアタッチされます。

注意点1

ネット検索をするとマップエントリーの hostname の代わりに matcher[3] を設定する例を見かけますが matcher だとデプロイ後に「エラーコード 52」でロードバランサがエラーになります。

https://cloud.google.com/iap/docs/faq?hl=ja

あとマップエントリーの hostname と証明書のドメインが一致しないとterraform applyが失敗するので必ず一致するように設定してください。

注意点2

ロードバランサの certificate_map の指定方法が少し特殊です。リソースIDの前に//certificatemanager.googleapis.com/という文字列を連結する必要があります。

for_eachを使ったサンプルコード

今回紹介したコードはわかりやすさのため冗長に書きましたが実運用では for_each を使ったほうが良いです。for_eachを使ったコード例が気になる方はこちらを見てください。

for_eachを使ったコード例
locals {
  domains = [
    "example.com",
    "abc.example.net",
  ]
}
# DNS認証
resource "google_certificate_manager_dns_authorization" "authorizations" {
  for_each = toset(local.domains)
  name     = "dns-authz-${replace(each.key, ".", "-")}"

  domain   = each.key
}
# 証明書
resource "google_certificate_manager_certificate" "certificates" {
  for_each = toset(local.domains)
  name     = "cert-manager-${replace(each.key, ".", "-")}"

  managed {
    domains = [
      each.key
    ]
    dns_authorizations = [
      google_certificate_manager_dns_authorization.authorizations[each.key].id
    ]
  }
}
# Certificate Map
resource "google_certificate_manager_certificate_map" "map" {
  name = "cert-map"
}
# マップエントリー
resource "google_certificate_manager_certificate_map_entry" "map_entries" {
  for_each = toset(local.domains)
  name     = "map-entry-${replace(each.key, ".", "-")}"

  map      = google_certificate_manager_certificate_map.map.name
  hostname = each.key

  certificates = [
    google_certificate_manager_certificate.certificates[each.key].id
  ]
}

Managed SSLからCertificate Managerに移行する

こちらに記事を書きましたので参考にしてください。
https://zenn.dev/glassonion1/articles/0c32d98354bdb8

参考記事

脚注
  1. google_certificate_manager_dns_authorizationを設定しなかった場合はロードバランサ認証になります(Managed SSLと同じ認証方法)。 ↩︎

  2. コード例ではやりませんでしたが、ワイルドカード証明書の設定もできます。 ↩︎

  3. matcherについていろいろ調べたのですが有効な使い方はわからなかったです。ご存知の方がいれば追加情報をお願いします。 ↩︎

Discussion