map list of maps to a list of selected field values in terraform - terraform

If resources use a count parameter to specify multi resources in terraform there is a simple syntax for providing a list/array of dedicated fields for the resource instances.
for example
aws_subnet.foo.*.id
Since quite a number of versions it is possible to declare variables with a complex structure, for example lists of maps.
variable "data" {
type = "list"
default = [
{
id = "1"
...
},
{
id = "10"
...
}
]
}
I'm looking for a possibility to do the same for varaibles I can do for multi resources: a projection of an array to an array of field values of the array elements.
Unfortunately
var.data.*.id
does not work as for resources. Is there any possibility to do this?

UPDATE
Massive fancy features have been added into terraform since Terraform 0.12 was released, e.g., list comprehension, with which the solution is super easy.
locals {
ids = [for d in var.data: d.id]
#ids = [for d in var.data: d["id"]] #same
}
# Then you could get the elements this way,
# local.ids[0]
Solution before terraform 0.12
template_file can help you out.
data "template_file" "data_id" {
count = "${length(var.data)}"
template = "${lookup(var.data[count.index], "id")}"
}
Then you get a list "${data.template_file.data_id.*.rendered}", whose elements are value of "id".
You can get its element by index like this
"${data.template_file.data_id.*.rendered[0]}"
or through function element()
"${element(data.template_file.data_id.*.rendered, 0)}"

NOTE: This answer and its associated question are very old at this point, and this answer is now totally stale. I'm leaving it here for historical reference, but nothing here is true of modern Terraform.
At the time of writing, Terraform doesn't have a generalized projection feature in its interpolation language. The "splat syntax" is implemented as a special case for resources.
While deep structure is possible, it is not yet convenient to use, so it's recommended to still keep things relatively flat. In future it is likely that new language features will be added to make this sort of thing more usable.

If have found a working solution using template rendering to by-pass the list of map's issue:
resource "aws_instance" "k8s_master" {
count = "${var.master_count}"
ami = "${var.ami}"
instance_type = "${var.instance_type}"
vpc_security_group_ids = ["${aws_security_group.k8s_sg.id}"]
associate_public_ip_address = false
subnet_id = "${element(var.subnet_ids,count.index % length(var.subnet_ids))}"
user_data = "${file("${path.root}/files/user_data.sh")}"
iam_instance_profile = "${aws_iam_instance_profile.master_profile.name}"
tags = "${merge(
local.k8s_tags,
map(
"Name", "k8s-master-${count.index}",
"Environment", "${var.environment}"
)
)}"
}
data "template_file" "k8s_master_names" {
count = "${var.master_count}"
template = "${lookup(aws_instance.k8s_master.*.tags[count.index], "Name")}"
}
output "k8s_master_name" {
value = [
"${data.template_file.k8s_master_names.*.rendered}",
]
}
This will result in the following output:
k8s_master_name = [
k8s-master-0,
k8s-master-1,
k8s-master-2
]

A potentially simpler answer is to use the zipmap function.
Starting with an environment variable map compatible with ECS template definitions:
locals {
shared_env = [
{
name = "DB_CHECK_NAME"
value = "postgres"
},
{
name = "DB_CONNECT_TIMEOUT"
value = "5"
},
{
name = "DB_DOCKER_HOST_PORT"
value = "35432"
},
{
name = "DB_DOCKER_HOST"
value = "localhost"
},
{
name = "DB_HOST"
value = "my-db-host"
},
{
name = "DB_NAME"
value = "my-db-name"
},
{
name = "DB_PASSWORD"
value = "XXXXXXXX"
},
{
name = "DB_PORT"
value = "5432"
},
{
name = "DB_QUERY_TIMEOUT"
value = "30"
},
{
name = "DB_UPGRADE_TIMEOUT"
value = "300"
},
{
name = "DB_USER"
value = "root"
},
{
name = "REDIS_DOCKER_HOST_PORT"
value = "6380"
},
{
name = "REDIS_HOST"
value = "my-redis"
},
{
name = "REDIS_PORT"
value = "6379"
},
{
name = "SCHEMA_SCRIPTS_PATH"
value = "db-scripts"
},
{
name = "USE_LOCAL"
value = "false"
}
]
}
In the same folder launch terraform console for testing built-in functions. You may need to terraform init if you haven't already.
terraform console
Inside the console type:
zipmap([for m in local.shared_env: m.name], [for m in local.shared_env: m.value])
Observe the output of each list-item-map being a name-value-pair of a single map:
{
"DB_CHECK_NAME" = "postgres"
"DB_CONNECT_TIMEOUT" = "5"
"DB_DOCKER_HOST" = "localhost"
"DB_DOCKER_HOST_PORT" = "35432"
"DB_HOST" = "my-db-host"
"DB_NAME" = "my-db-name"
"DB_PASSWORD" = "XXXXXXXX"
"DB_PORT" = "5432"
"DB_QUERY_TIMEOUT" = "30"
"DB_UPGRADE_TIMEOUT" = "300"
"DB_USER" = "root"
"REDIS_DOCKER_HOST_PORT" = "6380"
"REDIS_HOST" = "my-redis"
"REDIS_PORT" = "6379"
"SCHEMA_SCRIPTS_PATH" = "db-scripts"
"USE_LOCAL" = "false"
}

