Merging remote state file objects in Terraform 0.12.x

Using Terraform v0.12.9

I’m working on upgrading a project from version 0.11.x to 0.12.9. In this project, each terraform stack references a large number of remote state files.

Rather than defining the remote state files by hand, I kept the code neat and short by defining the remote states using a list, like this:

variable "remote_states" {
  default = ["network", "remote_component1","remote_component2","etc"]
}

data "terraform_remote_state" "remote_states" {
  count   = "${length(var.remote_states)}"
  backend = "s3"

  config {
    bucket = "${var.project}-${var.account_id}-${var.region}"
    key    = "/${var.environment}/${element(var.remote_states,count.index)}.tfstate"
    region = "${var.region}"
  }
}

and then I referred to the outputs like this:
= "${data.terraform_remote_state.remote_states.*.output_name[0]}"

and that always worked, so long as there were never any outputs with the same name as others.

However, I can’t get it working in version 0.12.9. When running the same code I get This object does not have an attribute named “output_name”

I figured I could use a local to amalgamate the remote states, but have tried to use toset() or concat() etc, but I get errors like: “Invalid value for “v” parameter: cannot convert tuple to set of any single type”

Essentially, I want to convert this:

 {
    "output_1" = "foo"
  },
  {
    "output_2" = "bar"
  }

into:

 {
    "output_1" = "foo"
    "output_2" = "bar"
  }

Is there a way of doing this? If not, can we create a way?

It looks like you’re running into the change described under Remote state references in the upgrade guide.

The short version is that terraform_remote_state now presents all of the output values together in a single mapping outputs, to make it easier to use all of the outputs together as a single object and to avoid conflicts between output value names and the arguments/attributes of terraform_remote_state.

To fix it, I think it should be sufficient to just add .outputs into your expressions:

data.terraform_remote_state.remote_states.*.outputs.output_name[0]

Based on the fact that your examples are still using interpolation syntax for single expressions throughout, I’m assuming you’re handling your upgrading manually rather than using the upgrade tool. Even if you ultimately discard its result and still upgrade manually, I’d still recommend running it against your old configuration and reviewing the changes it proposes, since it may show you other situations where changes are needed to avoid errors in Terraform 0.12.


If I’m understanding the second part of your question correctly, I think you could combine all of these into a single mapping (as long as there are no conflicting output value names between these remote states) using merge:

merge(data.terraform_remote_state.remote_states.*.outputs...)

The ... on the end there is for expanding function arguments: rather than passing a single list argument to merge, it instead passes each of the elements of that list as a separate argument.

You could also consider using the new resource for_each feature to make your remote states be identified by their names rather than by positions in the list:

variable "remote_states" {
  type = set(string)
  default = ["network", "remote_component1","remote_component2","etc"]
}

data "terraform_remote_state" "remote_states" {
  count   = var.remote_states

  backend = "s3"

  config {
    bucket = "${var.project}-${var.account_id}-${var.region}"
    key    = "/${var.environment}/${each.key}.tfstate"
    region = "${var.region}"
  }
}

This would allow you to reference the outputs using expressions like data.terraform_remote_state.remote_states["network"].outputs.vpc_id. You could also then simplify this structure using a local value:

locals {
  external = {
    for k, rs in data.terraform_remote_state.remote_states : k => r.outputs
  }
}

That extracts just the outputs for each one and allows you to reference them using a nice terse expression like local.external.network.vpc_id. You could put that in a data-only module to reuse it across multiple configurations, if desired.

1 Like

Thanks so much for your suggestions. This is really helpful.

This is how my code looks now:

variable “remote_states” {
type = set(string)
default = [“network”, “etc”]
}

data “terraform_remote_state” “remote_states” {
for_each = var.remote_states
backend = “s3”
config = {
bucket = “{var.project}-{var.account_id}-{var.region}" key = "{var.workload}/{var.account_id}/{var.region}/{var.environment}/{each.key}.tfstate”
region = var.region
}
}

locals {
merged_remote_states = {
for k, rs in data.terraform_remote_state.remote_states : k => rs.outputs
}
}
output “testing_the_merged_states_outputs” {
value = local.merged_remote_states
}

This is great, because it is merging the remote states into one output.

