Selecting specific items in a map - terraform

Terraform Version 0.14
I have a JSON file that has multiple objects. I get that file using the local_file data source.
data "local_file" "getJson" {
filename = "${path.module}/sampleObject.json"
}
sampleObject.json:
{
"config":[
{
"id":1,
"type":"typeA",
"vm_name":"Linux1"
},
{
"id":2,
"type":"typeB",
"vm_name":"Windows1"
},
{
"id":3,
"type":"typeB",
"vm_name":"Windows2"
}
]
}
I use a local variable to save and convert the contents of my JSON file to a usable Terraform objects with the jsondecode function. I believe the jsondecode function is converting my JSON object to a map ( I could be wrong). One of the attributes of my JSON object(s) is type. There are two possible values for type (typeA & typeB). I need a way to select all the objects in my JSON object that match a specific type (ie: type == "typeA"). The roadblock for me has been making sure the new collection is either a map or another collection that can be converted to a map using the tomap() function.
My sample code below does exactly what I need it to do using a for loop except... it saves it as a Tuple. In the For loop documentation, there is alternative to save it to an object using {} but I haven't been able to figure out how that works in my given example.
I've reached a point where I think I'm going about the problem wrong with using a for loop. I could use some advise on this. I also think If I can get the for loop to work using {} so the results are saved as an object, I should be able to convert that object to a map using the tomap() function without any issues.
I'm still new to Terraform and any guidance would be appreciated.
For loop reference
locals {
typeA = [ for x in jsondecode(data.local_file.getJson.content).config : x if x.type == "typeA" ]
typeB = [for x in jsondecode(data.local_file.getJson.content).config : x if x.type == "typeB"]
}
This is a redacted sample of how I would use the results.
module "typeABuilder" {
for_each = local.typeA
vm_name = each.value.vm_name
// Assign each attribute from the typeA objects
// ...
// ...
}
module "typeBBuilder" {
for_each = local.typeB
vm_name = each.value.vm_name
// Assign each attribute from the typeB objects
// ...
// ...
}

Related

Merging complex types

