cloudflare / terraform-provider-cloudflare

Cloudflare Terraform Provider
https://registry.terraform.io/providers/cloudflare/cloudflare
Mozilla Public License 2.0
773 stars 597 forks source link

cloudflare_record attempted to override existing record however didn't find an exact match #2407

Closed ajax-mykhailo-oleksiuk closed 1 year ago

ajax-mykhailo-oleksiuk commented 1 year ago

Confirmation

Terraform and Cloudflare provider version

Terraform v1.3.7
on darwin_arm64
+ provider registry.terraform.io/cloudflare/cloudflare v4.5.0
+ provider registry.terraform.io/hashicorp/aws v4.59.0

Affected resource(s)

cloudflare_record

Terraform configuration files

terraform {
  required_version = "1.3.7"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.59.0"
    }

    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "4.5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

provider "cloudflare" {
  api_token = ""
}

locals {
  domain = "*.ajax.systems"
}

data "cloudflare_zone" "this" {
  name = trimprefix(local.domain, "*.")
}

module "this" {
  source  = "terraform-aws-modules/acm/aws"
  version = "4.3.2"

  zone_id     = data.cloudflare_zone.this.id
  domain_name = local.domain

  create_route53_records  = false
  validation_record_fqdns = cloudflare_record.this[*].hostname

  validation_method    = "DNS"
  wait_for_validation  = true
  validate_certificate = true
}

# -------------------------------------
# Certificate validation via Cloudflare
# -------------------------------------
resource "cloudflare_record" "this" {
  count = length(module.this.distinct_domain_names)

  zone_id = data.cloudflare_zone.this.id
  name    = element(module.this.validation_domains, count.index)["resource_record_name"]
  type    = element(module.this.validation_domains, count.index)["resource_record_type"]
  value   = trimsuffix(element(module.this.validation_domains, count.index)["resource_record_value"], ".")
  ttl     = 60
  proxied = false

  allow_overwrite = true
}

Link to debug output

https://gist.github.com/ajax-mykhailo-oleksiuk/36b1b8b106a365b03e0dabd8146f6f45

Panic output

No response

Expected output

records is overridden

Actual output

