Unable to create AWS ingress rule dynamically: resource has "count" set, its attributes must be accessed on specific instance

What is the difference between a set and a list in terraform (if any)?

Also, I was looking to create and AWS security group where it has common ingress { } rules for all lower and higher environments (my main.tf is used to create our resources for all our nonprod/lower and prod/higher environments), and also a specific ingress rule created for only certain higher environments/ Terraform workspaces (specifically, preview and prod) using the AWS provider data "aws_security_group" data source and also the resource "aws_security_group" resource like so:

# data source SG, for *only* `prod` and `preview` environments
data "aws_security_group" "security_group_x" {
  count = contains(["preview", "prod"], terraform.workspace) ? 1 : 0
  name = "sg-x"
}

resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  description = "Allow TLS inbound traffic"
  vpc_id      = aws_vpc.main.id

  # common ingress rule for all environments, like devtest, externaltest, etc, and preview and prod
  ingress {
    description      = "TLS from VPC"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    security_groups  = [data.aws_security_group.sg_a.id]
  }

  dynamic "ingress" {
    for_each = contains(["preview", "prod"], terraform.workspace) ? [terraform.workspace] : []
    content {
      description = "security group x"
      from_port = 1234
      to_port = 1234
      protocol = "TCP"
      security_groups = [data.aws_security_group.security_group_x.id]
      }
    }
...

It’s a bit confusing so far, since if I do the above as is, I get this error:

Error: Missing resource instance key
Because data.aws_security_group.default_sg has "count" set, its attributes must be accessed on specific instance.

For example, to correlate with indices of a referring resource, use: 
   data.aws_security_group.default_sg[count.index]

Then I try to add that [count.index] to my code in the dynamic block further above with this line:

      security_groups = [data.aws_security_group.security_group_x[count.index].id]

and then I get this error:

Error: Reference to "count" in a non-counted context
The "count" object can only be used in "module", "resource", and "data" blocks, and only when the "count" argument is set.

But wasn’t my "count" argument set to 1 by my expression in this part of my code mentioned at the top above?:

# data source SG, for *only* `prod` and `preview` environments
data "aws_security_group" "security_group_x" {
  count = contains(["preview", "prod"], terraform.workspace) ? 1 : 0
  name = "sg-x"
}

Seems I just need a small tweak and can be on the way, creating this single ingress rule just for prod and preview environments.

Hi @aaa,

The first error message is suggesting a solution for one particular goal (creating references between resources that have the same count), but that suggestion doesn’t apply in your case because you have a different goal: referring to the one security group only if it was enabled.

I assume your intent would be to omit that ingress block altogether if there isn’t a security group for it to refer to, and so I would frame this intent as “declare one ingress rule for each instance of the security group resource”: if there is one instance then one block, and if there are no instances then no blocks.

To achieve that you can generate ingress rules dynamically using a dynamic "ingress" block:

  dynamic "ingress" {
    for_each = data.aws_security_group.sg_a.id
    content {
      # ...
      security_groups = [ingress.value.id]
    }
  }
1 Like

Thank you @apparentlymart ! Hope all is well. I did that, but then for non-applicable workspaces / environments - where we don’t want that ingress dynamic rule in the SG resource, such as in the "devtest" workspace/environment - I get this on the terraform plan

Error: Missing resource instance key

41:   for_each = data.aws_security_group.sg_a.id

Because data.aws_security_group.sg_a.id has "count" set, its attributes must be accessed on specific instances.

I have kept those original lines of code that had the conditional data source creation for the SG if we were in the preview or prod workspaces/environments:

# data source SG, for *only* `prod` and `preview` environments
data "aws_security_group" "security_group_a" {
  count = contains(["preview", "prod"], terraform.workspace) ? 1 : 0
  name = "sg-a"
}

in the dynamic "ingress" { } block, should the for_each have a conditional perhaps, such as the below?:

dynamic "ingress" {
    for_each = data.aws_security_group.sg_a.id ? [data.aws_security_group.sg_a.id] : []
    content {
      # ...
      security_groups = [ingress.value.id]
    }
  }

where the [] means that dynamic ingress rule won’t be created for the resource if we were say, terraforming the “devtest” environment, since the data source declaration won’t create that data source, unless the workspace / environment is ["preview", "prod"] ?


On another note, question for this line you had:

 security_groups = [ingress.value.id]

is the ingress referred to above, get assigned to / associated to the?:

data.aws_security_group.sg_a.id

based on your for_each line below?:

    for_each = data.aws_security_group.sg_a.id

or if my fix is correct, does the ingress you mentioned get assigned to:

[data.aws_security_group.sg_a.id]

Wasn’t sure on that.

@apparentlymart , was wondering if you had some insight on the above or advice.

Sorry @aaa!

I made an editing error when I was preparing that example. I didn’t intend to include the .id on the end of the for_each expression, because the goal there is to refer to the collection as a whole, not to the id attribute of one element in particular.

1 Like