However, the resulting object looks like this:

"etc" = {
  "ips" = [
    "10.10.10.10",
    "10.10.10.11",
  ]
}
"network" = {
  "vpc_cidr" = "10.10.10.0/16"
  "vpc_id" = "vpc-00000000001"
}

Wheras what I want to acheive is it looking like this:

"ips" = [
  "10.10.10.10",
  "10.10.10.11",
]
"vpc_cidr" = "10.10.10.0/16"
"vpc_id" = "vpc-00000000001"

But I can’t quite work out what to do do achieve that.

It looks like you took my second suggestion for having a separate map entry for each remote state, but what you wanted was my first answer of using merge. What I didn’t show was how to combine the suggestion of using for_each with the suggestion of using merge, since we need a slightly different expression once data.terraform_remote_states.remote_states is a map rather than a list:

output “testing_the_merged_states_outputs” {
  value = merge([
    for v in data.terraform_remote_state.remote_states : v.outputs
  ]...)
}

The different parts of the above are:

  • A for expression to project the map of remote state data source objects into a list of just the output mappings from each one.
  • A call to merge to combine all of those mappings together, using the ... expansion symbol to pass each of the output mappings as a separate argument.

In this case you’ll need to make sure that there are no naming conflicts between the different remote states, or else some of the output values will be unavailable in the resulting merged map.

Thanks Martin, I’m really appreciating your help here. Unfortunately I’m having trouble getting it working though.

Unfortunately I’m getting this error, though:

Error: Invalid expanding argument value

on data.tf line 38, in output “testing_the_merged_states_outputs”:
38: value = merge([
39: for v in data.terraform_remote_state.remote_states : v.outputs
40: ]…)

The expanding argument (indicated by …) must be of a tuple, list, or set
type.

I’m not really sure why the expanding argument isn’t a tuple. Do you have any ideas on this? Thanks

Hmm… that is indeed strange. I don’t know how for expression would produce anything other than a tuple.

Perhaps hinting a type conversion to Terraform would help here:

  value = merge(tolist([for v in (etc etc)]))

That shouldn’t be necessary, so if that fixes it then there’s still some bug to be fixed here, but hopefully that will give you a working answer in the meantime.

Thanks Martin

I really don’t understand what’s happening here.

If I do this:

 value = merge(
     [for rs in data.terraform_remote_state.remote_states: rs.outputs]
     )

I get:

Error: Error in function call

  on data.tf line 39, in output "test3":
  39:   value = merge(
  40:
  41:
    |----------------
    | data.terraform_remote_state.remote_states is object with 2 attributes

Call to function "merge" failed: arguments must be maps or objects, got
"tuple".

Yet if I try this:

 value = merge(
   [for rs in data.terraform_remote_state.remote_states: rs.outputs]...
   )

I get:

Error: Invalid expanding argument value

  on data.tf line 40, in output "test3":
  39:
  40:     [for rs in data.terraform_remote_state.remote_states: rs.outputs]...
  41:

The expanding argument (indicated by ...) must be of a tuple, list, or set
type.

And if I try

  value = merge(tolist(
    [for rs in data.terraform_remote_state.remote_states: rs.outputs]
    ))

I get:

Error: Invalid function argument

  on data.tf line 40, in output "test3":
  39:
  40:     [for rs in data.terraform_remote_state.remote_states: rs.outputs]
  41:
    |----------------
    | data.terraform_remote_state.remote_states is object with 2 attributes

Invalid value for "v" parameter: cannot convert tuple to list of any single
type.

Sometimes it seems to think the expression is producing a tuple, sometimes it’s suggesting it’s not a tuple. I really can’t work out what I’m doing wrong here.

Thanks

Dicky

Ahh yes, I forgot that all of the objects in this tuple don’t have the same type, so converting to a list can’t work. Sorry… got distracted by the local problem and forgot the global context.

I’m not sure what’s going on here either. I think you’ve found a bug, because I think this should be working:

 value = merge(
   [for rs in data.terraform_remote_state.remote_states: rs.outputs]...
)

Would you mind opening a bug report about this? If you do, it’d be great to also link to the issue from this thread in case others find it in future and want to see how the investigation continued. Thanks!