╷
│ Error: attempted to override existing record however didn't find an exact match
│ 
│   with cloudflare_record.this[0],
│   on certificate.tf line 19, in resource "cloudflare_record" "this":
│   19: resource "cloudflare_record" "this" {
│ 
╵

Steps to reproduce

  1. Request a new certificate from Amazon with auto-renew and validation from Cloudflare. (For example region eu-west-1) This step completes successfully because Cloudflare doesn't have cname record for a new certificate.
  2. Request a new certificate for the same domain from Amazon but in another region (for example us-east-1). This step fails with the error above. (Difference between codebase for different region in aws provider only)

Additional factoids

  1. I tried to import the existing record created by codebase in eu-west-1 to codebase in us-east-1 and hoped to have a workaround for this but getting "recreate" stage in codebase for us-east-1:
-/+ resource "cloudflare_record" "this" {
      + allow_overwrite = true
      ~ created_on      = "2023-05-02T07:57:20.150533Z" -> (known after apply)
      ~ hostname        = "_c5ff38ec8554d7a1bfb88decc1f2440c.ajax.systems" -> (known after apply)
      ~ id              = "7d0451da4cf5eca917e6aeddd9193467" -> (known after apply)
      ~ metadata        = {
          - "auto_added"             = "false"
          - "managed_by_apps"        = "false"
          - "managed_by_argo_tunnel" = "false"
          - "source"                 = "primary"
        } -> (known after apply)
      ~ modified_on     = "2023-05-02T07:57:20.150533Z" -> (known after apply)
      ~ name            = "_c5ff38ec8554d7a1bfb88decc1f2440c" -> "_c5ff38ec8554d7a1bfb88decc1f2440c.ajax.systems." # forces replacement
      ~ proxiable       = false -> (known after apply)
      - tags            = [] -> null
        # (5 unchanged attributes hidden)
    }
  1. In terraform debug logs I see that it finds existing record but then says attempted to override existing record however didn't find an exact match. But when I set (for test purposes) allow_overwrite = false I'm getting error showing that match was successful:

Screenshot 2023-05-03 at 09 39 44

References

No response

github-actions[bot] commented 1 year ago

Community Note

Voting for Prioritization

Volunteering to Work on This Issue

jacobbednarz commented 1 year ago

hi 👋 per the issue template, we don't accept reproduction cases that are using modules or dynamic expressions. for someone to triage this further, we need a reproduction case that only has the provider code; not the potential of logic bugs with client side expressions.

additionally, the error message you got indicates that you are attempting to overwrite but not finding a single record to match on and overwrite so it is bailing out. I suspect this is either an issue with your variables or the way your module is built.

ajax-mykhailo-oleksiuk commented 1 year ago

Hi @jacobbednarz

I've been able to reproduce it without public module. Here is a code:

locals {
  domain = "*.ajax.systems"

  # output for this local variable validation_domains is
  #validation_domains = tolist([
  #  {
  #    "domain_name" = "ajax.systems"
  #    "resource_record_name" = "_c5ff38ec8554d7a1bfb88decc1f2440c.ajax.systems."
  #    "resource_record_type" = "CNAME"
  #    "resource_record_value" = "_590e5921488d12cc15327231bfe08836.fpgkgnzppq.acm-validations.aws."
  #  },
  #])
  validation_domains = distinct(
    [for k, v in aws_acm_certificate.this.domain_validation_options : merge(
      tomap(v), { domain_name = replace(v.domain_name, "*.", "") }
    )]
  )
}

resource "aws_acm_certificate" "this" {
  domain_name               = local.domain
  validation_method         = "DNS"

  options {
    certificate_transparency_logging_preference = "ENABLED"
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_acm_certificate_validation" "this" {
  certificate_arn = aws_acm_certificate.this.arn
  validation_record_fqdns = [cloudflare_record.this.hostname]
}

# -------------------------------------
# Certificate validation via Cloudflare
# -------------------------------------
resource "cloudflare_record" "this" {
  zone_id = data.cloudflare_zone.this.id
  name    = local.validation_domains.0.resource_record_name
  type    = local.validation_domains.0.resource_record_type
  value   = trimsuffix(local.validation_domains.0.resource_record_value, ".")
  ttl     = 60
  proxied = false

  allow_overwrite = true
}

But getting the same error: attempted to override existing record however didn't find an exact match

Also, I'm little bit confused because I see in debug messages that cloudflare provider under the hood finds existing record correctly

2023-05-03T21:53:51.138+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: {"result":null,"success":false,"errors":[{"code":81053,"message":"An A, AAAA, or CNAME record with that host already exists. For more details, refer to \u003chttps://developers.cloudflare.com/dns/manage-dns-records/troubleshooting/records-with-same-name/\u003e."}],"messages":[]}
2023-05-03T21:53:51.138+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: Cloudflare Record already exists however we are overwriting it: tf_req_id=2067d3bc-6119-ee1b-0a92-b5f39e539176 @caller=github.com/cloudflare/terraform-provider-cloudflare/internal/sdkv2provider/resource_cloudflare_record.go:142 tf_provider_addr=registry.terraform.io/cloudflare/cloudflare tf_rpc=ApplyResourceChange @module=cloudflare tf_mux_provider=tf5to6server.v5tov6Server tf_resource_type=cloudflare_record timestamp=2023-05-03T21:53:51.138+0300
2023-05-03T21:53:51.139+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: 2023/05/03 21:53:51 
2023-05-03T21:53:51.139+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: GET /client/v4/zones/176b2792aab9d7de9f322bd71409307b HTTP/1.1
2023-05-03T21:53:51.139+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: Host: api.cloudflare.com
2023-05-03T21:53:51.139+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: User-Agent: terraform/1.3.7 terraform-plugin-sdk/2.10.1 terraform-provider-cloudflare/4.5.0
2023-05-03T21:53:51.139+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: Authorization: Bearer [redacted]
2023-05-03T21:53:51.141+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: Content-Type: application/json
2023-05-03T21:53:51.141+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: Accept-Encoding: gzip
2023-05-03T21:53:51.141+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: 
cloudflare_record.this: Still creating... [30s elapsed]
2023-05-03T21:53:51.586+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: 2023/05/03 21:53:51 [WARN] WaitForState timeout after 30s
2023-05-03T21:53:51.586+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: 2023/05/03 21:53:51 [WARN] WaitForState starting 30s refresh grace period
2023-05-03T21:53:51.587+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: 2023/05/03 21:53:51 [TRACE] Waiting 10s before next try
2023-05-03T21:53:51.587+0300 [DEBUG] provider.terraform-provider-cloudflare_v4.5.0: 2023/05/03 21:53:51 [ERROR] Context cancelation detected, abandoning grace period

-- and here says about mismatch

2023-05-03T21:53:51.588+0300 [ERROR] provider.terraform-provider-cloudflare_v4.5.0: Response contains error diagnostic: diagnostic_detail= diagnostic_severity=ERROR tf_req_id=2067d3bc-6119-ee1b-0a92-b5f39e539176 tf_rpc=ApplyResourceChange @caller=github.com/hashicorp/terraform-plugin-go@v0.15.0/tfprotov6/internal/diag/diagnostics.go:55 diagnostic_summary="attempted to override existing record however didn't find an exact match" tf_proto_version=6.3 tf_provider_addr=registry.terraform.io/cloudflare/cloudflare tf_resource_type=cloudflare_record @module=sdk.proto timestamp=2023-05-03T21:53:51.586+0300
2023-05-03T21:53:51.588+0300 [ERROR] vertex "cloudflare_record.this" error: attempted to override existing record however didn't find an exact match

and then says it's didn't find an exact match

jacobbednarz commented 1 year ago

that still relies on loops and accessing indexes directly. you should be able to build a reproduction case with just two cloudflare_record resources with static values. the first without a allow_overwrite, the second with it and a depends_on to ensure correct ordering.

ajax-mykhailo-oleksiuk commented 1 year ago

@jacobbednarz sure

  1. I've cleaned up all code related to AWS.

  2. Requested certificate in AWS ACM and here is its properties: Screenshot 2023-05-04 at 08 49 12

  3. And the complete example of code that still have the issue:

    
    terraform {
    required_version = "1.3.7"
    
    required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "4.5.0"
    }
    }
    }