I'm struggling with merging maps that have duplicate keys, since the built-in merge function will only keep the latest argument that has matching keys or attributes.
My maps are of the following shape:
{mykey = ["item1","item2","item3"]}
{mykey = ["item4","item5","item6"]}
The merge function simply returns {mykey = ["item4","item5","item6"]} (or whatever is the last argument). I'd like to compose a map like
{mykey = ["item1","item2","item3","item4","item5","item6"]}
I don't believe there's a function I can use to achieve this. However, I suspect that a for loop is the right approach, yet my knowledge is failing me.
Hi #MattSchuchard, thanks for your help. The parent map structure (if I am following you correctly), is a local variable. I am using keys and values from this local in order to retrieve object_ids from the azuread_users and azuread_groups data sources. My locals looks like this:
locals {
roles = {
role1 = {
users = ["user1#domain.com","user2#domain.com"]
groups = ["myGroup1","myGroup2"]
}
}
I am referencing this local in the data sources as follows:
data azuread_users.lookup {
for_each = local.roles
user_principal_names = each.values.users
}
data azuread_groups.lookup {
for_each = local.roles
display_names = each.values.groups
}
These data sources export object_ids as an attribute (which I will then use when adding members to groups in a resource block). The exported attributes are in the shape detailed in my original post.
I would like to compose a combined list of all object_ids exported from both data.azuread_users.lookup and data.azuread_groups.lookup which I can then provide as an argument when creating a group:
resource "azuread_group" "my_group" {
members = $myCombinedListOfObjectIDs
}
Depending on whether you want a list/map you can use this:
locals {
input = { mykey = ["item1", "item2", "item3"] }
input2 = { mykey = ["item4", "item5", "item6"] }
merged = concat(local.input.mykey, local.input2.mykey)
merged_mykey = {
mykeys = concat(local.input.mykey, local.input2.mykey)
}
}
The output is
> local.merged_mykey
{
"mykeys" = [
"item1",
"item2",
"item3",
"item4",
"item5",
"item6",
]
}
> local.merged
[
"item1",
"item2",
"item3",
"item4",
"item5",
"item6",
]
If you're worried about duplicates you can wrap it in a distinct call. https://www.terraform.io/language/functions/distinct

How to access a local using a variable in Terraform

I have the following code.
mymodule
variable "senses" {
type = string
}
locals {
sounds = {
"cat" = "meow"
"dog" = ["bark", "woof"]
}
}
output "noise" {
value = local[var.senses]["cat"]
}
call mymodule
module "mymodule" {
source = "../../../modules/mymodule"
senses = "sound"
}
returns error:
Error: Invalid reference
on ../../../modules/mymodule/outputs.tf line 62, in output "noise":
62: value = local[var.senses]["cat"]
The "local" object cannot be accessed directly. Instead, access one of its
attributes.
my code can't seem to handle
value = local[var.senses]["cat"]
Any suggestions on how i can get this to work?
I don't believe it's possible to use a variable to switch which local you're reading. I.e. local[var.senses] is the root of the issue.
If you refactor slightly and put your values inside a single, known, value--such as local.senses it should then let you do a key lookup within that value.
So, if you modify your locals to place your values in a senses key:
locals {
senses = {
"sounds" = {
"cat" = "meow"
"dog" = ["bark", "woof"]
}
}
}
and update your lookup to use that field:
value = local.senses[var.senses]["cat"]
Then I believe it will work, since your are doing a key lookup against a specific local rather than trying to dynamically select the local.

Iterate Through Map of Maps in Terraform 0.12

I need to build a list of templatefile's like this:
templatefile("${path.module}/assets/files_eth0.nmconnection.yaml", {
interface-name = "eth0",
addresses = element(values(var.virtual_machines), count.index),
gateway = element(var.gateway, count.index % length(var.gateway)),
dns = join(";", var.dns_servers),
dns-search = var.domain,
}),
templatefile("${path.module}/assets/files_etc_hostname.yaml", {
hostname = element(keys(var.virtual_machines), count.index),
}),
by iterating over a map of maps like the following:
variable templatefiles {
default = {
"files_eth0.nmconnection.yaml" = {
"interface-name" = "eth0",
"addresses" = "element(values(var.virtual_machines), count.index)",
"gateway" = "element(var.gateway, count.index % length(var.gateway))",
"dns" = "join(";", var.dns_servers)",
"dns-search" = "var.domain",
},
"files_etc_hostname.yaml" = {
"hostname" = "host1"
}
}
}
I've done something similar with a list of files:
file("${path.module}/assets/files_90-disable-console-logs.yaml"),
file("${path.module}/assets/files_90-disable-auto-updates.yaml"),
...but would like to expand this to templatefiles (above).
Here's the code I've done for the list of files:
main.tf
variable files {
default = [
"files_90-disable-auto-updates.yaml",
"files_90-disable-console-logs.yaml",
]
}
output "snippets" {
value = flatten(module.ingition_snippets.files)
}
modules/main.tf
variable files {}
resource "null_resource" "files" {
for_each = toset(var.files)
triggers = {
snippet = file("${path.module}/assets/${each.value}")
}
}
output "files" {
value = [for s in null_resource.files: s.triggers.*.snippet]
}
Appreciate any help!
Both of these use-cases can be met without using any resource blocks at all, because the necessary features are built in to the Terraform language.
Here is a shorter way to write the example with static files:
variable "files" {
type = set(string)
}
output "files" {
value = tomap({
for fn in var.files : fn => file("${path.module}/assets/${fn}")
})
}
The above would produce a map from filenames to file contents, so the calling module can more easily access the individual file contents.
We can adapt that for templatefile like this:
variable "template_files" {
# We can't write down a type constraint for this case
# because each file might have a different set of
# template variables, but our later code will expect
# this to be a mapping type, like the default value
# you shared in your comment, and will fail if not.
type = any
}
output "files" {
value = tomap({
for fn, vars in var.template_files : fn => templatefile("${path.module}/assets/${fn}", vars)
})
}
Again, the result will be a map from filename to the result of rendering the template with the given variables.
If your goal is to build a module for rendering templates from a source directory to publish somewhere, you might find the module hashicorp/dir/template useful. It combines fileset, file, and templatefile in a way that is hopefully convenient for static website publishing and similar use-cases. (At the time I write this the module is transitioning from being in my personal GitHub account to being in the HashiCorp organization, so if you look at it soon after you may see some turbulence as the docs get updated, etc.)

How to pass a dynamic value in a variable name on run-time?

Here is what I have:
locals {
timeseries = "desktop"
}
dynamic "request" {
for_each = var.query_"#{local.timeseries}"_timeseries
content {
q = request.value.q
type = request.value.type
style = request.value.style
}
}
What I expect:
for_each = var.query_desktop_timeseries
If I'm understanding your question correctly, you're trying to resolve a variable name via interpolation. In terraform, there's is no way to do this.
If you're looking to resolve to a particular list of values, based on the value of variables, you could do that using a map to, well, map from your value to the variables they resolve to.
For example you could have something like
locals {
timeseries = "desktop"
timeseries_lookup = {
desktop = var.query_desktop_timeseries
# Other mappings would go here
}
}
This could then be used, very similarly to your desired use-case, like the following
for_each = local.timeseries_lookup[local.timeseries]

Terraform - Variable inside a variable

I would like to use a variable inside a variable.
This is my resource:
resource "aws_route" "vpc_peering_accepter" {
provider = "aws.accepter"
count = length(data.terraform_remote_state.vpc.outputs.${var.region}-vpc-private_routing_tables)
route_table_id = tolist(data.terraform_remote_state.vpc.outputs.${var.region}-vpc-private_routing_tables)[count.index]
destination_cidr_block = var.vpc_cidr
vpc_peering_connection_id = aws_vpc_peering_connection.peer.*.id[0]
}
Of course this one is not working.
What's the best practice to do it?
Thanks,
Elad
You can combine Local Values with the lookup function to accomplish this.
In the following example the null datasource is mimicking data.terraform_remote_state.vpc.outputs:
variable "region" {
default = "us-east1"
}
locals {
vpc_private_routing_tables = "${var.region}-vpc-private_routing_tables"
}
data "null_data_source" "values" {
inputs = {
us-east1-vpc-private_routing_tables = "11111111"
us-east2-vpc-private_routing_tables = "22222222"
}
}
output "vpc_peering" {
value = lookup(data.null_data_source.values.inputs, local.vpc_private_routing_tables)
}
Because data.terraform_remote_state.vpc.outputs is a mapping, you can use either attribute syntax or index syntax to access the values inside:
Attribute syntax: data.terraform_remote_state.vpc.outputs.us-west-1-vpc-private_routing_tables
Index syntax: data.terraform_remote_state.vpc.outputs["us-west-1-vpc-private_routing_tables"]
An advantage of index syntax is that you can use any expression within those brackets as long as its result is a string. In particular, you can use the template interpolation syntax:
data.terraform_remote_state.vpc.outputs["${var.region}-vpc-private_routing_tables"]
With that said, in this sort of situation where you are producing the same information for a number of different objects -- regions, in this case -- it's more conventional to gather all of these values into a single mapping when you declare the output, so that these related values are explicitly grouped together in a single collection. For example:
output "vpc_private_routing_table_ids" {
value = {
us-east-1 = aws_route_table.us-east-1.id
us-west-2 = aws_route_table.us-west-2.id
}
}
Then from the perspective of the consumer -- that is, the module that is using data "terraform_remote_state" to access these outputs -- this appears as a simple map keyed by region:
data.terraform_remote_state.vpc.outputs.vpc_private_routing_table_ids[var.region]
If you are producing many different objects on a per-region basis then you might choose to gather all of their ids together into a single output, which might be more convenient to use elsewhere:
output "regions" {
value = {
us-east-1 = {
vpc_id = aws_vpc.us-east-1.id
subnet_ids = aws_subnet.us-east-1[*].id
private_route_table_id = aws_route_table.us-east-1.id
}
us-west-1 = {
vpc_id = aws_vpc.us-west-1.id
subnet_ids = aws_subnet.us-west-1[*].id
private_route_table_id = aws_route_table.us-west-1.id
}
}
}
...which would then appear as follows in the consumer module:
data.terraform_remote_state.vpc.outputs.regions[var.region].private_route_table_id
Ultimately you can structure your output values however you like, but I'd recommend choosing a shape that optimizes for clarity in the configuration that is referring to the data. That usually means making the referring expressions as simple as possible, and ideally avoiding complex expressions like string template syntax whenever possible.

Resources