How do I do a for_each loop for the following?
I want to create a tfe_variable node_count & vm_size.
I need both these tfe_variables in both wksp1 and wksp2
variable "custom_variables" {
type = map(object({
node_count = number
vm_size = string
}))
default = {
wksp1 = {
node_count = 2
vm_size = "Standard_D2_v3"
},
wksp2 = {
node_count = 5
vm_size = "Standard_D2_v5"
}
}
}
resource "tfe_variable" "custom" {
for_each = {
# for each workspace & variable in var.custom_variables create a tfe_variable
}
key = each.value.name
value = each.value.value
category = "terraform"
workspace_id = each.value.workspace_id
}
You're really close! Here are a couple of things to consider:
Option 1: Multiple tfe_variable resources
Create a tfe_variable resource for each variable you want to create
Make sure the key in you custom_variables map is the workspace ID.
variable "custom_variables" {
type = map(object({
node_count = number
vm_size = string
}))
default = {
wksp1_id = {
node_count = 2
vm_size = "Standard_D2_v3"
},
wksp2_id = {
node_count = 5
vm_size = "Standard_D2_v5"
}
}
}
resource "tfe_variable" "node_count" {
for_each = var.custom_variables
key = "node_count"
value = each.value.node_count
category = "terraform"
workspace_id = each.key
}
resource "tfe_variable" "vm_size" {
for_each = var.custom_variables
key = "vm_size"
value = each.value.vm_size
category = "terraform"
workspace_id = each.key
}
The drawback to this option is that you'll need an additional resource for each variable.
Option 2: A list of variable objects
Define a list of the keys, values, and workspace IDs of each variable
Use count to iterate the list
variable "custom_variables" {
type = list(object({
key = string
value = string
workspace_id = string
}))
default = [
{
key = "node_count"
value = "2"
workspace_id = "wksp1_id"
},
{
key = "node_count"
value = "5"
workspace_id = "wksp2_id"
},
{
key = "vm_size"
value = "Standard_D2_v3"
workspace_id = "wksp1_id"
},
{
key = "vm_size"
value = "Standard_D2_v5"
workspace_id = "wksp2_id"
}
]
}
resource "tfe_variable" "custom" {
count = length(var.custom_variables)
key = var.custom_variables[count.index].key
value = var.custom_variables[count.index].value
workspace_id = var.custom_variables[count.index].workspace_id
category = "terraform"
}
There are a couple of drawbacks to this approach as well:
There is a fair amount of duplicated code in the variable definition
The value must always be of the same type
If you're struggling with loop concepts in Terraform, this blog post might help you.
The main requirement to keep in mind for for_each is that we always need to create a map that has one element for each instance of the resource we want to create. In this case, that means you need a map with one element per workspace per variable, because tfe_variable describes a single variable on a single workspace.
Our job then is to write an expression to project the map-of-objects value coming in via the variable to a collection that has a separate element per variable. Here's one way to get that done, using the flatten function in a way similar to an example in its documentation:
locals {
workspace_variables = flatten([
for ws_name, ws in var.custom_variables : [
for var_name, value in ws : {
workspace = ws_name
variable = var_name
value = value
}
]
])
}
The above should produce a local.workspace_variables that looks like this:
[
{ workspace = "wksp1", variable = "node_count", value = 2 },
{ workspace = "wksp1", variable = "vm_size", value = "Standard_D2_v3" },
{ workspace = "wksp2", variable = "node_count", value = 5 },
{ workspace = "wksp2", variable = "vm_size", value = "Standard_D2_v5" },
]
This now meets the requirement of having one element per desired tfe_variable instance, so our only remaining job is to project this into a map to provide unique identifiers for each element and describe how to populate the tfe_variable arguments based on these objects:
resource "tfe_variable" "custom" {
for_each = {
for wsv in local.workspace_variables : "${wsv.workspace}.${wsv.variable}" => wsv
}
key = each.value.variable
value = each.value.value
category = "terraform"
workspace_id = each.value.workspace
}
One thing I didn't contend with above, because it wasn't directly you question, is the value of workspace_id in tfe_variable. If I recall correctly, that argument is expecting a workspace id rather than a workspace name, in which case you might need a slightly more complicated expression for the workspace_id argument. If you already have a tfe_workspace resource using the workspace names as keys then something like this might work, for example:
workspace_id = tfe_workspace.example[each.value.workspace].id
If your workspaces are created in a different way then you may have to do something more complicated here, but that's getting far off the topic of your original question so I won't try to dig into that here. I'm happy to try to help with it in a separate question on this site though, if you like.
Related
I'm using the AWS provider for creating CloudWatch Metric Alarms. I created a module which takes in a variable that is a list of instance IDs, and the resource it has uses the "count" functionality to create an alarm per an Instance ID from that variable.
The "aws_cloudwatch_metric_alarm" resource can take in multiple "metric_query" blocks, and my plan was to do this as dynamic block to be able to define as many as needed in the root module.
Issue I'm experiencing is with accessing the "for_each" iterator values.
The high-level end solution should be something among these lines: Use 3 metric blocks, two are available metrics and a third one for an expression on top of the other two, and create this alarm for every instance that is provided in the instance list.
Resource definition, module code:
resource "aws_cloudwatch_metric_alarm" "alarm" {
count = length(var.dimension_values)
alarm_name = "${var.alarm_name}_${var.dimension_values[count.index]}"
comparison_operator = var.comparison_operator
evaluation_periods = var.evaluation_periods
threshold = var.threshold
actions_enabled = var.actions_enabled
alarm_actions = var.alarm_actions
dynamic "metric_query" {
for_each = var.metric_queries
content {
id = metric_queries.value.id
return_data = metric_queries.value.return_data
expression = metric_queries.value.expression
label = metric_queries.value.label
metric {
namespace = metric_queries.value.namespace
metric_name = metric_queries.value.metric_name
period = metric_queries.value.period
stat = metric_queries.value.stat
dimensions = {
"${metric_queries.value.dimension_name}" = var.dimension_values[count.index]
}
}
}
}
tags = merge(
var.common_tags,
{
Name = "${var.alarm_name}_${var.dimension_values[count.index]}"
}
)
}
Module variables (only metric_queries pasted):
variable "metric_queries" {
type = list(object({
id = string
return_data = bool
expression = string
label = string
namespace = string
metric_name = string
period = number
stat = string
dimension_name = string
}))
description = "Metric query for the CloudWatch alarm"
default = []
}
And finally, the root module:
module "cpu_alarms" {
source = "../../Modules/cloudwatch_metric_alarm/"
common_tags = local.common_tags
# Metrics
alarm_name = "EC2_CPU_80_PERCENT"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 3
threshold = 80
actions_enabled = true
alarm_actions = ["redacted"]
dimension_values = local.all_ec2_instance_ids
metric_queries = [
{
id = "m1"
return_data = true
expression = null
label = "CPU utilization"
namespace = "AWS/EC2"
metric_name = "CPUUtilization"
period = 60
stat = "Average"
dimension_name = "InstanceId"
}
]
}
I'm getting two separate errors with this approach depending on how I'm referring to the "for_each" iterator object.
When using "each" as reference to the iterator the error is:
A reference to "each.value" has been used in a context in which it unavailable, such as when the configuration no longer contains the value in its "for_each" expression. Remove this reference to each.value in your configuration to work │ around this error.
When using "metric_queries" as reference to the iterator the error is:
A managed resource "metric_queries" "value" has not been declared in module.cpu_alarms.
What could be the root cause of this?
Please see the documentation on dynamic blocks. You are trying to use the syntax for the resource level for_each meta-argument, not the syntax for dynamic blocks. It's confusing that they have different syntax, but since a dynamic block could exist inside a resource with for_each, the syntax has to be different to prevent name clashes.
For dynamic blocks the name of the variable is what you put after the dynamic key word, in your case "metric_query". So your code should look like this:
dynamic "metric_query" {
for_each = var.metric_queries
content {
id = metric_query.value.id
return_data = metric_query.value.return_data
expression = metric_query.value.expression
label = metric_query.value.label
metric {
namespace = metric_query.value.namespace
metric_name = metric_query.value.metric_name
period = metric_query.value.period
stat = metric_query.value.stat
dimensions = {
"${metric_query.value.dimension_name}" = var.dimension_values[count.index]
}
}
}
}
I am trying to output gcp project information by doing something like this :
output "projects" {
value = tomap({
for project_name in ["project_1", "project_2", "project_3"] :
project_name => tomap({
id = google_project."${project_name}".id
number = google_project."${project_name}".number
})
})
description = "Projects"
}
Or like this :
output "projects" {
value = tomap({
for_each = toset([google_project.project_1,google_project.project_2])
id = each.key.id
number = each.key.number
})
description = "Projects"
}
Is it at all possible to use resource names this way? Do I have to specify every resource by duplicating code?
E.g.
output "projects" {
value = tomap({
project_1 = tomap({
id = google_project.project_1.id
number = google_project.project_1.number
})
project_2 = tomap({
id = google_project.project_2.id
number = google_project.project_2.number
})
project_3 = tomap({
id = google_project.project_3 .id
number = google_project.pproject_3 .number
})
})
description = "Projects"
}
EDIT : declared resources.
In main.tf projects 1 to 3 are declared the same way.
resource "google_project" "project_3" {
name = var.projects.project_3.name
project_id = var.projects.project_3.id
folder_id = google_folder.parent.name
billing_account = data.google_billing_account.acct.id
auto_create_network = false
}
in variables.tf
variable "projects" {
type = map(object({
name = string
id = string
}))
}
in variables.tfvars
projects = {
project_1= {
name = "project_1"
id = "project_1-12345"
}
project_2= {
name = "project_2"
id = "project_2-12345"
}
project_3= {
name = "project_2"
id = "project_2-12345"
}
}
I misunderstood your question originally. I see now that you want to reference a resource by a variable name. No you cannot do that. But your setup here doesn't really make sense, and seems more complex than it needs to be.
Consider if these options would improve your setup.
locals {
projects = { # This is equivalent to your input.
project_1 = {
name = "project_1"
id = "project_1-12345"
}
project_2 = {
name = "project_2"
id = "project_2-12345"
}
project_3 = {
name = "project_3"
id = "project_3-12345"
}
}
}
resource "google_project" "this" {
for_each = local.projects
name = each.key # or each.value.name / don't really need name
project_id = each.value.id
folder_id = google_folder.parent.name
billing_account = data.google_billing_account.acct.id
auto_create_network = false
}
output "projects_from_input" {
description = "You can of course, just use the input."
value = local.projects
}
output "projects_explicit_values" {
description = "Alternatively, if you need a subset of resource values."
value = { for k, v in google_project.this : k => {
name = v.name
id = v.project_id
} }
}
output "complete_resources" {
description = "But you can just output the complete resource."
value = google_project.this
}
I edited my initial answer after seeing the Terraform resource that creates a project. The need is a way to get a resource name in the output bloc with interpolation.
I think if a single resource is used to create all the projets instead of one resource per projet, it's easier to expose this resource in the output bloc.
For example you can configure projects metadata information from a json file, or directly a local variable or a var if needed :
Example for a json file and local variable
mymodule/resource/projects.json :
{
"projects": {
"project_1": {
"id": "project_1",
"number": "23333311"
},
"project_2": {
"id": "project_2",
"number": "33399999"
}
}
}
Then retrieve projects as a variable from locals.tf file :
mymodule/locals.tf :
locals {
projects = jsondecode(file("${path.module}/resource/projects.json"))["projects"]
}
Create your projects in a single resource with a foreach :
resource "google_project" "projects" {
for_each = local.projects
name = each.key
project_id = each.value["id"]
folder_id = google_folder.parent.name
billing_account = data.google_billing_account.acct.id
auto_create_network = false
}
Expose the projects resource in an output.tf file :
output "projects" {
value = google_project.projects
description = "Projects"
}
The same principle can be done with a var instead of local variable.
Newbie question: Is there any way to loop the variable to storing in mentioned code?. below is my configurations variable.
config= [{
"name" = shared_preload_libraries,
"values" = ["EXAMPLE1", "EXAMPLE2"]
},
{
"name" = "azure.extensions"
"values" = ["EXAMPLE1", "EXAMPLE2", "EXAMPLE3"]
}]
I need to iterate in such a way that, for each name in variable the corresponding values should inserterd one by one in below code.
resource "azurerm_postgresql_flexible_server_configuration" "example" {
name = name
server_id = azurerm_postgresql_flexible_server.example.id
value = values
}
You have to flatten your config as follows:
locals {
flat_config = merge([
for single_config in var.config: {
for value in single_config.values:
"${single_config.name}-${value}" => {
"name" = single_config.name
"value" = value
}
}
]...) # do NOT remove the dots
}
then
resource "azurerm_postgresql_flexible_server_configuration" "example" {
for_each = local.flat_config
name = each.value.name
server_id = azurerm_postgresql_flexible_server.example.id
value = each.value.value
}
I'm trying to write code for AKS with all possible arguments, and then in variables I specify map. Problem is I don't want to specify all of the optional arguments in my variables each time, I wonder if there is a possibility to skip them in variables.
Here is my example:
main.tf
resource "azurerm_kubernetes_cluster" "this" {
for_each = var.AKS_Config
name = each.value.AKS_Name
location = each.value.AKS_Location
resource_group_name = each.value.AKS_ResourceGroupName
dns_prefix = each.value.dns_prefix
#dns_prefix_private_cluster = # dns_prefix or dns_prefix_private_cluster must be specified
kubernetes_version = each.value.kubernetes_version
automatic_channel_upgrade = each.value.automatic_channel_upgrade
api_server_authorized_ip_ranges = each.value.api_server_authorized_ip_ranges
dynamic "identity" {
for_each = each.value.identity
content {
type = identity.value.type
user_assigned_identity_id = can(identity.value.user_assigned_identity_id) == true ? identity.value.user_assigned_identity_id : {}
}
}
}
variables.tf
variable "AKS_Config" {
default = {
"AKS_1" = {
AKS_Name = "AKSTest001"
AKS_Location = "West Europe"
AKS_ResourceGroupName = "SDSADAS"
dns_prefix = "AKSTESTPREFIX"
default_node_pool_name = "test-name"
node_count = 1
vm_sku = "Standard_D2_v2"
kubernetes_version = "1.21.7"
automatic_channel_upgrade = "stable"
api_server_authorized_ip_ranges = ["16.0.0.0/16"]
identity = {
type = "SystemAssigned"
}
default_node_pool = {
name = "nodetest01"
node_count = 3
vm_size = "Standard_D2_v2"
availability_zones = ["1", "2", "3"]
auto_scaling_enabled = true
enable_host_encryption = false
enable_node_public_ip = true
fips_enabled = true
kubelet_disk_type = "OS"
max_pods = 2
}
}
}
}
As you can see there are bunch of optional arguments for AKS, but I don't want to specify each one of them as 'null' every deployment.
Is there a option to achieve that? Some kind of function 'if key does not exist skip it'
Thank you
EDIT:
What do you think about this "workaround"?
dynamic "identity" {
for_each = can(each.value.identity) == true ? each.value.identity : {}
content {
type = each.value.identity.type
user_assigned_identity_id = can(each.value.identity.user_assigned_identity_id) == true ? each.value.identity.user_assigned_identity_id : null
}
}
Using can it's working, or at least terraform validate doesn't throw any problems and I don't need to specify optional parameters in map, but I don't know if this should be avoided in my case
Generally there are two options I see to handle this problem:
Split the default map to different vaiables, and assign to each variable a default value.
Use the merge() terraform function in order to override specific values in the default map you declared to be used in the for_each.
I would like to pass a variable that will allow me to specify the list of VPC and subnet settings for an AWS instance. There are fixed VPC and subnet settings that make sense so I just want to allow a user to pick one using a single variable, i.e. use A or B.
For instance, let's say I have two available VPCs, and these are specified in a variables.tf file for a module my_instance:
variable "a_vpc_cidr_block" { default = "105.191.44.0/22" }
variable "a_vpc_id" { default = "id_a"}
variable "a_vpc_name" { default = "vpc_a" }
variable "a_subnet_availability_zone" { default = "us-east-1a" }
variable "a_subnet_cidr_block" { default = "105.191.25.0/25" }
variable "a_subnet_name" { default = "instance_A" }
variable "b_vpc_cidr_block" { default = "105.191.45.0/22" }
variable "b_vpc_id" { default = "id_b"}
variable "b_vpc_name" { default = "vpc_b" }
variable "b_subnet_availability_zone" { default = "us-east-1a" }
variable "b_subnet_cidr_block" { default = "105.191.35.0/25" }
variable "b_subnet_name" { default = "instance_B" }
The my_instance module will take a single input variable that an environment will specify, with a value of either 'A' or 'B' (is there a way to limit options for a variable to a list of values such as options=['A', 'B']?), and will be called like so in the terraform.tf for a Terraform configuration with a single instance:
module "my_instance" {
source = "../../modules/my_instance"
option = "A"
}
I want to now implement some logic within the module's main file (modules/my_instance/my_instance.tf) where it decides on which of the two collections of VPC and subnet settings it should use from the ones in modules/my_instance/variables.tf. I want to something like this (pseudocode):
if var.option == 'A'
vpc_cidr_block = var.a_vpc_cidr_block
vpc_id = var.a_vpc_id
vpc_name = var.a_vpc_name
subnet_availability_zone = var.a_subnet_availability_zone
subnet_cidr_block = var.a_subnet_cidr_block
subnet_name = var.a_subnet_name
else if var.option == 'B'
vpc_cidr_block = var.b_vpc_cidr_block
vpc_id = var.b_vpc_id
vpc_name = var.b_vpc_name
subnet_availability_zone = var.b_subnet_availability_zone
subnet_cidr_block = var.b_subnet_cidr_block
subnet_name = var.b_subnet_name
else
raise an error
# get a data resource identified by the VPC variables
data "aws_vpc" "instance_vpc" {
cidr_block = var.vpc_cidr_block
tags = {
Name = var.vpc_name
}
}
# get a data resource identified by the VPC variables
data "aws_subnet" "instance_subnet" {
vpc_id = var.vpc_id
cidr_block = var.subnet_cidr_block
availability_zone = var.subnet_availability_zone
tags = {
Name = var.subnet_name
}
}
# create an AWS key pair resource
resource "aws_key_pair" "instance_aws_key_pair" {
key_name = "component_key_${terraform.workspace}"
public_key = file("~/.ssh/terraform.pub")
}
# create the AWS EC2 instance
resource "aws_instance" "my_aws_instance" {
key_name = aws_key_pair.instance_aws_key_pair.key_name
ami = "ami-b12345"
instance_type = "t2.micro"
subnet_id = data.aws_subnet.instance_subnet.id
connection {
type = "ssh"
user = "terraform"
private_key = file("~/.ssh/terraform")
host = self.public_ip
}
tags = {
"Name" : "my_instance_name"
"Terraform" : "true"
}
}
Is this a matter of somehow using a count, something like this:
count = var.option == 'A'? 1 : 0
Is there a way to do this, or is there a better approach? I am very new to Terraform so I may be missing something obvious.
You have a couple of questions here.
Firstly, you should be able to use the newer, experimental custom validation rules to assert that a value is in a specific list of values.
Secondly, for determining which set of variables to use, I'd recommend going with a good old map in a local value.
For example,
locals {
vpc_info = {
"A" = {
vpc_cidr_block = var.a_vpc_cidr_block
vpc_id = var.a_vpc_id
vpc_name = var.a_vpc_name
subnet_availability_zone = var.a_subnet_availability_zone
subnet_cidr_block = var.a_subnet_cidr_block
subnet_name = var.a_subnet_name
}
"B" = {
vpc_cidr_block = var.b_vpc_cidr_block
vpc_id = var.b_vpc_id
vpc_name = var.b_vpc_name
subnet_availability_zone = var.b_subnet_availability_zone
subnet_cidr_block = var.b_subnet_cidr_block
subnet_name = var.b_subnet_name
}
}
}
Then you should be able to reference a specific field, within the chose option like the following
local.vpc_info[var.option].vpc_name
Let me know if this hits all your questions.