Terraform Loop with Range - terraform

Is there a way to loop through a range or list for a variable in terraform.
Below is what I would like to accomplish but I am not sure how to do it.
module "vlan_list" {
depends_on = [module.vlan_pools]
source = "../modules/add_vlans"
vlan_list = {
for i in range(1,100): {
"access" = {
vlan_pool = module.vlan_pools.vlan_pool["access"]
from = i
to = i
}
}
}
}
Let me add more information to it because unfortunately that didn't work. I get:
Error: Invalid value for module argument
on pools_vlan.tf line 34, in module "vlan_list":
34: vlan_list = {
35: for i in range(1, 100):
36: "access" => {
37: vlan_pool = module.vlan_pools.vlan_pool["access"]
38: from = i
39: to = i
40: }...
41: }
The given value is not suitable for child module variable "vlan_list" defined
at ../modules/add_vlans/variables.tf:5,1-21: element "access": object
required.
So I have created a module with the following
resource "aci_ranges" "add_vlan" {
for_each = local.vlan_list
alloc_mode = each.value["alloc_mode"]
annotation = each.value["annotation"]
name_alias = each.value["name_alias"]
vlan_pool_dn = each.value["vlan_pool"]
role = each.value["role"]
from = "vlan-${each.value["from"]}"
to = "vlan-${each.value["to"]}"
}
From Here I have defined a variables file to make it so users don't have to enter every variable... they can accept defaults
terraform {
experiments = [module_variable_optional_attrs]
}
variable "vlan_list" {
description = "Add VLANs to VLAN Pools"
type = map(object({
alloc_mode = optional(string)
annotation = optional(string)
from = optional(number)
name_alias = optional(string)
role = optional(string)
to = optional(number)
vlan_pool = optional(string)
}))
}
locals {
vlan_list = {
for k, v in var.vlan_list : k => {
alloc_mode = coalesce(v.alloc_mode, "static")
annotation = (v.annotation != null ? v.annotation : "")
from = (v.from != null ? v.from : 1)
name_alias = (v.name_alias != null ? v.name_alias : "")
role = coalesce(v.role, "external")
to = coalesce(v.to, 1)
vlan_pool = (v.vlan_pool != null ? v.vlan_pool : "")
}
}
}
So what I shared above is what someone would enter to consume the module... Here is a more complete example that I would like to do:
module "vlan_list" {
depends_on = [module.vlan_pools]
source = "../modules/add_vlans"
vlan_list = {
for i in range(1, 100):
"access" => {
vlan_pool = module.vlan_pools.vlan_pool["access"]
from = i
to = i
}...
for i in ranges([1000-1200], [1300-1400]):
"vmm_dynamic" => {
alloc_mode = "dynamic"
vlan_pool = module.vlan_pools.vlan_pool["vmm_dynamic"]
from = i
to = i
}...
for i in list[4, 100, 101]:
"l3out" => {
vlan_pool = module.vlan_pools.vlan_pool["l3out"]
from = i
to = i
}...
}
}
I know the second range on the vmm_dynamic key is not correct at all... I am just trying to show what I would like to be able to do if possible.
When the resource creates the entries, from the API, if I do it in a range as shown below; if someone needed to change the range for the first pool (in example) to 1-50,52-99, it would delete the entry and re-create it. whereas if they are creating the entry with each entry being created individually it wouldn't delete all of the individual entries, optimally.
I can do the following and it works fine... but being able to add the VLANs individually from a loop would be preferable.
module "vlan_list" {
depends_on = [module.vlan_pools]
source = "../modules/add_vlans"
vlan_list = {
"access" = {
vlan_pool = module.vlan_pools.vlan_pool["access"]
from = 1
to = 99
},
"vmm_dynamic" = {
alloc_mode = "dynamic"
vlan_pool = module.vlan_pools.vlan_pool["vmm_dynamic"]
from = 1000
to = 1199
},
"l3out_1" = {
vlan_pool = module.vlan_pools.vlan_pool["l3out"]
from = 4
to = 4
},
"l3out_2" = {
vlan_pool = module.vlan_pools.vlan_pool["l3out"]
from = 100
to = 101
},
}
}
Thanks in advance for help on this.
Just as one more point of reference... This is how I had previously accomplished this with Python, but I am trying to move this to native Terraform
def vlan_list_full(vlan_list):
full_vlan_list = []
if re.search(r',', str(vlan_list)):
vlist = vlan_list.split(',')
for v in vlist:
if re.fullmatch('^\\d{1,4}\\-\\d{1,4}$', v):
a,b = v.split('-')
a = int(a)
b = int(b)
vrange = range(a,b+1)
for vl in vrange:
full_vlan_list.append(vl)
elif re.fullmatch('^\\d{1,4}$', v):
full_vlan_list.append(v)
elif re.search('\\-', str(vlan_list)):
a,b = vlan_list.split('-')
a = int(a)
b = int(b)
vrange = range(a,b+1)
for v in vrange:
full_vlan_list.append(v)
else:
full_vlan_list.append(vlan_list)
return full_vlan_list
def vlan_pool
for z in range(1, 3):
vgroup = 'VLAN_Grp%s' % (z)
vgrp = 'VGRP%s_Allocation' % (z)
templateVars['Allocation_Mode'] = templateVars[vgrp]
if re.search(r'\d+', str(templateVars[vgroup])):
vlan_list = vlan_list_full(templateVars[vgroup])
for v in vlan_list:
vlan = str(v)
if re.fullmatch(r'\d+', vlan):
templateVars['VLAN_ID'] = int(vlan)
# Add VLAN to VLAN Pool File
create_tf_file('a+', dest_dir, dest_file, template, **templateVars)
I can't seem to find any examples of how to do this without Python.