Related

Is there possibility to dynamicly pass user-defined variable (key = value) to terraform module?

There is resource:
resource "resource_name" "foo" {
name = "test"
config {
version = 14
resources {
disk_type_id = "network-ssd"
}
postgresql_config = {
enable_parallel_hash = true
}
}
}
I need a module which accepts optional user variables in "postgresql_config". There can be many such variables.
I tried next:
variables.tf
variable "postgresql_config" {
description = "User defined for postgresql_config"
type = list(object({
# key1 = value1
# ...
# key50 = value50
}))
}
variable "config" {
description = "for dynamic block 'config' "
type = list(object({
version = number
}))
default = [{
version = 14
}]
}
variable "resources" {
description = "for dynamic block 'resources' "
type = list(object({
disk_type_id = string
}))
default = [{
disk_type_id = "network-hdd"
}]
}
module/postgresql/main.tf
resource "resource_name" "foo" {
name = "test"
dynamic "config" {
for_each = var.config
content {
version = config.value["version"]
dynamic "resources" {
for_each = var.resources
content {
disk_type_id = resources.value["disk_type_id"]
}
}
# problem is here
postgresql_config = {
for_each = var.postgresql_config
each.key = each.value
}
}
}
example/main.tf
module "postgresql" {
source = "../module/postgresql"
postgresql_config = [{
auto_explain_log_buffers = true
log_error_verbosity = "LOG_ERROR_VERBOSITY_UNSPECIFIED"
max_connections = 395
vacuum_cleanup_index_scale_factor = 0.2
}]
That is, I understand that I need to use "dynamic", but it can only be applied to the block "config" and the nested block "resource_name".
How can I pass values for "postgresql_config" from main.tf to module? Of course, my example with for_each = var.postgresql_config doesn't work, but I hope this way to give an idea of what I need.
Or does terraform have no such option to use custom variables dynamically at all, and all of them must be specified explicitly?
Any help would be appreciated, thank you
from what I understand , you are trying to create a map dynamically for your resource postgres_config.
I would recommend using a for expression to solve that problem.
However, I think your problem lies in how you have defined variables for your module . You might run into a problem if your postgress_config list has multiple configs in it because that config can only take a map by the looks of it.
have a look at the following documentation:
this one is for how to define your variables
https://www.terraform.io/language/expressions/dynamic-blocks#multi-level-nested-block-structures
for expressions
https://www.terraform.io/language/expressions/for
my solution for your config problem ,would be something like this assuming that the postgres_config list has one element all the time:
# problem is here
postgresql_config = var.postgresql_config[0]

terraform for_each implementation with values from .tfvars

I have a common.tfvars file with definition of a variables as:
bqtable_date_partition = [
{ dataset = "d1", table_name = "d1-t1", part_col = "partition_date",
part_type = "DAY", schema_file = "data_tables/d1-t1.json" },
{ dataset = "d1", table_name = "d1-t2", part_col = "tran_dt",
part_type = "DAY", schema_file = "data_tables/d1-t2.json" },
{ dataset = "d2", table_name = "d2-t1", part_col = "tran_dt",
part_type = "DAY", schema_file = "data_tables/d2-t1.json" },
]
and I am referencing this var in main.tf file with following resource defintion:
resource "google_bigquery_table" "bq_tables_dt_pt" {
count = length(var.bqtable_date_partition)
project = var.project_id
dataset_id = "${var.bqtable_date_partition[count.index].dataset}_${var.env}"
table_id = var.bqtable_date_partition[count.index].table_name
time_partitioning {
type = var.bqtable_date_partition[count.index].part_type
field = var.bqtable_date_partition[count.index].part_col
}
schema = file("${path.module}/tables/${var.bqtable_date_partition[count.index].schema_file}")
depends_on = [google_bigquery_dataset.crte_bq_dataset]
labels = {
env = var.env
ind = "corp"
}
}
I want to change the resource definition to use "for_each" instead of "count" to loop through the list:
My motive to change from count to for_each is to eliminate the dependency on the order in which I have written the elements of the variable "bqtable_date_partition "
I did this:
resource "google_bigquery_table" "bq_tables_dt_pt" {
for_each = var.bqtable_date_partition
project = var.project_id
dataset_id = "${each.value.dataset}_${var.env}"
table_id = each.value.table_name
time_partitioning {
type = each.value.part_type
field = each.value.part_col
}
schema = file("${path.module}/tables/${each.value.schema_file}")
depends_on = [google_bigquery_dataset.crte_bq_dataset]
labels = {
env = var.env
ind = "corp"
}
}
I got the following error as expected:
The given "for_each" argument value is unsuitable: the "for_each"
argument must be a map or set of strings, and you have provided a
value of type list of map of string.
Can anyone help me with what changes I need do to make in the resource definition to use "for_each"?
Terraform version - 0.14.x
Error says it only accepts the map or set of strings. So we have to convert our input variable to either map or set of strings.
https://www.terraform.io/docs/language/expressions/for.html
resource "google_bigquery_table" "bq_tables_dt_pt" {
for_each = { for index, data_partition in var.bqtable_date_partition : index => data_partition }
project = var.project_id
dataset_id = "${each.value.dataset}_${var.env}"
table_id = each.value.table_name
time_partitioning {
type = each.value.part_type
field = each.value.part_col
}
schema = file("${path.module}/tables/${each.value.schema_file}")
depends_on = [google_bigquery_dataset.crte_bq_dataset]
labels = {
env = var.env
ind = "corp"
}
}
So basically, here we are converting for_each input into the following format. and only referencing value in from newly created map.
{
"0" = {
"dataset" = "d1"
"part_col" = "partition_date"
"part_type" = "DAY"
"schema_file" = "data_tables/d1-t1.json"
"table_name" = "d1-t1"
}
"1" = {
"dataset" = "d1"
"part_col" = "tran_dt"
"part_type" = "DAY"
"schema_file" = "data_tables/d1-t2.json"
"table_name" = "d1-t2"
}
"2" = {
"dataset" = "d2"
"part_col" = "tran_dt"
"part_type" = "DAY"
"schema_file" = "data_tables/d2-t1.json"
"table_name" = "d2-t1"
}
}
There are two main requirements for using for_each:
You must have a collection that has one element for each resource instance you want to declare.
There must be some way to derive a unique identifier from each element of that collection which Terraform will then use as the unique instance key.
It seems like your collection meets both of these criteria, assuming that table_name is a unique string across all of those values, and so all that remains is to project the collection into a map so that Terraform can see from the keys that you intend to use the table_name for the unique tracking keys:
resource "google_bigquery_table" "bq_tables_dt_pt" {
for_each = {
for o in var.bqtable_date_partition : o.table_name => o
}
# ...
}
Here I've used a for expression to project from a sequence to a mapping, where each element is identified by the value in its table_name attribute.
If you are in a situation where you're able to change the interface to this module then you could simplify things by changing the variable's declaration to expect a map instead of a list, which would then avoid the need for the projection and make it explicit to the module caller that the table IDs must be unique:
variable "bqtable_date_partition" {
type = map(object({
dataset = string
part_col = string
part_type = string
schema_file = string
}))
}
Then you could just assign var.bqtable_date_partition directly to for_each as you tried before, because it'll already be of a suitable type. But would also require changing your calling module to pass a map value instead of a list value, and so this might not be practical if your module has many callers that would all need to be updated to remain compatible.

Get resources based on a value created using count

I am using Terraform v12.19 with the aws provider v2.34.0.
Imagine, I have a resource generated with a count value:
resource "aws_iam_role" "role" {
count = length(var.somevariable)
name = var.somevariable[count.index]
}
Later on, I want to reference one specific resource instance in that way, e. g.:
resource "aws_iam_role_policy_attachment" "polatt" {
role = aws_iam_role.role["TheRoleNameIWant"].id
policy_arn = "arn:aws:iam::aws:policy/..."
}
I don't know the index, I can just rely on the name, provided by the variable. Thats because the values of the variable are provided by an external source and the order could change...
Any ideas how to do this?
You should be able to accomplish this using the index terraform function.
Here's a minimal example using null_resources to test it out
locals {
role_names = [
"role-a",
"role-b",
"role-c",
"role-d",
]
target_role_name = "role-c"
}
resource "null_resource" "hi" {
count = length(local.role_names)
}
output "target_resource" {
value = null_resource.hi[index(local.role_names, local.target_role_name)].id
}
output "all_resources" {
value = [for r in null_resource.hi : r.id]
}
This outputs, for example
all_resources = [
"4350570701002192774",
"9173388682753384584",
"1634695740603384613",
"2098863759573339880",
]
target_resource = 1634695740603384613
So your example, I suppose, would look like
resource "aws_iam_role_policy_attachment" "polatt" {
role = aws_iam_role.role[index(var.somevariable, "TheRoleNameIWant")].id
policy_arn = "arn:aws:iam::aws:policy/..."
}
Update
Your comment below mentions that you actually have a more complicated data structure than just a list of names. I just wanted to mention that you can derive names from your JSON structure.
Assuming you have something like the following
variable "role_values" {
value = [
{
name = "foo",
other = "details",
fields = 3
},
{
name = "bar",
other = "yet more details",
fields = 3
}
]
}
you could derive just the names by using a local and the newer for loops TF 0.12 offers
locals {
role_names = [for role in var.role_values: role.name]
}
That way you don't have to store the names twice.

How can I pass a comma separated array to a resource in terraform v0.12.0?

In the following code block I'm trying to pass an array of server names to the attributes_json block:
resource "aws_instance" "consul-server" {
ami = var.consul-server
instance_type = "t2.nano"
key_name = var.aws_key_name
iam_instance_profile = "dna_inst_mgmt"
vpc_security_group_ids = [
"${aws_security_group.yutani_consul.id}",
"${aws_security_group.yutani_ssh.id}"
]
subnet_id = "${aws_subnet.public_1_subnet_us_east_1c.id}"
associate_public_ip_address = true
tags = {
Name = "consul-server${count.index}"
}
root_block_device {
volume_size = "30"
delete_on_termination = "true"
}
connection {
type = "ssh"
user = "chef"
private_key = "${file("${var.aws_key_path}")}"
timeout = "2m"
agent = false
host = self.public_ip
}
count = var.consul-server_count
provisioner "chef" {
attributes_json = <<-EOF
{
"consul": {
"servers": ["${split(",",aws_instance.consul-server[count.index].id)}"]
}
}
EOF
use_policyfile = true
policy_name = "consul_server"
policy_group = "aws_stage_enc"
node_name = "consul-server${count.index}"
server_url = var.chef_server_url
recreate_client = true
skip_install = true
user_name = var.chef_username
user_key = "${file("${var.chef_user_key}")}"
version = "14"
}
}
Running this gives me an error:
Error: Cycle: aws_instance.consul-server[1], aws_instance.consul-server[0]
(This is after declaring a count of 2 in a variable for var.consul-server_count)
Can anyone tell me what the proper way is to do this?
There are two issues here: (1) How to interpolate a comma-separated list in a JSON string ; and (2) What is causing the cyclic dependency error.
How to interpolate a list to make a valid JSON array
Use jsonencode
The cleanest method is to not use a heredoc at all and just use the jsonencode function.
You could do this:
locals {
arr = ["host1", "host2", "host3"]
}
output "test" {
value = jsonencode(
{
"consul" = {
"servers" = local.arr
}
})
}
And this yields as output:
Outputs:
test = {"consul":{"servers":["host1","host2","host3"]}}
Use the join function and a heredoc
The Chef provisioner's docs suggest to use a heredoc for the JSON string, so you can also do this:
locals {
arr = ["host1", "host2", "host3"]
sep = "\", \""
}
output "test" {
value = <<-EOF
{
"consul": {
"servers": ["${join(local.sep, local.arr)}"]
}
}
EOF
}
If I apply that:
Outputs:
test = {
"consul": {
"servers": ["host1", "host2", "host3"]
}
}
Some things to pay attention to here:
You are trying to join your hosts so that they become valid JSON in the context of a JSON array. You need to join them with ",", not just a comma. That's why I've defined a local variable sep = "\", \"".
You seem to be trying to split there when you apparently need join.
Cyclic dependency issue
The cause of the error message:
Error: Cycle: aws_instance.consul-server[1], aws_instance.consul-server[0]
Is that you have a cyclic dependency. Consider this simplified example:
resource "aws_instance" "example" {
count = 3
ami = "ami-08589eca6dcc9b39c"
instance_type = "t2.micro"
user_data = <<-EOF
hosts="${join(",", aws_instance.example[count.index].id)}"
EOF
}
Or you could use splat notation there too for the same result i.e. aws_instance.example.*.id.
Terraform plan then yields:
▶ terraform012 plan
...
Error: Cycle: aws_instance.example[2], aws_instance.example[1], aws_instance.example[0]
So you get a cycle error there because aws_instance.example.*.id depends on the aws_instance.example being created, so the resource depends on itself. In other words, you can't use a resources exported values inside the resource itself.
What to do
I don't know much about Consul, but all the same, I'm a bit confused tbh why you want the EC2 instance IDs in the servers field. Wouldn't the Consul config be expecting IP addresses or hostnames there?
In any case, you probably need to calculate the host names yourself outside of this resource, either as a static input parameter or something that you can calculate somehow. And I imagine you'll end up with something like:
variable "host_names" {
type = list
default = ["myhost1"]
}
resource "aws_instance" "consul_server" {
...
provisioner "chef" {
attributes_json = jsonencode(
{
"consul" = {
"servers" = var.host_names
}
})
}
}

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