Outputting null_data_source output as plain maps instead of array of maps in terraform - terraform

Terraform v0.11.11
+ provider.null v2.0.0
variable "vpc1" {
type = "list"
default = [
"name1:ip1:az1:dedicated",
"name2:ip2:az2:dedicated",
]
}
variable vpc2 {
type = "list"
default = [
"foo:10.1:east:yes",
"bar:10.2:west:no",
]
}
data "null_data_source" "values" {
count = "${length(var.vpc1)}"
inputs = "${
map (
"${element(split(":",var.vpc1[count.index]),0)}",
"${element(split(":", var.vpc2[count.index]),0)}"
)
}"
}
output "mappings" {
value = "${data.null_data_source.values.*.outputs}"
}
How to get the desired output , What am i missing

You need function of flatten()
output "mappings" {
value = "${flatten(data.null_data_source.values.*.outputs)}"
}
And there is easy way to get what you need with function concat()
variable "vpc1" {
type = "list"
default = [
"name1:ip1:az1:dedicated",
"name2:ip2:az2:dedicated",
]
}
variable vpc2 {
type = "list"
default = [
"foo:10.1:east:yes",
"bar:10.2:west:no",
]
}
output "mappings" {
value = "${flatten(concat(var.vpc1, var.vpc2))}"
}
Here is the output
$ terraform apply
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
mappings = [
name1:ip1:az1:dedicated,
name2:ip2:az2:dedicated,
foo:10.1:east:yes,
bar:10.2:west:no
]
reference:
https://www.terraform.io/docs/configuration/interpolation.html#flatten-list-of-lists-
https://www.terraform.io/docs/configuration/interpolation.html#concat-list1-list2-

Related

did I do something wrong? or I found a bug?

this is to prove that there might be a bug in setintersection() or keys() in terraform v1.2.1
that it treats local and var differently.
I hope someone can help to confirm, or point out if I did something wrong?
# this file is to prove that there might be a bug in setintersection() or keys() in terraform v1.2.1
# that it treats local and var differently.
# I hope someone can help to confirm, or point out if I did something wrong?
# local.regions is to serve as the setintersection control set of strings
# as the keys(s_v) has non-region-related keys
locals {
regions = ["region-1", "region-2"]
}
# local.local_obj has the exact same data structure as of var.var_obj
locals {
local_obj = {
collection_local = {
subnet_local = {
other_keys = "somthing"
region-1 = "cidr-for-region-1-for-local-obj"
# region-2 = "cidr-for-region-2-for-local-obj"
}
}
}
}
# var.var_obj is a map(map(object())) data structure, outter map is the collection of subnets,
# inner map is the subnets within a collection, object is the subnet config
variable "var_obj" {
type = map(
map (
object ({
other_keys = "whatever"
region-1 = optional(string)
region-2 = optional(string)
})
)
)
default = {
collection_var = {
subnet_var = {
other_keys = "something_else"
region-1 = "cidr-for-region-1-for-var-obj"
# region-2 = "cidr-for-region-2-for-var-obj"
}
}
}
}
# local.transform-local and local.transform-var are two transforming locals to suite subnet resource needs
locals {
transform-local = flatten([
for c_k,c_v in local.local_obj : [
for s_k, s_v in c_v : [
for rg in setintersection(local.regions, keys(s_v)) : {
name = "${rg}-${s_k}"
region = rg
ip_cidr_range = s_v[rg]
}
]
]
])
transform-var = flatten([
for c_k,c_v in var.var_obj : [
for s_k, s_v in c_v : [
for rg in setintersection(local.regions, keys(s_v)) : {
name = "${rg}-${s_k}"
region = rg
ip_cidr_range = s_v[rg]
}
]
]
])
}
# output.contrast is to show the different behavior of setintersection() and keys() on local and var
# I think it is a bug?
# as commenting out the region-2 in local and var has different behavior,
# local behaves corretly, not producing region-2 record at all,
# but var transformer produced a region-2 record without ip_cidr_range, which will break the resource code execution
output "contrast" {
value = {
transform-local = local.transform-local
transform-var = local.transform-var
}
}
if uncommenting the region-2 line, setintersection() and keys() behave exactly the same on local and var:
output:
contrast : {
transform-local : [
{
name : "region-1-subnet_local"
ip_cidr_range : "cidr-for-region-1-for-local-obj"
region : "region-1"
}
{
name : "region-2-subnet_local"
ip_cidr_range : "cidr-for-region-2-for-local-obj"
region : "region-2"
}
]
transform-var : [
{
name : "region-1-subnet_var"
ip_cidr_range : "cidr-for-region-1-for-var-obj"
region : "region-1"
}
{
name : "region-2-subnet_var"
ip_cidr_range : "cidr-for-region-2-for-var-obj"
region : "region-2"
}
]
}
but if I comment out the region-2 line (as shown in code above), transform-var output is not desirable as it produces something that will cause malfunctioning
output:
contrast : {
transform-local : [
{
name : "region-1-subnet_local"
ip_cidr_range : "cidr-for-region-1-for-local-obj"
region : "region-1"
}
]
transform-var : [
{
name : "region-1-subnet_var"
ip_cidr_range : "cidr-for-region-1-for-var-obj"
region : "region-1"
}
{
name : "region-2-subnet_var"
region : "region-2"
}
]
}