If you want to create a map with a single key of access and a list of values, then you can use ellipsis operator (...). Also your syntax is incorrect. Thus, the following should be used in this case:
vlan_list = {
for i in range(1, 100):
"access" => {
vlan_pool = module.vlan_pools.vlan_pool["access"]
from = i
to = i
}...
}

Here is how I recently accomplished what I asked above. It was about a year to figure it out but this is the way I could accomplish it.
# Map of Object input Variables is as shown below
variable "vlan_pools" {
default = {
"default" = {
alias = ""
allocation_mode = "dynamic"
description = ""
encap_blocks = {
"default" = {
allocation_mode = "inherit"
description = ""
role = "external"
vlan_range = "**REQUIRED**"
}
}
}
}
description = <<-EOT
key - name of the VLAN Pool
* alias: A changeable name for a given object. While the name of an object, once created, cannot be changed, the alias is a field that can be changed.
* allocation_mode: The allocation mode. The values can be:
- dynamic (default): Managed internally by the APIC to allocate VLANs for endpoint groups (EPGs). A vCenter Domain can associate only to a dynamic pool.
- static: One or more EPGs are associated with a domain, and that domain is associated with a static range of VLANs. You must configure statically deployed EPGs within that range of VLANs.
When you create VLAN ranges, you can also assign the allocation mode to be inherited from the parent.
* description: Description to add to the Object. The description can be up to 128 alphanumeric characters.
* encap_blocks:
- allocation_mode: The allocation mode. The values can be:
* dynamic: Managed internally by the APIC to allocate VLANs for endpoint groups (EPGs). A vCenter Domain can associate only to a dynamic pool.
* inherit (default): The inherited mode from the parent device.
* static: One or more EPGs are associated with a domain, and that domain is associated with a static range of VLANs. You must configure statically deployed EPGs within that range of VLANs.
- description: Description to add to the Object. The description can be up to 128 alphanumeric characters.
- role: Role of the VLAN range. The options are:
* external (Default): Used for allocating VLANs for each EPG assigned to the domain. The VLANs are used when packets are sent to or from leafs.
* Internal: Used for private VLAN allocations in the internal vSwitch by the Cisco ACI Virtual Edge (AVE). The VLANs are not seen outside the ESX host or on the wire.
- vlan_range: single vlan; i.e. 1. range of vlans; i.e. 1-5. Or List of Vlans; i.e. 1-5,10-15
EOT
type = map(object(
{
alias = optional(string)
allocation_mode = optional(string)
description = optional(string)
encap_blocks = map(object(
{
allocation_mode = optional(string)
description = optional(string)
role = optional(string)
vlan_range = string
}
))
}
))
}
# Which I then run some conditional modifications in locals to format the variables.
locals {
#__________________________________________________________
#
# VLAN Pools Variables
#__________________________________________________________
# This first loop is to handle optional attributes and return
# default values if the user doesn't enter a value.
vlan_pools = {
for k, v in var.vlan_pools : k => {
alias = v.alias != null ? v.alias : ""
allocation_mode = v.allocation_mode != null ? v.allocation_mode : "dynamic"
description = v.description != null ? v.description : ""
encap_blocks = v.encap_blocks != null ? v.encap_blocks : {}
}
}
# Loop 1 is to determine if the encap_blocks are:
# A Single number 1
# A Range of numbers 1-5
# A List of numbers 1-5,10-15
# And then to return these values as a list
vlan_ranges_loop_1 = flatten([
for key, value in local.vlan_pools : [
for k, v in value.encap_blocks : {
allocation_mode = v.allocation_mode != null ? v.allocation_mode : "inherit"
description = v.description != null ? v.description : ""
key1 = key
key2 = k
role = v.role != null ? v.role : "external"
vlan_split = length(regexall("-", v.vlan_range)) > 0 ? tolist(split(",", v.vlan_range)) : length(
regexall(",", v.vlan_range)) > 0 ? tolist(split(",", v.vlan_range)
) : [v.vlan_range]
vlan_range = v.vlan_range
}
]
])
# Loop 2 takes a list that contains a "-" or a "," and expands those values
# into a full list. So [1-5] becomes [1, 2, 3, 4, 5]
vlan_ranges_loop_2 = {
for k, v in local.vlan_ranges_loop_1 : "${v.key1}_${v.key2}" => {
allocation_mode = v.allocation_mode
description = v.description
key1 = v.key1
key2 = v.key2
role = v.role
vlan_list = length(regexall("(,|-)", jsonencode(v.vlan_range))) > 0 ? flatten([
for s in v.vlan_split : length(regexall("-", s)) > 0 ? [for v in range(tonumber(
element(split("-", s), 0)), (tonumber(element(split("-", s), 1)) + 1)
) : tonumber(v)] : [s]
]) : v.vlan_split
}
}
# Loop 3 will take the vlan_list created in Loop 2 and expand this
# out to a map of objects per vlan.
vlan_ranges_loop_3 = flatten([
for k, v in local.vlan_ranges_loop_2 : [
for s in v.vlan_list : {
allocation_mode = v.allocation_mode
description = v.description
key1 = v.key1
role = v.role
vlan = s
}
]
])
# And lastly loop3's list is converted back to a map of objects
vlan_ranges = { for k, v in local.vlan_ranges_loop_3 : "${v.key1}_${v.vlan}" => v }
# End of Local Loops
}
# And lastly these values are consumed by the resources with for_each loops.
resource "aci_vlan_pool" "vlan_pools" {
for_each = local.vlan_pools
alloc_mode = each.value.allocation_mode
description = each.value.description
name = each.key
name_alias = each.value.alias
}
resource "aci_ranges" "vlans" {
depends_on = [
aci_vlan_pool.vlan_pools
]
for_each = local.vlan_ranges
description = each.value.description
alloc_mode = each.value.allocation_mode
from = "vlan-${each.value.vlan}"
to = "vlan-${each.value.vlan}"
role = each.value.role
vlan_pool_dn = aci_vlan_pool.vlan_pools[each.value.key1].id
}
I am sure someone with more experience might be able to accomplish this with fewer steps in locals but this worked for me at least.