provider "cloudflare" {}

data "cloudflare_zone" "this" { name = trimprefix(local.domain, "*.") }

locals { domain = "*.ajax.systems"

record_name = "_c5ff38ec8554d7a1bfb88decc1f2440c.ajax.systems." record_type = "CNAME" record_value = "_590e5921488d12cc15327231bfe08836.fpgkgnzppq.acm-validations.aws." }

resource "cloudflare_record" "first" { zone_id = data.cloudflare_zone.this.id name = local.record_name type = local.record_type value = local.record_value ttl = 60 proxied = false

allow_overwrite = false }

resource "cloudflare_record" "second" { zone_id = data.cloudflare_zone.this.id name = local.record_name type = local.record_type value = local.record_value ttl = 60 proxied = false

allow_overwrite = true

depends_on = [cloudflare_record.first] }


Result:

╷ │ Error: attempted to override existing record however didn't find an exact match │ │ with cloudflare_record.second, │ on certificate.tf line 37, in resource "cloudflare_record" "second": │ 37: resource "cloudflare_record" "second" { │ ╵



5. And this is debug logs for this code - https://gist.github.com/ajax-mykhailo-oleksiuk/e6c6d6d83d75721ce3e4c2871c468be7

As a result, the first record is created successfully and the second one fails.
sardonicsloth commented 1 year ago

I can verify this behavior as well. However if you import the record into tfstate it will work just fine. At least for me.

jacobbednarz commented 1 year ago

the value you are using won't work as if the value isn't @ or the root domain, we combine the name with the zone name to build the value - https://github.com/cloudflare/terraform-provider-cloudflare/blob/master/internal/sdkv2provider/resource_cloudflare_record.go#L144-L154.

to make this work, the name value should only be _c5ff38ec8554d7a1bfb88decc1f2440c.

ruslan-y commented 1 year ago

I'm facing a same issue. I've added two records in CF, name: "example", ip addresses "1.2.3.4" and "4.3.2.1" (for test purposes)

terraform {
  required_version = "1.5.2"
  required_providers {
    cloudflare = {
      source = "cloudflare/cloudflare"
      version = "~> 4.10"
    }
  }
}

