[小ネタ]Terraformでホストゾーンが分かれるSANs証明書をACMで発行する

2022.06.25

こんにちは!AWS事業本部コンサルティング部のたかくに(@takakuni_)です。

いきなりですが質問です。

CloudFrontでは「takakuni.net」、「takakuni.jp」等の複数の代替ドメインを設定できます。

しかし、証明書は1ディストリビューションあたり1つまで設定できます。

「dev.takakuni.net」、「image.takakuni.net」の組み合わせのようなトップレベルドメインが一緒の場合は、「*.takakuni.net」のようなワイルドカード証明書が使えます。

ですが、「takakuni.net」、「takakuni.jp」等のトップレベルドメインが重複しない場合どのように実装しますか?

答えは、「サブジェクト代替名を用いた証明書を使用する」です。

懺悔します。私、たかくにはACMがサブジェクト代替名で設計されていることに気がつかず、2つ以上の証明書を作成してCloudFrontに組み込もうとしてしまいました。(もちろんエラーで詰まりました。)

今回は、(勝手に)懺悔ブログです。(会社で懺悔を強要することはないのでご安心を)

Route53ホストゾーンが異なるパターンをTerraformで書いたことなかったためブログにしようと思いました。

前提

今回は、「takakuni.net」、「takakuni.jp」のような別々のホストゾーンに対して、SANs証明書をACMで発行することを目的としています。

同一ホストゾーンでSANs証明書をACMで発行するパターンは対象外とします。

本題

本題に入りますが、「takakuni.net」、「takakuni.jp」のようなトップレベルドメインが重複しない場合、Route53ホストゾーンが別々になります。

したがって、以下の記載されているコードだとエラーを返します。(本来、「takakuni.jp」に対して設定するレコードおよびレコードの検証分が、ホストゾーン「takakuni.net」に対して行われているため)

resource "aws_acm_certificate" "example" {
  domain_name               = "takakuni.net"
  validation_method         = "DNS"
  subject_alternative_names = ["takakuni.jp"]
}

data "aws_route53_zone" "example" {
  name         = "takakuni.net"
  private_zone = false
}

resource "aws_route53_record" "example" {
  for_each = {
    for dvo in aws_acm_certificate.example.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.example.zone_id
}

resource "aws_acm_certificate_validation" "example" {
  certificate_arn         = aws_acm_certificate.example.arn
  validation_record_fqdns = [for record in aws_route53_record.example : record.fqdn]
}

別々のホストゾーンで発行する場合、公式ドキュメントのサンプルコードでは、以下の処理が行われているように見受けられます。

  • ホストゾーンごとにdata.aws_route53_zoneブロックを定義して参照する
  • 三項演算子を用いてホストゾーンの条件分岐を行う
resource "aws_acm_certificate" "example" {
  domain_name               = "example.com"
  subject_alternative_names = ["www.example.com", "example.org"]
  validation_method         = "DNS"
}

data "aws_route53_zone" "example_com" {
  name         = "example.com"
  private_zone = false
}

data "aws_route53_zone" "example_org" {
  name         = "example.org"
  private_zone = false
}

resource "aws_route53_record" "example" {
  for_each = {
    for dvo in aws_acm_certificate.example.domain_validation_options : dvo.domain_name => {
      name    = dvo.resource_record_name
      record  = dvo.resource_record_value
      type    = dvo.resource_record_type
      zone_id = dvo.domain_name == "example.org" ? data.aws_route53_zone.example_org.zone_id : data.aws_route53_zone.example_com.zone_id
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = each.value.zone_id
}

resource "aws_acm_certificate_validation" "example" {
  certificate_arn         = aws_acm_certificate.example.arn
  validation_record_fqdns = [for record in aws_route53_record.example : record.fqdn]
}

resource "aws_lb_listener" "example" {
  # ... other configuration ...

  certificate_arn = aws_acm_certificate_validation.example.certificate_arn
}

Resource: aws_acm_certificate_validationより引用

上記のパターンで実装するのも良いと思いますが、「Terraformの三項演算子は3パターン以上に対応できない」、「ACMでは1つの証明書あたり最大100ドメインまで設定可能」の懸念事項があるため対応できるようにカイゼンしてみます。

作ってみたコード

moduleで作成しましたが以下のコードが完成したコードになります。こだわりは、countを使わずfor_eachで作成してみました。

aws_route53_recordzone_idの受け渡しもイイ感じにかけたかなと思います。

/modules/san_acm/main.tf

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "4.1.0"
    }
  }
}

resource "aws_acm_certificate" "cert" {
  domain_name       = var.domain_name
  subject_alternative_names = var.subject_domain_names
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

data "aws_route53_zone" "cert" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {}
  }

  name         = each.key
  private_zone = false
}

resource "aws_route53_record" "cert" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  type            = each.value.type
  ttl             = 60
  zone_id         = data.aws_route53_zone.cert[each.key].zone_id
}

resource "aws_acm_certificate_validation" "cert" {
  certificate_arn         = aws_acm_certificate.cert.arn
  validation_record_fqdns = [for record in aws_route53_record.cert : record.fqdn]
}

/modules/san_acm/variables.tf

variable "domain_name" {
  type = string

  default = ""
}

variable "subject_domain_names" {
  type = list(string)

  default = []
}

/modules/san_acm/outputs.tf

output "cert_arn" {
  value = aws_acm_certificate.cert.arn
}

使ってみた

使い方としては、シンプルにmoduleを呼び出します。subject_domain_namesに追加したい「サブジェクト代替名」のドメインを定義するような使い方です。

module "san_acm_cf" {
  source = "/../modules/san_acm"

  domain_name    =  "takakuni.net"
  subject_domain_names = [
    "dev.takakuni.link"
  ]
}

無事、別々のホストゾーンに対応したSANs証明書を発行できました。

終わりに

以上、「Terraformでホストゾーンが分かれるSANs証明書をACMで発行する」でした。

小ネタすぎて、どのニーズに刺さるのか怪しいですが、この記事がどなたかの参考になれば幸いです。

以上、AWS事業本部コンサルティング部のたかくに(@takakuni_)でした!