Related

How to convert list to map in terraform?

string vals=var.vals // which will contain string in the format below
//"hr:recruitment,department:human,tool:sap,fruit:apple"
labels={
hostname=var.hostname
monitored=var.monitored
customer=var.cust1
machine-type=var.machinetype
}
I need to set labels(key,value) pairs for google_compute_instance by combining the above 2 properties to form a map.
So i converted vals to list by this code split(",",var.vals)
This gives me a list
tolist([
"hr:recruitment",
"department:human",
"tool:sap",
"fruit:apple",
])
Expected Output consisting of only map(string) combining labels and vals
labels:
{
hostname:var.hostname
monitored:var.monitored
customer:var.cust1
machine-type:var.machinetype
hr:"recruitment",
department:"human",
tool:"sap",
fruit:"apple"
}
How to convert this list to a map and combine it with labels?
I tried below & it seems to be working but it heavily relies on data quality of vals string (shouldn't contain duplicate keys). At it's core it relies on zipmap to build the map from keys & values lists.
// Having vals as a string variable
variable "vals" {
description = "The name of availability set"
type = string
default = "hr:recruitment,department:human,tool:sap,fruit:apple"
}
// hostname, monitored, cust1 & machinetype vars
variable "hostname" {
type = string
default = "dummy.net"
}
variable "monitored" {
type = bool
default = true
}
variable "cust1" {
type = string
default = "xyz"
}
variable "machinetype" {
type = string
default = "linux"
}
locals {
// parsing locally to split the vals string to fetch the keys
keys = [
for s in split(",", var.vals) : split(":", s)[0]
]
// parsing locally to split the vals string to fetch the values
values = [
for s in split(",", var.vals) : split(":", s)[1]
]
//I used labels also as vars
labels = {
hostname = var.hostname
monitored = var.monitored
customer = var.cust1
machine-type = var.machinetype
}
}
// Finally, zipmap to construct map from keys & values & merge with local.labels
output "final_map" {
value = merge(local.labels, zipmap(local.keys, local.values))
}
Finally, the output looks like below ::
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
final_map = {
"customer" = "xyz"
"department" = "human"
"fruit" = "apple"
"hostname" = "dummy.net"
"hr" = "recruitment"
"machine-type" = "linux"
"monitored" = true
"tool" = "sap"
}

How to prevent Terraform from deleting a resource when index [0] is out of range for count

I have previously created A type cloudflare record and would like to keep it while adding another one. So I defined a flag to just skip it.
resource "cloudflare_record" "cloudflare-a-record" {
count = var.flag != false ? 1 : 0
zone_id = var.zone_id
name = var.sub_domain
type = "A"
value = aws_eip.my_eip.public_ip
ttl = 1
proxied = false
}
resource "cloudflare_record" "vault-cloudflare-cname-record" {
count = var.flag == false ? 1 : 0
zone_id = var.zone_id
name = cloudflare_record.cloudflare-a-record.hostname
type = "CNAME"
value = aws_eip.my_eip.public_dns
ttl = 1
proxied = false
}
But Terraform deleted this resource with the following message:
cloudflare_record.vault-cloudflare-a-record[0] will be destroyed
(because index [0] is out of range for count)
Is there another way to ignore this resource? Or is the code wrong?
In this case, you cannot use the same flag for two different resources as the variable value will remain the same for both A and CNAME resources. The way I see it there are two possible options with the current code since you are using different conditionals (in A you use == and in CNAME you use !=):
var.flag == false ? 1 : 0 # A record
var.flag != false ? 1 : 0 # CNAME record
This means if the flag = false the A record will be created (as the count will be 1) and the CNAME record will not be created (as the count will be 0). Now, if the flag's value changes to true, then the A record will be deleted (as true == false will return false) and the CNAME record will be created (as true != false will be true). This means that the same flag should not be used for two different resources. You could use the same conditional for both resources, which means that both would be created/deleted together (not sure if that is what you want). A better way would be to define two variables, one for A and one for CNAME record:
variable "create_a_record" {
type = bool
}
variable "create_cname_record" {
type = bool
}
Then, in your code you would change the lines that use the count meta-argument to:
resource "cloudflare_record" "cloudflare-a-record" {
count = var.create_a_record ? 1 : 0
zone_id = var.zone_id
name = var.sub_domain
type = "A"
value = aws_eip.my_eip.public_ip
ttl = 1
proxied = false
}
resource "cloudflare_record" "vault-cloudflare-cname-record" {
count = var.create_cname_record ? 1 : 0
zone_id = var.zone_id
name = cloudflare_record.cloudflare-a-record.hostname
type = "CNAME"
value = aws_eip.my_eip.public_dns
ttl = 1
proxied = false
}
This way you can control if you want to have both created or only one. Also note the following:
count = var.create_a_record ? 1 : 0
count = var.create_cname_record ? 1 : 0
When variables are of type bool (true or false), when using them in conditionals, you do not have to check their equality against another boolean, as the left-most value will anyway be either true or false. So for example, if you set create_a_record = true, that would make the above expression:
count = true ? 1 : 0
and that would evaluate to count = 1. You could also set default values for variables, e.g., if you want to make sure the A record is always there, you can just do this:
variable "create_a_record" {
type = bool
default = true
}
[1] https://www.terraform.io/language/expressions/conditionals

If conditions in terraform map and objects to index based on key

I have a strange situation as shown below
variable is declared as :
variable "subnet_info" {
type = map(object({vpc_name=string, subnet_name=string, subnet_ip_range=string, region=string}))
default = {
abc = {
vpc_name = "abc"
subnet_name = "abc"
subnet_ip_range = ""
region = "europe-west3"
}
bcd = {
vpc_name = "bcd"
subnet_name = "bcd"
subnet_ip_range = ""
region = "europe-west3"
}
}
}
I have declared a modules as follow
module "abc" {
source = "./modular_approach/modules/general_network/"
for_each = var.subnet_info
individual_vpc_name = each.value
individual_subnet_name = each.value
}
I want to pass the value of vpc and subnet from the variable subnet_info. The problem is that there will be many values in that variable. If I run the for_each I want to condition in the module that if the key is "abc" then access the internal (key,value) other wise skip. Is this possible in terraform?
Thanks in advance!
If you really wish to filter like that, the following construct should do :
for_each = {for k,v in var.subnet_info: k=>v if k == "abc"}

Alicloud CMS alarm not showing data

I have created alibaba cms alarm using terraform but it does not show any data. Below is my code.
resource "alicloud_cms_alarm" "scaleOut-alarm" {
contact_groups = ["example"]
dimensions = {
region = "shanghai"
queue = "queue"
}
enabled = true
metric = "ActiveMessages"
name = "alarm-name"
period = 300
project = "acs_mns_new"
silence_time = 300
operator = ">"
threshold = 300
statistics = "Average"
}
Any help will be appreciated.
this is basic example
resource "alicloud_cms_alarm" "basic" {
name = "tf-testAccCmsAlarm_basic"
project = "acs_ecs_dashboard"
metric = "disk_writebytes"
dimensions = {
instanceId = "i-bp1247,i-bp11gd"
device = "/dev/vda1,/dev/vdb1"
}
statistics = "Average"
period = 900
operator = "<="
threshold = 35
triggered_count = 2
contact_groups = ["test-group"]
effective_interval = "0:00-2:00"
notify_type = 1
webhook = "https://${data.alicloud_account.current.id}.eu-central-1.fc.aliyuncs.com/2016-08-15/proxy/Terraform/AlarmEndpointMock/"
}

Override variables in Terraform

I've started to use Terraform lately, and i need some help. I hope it's not too basic. I've the following Terraform data structure.
abc_template = {
a = var.a
b = var.b
c = var.c
d = var.d
....
....
....
k = var.k
}
And then i run:
resource "local_file" "aaa" {
count = 1
content = templatefile("${path.module}/templates/abc.tmpl", local.abc_template)
....
....
}
I need to create a new template (xyz_template), Which should be very similar to abc_template, While only a few variables will changed from the original template. What can i do instead of duplicating so many code lines? Is there a way to inherit abc_template, and just to override the relevant variables, Instead of creating xyz_template which might be very similar to abc_template?
Please advise.
You could use a map:
variable "global" {
type = "map"
default = {
name = "TEST"
addr = "Test123"
}
}
output "example" {
value = templatefile("${path.module}/web.tpl", {
global = var.global
})
}
template:
My name is ${global.name}.
And you can override values in a map using the merge() function.
You can use the merge function to produce a new map using a blend of elements from multiple maps.
For example:
locals {
abc_template = {
a = var.a
b = var.b
c = var.c
d = var.d
}
xyz_template = merge(
local.abc_template,
{
d = var.other_d
x = var.x
y = var.y
z = var.z
},
)
}
In the above example, local.xyz_template would have all of the same elements as local.abc_template except that d is overridden, and it would additionally have elements x, y, and z.

Resources