Terraform output from several resources generated with count - terraform

I have these outputs:
output "ec2_id" {
value = aws_instance.ec2instance[*].id
}
output "ec2_name" {
value = aws_instance.ec2instance[*].tags["Name"]
}
output "ec2_mgmt_eip" {
value = aws_eip.eip_mgmt_ec2instance[*].public_ip
}
I want to make an output like:
"<instanceName>: <instanceID> -> <publicIP>"
(all data in same line for same ec2 instance).
In any non-declarative language i can use something like for (var i=0; i<length(myarray);i++) and use "i" as index for each list, in every index concatenate in a new string, but in terraform I can't find how to do it.
Thanks!

Even though you got the answer in the comments, I will add an example. So, the thing you want does exist in terraform as it also has for loops [1]. A for loop along with the right syntax will give you a desired output, which is a map in terraform:
output "ec2_map" {
value = { for i in aws_instance.ec2instance: i.tags.Name => "${i.id}:${i.public_ip}" }
}
The output you said you want is quite similar to this. Also, there is no concept of "same line" in terraform. In this case, since this is a map, the keys will be instance names and value will be a combination of instance id and the public IP, but that will be a string.
[1] https://www.terraform.io/language/expressions/for

Related

Can you retrieve an item from a list using regex in Terraform?

The problem I am trying to solve is I need to identify one of the Azure subnets in a virtual network by part of it's name. This is so I can then later retrieve it's CIDR. I only know part beforehand such as "mgmt-1" or "egress-1". The actual name of the subnet is much longer but will end in something like that. This was my process:
I have the vnet name so I pull all subnets:
data "azurerm_virtual_network" "this" {
name = local.vnet
resource_group_name = "myrg"
}
Now what I wish I could do is this:
locals {
mgmt_index = index(data.azurerm_virtual_network.this.subnets, "*mgmt-1")
mgmt_subnet = data.azurerm_virtual_network.this.subnets[local.mgmt_index]
}
However index wants an exact match, not a regex. Is this possible to do? Perhaps a better way?
Thank you,
It is not possible to directly look up a list item using a regex match, but you can use for expressions to apply arbitrary filters to a collection when constructing a new collection:
locals {
mgmt_subnets = toset([
for s in data.azurerm_virtual_network.this.subnets : s
if can(regex(".*?mgmt-1", s.name))
])
}
In principle an expression like the above could match more than one object, so I wrote this to produce a set of objects that match.
If you expect that there will never be more than one object whose name matches the pattern then you can use Terraform's one function to assert that and then Terraform will check to confirm that there's no more than one element (returning an error if not) and then return that one value.
locals {
mgmt_subnet = one([
for s in data.azurerm_virtual_network.this.subnets : s
if can(regex(".*?mgmt-1", s.name))
])
}
If the condition doesn't match any of the subnet objects then in the first case you'll have an empty set and in the second case you'll have the value null.

How to combine and sort key-value pair in Terraform

