Terraformを使ってゼロダウンタイムで証明書切替ができるCertificate Managerをデプロイする
はじめに
普段 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 の詳細はこちらを参照してください(この記事で紹介する使い方以外にも自己証明書の生成やサードパーティの証明書の設定なんかもできます)。
利用する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_map
とgoogle_certificate_manager_certificate_map_entry
を使ってロードバランサに証明書をアタッチします。
設定のポイントはCertificate MapとMap Entry
google_certificate_manager_certificate_map
とgoogle_certificate_manager_certificate_map_entry
の関係が少々分かりずらいので補足します。ロードバランサに複数の証明書をアタッチする場合、リソースの実態は以下のようになります(map_entryにそれぞれ証明書が紐づいてます)。
図のような構成になっているおかげで証明書を新たに追加したり不要になった証明書を削除する場合は、google_certificate_manager_certificate_map_entry
の追加と削除だけで済みます。このようにgoogle_certificate_manager_certificate_map
がワンクッション挿んであることで、証明書の追加削除がロードバランサの設定に影響しないようになっています。
証明書の設定例
example.com
とabc.example.net
の2つのドメイン証明書をロードバランサにアタッチする Terraform のサンプルコードを紹介します。初めに以下のローカル変数を用意します。
# ローカル変数
locals {
domain_example = "example.com"
domain_abc = "abc.example.net"
}
次にexample.com
とabc.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」でロードバランサがエラーになります。
あとマップエントリーの 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に移行する
こちらに記事を書きましたので参考にしてください。
参考記事
- 証明書を管理
- DNS 認証を使用して、グローバル Google マネージド証明書をデプロイする
- Certificate Manager の仕組み
- google_certificate_manager_certificate
- GoogleマネージドSSL/TLS証明書でWebサーバーを構築してみた
- Certificate Managerを使ってみる
- メールアドレスやURLの例に使えるサンプル用ドメイン
Discussion