resource "cloudflare_record" "example1" {
  zone_id = data.cloudflare_zone.default.id
  name    = "example"
  type    = "A"
  value   = "1.2.3.4"
  ttl     = 1
  proxied = true
  allow_overwrite = true
}

resource "cloudflare_record" "example2" {
  zone_id = data.cloudflare_zone.default.id
  name    = "example"
  type    = "A"
  value   = "4.3.2.1"
  ttl     = 1
  proxied = true
  allow_overwrite = true
}

data "cloudflare_zone" "default" {
  name = var.zone
}

After terraform apply i get error:

cloudflare_record.example2: Creating...
cloudflare_record.example1: Creating...
cloudflare_record.example2: Still creating... [10s elapsed]
cloudflare_record.example1: Still creating... [10s elapsed]
cloudflare_record.example1: Still creating... [20s elapsed]
cloudflare_record.example2: Still creating... [20s elapsed]
cloudflare_record.example1: Still creating... [30s elapsed]
cloudflare_record.example2: Still creating... [30s elapsed]
╷
│ Error: attempted to override existing record however didn't find an exact match
│
│   with cloudflare_record.example1,
│   on test.tf line 11, in resource "cloudflare_record" "example1":
│   11: resource "cloudflare_record" "example1" {
│
╵
╷
│ Error: attempted to override existing record however didn't find an exact match
│
│   with cloudflare_record.example2,
│   on test.tf line 21, in resource "cloudflare_record" "example2":
│   21: resource "cloudflare_record" "example2" {
│
╵

And if I change allow_overwrite = false I'm getting error:

cloudflare_record.example1: Creating...
cloudflare_record.example2: Creating...
cloudflare_record.example1: Still creating... [10s elapsed]
cloudflare_record.example2: Still creating... [10s elapsed]
cloudflare_record.example2: Still creating... [20s elapsed]
cloudflare_record.example1: Still creating... [20s elapsed]
cloudflare_record.example2: Still creating... [30s elapsed]
cloudflare_record.example1: Still creating... [30s elapsed]
╷
│ Error: expected DNS record to not already be present but already exists
│
│   with cloudflare_record.example1,
│   on test.tf line 11, in resource "cloudflare_record" "example1":
│   11: resource "cloudflare_record" "example1" {
│
╵
╷
│ Error: expected DNS record to not already be present but already exists
│
│   with cloudflare_record.example2,
│   on test.tf line 21, in resource "cloudflare_record" "example2":
│   21: resource "cloudflare_record" "example2" {
│
╵

Help me please @jacobbednarz @ajax-mykhailo-oleksiuk What is the problem in my case?

Dmitry1987 commented 1 year ago

same, allow_overwrite doesn't work...

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

  zone_id = var.cloudflare_zone_id
  name    = each.value.name
  value   = each.value.record
  type    = each.value.type
  ttl     = 60
  allow_overwrite = true
  proxied = false
}

loops are not the reason, because it works correctly for the first run, creates the record, and then on subsequent run it fails - idempotency of the provider doesn't work, it should either fail both times or pass both times 🤷‍♂️

scott-doyland-burrows commented 9 months ago

I have the same issue as the OP.

In my case I create a certificate in AWS that allows:

example.com *.example.com

AWS creates validation records that need to be inserted into Cloudflare. However both validation records are exactly the same and I cannot define two exact same records, so I need to use allow_overwrite = true.

However, in terraform I then receive Error: attempted to override existing record however didn't find an exact match for the second validation record.

The reason is that Cloudflare strips off the . from the ends of the name and value of the validation records, so terrafom (Cloudflare??) cannot find the record to overwrite.

So to workaround this I trim off the . before writing the record, this then results in a different error when deleting the record Error: error deleting Cloudflare Record: Record does not exist. (81044). Terraform says it cannot find the record, but still deletes it on the first apply, and a second apply then runs with any error.

I appreciate the Cloudflare provider cannot account for all types of input coming from other resources, however this seems to be a specific issue in the Cloudflare provider. It should handle the above scenario in some shape or form, either allowing the overwrite to work, or allowing the delete the work when managing AWS certificate validation records. I guess this is a very common task people need to do so the provider should incorporate whatever is required to make it work.

scott-doyland-burrows commented 9 months ago

I'm facing a same issue. I've added two records in CF, name: "example", ip addresses "1.2.3.4" and "4.3.2.1" (for test purposes)

terraform {
  required_version = "1.5.2"
  required_providers {
    cloudflare = {
      source = "cloudflare/cloudflare"
      version = "~> 4.10"
    }
  }
}

resource "cloudflare_record" "example1" {
  zone_id = data.cloudflare_zone.default.id
  name    = "example"
  type    = "A"
  value   = "1.2.3.4"
  ttl     = 1
  proxied = true
  allow_overwrite = true
}

resource "cloudflare_record" "example2" {
  zone_id = data.cloudflare_zone.default.id
  name    = "example"
  type    = "A"
  value   = "4.3.2.1"
  ttl     = 1
  proxied = true
  allow_overwrite = true
}

data "cloudflare_zone" "default" {
  name = var.zone
}

After terraform apply i get error:

cloudflare_record.example2: Creating...
cloudflare_record.example1: Creating...
cloudflare_record.example2: Still creating... [10s elapsed]
cloudflare_record.example1: Still creating... [10s elapsed]
cloudflare_record.example1: Still creating... [20s elapsed]
cloudflare_record.example2: Still creating... [20s elapsed]
cloudflare_record.example1: Still creating... [30s elapsed]
cloudflare_record.example2: Still creating... [30s elapsed]
╷
│ Error: attempted to override existing record however didn't find an exact match
│
│   with cloudflare_record.example1,
│   on test.tf line 11, in resource "cloudflare_record" "example1":
│   11: resource "cloudflare_record" "example1" {
│
╵
╷
│ Error: attempted to override existing record however didn't find an exact match
│
│   with cloudflare_record.example2,
│   on test.tf line 21, in resource "cloudflare_record" "example2":
│   21: resource "cloudflare_record" "example2" {
│
╵

And if I change allow_overwrite = false I'm getting error:

cloudflare_record.example1: Creating...
cloudflare_record.example2: Creating...
cloudflare_record.example1: Still creating... [10s elapsed]
cloudflare_record.example2: Still creating... [10s elapsed]
cloudflare_record.example2: Still creating... [20s elapsed]
cloudflare_record.example1: Still creating... [20s elapsed]
cloudflare_record.example2: Still creating... [30s elapsed]
cloudflare_record.example1: Still creating... [30s elapsed]
╷
│ Error: expected DNS record to not already be present but already exists
│
│   with cloudflare_record.example1,
│   on test.tf line 11, in resource "cloudflare_record" "example1":
│   11: resource "cloudflare_record" "example1" {
│
╵
╷
│ Error: expected DNS record to not already be present but already exists
│
│   with cloudflare_record.example2,
│   on test.tf line 21, in resource "cloudflare_record" "example2":
│   21: resource "cloudflare_record" "example2" {
│
╵

Help me please @jacobbednarz @ajax-mykhailo-oleksiuk What is the problem in my case?

Your example is wrong. allow_overwrite allows you to overwrite a record that is exactly the same as a current record. In your example the value is different so it is not supposed to overwrite it.

All you need to do is remove the second resource and just update the first resource with the value you want.

allow_overwrite is designed to be used where another resource is creating duplicate records, such as AWS certificate validation records, and those duplicate records are being passed to Cloudflare.

waddles commented 7 months ago

As @jacobbednarz alluded to in https://github.com/cloudflare/terraform-provider-cloudflare/issues/2407#issuecomment-1537593487, if you trim the zone's name from the record's name it does work, so the correct workaround for making domain validation records from an AWS certificate request is:

data "cloudflare_zone" "default" {
  name = var.zone
}

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

  zone_id         = data.cloudflare_zone.default.zone_id
  name            = trimsuffix(each.value.name, ".${data.cloudflare_zone.default.name}.")
  value           = each.value.record
  type            = each.value.type
  allow_overwrite = true
}
rigman24 commented 4 months ago

Thank you @waddles this is exactly what I was looking for. A recent update to our provider introduced this error message we had never seen before and caused quite a bit of chaos.

infowolfe commented 4 months ago

If you're reading this because you're using cdktf and tried to Token.as_list(records_iterator.pluck_property("name")) use the hostname property instead and aws_acm_certificate_validation won't complain anymore, since cloudflare_record's hostname == route53_record's fqdn.