In terraform, how to loop over a list of objects and update values?

I will be providing values with terragrunt. The variable, push_subscriptions, is a list of maps and I want to modify the values of the map. For example, append a prefix to the push subscription name in place like so (within the main.tf):
push_subscriptions[index]['name'] = "$pbsb-push-${var.product_environment_code}-push_subscriptions[index]['name']"
main.tf
module "pubsub" {
push_subscriptions = var.push_subscriptions
}
terragrunt.hcl
include "product_vars" {
path = find_in_parent_folders("_terragrunt.hcl")
}
inputs = {
push_subscriptions = [
{
name = "push-sub-1"
ack_deadline_seconds = 20
push_endpoint = "https://example.com"
},
{
name = "push-sub-2"
ack_deadline_seconds = 20
push_endpoint = "https://example.com"
}
]
}
Shouldn't be a problem. Just create a local where you'll be using it, that iterates over the list and returns another list of objects with the updated values.
In this example, local.subs is used in lieu of your variable, but you would just replace local.subs with var.push_subscriptions in your case.
locals {
subs = [
{ name = "foo" },
{ name = "bar" },
]
updated = [for sub in local.subs : { name = "some-prefix-${sub.name}" }]
}
output "updated" {
value = local.updated
}
Which gives:
Changes to Outputs:
+ updated = [
+ {
+ name = "some-prefix-foo"
},
+ {
+ name = "some-prefix-bar"
},
]
So that is a new value you can use with prefixes.
Or you could do this entirely in line, with something like:
module "pubsub" {
push_subscriptions = [for sub in var.push_subscriptions : merge(sub, {
name = "pbsb-push-${var.product_environment_code}-some-prefix-${sub.name}"
})]
}
Using merge here allows you to maintain all the other values.

Extract Values from terraform set variable

