SANsにワイルドカードが入ったACMのDNS認証なSSL証明書をTerraformで作るときのハマりどころ

この記事の内容はaws provider v2まで古いバージョンに関する内容です。

aws provider v3以降については下記URLの公式ドキュメントを参照してください。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate#referencing-domain_validation_options-with-for_each-based-resources

はい。

Terraform、便利ですよね。

AWSAmazon Certificate Managerも、CloudfrontやELBで使えるSSL証明書が無料で発行出来て便利ですよね。

ACMは2017年11月にDNS認証でのSSL証明書発行ができるようになりましたが、コモンネームのサブドメインワイルドカードでSANsに指定した証明書をTerraformからDNS認証で作ろうとすると、微妙にハマりどころがありますので、今回はそれについての記事です。

この記事はTerraform 0.12.9、terraform-provider-aws 2.30.0時点の情報です。

三行で

  • ACMDNS認証なSSL証明書において、ワイルドカードドメインの認証レコードはその親ドメインと同じ内容
  • そのため素直に記述するとterraformが同じレコードを2つ作ろうとしてしまいコンフリクトして失敗する
  • そのためdistinct関数で重複を除く必要があるが、それにも工夫が要る

エラーになる書き方

SANsにワイルドカードも含んだ状態の認証レコードを素直に書こうとするとこうなると思います。 *1

resource "aws_acm_certificate" "example" {
  validation_method = "DNS"
  domain_name       = "example.fusagiko.jp"

  subject_alternative_names = [
    "*.example.fusagiko.jp"
  ]
}

resource "aws_route53_record" "validation_records" {
  count   = length(aws_acm_certificate.example.domain_validation_options)
  zone_id = data.aws_route53_zone.zone.zone_id
  ttl     = 300

  name    = element(aws_acm_certificate.example.domain_validation_options, count.index).resource_record_name
  type    = element(aws_acm_certificate.example.domain_validation_options, count.index).resource_record_type
  records = [
    element(aws_acm_certificate.example.domain_validation_options, count.index).resource_record_value
  ]
}

しかし、これを適用しようとすると、下記のようなエラーが出てしまいます。

------------------------------------------------------------------------

Error: Invalid count argument

  on test.tf line 39, in resource "aws_route53_record" "validation_records":
  39:   count   = length(aws_acm_certificate.example.domain_validation_options)

The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the count depends on.

なぜならば、aws_acm_certificate.exampleが作成されるまでaws_acm_certificate.example.domain_validation_optionsが何要素の配列になるかわからないため、aws_route53_record.validation_recordsを何個作ればよいか決定できないからです。

これを回避するためにはエラーメッセージに書いてある通り-targetオプションでaws_acm_certificate.exampleを先に作成すればいいのですが、それでもエラーが出てしまいます。

aws_route53_record.validation_records[0]: Creating...
aws_route53_record.validation_records[1]: Creating...
aws_route53_record.validation_records[1]: Still creating... [10s elapsed]
aws_route53_record.validation_records[1]: Still creating... [20s elapsed]
aws_route53_record.validation_records[1]: Still creating... [30s elapsed]
aws_route53_record.validation_records[1]: Creation complete after 34s [id=HOGEHOGEHOGEHO__4a20d7faeee244c473bf78955c41b7a0.example.fusagiko.jp._CNAME]

Error: [ERR]: Error building changeset: InvalidChangeBatch: [Tried to create resource record set [name='_4a20d7faeee244c473bf78955c41b7a0.example.fusagiko.jp.', type='CNAME'] but it already exists]
        status code: 400, request id: 5f34c889-5549-4e34-b217-f252400ab52e

  on example.tf line 34, in resource "aws_route53_record" "validation_records":
  34: resource "aws_route53_record" "validation_records" {

これはaws_acm_certificate.example.domain_validation_optionsをoutputしてみればわかるのですが、ワイルドカードサブドメインはその親ドメインと認証レコードが同じであり、重複してしまうためです。

acm_validation_records = [
  {
    "domain_name" = "example.fusagiko.jp"
    "resource_record_name" = "_4a20d7faeee244c473bf78955c41b7a0.example.fusagiko.jp."
    "resource_record_type" = "CNAME"
    "resource_record_value" = "_4c37d9c32e02cb55007e1f91b58bfe9f.olprtlswtu.acm-validations.aws."
  },
  {
    "domain_name" = "*.example.fusagiko.jp"
    "resource_record_name" = "_4a20d7faeee244c473bf78955c41b7a0.example.fusagiko.jp."
    "resource_record_type" = "CNAME"
    "resource_record_value" = "_4c37d9c32e02cb55007e1f91b58bfe9f.olprtlswtu.acm-validations.aws."
  },
]

ところでTerraformにはdistinct関数が存在し、これを使うと配列中の重複する要素を除ける…のですが。
上記outputを見ていただくとわかるのですがdomain_nameが異なるために単にdistnict関数に渡すだけでは重複が解消されません。

エラーにならない書き方

というわけで、aws_acm_certificate.example.domain_validation_optionsの中身のmapから、for~inを使ってdomain_name以外を持ったmapを作ってdistnictし、これを使います。

全体像がこちら。

locals {
  acm_validation_records_distincted = distinct([
    for record in aws_acm_certificate.example.domain_validation_options.* :
    {
      resource_record_name = record.resource_record_name
      resource_record_type = record.resource_record_type
      resource_record_value = record.resource_record_value
    }
  ])
}

resource "aws_acm_certificate" "example" {
  validation_method = "DNS"
  domain_name       = "example.fusagiko.jp"

  subject_alternative_names = [
    "*.example.fusagiko.jp"
  ]
}

resource "aws_route53_record" "validation_records" {
  count   = length(local.acm_validation_records_distincted)
  zone_id = data.aws_route53_zone.zone.zone_id
  ttl     = 300

  name    = element(local.acm_validation_records_distincted, count.index).resource_record_name
  type    = element(local.acm_validation_records_distincted, count.index).resource_record_type
  records = [
    element(local.acm_validation_records_distincted, count.index).resource_record_value
  ]
}

こうすることでSANにワイルドカードが含まれていたとしてもエラーにならずにapplyすることができます。

とはいえ、aws_acm_certificate.exampleをtarget指定しての先行applyは変わらず必要です。

これはplanの時点で全てのリソースの作成数などを確定しておかなければならないterraformの仕様上、現時点では仕方ないと思われます。
Terraformの今後の開発に期待しましょう。

もしくはこれの回避方法をご存知の方がおられましたらご教授ください。

*1:aws_route53_zoneやaws_acm_certificate_validationは省略しています