Terraform: Cloudformation with count not returning template outputs

Created on 5 Dec 2018  ยท  10Comments  ยท  Source: hashicorp/terraform

Terraform Version

Terraform v0.11.10
+ provider.aws v1.21.0

Terraform Configuration Files

resource "aws_cloudformation_stack" "example" {
  count = "2"

  name = "example-${count.index}"

  template = <<STACK
  {
    "Resources" : {
      "MyVPC": {
        "Type" : "AWS::EC2::VPC",
        "Properties" : {
          "CidrBlock" : "10.0.0.0/16",
          "Tags" : [
            {"Key": "Name", "Value": "Primary_CF_VPC"}
          ]
        }
      }
    },
    "Outputs" : {
      "VpcID" : {
        "Description": "The VPC ID",
        "Value" : { "Ref" : "MyVPC" }
      }
    }
  }
  STACK
}

output "VpcID" {
  value = "${aws_cloudformation_stack.example.*.outputs["VpcID"]}"
}

Crash Output

* module.example.output.DefaultDNSTarget: __builtin_StringToInt: strconv.ParseInt: parsing "VpcID": invalid syntax in:

${aws_cloudformation_stack.example.*.outputs["VpcID"]}

Most helpful comment

When using count, you'll need to use coalescelist to access cloudformation output.

So for example, we have a cloudformation module called foo, with template returning output BAR and BAZ.

locals {
  empty_map = {
    BAR = ""
    BAZ = ""
  }

  tmp_list       = "${coalescelist(aws_cloudformation_stack.foo.*.outputs, list(local.empty_map))}"
  tmp_map        = "${local.tmp_list[0]}"
  cf_outputs_BAR = "${lookup(local.tmp_map, "BAR", "")}"
  cf_outputs_BAZ = "${lookup(local.tmp_map, "BAZ", "")}"
}

output "BAR" {
  value = "${local.cf_outputs_BAR}"
}

output "BAZ" {
  value = "${local.cf_outputs_BAZ}"
}

Huge thanks to @maartenvanderhoef for providing this answer in Gitter

All 10 comments

Hi @skinofstars! Sorry for this strange behavior.

The reason for the result you saw here is that the splat operator .* only consumes attribute access operations after it, and is interpreting ["VpcID"] as an index into the resulting list of output maps, rather than as a key into each of the maps.

In other words, it's evaluating aws_cloudformation_stack.example.*.outputs first to produce a list of maps and then applying ["VpcID"] to that list, whereas you wanted it to apply the full .outputs["VpcID"] traversal to each item, producing a list of strings.

The easiest way to get what you wanted here in Terraform 0.11 is to use attribute syntax instead:

  value = "${aws_cloudformation_stack.example.*.outputs.VpcID}"

The forthcoming Terraform 0.12 release will introduce a new form of splat expression that will consume index operations too, so once that is released you'd be able to alternatively write this as:

  value = "${aws_cloudformation_stack.example[*].outputs["VpcID"]}"

However, in this sort of situation we usually prefer to use the attribute syntax anyway because it expresses the same idea more concisely; the index syntax is intended for situations where the key itself is a dynamic value.

Hi @apparentlymart, thanks for getting back.

Thanks for explaining the reason. Unfortunately your proposed solution, to use attribute syntax, doesn't work for aws_cloudformation_stack (perhaps a different bug?). It results in the following

Error: Error running plan: 1 error(s) occurred:

* module.example.output.VpcID: Resource 'aws_cloudformation_stack.example' does not have attribute 'outputs.VpcID' for variable 'aws_cloudformation_stack.example.*.outputs.VpcID'

I'm seeing the same issue as above when using count for a cloud formation template. Is it not possible to use count?

Just to add some flavor to the above comment:

Here is my stack instantiation:

# Cloudformation template
resource "aws_cloudformation_stack" "window" {
        count           = "${length(var.days) * length(var.time)}"
        name            = "vpc-maintenance-window"
        parameters {
                window_name      = "mw-AutomatedPatching-runson-${var.day[count.index % local.counter["day"]]}-at-${var.time[count.index / local.counter["days"]]}hours-EST"
                window_schedule  = "cron(0 ${var.time[count.index / local.counter["days"]]} ? * ${var.days[count.index % local.counter["days"]]} *)"
                window_duration  = 2
                window_cutoff    = 0
        }
        template_body = "${file("${path.module}/window.json")}"
}
#output "CFWindow_ID" {
#       value = "${aws_cloudformation_stack.window.*.outputs.CFWindow_ID}"
#}
#output "CFWindow_Name" {
#       value = "${aws_cloudformation_stack.window.*.outputs.CFWindow_Name}"
#}

Now with the above outputs commented out, a Terraform plan will run without issue.

removing the comments from the output results in:

* aws_ssm_maintenance_window_target.target_install[2]: Resource 'aws_cloudformation_stack.window' not found for variable 'aws_cloudformation_stack.window.outputs'
* aws_ssm_maintenance_window_target.target_install[3]: Resource 'aws_cloudformation_stack.window' not found for variable 'aws_cloudformation_stack.window.outputs'
* aws_ssm_maintenance_window_target.target_install[4]: Resource 'aws_cloudformation_stack.window' not found for variable 'aws_cloudformation_stack.window.outputs'
* aws_ssm_maintenance_window_target.target_install[5]: Resource 'aws_cloudformation_stack.window' not found for variable 'aws_cloudformation_stack.window.outputs'
* aws_ssm_maintenance_window_target.target_install[0]: Resource 'aws_cloudformation_stack.window' not found for variable 'aws_cloudformation_stack.window.outputs'
* aws_ssm_maintenance_window_target.target_install[1]: Resource 'aws_cloudformation_stack.window' not found for variable 'aws_cloudformation_stack.window.outputs'

with calling the following ssm_maintenance_window_target:

resource "aws_ssm_maintenance_window_target" "target_install" {
  count = "${length(var.days) * length(var.time)}"
  window_id     = "${aws_cloudformation_stack.window.outputs.CFWindow_ID}"
  resource_type = "INSTANCE"
  targets {
    key    = "tag:maintenancewindow"
    values = ["${var.day[count.index  % local.counter["day"]]}@${var.time[count.index / local.counter["days"]]}"]
  }

When using count, you'll need to use coalescelist to access cloudformation output.

So for example, we have a cloudformation module called foo, with template returning output BAR and BAZ.

locals {
  empty_map = {
    BAR = ""
    BAZ = ""
  }

  tmp_list       = "${coalescelist(aws_cloudformation_stack.foo.*.outputs, list(local.empty_map))}"
  tmp_map        = "${local.tmp_list[0]}"
  cf_outputs_BAR = "${lookup(local.tmp_map, "BAR", "")}"
  cf_outputs_BAZ = "${lookup(local.tmp_map, "BAZ", "")}"
}

output "BAR" {
  value = "${local.cf_outputs_BAR}"
}

output "BAZ" {
  value = "${local.cf_outputs_BAZ}"
}

Huge thanks to @maartenvanderhoef for providing this answer in Gitter

I'm glad to hear you found a (working) workaround! I'm going to close this issue, but please feel free to open another issue if needed.

@skinofstars i am a little bit confused on how to use your solution.
How can i use this with count in the same module ?
I want to reference output ARN from cloudformation and set it as replication task target endpoint.

I want to do something like this
target_endpoint_arn = "${element(aws_cloudformation_stack.s3_endpoint_cf.*.outputs.ARN, count.index)}"

@AleksandarTokarev I believe they key aspect of this is to use coalescelist. As I said, this solution was provided by someone else, so I'm not the expert on this.

Maybe you need something like the following... This assumes an outputs map an ARN list, so you can see some different ways of interacting with results.

resource "aws_cloudformation_stack" "my_stack" {
  name  = "my_stack"
  template_body = "${file("${path.module}/my_stack.json")}"
}

locals {
  policy_arn_list = "${coalescelist(aws_iam_policy.my_stack.*.arn, list("", ""))}"
  outputs_list    = "${coalescelist(aws_cloudformation_stack.my_stack.*.outputs, list(map("VPCID", "")))}"
  map             = "${local.outputs_list[0]}"
  vpc_id          = "${lookup(local.map, "VPCID", "")}"
}

resource "aws_iam_user_policy_attachment" "my_iam_user_policy" {
  user       = "${var.iam_user_name}"
  policy_arn = "${local.policy_arn_list[0]}"
}

resource "aws_security_group" "subnet" {
  vpc_id = "${local.vpc_id}"
}

Hmm my problem is the same as your original post, where i have a count with cloudformation, and i am getting multiple cloudformation templates each one with 1 output, so my question is whether it is possible to achieve this with the thing u had posted?

@AleksandarTokarev Note that the above example is doing:
map = "${local.outputs_list[0]}"

What you can do is run the lookup inside your resource, though it's a little harder to read:

resource aws_network_interface_sg_attachment payg_f5 {
  count             = "${aws_cloudformation_stack.payg_f5.count}"
  security_group_id = "${aws_security_group.f5.id}"

  network_interface_id = "${lookup(local.payg_outputs_list[count.index], "Bigip1subnet1Az1Interface", "")}"
}

I'm going to lock this issue because it has been closed for _30 days_ โณ. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

franklinwise picture franklinwise  ยท  3Comments

rnowosielski picture rnowosielski  ยท  3Comments

rjinski picture rjinski  ยท  3Comments

jrnt30 picture jrnt30  ยท  3Comments

rkulagowski picture rkulagowski  ยท  3Comments