0

I'm creating Cloudflare's records with Terraform using a module I created from the resource's documentation- https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/record.

I created a key value variable in which the key is the record's region and the value is the tenants in this region. I read this variable from a JSON file by jsondecode function. The module reads this variable and runs inside for loops in order to get the variable separated by region:tenant, region:tenant.

First running region_tenants_ids.json:

{
    "eu": [
        "test1"
    ],
    "us": [
        "test2"
    ]
}

Module:

 locals {
  tenants = flatten([
    for region, tenant_list in var.region_tenants_ids : [
      for tenant in tenant_list : {
        region = region
        tenant = tenant
      }
    ]
  ])
}

resource "cloudflare_record" "records" {
  count           = length(local.tenants)
  allow_overwrite = false
  zone_id         = var.zone_id
  type            = var.record_type
  proxied         = var.proxied_record

  name  = local.tenants[count.index].tenant
  value = "${var.prefix_lb_name}-${local.tenants[count.index].region}.${var.zone_name}"
}

resource "cloudflare_regional_hostname" "regional_hostname" {
  depends_on = [cloudflare_record.records]
  count      = length(local.tenants)
  zone_id    = var.zone_id
  hostname   = "${local.tenants[count.index].tenant}.${var.zone_name}"
  region_key = local.tenants[count.index].region
}

Every time I add another tenant to the region's list, the running forces replace of existing resources claiming the reason is because of the name, and creating the records from the list.

Second-running-adding-another-tenant:

{
    "eu": [
        "test1",
        "lasttest3"
    ],
    "us": [
        "test2"
    ]
}

I looked at the terraform.tfstate and could see that every time I add a new tenant the index_key is updated and messes up the order based on the alphabetic order, I assume this is why it's trying to replace the resources.

Plan- replacement error enforcement:

  # module.router_records.cloudflare_record.records[1] must be replaced
-/+ resource "cloudflare_record" "records" {
      ~ created_on      = "2023-08-30T21:29:11.355402Z" -> (known after apply)
      ~ hostname        = "test2.testcloudflare.dev.com" -> (known after apply)
      ~ id              = "5a9d8e71f668981e337a0cda4d259924" -> (known after apply)
      ~ metadata        = {
          - "auto_added"             = "false"
          - "managed_by_apps"        = "false"
          - "managed_by_argo_tunnel" = "false"
          - "source"                 = "primary"
        } -> (known after apply)
      ~ modified_on     = "2023-08-30T21:29:11.355402Z" -> (known after apply)
      ~ name            = "test2" -> "lasttest3" # forces replacement
      ~ proxiable       = true -> (known after apply)
      - tags            = [] -> null
      ~ ttl             = 1 -> (known after apply)
      ~ value           = "lb-us.testcloudflare.dev.com" -> "lb-eu.testcloudflare.dev.com"
        # (4 unchanged attributes hidden)
    }
 # module.router_records.cloudflare_record.records[2] will be created
  + resource "cloudflare_record" "records" {
      + allow_overwrite = false
      + created_on      = (known after apply)
      + hostname        = (known after apply)
      + id              = (known after apply)
      + metadata        = (known after apply)
      + modified_on     = (known after apply)
      + name            = "test2"
      + proxiable       = (known after apply)
      + proxied         = true
      + ttl             = (known after apply)
      + type            = "CNAME"
      + value           = "lb-us.testcloudflare.dev.com"
      + zone_id         = "xxx"
    }

  # module.router_records.cloudflare_regional_hostname.regional_hostname[1] will be updated in-place
  ~ resource "cloudflare_regional_hostname" "regional_hostname" {
      ~ hostname   = "test2.testcloudflare.dev.com" -> "lasttest3.testcloudflare.dev.com"
        id         = "test2.testcloudflare.dev.com"
      ~ region_key = "us" -> "eu"
        # (2 unchanged attributes hidden)
    }

  # module.router_records.cloudflare_regional_hostname.regional_hostname[2] will be created
  + resource "cloudflare_regional_hostname" "regional_hostname" {
      + created_on = (known after apply)
      + hostname   = "test2.testcloudflare.dev.com"
      + id         = (known after apply)
      + region_key = "us"
      + zone_id    = "xxx"
    }