since the last update of the Logicmonitor provider in Terraform we're struggling with a sorting isse.
In LogicMonitor the properties of a device are a name-value pair, and they are presented alfabetically by name. Also in API requests the result is alphabetical. So far nothing fancy.
But... We build our Cloud devices using a module. Calling the module we provide some LogicMonitor properties specially for this device, and a lot more are provided in the module itself.
In the module this looks like this:
`
custom_properties = concat([
{
name = "host_fqdn"
value = "${var.name}.${var.dns_domain}"
},
{
name = "ocid"
value = oci_core_instance.server.id
},
{
name = "private_ip"
value = oci_core_instance.server.private_ip
},
{
name = "snmp.version"
value = "v2c"
}
],
var.logicmonitor_properties)
`
The first 4 properties are from the module and combined with anyting what is in var.logicmonitor_properties. On the creation of the device in LogicMonitor all properties are set in the order the are and no problem.
The issue arises when there is any update on a terraform file in this environment. Due to the fact the properties are presented in alphabetical order, Terraform is showing a lot of changes if finds (but which are in fact just a mixed due to sorting).
The big question is: How can I sort the complete list of properties bases on the "name".
Tried to work with maps, sort and several other functions and examples, but got nothing working on key-value pairs. Merging single key's works fine in a map, but how to deal with name/value pairs/
I think you were on the right track with maps and sorting. Terraform maps do not preserve any explicit ordering themselves, and so whenever Terraform needs to iterate over the elements of a map in some explicit sequence it always do so by sorting the keys lexically (by Unicode codepoints) first.
Therefore one answer is to project this into a map and then project it back into a list of objects again. The projection back into list of objects will implicitly sort the map elements by their keys, which I think will get the effect you wanted.
variable "logicmonitor_properties" {
type = list(object({
name = string
value = string
}))
}
locals {
base_properties = tomap({
host_fqdn = "${var.name}.${var.dns_domain}"
ocid = oci_core_instance.server.id
private_ip = oci_core_instance.server.private_ip
"snmp.version" = "v2c"
})
extra_properties = tomap({
for prop in var.logicmonitor_properties : prop.name => prop.value
})
final_properties = merge(local.base_properties, local.extra_properties)
# This final step will implicitly sort the final_properties
# map elements by their keys.
final_properties_list = tolist([
for k, v in local.final_properties : {
name = k
value = v
}
])
}
With all of the above, local.final_properties_list should be similar to the custom_properties structure you showed in your question except that the elements of the list will be sorted by their names.
This solution assumes that the property names will be unique across both base_properties and extra_properties. If there are any colliding keys between both of those maps then the merge function will prefer the value from extra_properties, overriding the element of the same key from base_properties.
First, use the sort() function to sort the keys in alphabetical order:
sorted_keys = sort(keys(var.my_map))
Next, use the map() function to create a new map with the sorted keys and corresponding values:
sorted_map = map(sorted_keys, key => var.my_map[key])
Finally, you can use the jsonencode() function to print the sorted map in JSON format:
jsonencode(sorted_map)```

Is there a way to pass attributes to data source in terraform?

I'm trying to tell data.github_ip_ranges to what name to use so I could create a list of CIDRs and my code look cleaner. I was trying to find answers, but no luck so far.
And I'm trying to see if there is a way of passing my variables to it...
variable "git_services" {
default = ["hooks_ipv4", "dependabot_ipv4", "dependabot_ipv6", "git_ipv4", "hooks_ipv6"]
}
locals {
github_ips = concat(data.github_ip_ranges.git.name) # name is my custom variable
}
Here is my original approach
locals {
github_ips = concat(data.github_ip_ranges.git.hooks_ipv4, data.github_ip_ranges.git.hooks_ipv6,
data.github_ip_ranges.git.dependabot_ipv4, data.github_ip_ranges.git.dependabot_ipv6)
}
Please help if you could. Thank you!
I think I understand what you're trying to accomplish. You would do it like so:
variable "git_services" {
default = ["hooks_ipv4", "dependabot_ipv4", "dependabot_ipv6", "git_ipv4", "hooks_ipv6"]
}
locals {
github_ips = distinct(flatten([
for service in var.git_services:
data.github_ip_ranges.git[service]
]))
}
What this is doing is creating a list of lists, where each element in the first level is for a service, and each element in the second level is a CIDR bock for that service. The flatten function turns this list of lists into a flat list of CIDR blocks. Since the same CIDR might be used for multiple services, and we probably don't want duplicates if we're using this for something like security group rules, we use the distinct function to remove any duplicate CIDR blocks.

Terraform v0.13 - Check if password or secret provided, use a randomly generated one if not

I'm working to fine-tune some of my Terraform modules, specifically around the google_compute_vpn_tunnel, google_compute_router_interface, and google_compute_router_peer resources. I'd like to make things similar to AWS, where pre-shared keys and tunnel interface IP addresses are randomized by default, but can be overridden by the user (provided they are within a certain range).
The random option is working fine. For example, to create a 20-character random password, I do this:
resource "random_password" "RANDOM_PSK" {
length = 20
special = false
}
But, I only want to use this value if an input variable called vpn_shared_secret was not defined. Seems like this should work:
variable "vpn_shared_secret" {
type = string
default = null
}
locals {
vpn_shared_secret = try(var.vpn_shared_secret, random_password.RANDOM_PSK.result)
}
resource "google_compute_vpn_tunnel" "VPN_TUNNEL" {
shared_secret = local.vpn_shared_secret
}
Instead, it seems to ignore the vpn_shared_secret input variable and just go with the randomly generated one each time.
Is try() the correct way to be doing this? I'm just now learning Terraform if/else and map statements.
How about the coalesce() function?
The coalesce function takes any number of arguments, and returns the first argument that isn't null or an empty string.
locals {
vpn_shared_secret = coalesce(var.vpn_shared_secret, random_password.RANDOM_PSK.result)
}

How can I output a data source that uses count?

I want to output each VM created and their UUID e.g
data "vsphere_virtual_machine" "vms" {
count = "${length(var.vm_names)}"
name = "${var.vm_names[count.index]}"
datacenter_id = "12345"
}
output "vm_to_uuid" {
# value = "${data.vsphere_virtual_machine.newvms[count.index].name}"
value = "${data.vsphere_virtual_machine.newvms[count.index].id}"
}
Example output I'm looking for:
"vm_to_uuids":[
{
"name":"node1",
"id":"123456",
},
{
"name":"node2",
"id":"987654",
}
]
Use the wildcard attribute in the expression given for the output value to get the list of ids for the created VMs. e.g.
output "vm_to_uuids" {
value = "${data.vsphere_virtual_machine.*.id}"
}
The required syntax provided in your question is one exemption where to prefer function over form.
Writing a terraform configuration that provides that isn't straightforward.
Perhaps, I suggest to adopt other simpler ways to output this same information.
Names mapped to ids can be output:
output "vm_to_uuids" {
value = "${zipmap(
data.vsphere_virtual_machine.*.name,
data.vsphere_virtual_machine.*.id)}"
}
A map of names and ids can be output in a columnar manner:
output "vm_to_uuids" {
value = "${map("name",
data.vsphere_virtual_machine.*.name,
"id",
data.vsphere_virtual_machine.*.id)}"
}
A list of names and ids can be output in a columnar manner:
output "vm_to_uuids" {
value = "${list(
data.vsphere_virtual_machine.*.name,
data.vsphere_virtual_machine.*.id)}"
}
One thing you could do (if you wanted exactly that output), is use formatlist(format, args, ...)
data "vsphere_virtual_machine" "vms" {
count = "${length(var.vm_names)}"
name = "${var.vm_names[count.index]}"
datacenter_id = "12345"
}
output "vm_to_uuid" {
value = "${join(",", formatlist("{\"name\": \"%s\", \"id\": \"%s\"}", data.vsphere_virtual_machine.newvms.*.name, data.vsphere_virtual_machine.newvms.*.id))}"
}
Haven't tested the code, but you get the idea. Especially the quote escape is just a guess, but that's easy to figure out from here.
What happens is you take two lists (names and ID's) and format dict strings from each entry, after which you join them together using comma separation.

Resources