diff --git a/aws/resource_aws_acm_certificate.go b/aws/resource_aws_acm_certificate.go index c7131025b3b..897eb8c3419 100644 --- a/aws/resource_aws_acm_certificate.go +++ b/aws/resource_aws_acm_certificate.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acm" + "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/helper/validation" @@ -95,7 +96,7 @@ func resourceAwsAcmCertificate() *schema.Resource { Computed: true, }, "domain_validation_options": { - Type: schema.TypeList, + Type: schema.TypeSet, Computed: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -117,6 +118,7 @@ func resourceAwsAcmCertificate() *schema.Resource { }, }, }, + Set: acmDomainValidationOptionsHash, }, "validation_emails": { Type: schema.TypeList, @@ -158,6 +160,36 @@ func resourceAwsAcmCertificate() *schema.Resource { }, "tags": tagsSchema(), }, + CustomizeDiff: func(diff *schema.ResourceDiff, v interface{}) error { + // Attempt to calculate the domain validation options based on domains present in domain_name and subject_alternative_names + if diff.Get("validation_method").(string) == "DNS" && (diff.HasChange("domain_name") || diff.HasChange("subject_alternative_names")) { + domainValidationOptionsList := []interface{}{map[string]interface{}{ + "domain_name": strings.TrimSuffix(diff.Get("domain_name").(string), "."), + }} + + if sanSet, ok := diff.Get("subject_alternative_names").(*schema.Set); ok { + for _, sanRaw := range sanSet.List() { + san, ok := sanRaw.(string) + + if !ok { + continue + } + + m := map[string]interface{}{ + "domain_name": strings.TrimSuffix(san, "."), + } + + domainValidationOptionsList = append(domainValidationOptionsList, m) + } + } + + if err := diff.SetNew("domain_validation_options", schema.NewSet(acmDomainValidationOptionsHash, domainValidationOptionsList)); err != nil { + return fmt.Errorf("error setting new domain_validation_options diff: %w", err) + } + } + + return nil + }, } } @@ -430,6 +462,20 @@ func resourceAwsAcmCertificateImport(conn *acm.ACM, d *schema.ResourceData, upda return conn.ImportCertificate(params) } +func acmDomainValidationOptionsHash(v interface{}) int { + m, ok := v.(map[string]interface{}) + + if !ok { + return 0 + } + + if v, ok := m["domain_name"].(string); ok { + return hashcode.String(v) + } + + return 0 +} + func expandAcmCertificateOptions(l []interface{}) *acm.CertificateOptions { if len(l) == 0 || l[0] == nil { return nil diff --git a/aws/resource_aws_acm_certificate_test.go b/aws/resource_aws_acm_certificate_test.go index 56f749f9136..41e179cb421 100644 --- a/aws/resource_aws_acm_certificate_test.go +++ b/aws/resource_aws_acm_certificate_test.go @@ -164,10 +164,10 @@ func TestAccAWSAcmCertificate_dnsValidation(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", domain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", domain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": domain, + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), @@ -198,10 +198,10 @@ func TestAccAWSAcmCertificate_root(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", rootDomain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", rootDomain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": rootDomain, + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), @@ -265,10 +265,10 @@ func TestAccAWSAcmCertificate_root_TrailingPeriod(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile(`certificate/.+`)), resource.TestCheckResourceAttr(resourceName, "domain_name", strings.TrimSuffix(domain, ".")), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", strings.TrimSuffix(domain, ".")), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": strings.TrimSuffix(domain, "."), + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), @@ -300,14 +300,14 @@ func TestAccAWSAcmCertificate_rootAndWildcardSan(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", rootDomain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", rootDomain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.domain_name", wildcardDomain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": rootDomain, + "resource_record_type": "CNAME", + }), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": wildcardDomain, + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), tfawsresource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", wildcardDomain), @@ -341,14 +341,14 @@ func TestAccAWSAcmCertificate_san_single(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", domain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", domain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.domain_name", sanDomain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": domain, + "resource_record_type": "CNAME", + }), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": sanDomain, + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), tfawsresource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", sanDomain), @@ -383,18 +383,18 @@ func TestAccAWSAcmCertificate_san_multiple(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", domain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "3"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", domain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.domain_name", sanDomain1), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_value"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.2.domain_name", sanDomain2), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.2.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.2.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.2.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": domain, + "resource_record_type": "CNAME", + }), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": sanDomain1, + "resource_record_type": "CNAME", + }), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": sanDomain2, + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "2"), tfawsresource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", sanDomain1), @@ -429,14 +429,14 @@ func TestAccAWSAcmCertificate_san_TrailingPeriod(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile(`certificate/.+`)), resource.TestCheckResourceAttr(resourceName, "domain_name", domain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", domain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.domain_name", strings.TrimSuffix(sanDomain, ".")), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": domain, + "resource_record_type": "CNAME", + }), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": strings.TrimSuffix(sanDomain, "."), + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), tfawsresource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", strings.TrimSuffix(sanDomain, ".")), @@ -469,10 +469,10 @@ func TestAccAWSAcmCertificate_wildcard(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", wildcardDomain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", wildcardDomain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": wildcardDomain, + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), @@ -504,14 +504,14 @@ func TestAccAWSAcmCertificate_wildcardAndRootSan(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", wildcardDomain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", wildcardDomain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.domain_name", rootDomain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.1.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.1.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": rootDomain, + "resource_record_type": "CNAME", + }), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": wildcardDomain, + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), tfawsresource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", rootDomain), @@ -543,10 +543,10 @@ func TestAccAWSAcmCertificate_disableCTLogging(t *testing.T) { testAccMatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", rootDomain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.domain_name", rootDomain), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_name"), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.0.resource_record_type", "CNAME"), - resource.TestCheckResourceAttrSet(resourceName, "domain_validation_options.0.resource_record_value"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": rootDomain, + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), diff --git a/aws/resource_aws_acm_certificate_validation_test.go b/aws/resource_aws_acm_certificate_validation_test.go index 7b39b0f0c0f..99b8b48a457 100644 --- a/aws/resource_aws_acm_certificate_validation_test.go +++ b/aws/resource_aws_acm_certificate_validation_test.go @@ -6,13 +6,14 @@ import ( "strconv" "testing" - "github.com/aws/aws-sdk-go/service/acm" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" ) func TestAccAWSAcmCertificateValidation_basic(t *testing.T) { rootDomain := testAccAwsAcmCertificateDomainFromEnv(t) domain := testAccAwsAcmCertificateRandomSubDomain(rootDomain) + certificateResourceName := "aws_acm_certificate.test" + resourceName := "aws_acm_certificate_validation.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -23,7 +24,7 @@ func TestAccAWSAcmCertificateValidation_basic(t *testing.T) { { Config: testAccAcmCertificateValidation_basic(rootDomain, domain), Check: resource.ComposeTestCheckFunc( - testAccMatchResourceAttrRegionalARN("aws_acm_certificate_validation.cert", "certificate_arn", "acm", regexp.MustCompile("certificate/.+$")), + resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, }, @@ -50,6 +51,8 @@ func TestAccAWSAcmCertificateValidation_timeout(t *testing.T) { func TestAccAWSAcmCertificateValidation_validationRecordFqdns(t *testing.T) { rootDomain := testAccAwsAcmCertificateDomainFromEnv(t) domain := testAccAwsAcmCertificateRandomSubDomain(rootDomain) + certificateResourceName := "aws_acm_certificate.test" + resourceName := "aws_acm_certificate_validation.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -65,7 +68,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdns(t *testing.T) { { Config: testAccAcmCertificateValidation_validationRecordFqdnsOneRoute53Record(rootDomain, domain), Check: resource.ComposeTestCheckFunc( - testAccMatchResourceAttrRegionalARN("aws_acm_certificate_validation.cert", "certificate_arn", "acm", regexp.MustCompile("certificate/.+$")), + resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, }, @@ -91,6 +94,8 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsEmail(t *testing.T) func TestAccAWSAcmCertificateValidation_validationRecordFqdnsRoot(t *testing.T) { rootDomain := testAccAwsAcmCertificateDomainFromEnv(t) + certificateResourceName := "aws_acm_certificate.test" + resourceName := "aws_acm_certificate_validation.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -100,7 +105,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsRoot(t *testing.T) { Config: testAccAcmCertificateValidation_validationRecordFqdnsOneRoute53Record(rootDomain, rootDomain), Check: resource.ComposeTestCheckFunc( - testAccMatchResourceAttrRegionalARN("aws_acm_certificate_validation.cert", "certificate_arn", "acm", regexp.MustCompile("certificate/.+$")), + resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, }, @@ -110,6 +115,8 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsRoot(t *testing.T) func TestAccAWSAcmCertificateValidation_validationRecordFqdnsRootAndWildcard(t *testing.T) { rootDomain := testAccAwsAcmCertificateDomainFromEnv(t) wildcardDomain := fmt.Sprintf("*.%s", rootDomain) + certificateResourceName := "aws_acm_certificate.test" + resourceName := "aws_acm_certificate_validation.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -119,7 +126,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsRootAndWildcard(t * { Config: testAccAcmCertificateValidation_validationRecordFqdnsTwoRoute53Records(rootDomain, rootDomain, strconv.Quote(wildcardDomain)), Check: resource.ComposeTestCheckFunc( - testAccMatchResourceAttrRegionalARN("aws_acm_certificate_validation.cert", "certificate_arn", "acm", regexp.MustCompile("certificate/.+$")), + resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, }, @@ -130,6 +137,8 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsSan(t *testing.T) { rootDomain := testAccAwsAcmCertificateDomainFromEnv(t) domain := testAccAwsAcmCertificateRandomSubDomain(rootDomain) sanDomain := testAccAwsAcmCertificateRandomSubDomain(rootDomain) + certificateResourceName := "aws_acm_certificate.test" + resourceName := "aws_acm_certificate_validation.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -139,7 +148,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsSan(t *testing.T) { { Config: testAccAcmCertificateValidation_validationRecordFqdnsTwoRoute53Records(rootDomain, domain, strconv.Quote(sanDomain)), Check: resource.ComposeTestCheckFunc( - testAccMatchResourceAttrRegionalARN("aws_acm_certificate_validation.cert", "certificate_arn", "acm", regexp.MustCompile("certificate/.+$")), + resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, }, @@ -149,6 +158,8 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsSan(t *testing.T) { func TestAccAWSAcmCertificateValidation_validationRecordFqdnsWildcard(t *testing.T) { rootDomain := testAccAwsAcmCertificateDomainFromEnv(t) wildcardDomain := fmt.Sprintf("*.%s", rootDomain) + certificateResourceName := "aws_acm_certificate.test" + resourceName := "aws_acm_certificate_validation.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -158,7 +169,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsWildcard(t *testing { Config: testAccAcmCertificateValidation_validationRecordFqdnsOneRoute53Record(rootDomain, wildcardDomain), Check: resource.ComposeTestCheckFunc( - testAccMatchResourceAttrRegionalARN("aws_acm_certificate_validation.cert", "certificate_arn", "acm", regexp.MustCompile("certificate/.+$")), + resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, }, @@ -168,6 +179,8 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsWildcard(t *testing func TestAccAWSAcmCertificateValidation_validationRecordFqdnsWildcardAndRoot(t *testing.T) { rootDomain := testAccAwsAcmCertificateDomainFromEnv(t) wildcardDomain := fmt.Sprintf("*.%s", rootDomain) + certificateResourceName := "aws_acm_certificate.test" + resourceName := "aws_acm_certificate_validation.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -177,7 +190,7 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsWildcardAndRoot(t * { Config: testAccAcmCertificateValidation_validationRecordFqdnsTwoRoute53Records(rootDomain, wildcardDomain, strconv.Quote(rootDomain)), Check: resource.ComposeTestCheckFunc( - testAccMatchResourceAttrRegionalARN("aws_acm_certificate_validation.cert", "certificate_arn", "acm", regexp.MustCompile("certificate/.+$")), + resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, }, @@ -186,125 +199,210 @@ func TestAccAWSAcmCertificateValidation_validationRecordFqdnsWildcardAndRoot(t * func testAccAcmCertificateValidation_basic(rootZoneDomain, domainName string) string { return fmt.Sprintf(` -%s +resource "aws_acm_certificate" "test" { + domain_name = %[1]q + validation_method = "DNS" +} -data "aws_route53_zone" "zone" { - name = "%s." +data "aws_route53_zone" "test" { + name = %[2]q private_zone = false } -resource "aws_route53_record" "cert_validation" { - allow_overwrite = true # Enabled for test parallelization - name = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}" - type = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}" - zone_id = "${data.aws_route53_zone.zone.id}" - records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"] +# +# for_each acceptance testing requires SDKv2 +# +# resource "aws_route53_record" "test" { +# for_each = { +# for dvo in aws_acm_certificate.test.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.test.zone_id +# } + +resource "aws_route53_record" "test" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_value] ttl = 60 + type = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id } -resource "aws_acm_certificate_validation" "cert" { - depends_on = ["aws_route53_record.cert_validation"] +resource "aws_acm_certificate_validation" "test" { + depends_on = ["aws_route53_record.test"] - certificate_arn = "${aws_acm_certificate.cert.arn}" + certificate_arn = aws_acm_certificate.test.arn } -`, testAccAcmCertificateConfig(domainName, acm.ValidationMethodDns), rootZoneDomain) +`, domainName, rootZoneDomain) } func testAccAcmCertificateValidation_timeout(domainName string) string { return fmt.Sprintf(` -%s +resource "aws_acm_certificate" "test" { + domain_name = %[1]q + validation_method = "DNS" +} -resource "aws_acm_certificate_validation" "cert" { - certificate_arn = "${aws_acm_certificate.cert.arn}" +resource "aws_acm_certificate_validation" "test" { + certificate_arn = aws_acm_certificate.test.arn timeouts { create = "5s" } } -`, testAccAcmCertificateConfig(domainName, acm.ValidationMethodDns)) +`, domainName) } func testAccAcmCertificateValidation_validationRecordFqdnsEmailValidation(domainName string) string { return fmt.Sprintf(` -%s +resource "aws_acm_certificate" "test" { + domain_name = %[1]q + validation_method = "EMAIL" +} -resource "aws_acm_certificate_validation" "cert" { - certificate_arn = "${aws_acm_certificate.cert.arn}" +resource "aws_acm_certificate_validation" "test" { + certificate_arn = aws_acm_certificate.test.arn validation_record_fqdns = ["wrong-validation-fqdn.example.com"] } -`, testAccAcmCertificateConfig(domainName, acm.ValidationMethodEmail)) +`, domainName) } func testAccAcmCertificateValidation_validationRecordFqdnsOneRoute53Record(rootZoneDomain, domainName string) string { return fmt.Sprintf(` -%s +resource "aws_acm_certificate" "test" { + domain_name = %[1]q + validation_method = "DNS" +} -data "aws_route53_zone" "zone" { - name = "%s." +data "aws_route53_zone" "test" { + name = %[2]q private_zone = false } -resource "aws_route53_record" "cert_validation" { - allow_overwrite = true # Enabled for test parallelization - name = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}" - type = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}" - zone_id = "${data.aws_route53_zone.zone.id}" - records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"] +# +# for_each acceptance testing requires SDKv2 +# +# resource "aws_route53_record" "test" { +# for_each = { +# for dvo in aws_acm_certificate.test.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.test.zone_id +# } + +# resource "aws_acm_certificate_validation" "test" { +# certificate_arn = aws_acm_certificate.test.arn +# validation_record_fqdns = [for record in aws_route53_record.test: record.fqdn] +# } + +resource "aws_route53_record" "test" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_value] ttl = 60 + type = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id } -resource "aws_acm_certificate_validation" "cert" { - certificate_arn = "${aws_acm_certificate.cert.arn}" - validation_record_fqdns = ["${aws_route53_record.cert_validation.fqdn}"] +resource "aws_acm_certificate_validation" "test" { + certificate_arn = aws_acm_certificate.test.arn + validation_record_fqdns = [aws_route53_record.test.fqdn] } -`, testAccAcmCertificateConfig(domainName, acm.ValidationMethodDns), rootZoneDomain) +`, domainName, rootZoneDomain) } func testAccAcmCertificateValidation_validationRecordFqdnsTwoRoute53Records(rootZoneDomain, domainName, subjectAlternativeNames string) string { return fmt.Sprintf(` -%s +resource "aws_acm_certificate" "test" { + domain_name = %[1]q + subject_alternative_names = [%[2]s] + validation_method = "DNS" +} -data "aws_route53_zone" "zone" { - name = "%s." +data "aws_route53_zone" "test" { + name = %[3]q private_zone = false } -resource "aws_route53_record" "cert_validation" { - allow_overwrite = true # Enabled for test parallelization - name = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}" - type = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}" - zone_id = "${data.aws_route53_zone.zone.id}" - records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"] +# +# for_each acceptance testing requires SDKv2 +# +# resource "aws_route53_record" "test" { +# for_each = { +# for dvo in aws_acm_certificate.test.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.test.zone_id +# } + +# resource "aws_acm_certificate_validation" "test" { +# certificate_arn = aws_acm_certificate.test.arn +# validation_record_fqdns = [for record in aws_route53_record.test: record.fqdn] +# } + +resource "aws_route53_record" "test" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_name + records = [tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_value] ttl = 60 + type = tolist(aws_acm_certificate.test.domain_validation_options)[0].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id } -resource "aws_route53_record" "cert_validation_san" { - allow_overwrite = true # Enabled for test parallelization - name = "${aws_acm_certificate.cert.domain_validation_options.1.resource_record_name}" - type = "${aws_acm_certificate.cert.domain_validation_options.1.resource_record_type}" - zone_id = "${data.aws_route53_zone.zone.id}" - records = ["${aws_acm_certificate.cert.domain_validation_options.1.resource_record_value}"] +resource "aws_route53_record" "test2" { + allow_overwrite = true + name = tolist(aws_acm_certificate.test.domain_validation_options)[1].resource_record_name + records = [tolist(aws_acm_certificate.test.domain_validation_options)[1].resource_record_value] ttl = 60 + type = tolist(aws_acm_certificate.test.domain_validation_options)[1].resource_record_type + zone_id = data.aws_route53_zone.test.zone_id } -resource "aws_acm_certificate_validation" "cert" { - certificate_arn = "${aws_acm_certificate.cert.arn}" - - validation_record_fqdns = [ - "${aws_route53_record.cert_validation.fqdn}", - "${aws_route53_record.cert_validation_san.fqdn}", - ] +resource "aws_acm_certificate_validation" "test" { + certificate_arn = aws_acm_certificate.test.arn + validation_record_fqdns = [aws_route53_record.test.fqdn, aws_route53_record.test2.fqdn] } -`, testAccAcmCertificateConfig_subjectAlternativeNames(domainName, subjectAlternativeNames, acm.ValidationMethodDns), rootZoneDomain) +`, domainName, subjectAlternativeNames, rootZoneDomain) } func testAccAcmCertificateValidation_validationRecordFqdnsWrongFqdn(domainName string) string { return fmt.Sprintf(` -%s +resource "aws_acm_certificate" "test" { + domain_name = %[1]q + validation_method = "DNS" +} -resource "aws_acm_certificate_validation" "cert" { - certificate_arn = "${aws_acm_certificate.cert.arn}" +resource "aws_acm_certificate_validation" "test" { + certificate_arn = aws_acm_certificate.test.arn validation_record_fqdns = ["wrong-validation-fqdn.example.com"] } -`, testAccAcmCertificateConfig(domainName, acm.ValidationMethodDns)) +`, domainName) } diff --git a/website/docs/guides/version-3-upgrade.html.md b/website/docs/guides/version-3-upgrade.html.md index 01b50072de6..b0c68227800 100644 --- a/website/docs/guides/version-3-upgrade.html.md +++ b/website/docs/guides/version-3-upgrade.html.md @@ -161,6 +161,337 @@ output "lambda_result" { ## Resource: aws_acm_certificate +### domain_validation_options Changed from List to Set + +Previously, the `domain_validation_options` attribute was a list type and completely unknown until after an initial `terraform apply`. This generally required complicated configuration workarounds to properly create DNS validation records since referencing this attribute directly could produce errors similar to the below: + +``` +Error: Invalid for_each argument + + on main.tf line 16, in resource "aws_route53_record" "existing": + 16: for_each = aws_acm_certificate.existing.domain_validation_options + +The "for_each" 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 for_each depends on. +``` + +The `domain_validation_options` attribute is now a set type and the resource will attempt to populate the information necessary during the planning phase to handle the above situation in most environments without workarounds. This change also prevents Terraform from showing unexpected differences if the API returns the results in varying order. + +Configuration references to this attribute will likely require updates since sets cannot be indexed (e.g. `domain_validation_options[0]` or the older `domain_validation_options.0.` syntax will return errors). If the `domain_validation_options` list previously contained only a single element like the two examples just shown, it may be possible to wrap these references using the [`tolist()` function](/docs/configuration/functions/tolist.html) (e.g. `tolist(aws_acm_certificate.example.domain_validation_options)[0]`) as a quick configuration update, however given the complexity and workarounds required with the previous `domain_validation_options` attribute implementation, different environments will require different configuration updates and migration steps. Below is a more advanced example. Further questions on potential update steps can be submitted to the [community forums](https://discuss.hashicorp.com/c/terraform-providers/tf-aws/33). + +For example, given this previous configuration using a `count` based resource approach that may have been used in certain environments: + +```hcl +data "aws_route53_zone" "public_root_domain" { + name = var.public_root_domain +} + +resource "aws_acm_certificate" "existing" { + domain_name = "existing.${var.public_root_domain}" + subject_alternative_names = [ + "existing1.${var.public_root_domain}", + "existing2.${var.public_root_domain}", + "existing3.${var.public_root_domain}", + ] + validation_method = "DNS" +} + +resource "aws_route53_record" "existing" { + count = length(aws_acm_certificate.existing.subject_alternative_names) + 1 + + allow_overwrite = true + name = aws_acm_certificate.existing.domain_validation_options[count.index].resource_record_name + records = [aws_acm_certificate.existing.domain_validation_options[count.index].resource_record_value] + ttl = 60 + type = aws_acm_certificate.existing.domain_validation_options[count.index].resource_record_type + zone_id = data.aws_route53_zone.public_root_domain.zone_id +} + +resource "aws_acm_certificate_validation" "existing" { + certificate_arn = aws_acm_certificate.existing.arn + validation_record_fqdns = aws_route53_record.existing[*].fqdn +} + +``` + +It will receive errors like the below after upgrading: + +``` +Error: Invalid index + + on main.tf line 14, in resource "aws_route53_record" "existing": + 14: name = aws_acm_certificate.existing.domain_validation_options[count.index].resource_record_name + |---------------- + | aws_acm_certificate.existing.domain_validation_options is set of object with 4 elements + | count.index is 1 + +This value does not have any indices. +``` + +Since the `domain_validation_options` attribute changed from a list to a set and sets cannot be indexed in Terraform, the recommendation is to update the configuration to use the more stable [resource `for_each` support](/docs/configuration/resources.html#for_each-multiple-resource-instances-defined-by-a-map-or-set-of-strings) instead of [`count`](/docs/configuration/resources.html#count-multiple-resource-instances-by-count). Note the slight change in the `validation_record_fqdns` syntax as well. + +```hcl +resource "aws_route53_record" "existing" { + for_each = { + for dvo in aws_acm_certificate.existing.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.public_root_domain.zone_id +} + +resource "aws_acm_certificate_validation" "existing" { + certificate_arn = aws_acm_certificate.existing.arn + validation_record_fqdns = [for record in aws_route53_record.existing: record.fqdn] +} +``` + +After the configuration has been updated, a plan should no longer error and may look like the following: + +``` +------------------------------------------------------------------------ + +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create + - destroy +-/+ destroy and then create replacement + +Terraform will perform the following actions: + + # aws_acm_certificate_validation.existing must be replaced +-/+ resource "aws_acm_certificate_validation" "existing" { + certificate_arn = "arn:aws:acm:us-east-2:123456789012:certificate/ccbc58e8-061d-4443-9035-d3af0512e863" + ~ id = "2020-07-16 00:01:19 +0000 UTC" -> (known after apply) + ~ validation_record_fqdns = [ + - "_40b71647a8d88eb82d53fe988e8a3cc1.existing2.example.com", + - "_812ddf11b781af1eec1643ec58f102d2.existing.example.com", + - "_8dc56b6e35f699b8754afcdd79e9748d.existing3.example.com", + - "_d7112da809a40e848207c04399babcec.existing1.example.com", + ] -> (known after apply) # forces replacement + } + + # aws_route53_record.existing will be destroyed + - resource "aws_route53_record" "existing" { + - fqdn = "_812ddf11b781af1eec1643ec58f102d2.existing.example.com" -> null + - id = "Z123456789012__812ddf11b781af1eec1643ec58f102d2.existing.example.com._CNAME" -> null + - name = "_812ddf11b781af1eec1643ec58f102d2.existing.example.com" -> null + - records = [ + - "_bdeba72164eec216c55a32374bcceafd.jfrzftwwjs.acm-validations.aws.", + ] -> null + - ttl = 60 -> null + - type = "CNAME" -> null + - zone_id = "Z123456789012" -> null + } + + # aws_route53_record.existing[1] will be destroyed + - resource "aws_route53_record" "existing" { + - fqdn = "_40b71647a8d88eb82d53fe988e8a3cc1.existing2.example.com" -> null + - id = "Z123456789012__40b71647a8d88eb82d53fe988e8a3cc1.existing2.example.com._CNAME" -> null + - name = "_40b71647a8d88eb82d53fe988e8a3cc1.existing2.example.com" -> null + - records = [ + - "_638532db1fa6a1b71aaf063c8ea29d52.jfrzftwwjs.acm-validations.aws.", + ] -> null + - ttl = 60 -> null + - type = "CNAME" -> null + - zone_id = "Z123456789012" -> null + } + + # aws_route53_record.existing[2] will be destroyed + - resource "aws_route53_record" "existing" { + - fqdn = "_d7112da809a40e848207c04399babcec.existing1.example.com" -> null + - id = "Z123456789012__d7112da809a40e848207c04399babcec.existing1.example.com._CNAME" -> null + - name = "_d7112da809a40e848207c04399babcec.existing1.example.com" -> null + - records = [ + - "_6e1da5574ab46a6c782ed73438274181.jfrzftwwjs.acm-validations.aws.", + ] -> null + - ttl = 60 -> null + - type = "CNAME" -> null + - zone_id = "Z123456789012" -> null + } + + # aws_route53_record.existing[3] will be destroyed + - resource "aws_route53_record" "existing" { + - fqdn = "_8dc56b6e35f699b8754afcdd79e9748d.existing3.example.com" -> null + - id = "Z123456789012__8dc56b6e35f699b8754afcdd79e9748d.existing3.example.com._CNAME" -> null + - name = "_8dc56b6e35f699b8754afcdd79e9748d.existing3.example.com" -> null + - records = [ + - "_a419f8410d2e0720528a96c3506f3841.jfrzftwwjs.acm-validations.aws.", + ] -> null + - ttl = 60 -> null + - type = "CNAME" -> null + - zone_id = "Z123456789012" -> null + } + + # aws_route53_record.existing["existing.example.com"] will be created + + resource "aws_route53_record" "existing" { + + allow_overwrite = true + + fqdn = (known after apply) + + id = (known after apply) + + name = "_812ddf11b781af1eec1643ec58f102d2.existing.example.com" + + records = [ + + "_bdeba72164eec216c55a32374bcceafd.jfrzftwwjs.acm-validations.aws.", + ] + + ttl = 60 + + type = "CNAME" + + zone_id = "Z123456789012" + } + + # aws_route53_record.existing["existing1.example.com"] will be created + + resource "aws_route53_record" "existing" { + + allow_overwrite = true + + fqdn = (known after apply) + + id = (known after apply) + + name = "_d7112da809a40e848207c04399babcec.existing1.example.com" + + records = [ + + "_6e1da5574ab46a6c782ed73438274181.jfrzftwwjs.acm-validations.aws.", + ] + + ttl = 60 + + type = "CNAME" + + zone_id = "Z123456789012" + } + + # aws_route53_record.existing["existing2.example.com"] will be created + + resource "aws_route53_record" "existing" { + + allow_overwrite = true + + fqdn = (known after apply) + + id = (known after apply) + + name = "_40b71647a8d88eb82d53fe988e8a3cc1.existing2.example.com" + + records = [ + + "_638532db1fa6a1b71aaf063c8ea29d52.jfrzftwwjs.acm-validations.aws.", + ] + + ttl = 60 + + type = "CNAME" + + zone_id = "Z123456789012" + } + + # aws_route53_record.existing["existing3.example.com"] will be created + + resource "aws_route53_record" "existing" { + + allow_overwrite = true + + fqdn = (known after apply) + + id = (known after apply) + + name = "_8dc56b6e35f699b8754afcdd79e9748d.existing3.example.com" + + records = [ + + "_a419f8410d2e0720528a96c3506f3841.jfrzftwwjs.acm-validations.aws.", + ] + + ttl = 60 + + type = "CNAME" + + zone_id = "Z123456789012" + } + +Plan: 5 to add, 0 to change, 5 to destroy. +``` + +Due to the type of configuration change, Terraform does not know that the previous `aws_route53_record` resources (indexed by number in the existing state) and the new resources (indexed by domain names in the updated configuration) are equivalent. Typically in this situation, the [`terraform state mv` command](/docs/commands/state/mv.html) can be used to reduce the plan to show no changes. This is done by associating the count index (e.g. `[1]`) with the equivalent domain name index (e.g. `["existing2.example.com"]`), making one of the four commands to fix the above example: `terraform state mv 'aws_route53_record.existing[1]' 'aws_route53_record.existing["existing2.example.com"]'`. It is recommended to use this `terraform state mv` update process where possible to reduce chances of unexpected behaviors or changes in an environment. + +If using `terraform state mv` to reduce the plan to show no changes, no additional steps are required. + +In larger or more complex environments though, this process can be tedius to match the old resource address to the new resource address and run all the necessary `terraform state mv` commands. Instead, since the `aws_route53_record` resource implements the `allow_overwrite = true` argument, it is possible to just remove the old `aws_route53_record` resources from the Terraform state using the [`terraform state rm` command](/docs/commands/state/rm.html). In this case, Terraform will leave the existing records in Route 53 and plan to just overwrite the existing validation records with the same exact (previous) values. + +-> This guide is showing the simpler `terraform state rm` option below as a potential shortcut in this specific situation, however in most other cases `terraform state mv` is required to change from `count` based resources to `for_each` based resources and properly match the existing Terraform state to the updated Terraform configuration. + +```console +$ terraform state rm aws_route53_record.existing +Removed aws_route53_record.existing[0] +Removed aws_route53_record.existing[1] +Removed aws_route53_record.existing[2] +Removed aws_route53_record.existing[3] +Successfully removed 4 resource instance(s). +``` + +Now the Terraform plan will show only the additions of new Route 53 records (which are exactly the same as before the upgrade) and the proposed recreation of the `aws_acm_certificate_validation` resource. The `aws_acm_certificate_validation` resource recreation will have no effect as the certificate is already validated and issued. + +``` +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create +-/+ destroy and then create replacement + +Terraform will perform the following actions: + + # aws_acm_certificate_validation.existing must be replaced +-/+ resource "aws_acm_certificate_validation" "existing" { + certificate_arn = "arn:aws:acm:us-east-2:123456789012:certificate/ccbc58e8-061d-4443-9035-d3af0512e863" + ~ id = "2020-07-16 00:01:19 +0000 UTC" -> (known after apply) + ~ validation_record_fqdns = [ + - "_40b71647a8d88eb82d53fe988e8a3cc1.existing2.example.com", + - "_812ddf11b781af1eec1643ec58f102d2.existing.example.com", + - "_8dc56b6e35f699b8754afcdd79e9748d.existing3.example.com", + - "_d7112da809a40e848207c04399babcec.existing1.example.com", + ] -> (known after apply) # forces replacement + } + + # aws_route53_record.existing["existing.example.com"] will be created + + resource "aws_route53_record" "existing" { + + allow_overwrite = true + + fqdn = (known after apply) + + id = (known after apply) + + name = "_812ddf11b781af1eec1643ec58f102d2.existing.example.com" + + records = [ + + "_bdeba72164eec216c55a32374bcceafd.jfrzftwwjs.acm-validations.aws.", + ] + + ttl = 60 + + type = "CNAME" + + zone_id = "Z123456789012" + } + + # aws_route53_record.existing["existing1.example.com"] will be created + + resource "aws_route53_record" "existing" { + + allow_overwrite = true + + fqdn = (known after apply) + + id = (known after apply) + + name = "_d7112da809a40e848207c04399babcec.existing1.example.com" + + records = [ + + "_6e1da5574ab46a6c782ed73438274181.jfrzftwwjs.acm-validations.aws.", + ] + + ttl = 60 + + type = "CNAME" + + zone_id = "Z123456789012" + } + + # aws_route53_record.existing["existing2.example.com"] will be created + + resource "aws_route53_record" "existing" { + + allow_overwrite = true + + fqdn = (known after apply) + + id = (known after apply) + + name = "_40b71647a8d88eb82d53fe988e8a3cc1.existing2.example.com" + + records = [ + + "_638532db1fa6a1b71aaf063c8ea29d52.jfrzftwwjs.acm-validations.aws.", + ] + + ttl = 60 + + type = "CNAME" + + zone_id = "Z123456789012" + } + + # aws_route53_record.existing["existing3.example.com"] will be created + + resource "aws_route53_record" "existing" { + + allow_overwrite = true + + fqdn = (known after apply) + + id = (known after apply) + + name = "_8dc56b6e35f699b8754afcdd79e9748d.existing3.example.com" + + records = [ + + "_a419f8410d2e0720528a96c3506f3841.jfrzftwwjs.acm-validations.aws.", + ] + + ttl = 60 + + type = "CNAME" + + zone_id = "Z123456789012" + } + +Plan: 5 to add, 0 to change, 1 to destroy. +``` + +Once applied, no differences should be shown and no additional steps should be necessary. + ### subject_alternative_names Changed from List to Set Previously the `subject_alternative_names` argument was stored in the Terraform state as an ordered list while the API returned information in an unordered manner. The attribute is now configured as a set instead of a list. Certain Terraform configuration language features distinguish between these two attribute types such as not being able to index a set (e.g. `aws_acm_certificate.example.subject_alternative_names[0]` is no longer a valid reference). Depending on the implementation details of a particular configuration using `subject_alternative_names` as a reference, possible solutions include changing references to using `for`/`for_each` or using the `tolist()` function as a temporary workaround to keep the previous behavior until an appropriate configuration (properly using the unordered set) can be determined. Usage questions can be submitted to the [community forums](https://discuss.hashicorp.com/c/terraform-providers/tf-aws/33). diff --git a/website/docs/r/acm_certificate.html.markdown b/website/docs/r/acm_certificate.html.markdown index c75de966b67..40957104602 100644 --- a/website/docs/r/acm_certificate.html.markdown +++ b/website/docs/r/acm_certificate.html.markdown @@ -76,6 +76,29 @@ resource "aws_acm_certificate" "cert" { } ``` +### Referencing domain_validation_options With for_each Based Resources + +See the [`aws_acm_certificate_validation` resource](acm_certificate_validation.html) for a full example of performing DNS validation. + +```hcl +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 = aws_route53_zone.example.zone_id +} +``` + ## Argument Reference The following arguments are supported: @@ -108,7 +131,7 @@ In addition to all arguments above, the following attributes are exported: * `id` - The ARN of the certificate * `arn` - The ARN of the certificate * `domain_name` - The domain name for which the certificate is issued -* `domain_validation_options` - A list of attributes to feed into other resources to complete certificate validation. Can have more than one element, e.g. if SANs are defined. Only set if `DNS`-validation was used. +* `domain_validation_options` - Set of domain validation objects which can be used to complete certificate validation. Can have more than one element, e.g. if SANs are defined. Only set if `DNS`-validation was used. * `status` - Status of the certificate. * `validation_emails` - A list of addresses that received a validation E-Mail. Only set if `EMAIL`-validation was used. diff --git a/website/docs/r/acm_certificate_validation.html.markdown b/website/docs/r/acm_certificate_validation.html.markdown index 8cefaa487c5..1cd5fdb019b 100644 --- a/website/docs/r/acm_certificate_validation.html.markdown +++ b/website/docs/r/acm_certificate_validation.html.markdown @@ -23,91 +23,91 @@ deploy the required validation records and wait for validation to complete. ### DNS Validation with Route 53 ```hcl -resource "aws_acm_certificate" "cert" { +resource "aws_acm_certificate" "example" { domain_name = "example.com" validation_method = "DNS" } -data "aws_route53_zone" "zone" { - name = "example.com." +data "aws_route53_zone" "example" { + name = "example.com" private_zone = false } -resource "aws_route53_record" "cert_validation" { - name = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}" - type = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}" - zone_id = "${data.aws_route53_zone.zone.zone_id}" - records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"] - ttl = 60 +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" "cert" { - certificate_arn = "${aws_acm_certificate.cert.arn}" - validation_record_fqdns = ["${aws_route53_record.cert_validation.fqdn}"] +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" "front_end" { - # [...] - certificate_arn = "${aws_acm_certificate_validation.cert.certificate_arn}" +resource "aws_lb_listener" "example" { + # ... other configuration ... + + certificate_arn = aws_acm_certificate_validation.example.certificate_arn } ``` ### Alternative Domains DNS Validation with Route 53 ```hcl -resource "aws_acm_certificate" "cert" { +resource "aws_acm_certificate" "example" { domain_name = "example.com" subject_alternative_names = ["www.example.com", "example.org"] validation_method = "DNS" } -data "aws_route53_zone" "zone" { - name = "example.com." +data "aws_route53_zone" "example_com" { + name = "example.com" private_zone = false } -data "aws_route53_zone" "zone_alt" { - name = "example.org." +data "aws_route53_zone" "example_org" { + name = "example.org" private_zone = false } -resource "aws_route53_record" "cert_validation" { - name = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}" - type = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}" - zone_id = "${data.aws_route53_zone.zone.zone_id}" - records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"] - ttl = 60 -} - -resource "aws_route53_record" "cert_validation_alt1" { - name = "${aws_acm_certificate.cert.domain_validation_options.1.resource_record_name}" - type = "${aws_acm_certificate.cert.domain_validation_options.1.resource_record_type}" - zone_id = "${data.aws_route53_zone.zone.zone_id}" - records = ["${aws_acm_certificate.cert.domain_validation_options.1.resource_record_value}"] - ttl = 60 +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_route53_record" "cert_validation_alt2" { - name = "${aws_acm_certificate.cert.domain_validation_options.2.resource_record_name}" - type = "${aws_acm_certificate.cert.domain_validation_options.2.resource_record_type}" - zone_id = "${data.aws_route53_zone.zone_alt.zone_id}" - records = ["${aws_acm_certificate.cert.domain_validation_options.2.resource_record_value}"] - ttl = 60 +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_acm_certificate_validation" "cert" { - certificate_arn = "${aws_acm_certificate.cert.arn}" - - validation_record_fqdns = [ - "${aws_route53_record.cert_validation.fqdn}", - "${aws_route53_record.cert_validation_alt1.fqdn}", - "${aws_route53_record.cert_validation_alt2.fqdn}", - ] -} +resource "aws_lb_listener" "example" { + # ... other configuration ... -resource "aws_lb_listener" "front_end" { - # [...] - certificate_arn = "${aws_acm_certificate_validation.cert.certificate_arn}" + certificate_arn = aws_acm_certificate_validation.example.certificate_arn } ``` @@ -116,13 +116,13 @@ resource "aws_lb_listener" "front_end" { In this situation, the resource is simply a waiter for manual email approval of ACM certificates. ```hcl -resource "aws_acm_certificate" "cert" { +resource "aws_acm_certificate" "example" { domain_name = "example.com" validation_method = "EMAIL" } -resource "aws_acm_certificate_validation" "cert" { - certificate_arn = "${aws_acm_certificate.cert.arn}" +resource "aws_acm_certificate_validation" "example" { + certificate_arn = aws_acm_certificate.example.arn } ```