Does map sort keys?

After several months of investigations, with breaks, I finally realized what the problems were.
Is it normal that the keys in the map are sorted alphabetically?

I expected that as variables are added in this order they are, although it is not an array of data.

If this is normal behavior, then how can I have something similar to the one shown in the example, but in the same sequence as I set?

variable "cp_ip_addresses" {
	type    = map(list(string))
	default = {
		"public"       = ["69.168.x.y"]
		"mgmt"         = ["192.168.16.5"]
		"appliance"    = ["192.168.32.5"]
		"provisioning" = ["192.168.40.5"]
	}
}

terraform console
> var.cp_ip_addresses
{
  "appliance" = [
    "192.168.32.5",
  ]
  "mgmt" = [
    "192.168.16.5",
  ]
  "provisioning" = [
    "192.168.40.5",
  ]
  "public" = [
    "69.168.x.y",
  ]
}

As you can see, the key sequence is different from what was specified.

Yes, map keys are always iterated in lexicographical order.

To be able to suggest a solution, could you explain why the order of iteration is important? What are you trying to accomplish that is prevented by sorted map keys?

1 Like

Hello guys,
I face to the same problem and can’t find a solution for the following situation. Let’s say I would like to deploy VM with fixed order of NICs, because my following configuration tool depends on it.

The same problem is nicely interpreted here as well. Could tell me how to get keys or values in the same sequence as it’s set in variable please?

@Koleon Maps don’t have an order, but arrays do (in programming concepts, versus traversal, as referred to above). In the post that you linked to, it seems that their underlying issue could be solved or approached from using for_each to enumerate the resource rather than count. If you’re referencing values in the map by their key, rather than an “index” you’ll always get your expected value, and you can add/remove items from the map without forcing a new resource.

When I’m reading this post you’ve linked to, this line:

peer_vpc_id = "${element(values(var.apples_account_vpc_ids),count.index)}"

felt significant to me.

Why are they trying to use these functions and accessing the values related to count.index? I suspect because the post is dated about when resource for_each was released, which is now at your disposal (on later versions of 0.12 and up).

Hi @pselle, thank you for your prompt reply.
Let me give you more specific example. I have following module:

data "vsphere_network" "network" {
  count         = var.network_conf != null ? length(var.network_conf) : 0
  name          = keys(var.network_conf)[count.index]
  datacenter_id = data.vsphere_datacenter.dc.id
}