Plan: 3 to add, 1 to change, 1 to destroy.

Changes to Outputs:
  ~ router_records_names = [
        "test1",
      + "lasttest3",
        "test2",
    ]

Could someone say how I'll be able to keep the index key/ stop this replacement enforcement due to adding another tenant to the region? Would really appreciate your help, even if you aren't familiar with the solution vote it up will be amazing, thanks!

The Code in Github Repo.

P.S.

  1. I'm adding it through variable from type- map(any) because one of the requirements was to get only the tenant name for each region since the record's value is always the same.

  2. I know I could add them 1 by 1, but then I'll have to repeat on the many parameters(like value which is static for each region, record type which will always be CNAME, etc.) many times which is losing the point of using terraform.

  3. I tried running the code, The Code in Github Repo., then adding another tenant to the JSON. I expected that the recreation enforcement would not happen.

korenlios
  • 11
  • 2
  • 2
    Please use correctly formatted code blocks for TF code and errors, not screenshots. – Marcin Aug 30 '23 at 23:33
  • @Marcin Sure, thx for the heads up, I updated it, could you please remove your negative vote? (didn't ask a question for a few years..) – korenlios Aug 30 '23 at 23:46
  • 2
    Use `for_each` and `maps` instead of `list` and `count`. In the latter order is important, and it probably changes for you. – Marcin Aug 30 '23 at 23:48
  • @Marcin Hey, thank you! This is the updated JSON: { "eu": "test1", "eu": "lasttest3", "us": "test2", "us": "test3", "us": "test4" } And I updated the var region_tenants_ids to be map, and the module to support it and run it as for_each, the thing is that now it's creating only one instance form each region. (yes the region is unique and needs to be the same for each tenant based on the region the tenant is located) The output- Plan: 4 to add, 0 to change, 0 to destroy. Do you have any other suggestions for that? (while keeping the regions and having many tenants) – korenlios Aug 31 '23 at 00:37
  • I'll reinforce the recommendation from @Marcin : use `for_each` over a map of tenants rather than `count` over a list of tenants. If you are not getting the same number of tenants as before, that indicates that you have your map set up incorrectly – lxop Aug 31 '23 at 03:14
  • @lxop appriciate your comment, I did it, when using for_each and it created only one tenant from the the region list. – korenlios Aug 31 '23 at 15:51

1 Answers1

0

I found the solution for me was to update the variable type from list to list(object), then update the JSON format accordingly, and update the module to use for_each as @marcin and @lxop suggested.

The variable:

variable "region_tenants_ids" {
  type        = list(object({
    region = string
    tenant = string
  }))
  default     = []
  description = "All tenant names by regions."
}

The JSON file:

[
    { "region": "eu", "tenant": "test" },
    { "region": "eu", "tenant": "zzz" },
    { "region": "us", "tenant": "bbb" },
    { "region": "au", "tenant": "aaa" }
]

The module:

resource "cloudflare_record" "records" {
  for_each = { for rt in var.region_tenants_ids : "${rt.region}-${rt.tenant}" => rt }

  allow_overwrite = false
  zone_id         = var.zone_id
  type            = var.record_type
  proxied         = var.proxied_record

  name  = "${each.value.tenant}"
  value = "${var.prefix_lb_name}-${each.value.region}.${var.zone_name}"
}

resource "cloudflare_regional_hostname" "regional_hostname" {
  depends_on = [cloudflare_record.records]

  for_each   = { for rt in var.region_tenants_ids : "${rt.region}-${rt.tenant}" => rt }

  zone_id    = var.zone_id
  hostname   = "${each.value.tenant}.${var.zone_name}"
  region_key = each.value.region
}
korenlios
  • 11
  • 2