Terraform で AWS Certificate Manager 無料証明書を発行する(AWS Provider 3.0.0 以降の場合)

ACM の無料証明書発行、Terraform でやると面倒くさいかと思ってましたが、めっちゃ楽やん
2020.09.02

先日、Terraform で AWS Certificate Manager(以下、ACM) の無料証明書をリクエストおよび検証まで実装する機会がありました。ちょっと詰まったところがありましたので同じように悩んでココへ辿り着く誰かのためにまとめておきます。

AWS Provider 3.0.0 から仕様変更されています

今回の記事は AWS Provider 3.0.0 以降を対象とします。今回の実行環境は以下のとおりです。

$ terraform version
Terraform v0.12.29
+ provider.aws v3.4.

AWS Provider 2.x 以前は以下のエントリーを参考にしてください。

Error: Invalid index

従来は aws_acm_certificate で作成した無料証明書の検証用レコードは list タイプで domain_validation_options を返していました。そのため、以下のようにカウントインデックスをつけて aws_route53_record で検証用レコードを登録しています。

resource aws_route53_record cert_validation {
  zone_id = data.aws_route53_zone.route53-zone.zone_id
  name    = aws_acm_certificate.cert.domain_validation_options[0].resource_record_name
  type    = aws_acm_certificate.cert.domain_validation_options[0].resource_record_type
  records = [aws_acm_certificate.cert.domain_validation_options[0].resource_record_value]
  ttl     = 60
}

しかし、AWS Provider 3.0.0 以降で上記のように作成すると、以下のようなエラーになります。

Error: Invalid index

  on route53.tf line 42, in resource "aws_route53_record" "cert_validation":
  42:     name = aws_acm_certificate.cert.domain_validation_options[0].resource_record_name

This value does not have any indices.

list から set へ変更

domain_validation_options は以下のような形で返されます。ぱっと見た感じ list タイプなのですが、3.0.0 以降では set タイプに変更されています。

resource "aws_acm_certificate" "cert" {
    arn                       = "arn:aws:acm:ap-northeast-1:123456789012:certificate/46c469ae-8871-4b7f-a322-6f78005464f7"
    domain_name               = "acm.marumo.classmethod.info"
    domain_validation_options = [
        {
            domain_name           = "*.acm.marumo.classmethod.info"
            resource_record_name  = "_061e7ba0179d76ac63bcd370c87fb987.acm.marumo.classmethod.info."
            resource_record_type  = "CNAME"
            resource_record_value = "_47bf1202c836b55e68832c5933fa5d3a.zdxcnfdgtt.acm-validations.aws."
        },
        {
            domain_name           = "acm.marumo.classmethod.info"
            resource_record_name  = "_061e7ba0179d76ac63bcd370c87fb987.acm.marumo.classmethod.info."
            resource_record_type  = "CNAME"
            resource_record_value = "_47bf1202c836b55e68832c5933fa5d3a.zdxcnfdgtt.acm-validations.aws."
        },
    ]
    id                        = "arn:aws:acm:ap-northeast-1:123456789012:certificate/46c469ae-8871-4b7f-a322-6f78005464f7"
    status                    = "ISSUED"
    subject_alternative_names = [
        "*.acm.marumo.classmethod.info",
    ]
    tags                      = {}
    validation_emails         = []
    validation_method         = "DNS"

    options {
        certificate_transparency_logging_preference = "ENABLED"
    }
}

setlist とは異なり順序付けされません。よって、以前のようなカウントインデックスは割り当てられず Error: Invalid index となるわけです。

余談ですが list から set に変更された経緯は、list の場合 terraform apply が完了するまで、そのカウントインデックスが定まらず、属性を直接参照するような場合にエラーになる可能性があった、とのことです。詳しくは以下の issues に記載されていますので、興味があれば参照ください。

3.0.0 以降ではこうやる!

カウントインデックスを持たない set タイプのため for/for_each を利用します。コードは下記のとおりです。

data "aws_route53_zone" "domain" {
  name         = "acm.marumo.classmethod.info"
  private_zone = false
}

resource "aws_acm_certificate" "cert" {
  domain_name               = "acm.marumo.classmethod.info"
  subject_alternative_names = ["*.acm.marumo.classmethod.info"]
  validation_method         = "DNS"
}

resource "aws_route53_record" "cert_validation" {
  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]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.domain.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_validation : record.fqdn]
}

解説

3.0.0 以降で変更となった aws_route53_record 部分のみ取り上げて解説したいと思います。その他リソースの解説は、以前のかずえの記事に記載されていますので、そちらを参照ください。

aws_route53_record

まず for_each の部分で何をやっているか説明します。

  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
    }
  }

ここでは冒頭 domain_validation_options の値で記載している 2 つの set タイプのオブジェクトを以下のように domain_namekey とした map タイプに変換しています。

$ terraform console
> { 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 }}
{
  "*.acm.marumo.classmethod.info" = {
    "name" = "_061e7ba0179d76ac63bcd370c87fb987.acm.marumo.classmethod.info."
    "record" = "_47bf1202c836b55e68832c5933fa5d3a.zdxcnfdgtt.acm-validations.aws."
    "type" = "CNAME"
  }
  "acm.marumo.classmethod.info" = {
    "name" = "_061e7ba0179d76ac63bcd370c87fb987.acm.marumo.classmethod.info."
    "record" = "_47bf1202c836b55e68832c5933fa5d3a.zdxcnfdgtt.acm-validations.aws."
    "type" = "CNAME"
  }
}

次に、上記のように map 変換された値を使い、以下で *.acm.marumo.classmethod.info および acm.marumo.classmethod.infoeach.key とする 各 each.value の値を参照する形となっています。

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

このように記述することで、AWS Provider 3.0.0 以降の環境で無事に ACM 証明書の発行を Terraform で実装することができました!

さいごに

従来のカウントインデックスの方法で記述していたところエラーになりハマってしまいました。「実績のある方法で書いてるのに、なんでや・・・」と思っていたのですが、まさか domain_validation_optionslist タイプから set タイプに変更されていたとは。。

公式ドキュメントではすでに for_each の記述方式に変わっていましたので、「まず、公式ドキュメント読め!」事案でした。

以上!大阪オフィスの丸毛(@marumo1981)でした!

参考