I am trying to provision aws service catalog product using terraform resource
resource "aws_servicecatalog_provisioned_product" "example" {}
Terraform resource output description
one of the export value of the resource is outputs which is in form of set and i am collecting that into an output variable using below
output "Provisioned_Product_Outputs" {
value = aws_servicecatalog_provisioned_product.example.outputs
}
Output Looks Like
Provisioned_Product_Outputs = toset([
{
"description" = "Backup plan"
"key" = "BackupPlan"
"value" = "light"
},
{
"description" = "Current user zone to run"
"key" = "CurrentAZ"
"value" = "primary"
},
{
"description" = "InstanceID of Vm"
"key" = "EC2InstanceID"
"value" = "i-04*******"
},
{
"description" = "InstanceHostName"
"key" = "InstanceHostName"
"value" = "{\"fqdn\":\"foo.domain.com\"}"
},
{
"description" = "The ARN of the launched Cloudformation Stack"
"key" = "CloudformationStackARN"
"value" = "arn:aws:cloudformation:{region}:{AccountID}:stack/SC-{AccountID}-pp-iy******"
},
])
i would like to have only selected outputs values rather than entire set like below.
output "EC2InstanceID" {
value = "i-04*******"
}
output "InstanceHostName" {
value = ""{\"fqdn\":\"foo.domain.com\"}""
}
output "CloudformationStackARN" {
value = "arn:aws:cloudformation:{region}:{AccountID}:stack/SC-{AccountID}-pp-iy******"
}
Is there a way to apply or have some condition which allows me to check for the right values using key value pair and apply the value in the outputs
regards
Since you know that your output is set, you can create a filter on the objects inside the set using contains:
output "outputs" {
value = {
for output in aws_servicecatalog_provisioned_product.example.outputs : output.key =>
output.value if contains(["EC2InstanceID", "InstanceHostName", "CloudformationStackARN"], output.key)
}
}
The output will be similar to this:
outputs = {
"CloudformationStackARN" = "arn:aws:cloudformation:{region}:{AccountID}:stack/SC-{AccountID}-pp-iy******"
"EC2InstanceID" = "i-04*******"
"InstanceHostName" = "{\"fqdn\":\"foo.domain.com\"}"
}
If you want to have separate outputs, you have to type out each output manually:
output "EC2InstanceID" {
value = [for output in aws_servicecatalog_provisioned_product.example.outputs : output.value if output.key == "EC2InstanceID"][0]
}
output "InstanceHostName" {
value = [for output in aws_servicecatalog_provisioned_product.example.outputs : output.value if output.key == "InstanceHostName"][0]
}
output "CloudformationStackARN" {
value = [for output in aws_servicecatalog_provisioned_product.example.outputs : output.value if output.key == "CloudformationStackARN"][0]
}
You can not have a for_each attribute for outputs. Currently resource and module blocks support for_each attributes.

terraform flatten loop for 3 times

Has any one tried to get flatten for 3 loops working? I keep getting an error when i try the 3rd one:
I am trying to loop through a list within a data resource - data.instances.sg.ids
Variable example:
alarms = [
{
instances = data.instances.ids ## a list of instance ids
config = [
metric_name = "disk_free"
threshold = "GreaterThan"
]
}
]
locals {
configs = flatten([
for config_key, config_list in var.alarms : [
for instance in config_list.instances : {
for config in config_list.configs : {
instance_id = instance
metric_name = config.name
threshold = config.threshold
}
}
]
])
}
how can i properly loop through and flatten this list with data instances list.
Thanks
Not sure what exactly you want to construct, but I think it should be:
locals {
configs = flatten([
for config_key, config_list in var.alarms : [
for instance in config_list.instances : [
for config in config_list.configs :
{
instance_id = instance
metric_name = config.name
threshold = config.threshold
}
]
]
])
}

Iterate over list of list of maps in terraform

Consider I have a variable that is a list of list of maps.
Example:
processes = [
[
{start_cmd: "a-server-start", attribute2:"type_a"},
{start_cmd: "a-worker-start", attribute2:"type_b"}
{start_cmd: "a--different-worker-start", attribute2:"type_c"}
],
[
{start_cmd: "b-server-start", attribute2:"type_a"},
{start_cmd: "b-worker-start", attribute2:"type_b"}
]
]
In each iteration, I need to take out the array of maps, then iterate over that array and take out the values of the map. How do I achieve this in terraform?
I have considered having two counts and doing some arithmetic to trick terraform into performing a lookalike nested iteration Check reference here. But in our case the number of maps in the inner array can vary.
Also we are currently using the 0.11 terraform version but dont mind using the alpha 0.12 version of terraform if it is possible to achieve this in that version.
Edit:
Added how I would use this variable:
resource “create_application” “applications” {
// Create a resource for every array in the variable processes. 2 in this case
name = ""
migration_command = ""
proc {
// For every map create this attribute for the resource.
name = ““
init_command = “a-server-start”
type = “server”
}
}
Not sure if this clears up the requirement. Please do ask if it is still not clear.
Using terraform 0.12.x
locals {
processes = [
[
{ start_cmd: "a-server-start", type: "type_a", name: "inglorious bastards" },
{ start_cmd: "a-worker-start", type: "type_b", name: "kill bill" },
{ start_cmd: "a--different-worker-start", type: "type_c", name: "pulp fiction" },
],
[
{ start_cmd: "b-server-start", type: "type_a", name: "inglorious bastards" },
{ start_cmd: "b-worker-start", type: "type_b", name: "kill bill" },
]
]
}
# just an example
data "archive_file" "applications" {
count = length(local.processes)
type = "zip"
output_path = "applications.zip"
dynamic "source" {
for_each = local.processes[count.index]
content {
content = source.value.type
filename = source.value.name
}
}
}
$ terraform apply
data.archive_file.applications[0]: Refreshing state...
data.archive_file.applications[1]: Refreshing state...
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
If a create_application resource existed, it can be modeled like so
resource "create_application" "applications" {
count = length(local.processes)
name = ""
migration_command = ""
dynamic "proc" {
for_each = local.processes[count.index]
content {
name = proc.value.name
init_command = proc.value.start_cmd
type = proc.value.type
}
}
}
Here is my solution that work like charm. Just note the tricks google_service_account.purpose[each.value["name"]].name where I can retrieve the named array element by using its name.
variable "my_envs" {
type = map(object({
name = string
bucket = string
}))
default = {
"dev" = {
name = "dev"
bucket = "my-bucket-fezfezfez"
}
"prod" = {
name = "prod"
bucket = "my-bucket-ezaeazeaz"
}
}
}
resource "google_service_account" "purpose" {
for_each = var.my_envs
display_name = "blablabla (terraform)"
project = each.value["name"]
account_id = "purpose-${each.value["name"]}"
}
resource "google_service_account_iam_binding" "purpose_workload_identity_binding" {
for_each = var.my_envs
service_account_id = google_service_account.purpose[each.value["name"]].name
role = "roles/iam.whatever"
members = [
"serviceAccount:${each.value["name"]}.svc.id.goog[purpose/purpose]",
]
}
resource "google_storage_bucket_iam_member" "purpose_artifacts" {
for_each = var.my_envs
bucket = each.value["bucket"]
role = "roles/storage.whatever"
member = "serviceAccount:${google_service_account.purpose[each.value["name"]].email}"
}

Resources