Terraform resource scoped variables

Hey, I am trying to create compute instances (OpenStack) in multiple regions using Terraform - the regions are stored in a variable. I want to name the instance using the region but I don’t know how to do it without repeating code - just take a look at this snippet and you’ll get what I mean.

resource "openstack_compute_instance_v2" "test_resource" {
  provider = openstack.ovh

  count       = length(var.region) * local.instance_count
  name        = "instance-${self.region}"
  image_name  = "Ubuntu 20.04"
  flavor_name = "s1-2"

  region   = element(var.region, floor(count.index / local.instance_count))
  key_pair = element(openstack_compute_keypair_v2.my_keypair.*.name, floor(count.index / local.instance_count))

  network {
    name = "Ext-Net"
  }
}

This will throw the following Terraform error:

Error: Invalid “self” reference
The “self” object is not available in this context. This object can be used only in resource provisioner and connection blocks.

How else can I access the region of the current instance without repeating element(var.region, floor(count.index / local.instance_count))?

Hi @iipanda,

Aside from the repetition of that expression, your design here also has the problem that if you were to add a new region or increase the instance count later the indices of existing instances would no longer “line up” anymore, causing Terraform to think that you want to move existing objects into new regions, which I assume the provider wouldn’t be able to achieve without replacing those objects entirely.

To address both of these problems I’d suggest using resource for_each instead, and building an intermediate data structure describing each of the instances you’ll be declaring, which you can then use to systematically derive the real instance configuration that the provider expects.

locals {
  instances = flatten([
    for region in var.region : [
      for idx in range(local.instance_count) : {
        region       = region
        instance_idx = idx
      }
    ]
  ])
  instances_map = tomap({
    for inst in var.instances : "${inst.region}#${inst.instance_idx}" => inst
  })
}

resource "openstack_compute_keypair_v2" "example" {
  for_each = var.instances_map

  region = each.value.region
  # ...
}

resource "openstack_compute_instance_v2" "example" {
  for_each = var.instances_map

  name = "instance-${each.value.region}"
  # ...

  region   = each.value.region
  key_pair = openstack_compute_keypair_v2.example[each.key].name

  # ...
}

This will declare instances whose tracking addresses in Terraform will include both the region name and the index within the region, and so if you increase local.instance_count later Terraform will be able to see that you intend to add one new instance per region, without affecting the existing instances. Likewise, if you add or remove regions later Terraform will be able to add or remove just the instances related to the regions that changed.

Since you didn’t include your openstack_compute_keypair_v2.my_keypair in your example I just guessed that the goal was to have one keypair per instance. If you had something else in mind for that then it should be possible to achieve any sort of systematic mapping you need, such as one keypair per region, using similar techniques.

1 Like

Hello, thanks a lot for your response!

I modified the method you proposed a little bit to also achieve setting instance count per region and to have only one keypair per region. However - now one of my outputs which has previously worked stopped doing so.

Here’s my configuration right now (not including the provider and terraform blocks):

variable "region" {
  type = map(number)
  default = {
    "BHS5"  = 3
    "DE1"   = 3
    "GRA11" = 3
    "GRA5"  = 3
    "GRA7"  = 1
    "GRA9"  = 1
    "SBG5"  = 3
    "SGP1"  = 1
    "SYD1"  = 1
    "UK1"   = 3
    "WAW1"  = 2
  }
}

resource "openstack_compute_keypair_v2" "my_keypair" {
  provider = openstack.ovh

  for_each = var.region

  name       = "keypair-${each.key}"
  public_key = file("~/.ssh/id_rsa.pub")
  region     = each.key
}

locals {
  instances = flatten([
    for region, instance_count in var.region : [
      for index in range(instance_count) : {
        region = region
        index  = index
      }
    ]
  ])
  instance_map = tomap({
    for instance in local.instances : "${instance.region}#${instance.index}" => instance
  })
}

resource "openstack_compute_instance_v2" "my_instance" {
  provider = openstack.ovh

  for_each = local.instance_map

  name        = "instance-${each.value.region}"
  image_name  = "Ubuntu 20.04"
  flavor_name = "s1-2"

  region   = each.value.region
  key_pair = openstack_compute_keypair_v2.my_keypair[each.value.region].name

  network {
    name = "Ext-Net"
  }
}

output "instance_ips" {
  description = "Public IP addresses of instances"
  value       = openstack_compute_instance_v2.my_instance.*.access_ip_v4
}

The error message that Terraform gives is:

Error: Unsupported attribute
This object does not have an attribute named “access_ip_v4”.

Which of course it clearly does - https://registry.terraform.io/providers/terraform-provider-openstack/openstack/latest/docs/resources/compute_instance_v2#access_ip_v4.

Setting the value to something like [for index, _ in local.instance_map : openstack_compute_instance_v2.my_instance[index].access_ip_v4] does work but it seems kinda weird that I can’t address it like I did before.
Do you have any idea of what might be causing this?

EDIT: I just found this - https://github.com/hashicorp/terraform/issues/22476#issuecomment-532732699 - looks like it’s intentional. I’ll use what someone else suggested in the comments: values(openstack_compute_instance_v2.my_instance)[*].