resource "vsphere_virtual_machine" "virtual_machine_linux" {
  count                      = var.template_os_family == "linux" ? 1 : 0
  name                       = var.vm_name
  resource_pool_id           = data.vsphere_resource_pool.pool.id
  datastore_id               = var.datastore == "" ? null : data.vsphere_datastore.ds[0].id
  datastore_cluster_id       = var.datastore_cluster == "" ? null : data.vsphere_datastore_cluster.ds_cluster[0].id
  annotation                 = var.annotation
  num_cpus                   = var.num_cpus
  memory                     = var.memory
  folder                     = var.folder
  nested_hv_enabled          = var.nested_hv_enabled
  sync_time_with_host        = var.sync_time_with_host
  guest_id                   = data.vsphere_virtual_machine.template.guest_id
  scsi_type                  = data.vsphere_virtual_machine.template.scsi_type
  wait_for_guest_net_timeout = var.wait_for_guest_net_timeout

  dynamic "network_interface" {
    for_each = keys(var.network_conf)
    content {
      network_id = data.vsphere_network.network[network_interface.key].id
    }
  }

  dynamic "disk" {
    for_each = [for d in var.disk_conf : {
      num           = d.num
      size          = d.size
      thin          = d.thin
      eagerly_scrub = d.eagerly_scrub
    }]

    content {
      label            = "disk${disk.value.num}"
      size             = disk.value.size
      thin_provisioned = disk.value.thin
      eagerly_scrub    = disk.value.eagerly_scrub
      unit_number      = disk.value.num
    }
  }

  clone {
    template_uuid = data.vsphere_virtual_machine.template.id

    customize {
      linux_options {
        host_name = var.hostname
        domain    = var.domain
        time_zone = var.time_zone != "" ? var.time_zone : "UTC"
      }

      dynamic "network_interface" {
        for_each = var.network_conf
        content {
          ipv4_address = element(split("/", network_interface.value), 0)
          ipv4_netmask = element(split("/", network_interface.value), 1)
        }
      }

      ipv4_gateway    = var.ipv4_gateway
      dns_server_list = var.dns_servers
    }
  }

And the input value to this module looks like:

  network_conf  = {
    "a-nic-41-sid-5024"   = "10.1.0.1/24",
    "b-nic2-102-sid-5084" = "10.2.0.1/24"
  }

When I add or change the order of values in network_conf, the terraform deploy do not preserve it. Is there a way how to change the datasource or resource loop please?

I’d have the same suggestion: Why are you using count here rather than for_each? You could do this same null check and have an empty object in order to get “0” resources created, no? Then your name is each.key

I’m sorry, not sure I follow you… Are you suggesting something like this?

data "vsphere_network" "network" {
  for_each = var.network_conf
  name = each.key
  datacenter_id = data.vsphere_datacenter.dc.id
}

resource "vsphere_virtual_machine" "virtual_machine_linux" {
...
  dynamic "network_interface" {
    for_each = var.network_conf
    content {
      network_id = data.vsphere_network.network[network_interface.key].id
    }
  }
...

It works, but the problem preserve. The input value order is not respected.

If you are wanting things to be in order you need to switch from using a map to a list.

So for example change to something like:

network_conf = [
    {name = "a-nic-41-sid-5024", cidr = "10.1.0.1/24"},
    {name = "b-nic2-102-sid-5084", cidr = "10.2.0.1/24"}
]

You would then be able to use count and reference the data as (for example) var.network_conf[count.index].name

The general rule is if you want ordering use a list with count. Be aware that adding entries not at the end of the list (or removing entries not at the end) would cause multiple delete/create actions (as each entry is removed and replaced with the equivalent of the “next” resource).

If order doesn’t matter (which is often the case) use a map and for_each. You then have the advantage of only adding/removing anything you change, rather than other resources (as you would have with count & a list).

@stuart-c The issue with using count is you cannot use count in a dynamic attribute.

Here would be another example
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/imagebuilder_image_recipe

If I want to make ebs and component dynamic so I do not have to hard code my recipe I cannot, because order matters.

resource "aws_imagebuilder_image_recipe" "example" {
  block_device_mapping {
    device_name = "/dev/xvdb"

    ebs {
      delete_on_termination = true
      volume_size           = 100
      volume_type           = "gp2"
    }
  }

  component {
    component_arn = aws_imagebuilder_component.example.arn
  }

  name         = "example"
  parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x"
  version      = "1.0.0"
}

Hi @tikicoder,

This is an old topic so I must admit I’ve only really skimmed the context up to this point, but I think this new comment is talking about dynamic blocks while the rest of the topic until this point has been talking about resource-level count and for_each.

Although both of these constructs use an argument named for_each, dynamic blocks are different in that they behave as a sort of macro that just generates a series of blocks for the provider to interpret however it wishes to. The resource repetition arguments, on the other hand, generate entirely new resource instances that each need to have a unique identifier.

Because of that difference, Terraform will allow any collection value as the for_each in a dynamic block, including a list. Whether the provider ultimately pays attention to the order of the blocks is a decision that varies depending on the resource type, but from what you’ve said here it seems like the provider does consider the order of component blocks to be significant, in which case using a dynamic block with a list as the for_each would be a reasonable thing to do here, generating blocks in the same order as your list elements.

With that said, the same caveat would apply as for using count with a whole resource: if you remove items from your list later, the provider is might not understand that you removed a component block from the middle of the list and so might understand it as deleting the last item and then updating various others in order to make the indices line up again.

You can use count on the main resource but that doesn’t help you on the dynamic attribute.

I would have sworn in the past I tried an array of strings and it didn’t work, but in the latest version, it looks to have worked.

The document below seems to indicate you need to convert the string array to a set, which is where the issue comes. As soon as you convert it to a set it updates the order.

However, since for_each [“b”, “c”, “a”] seems to work on a dynamic attribute I guess I am good.

The provider uses the order of the component as the order for the recipe.