How to use count condition on template in Terraform - azure

In the belo code, I trying to fetch azure secret ( if exists) from keyvault and render it to generate template.
...
< Keyvault definition >
data "azurerm_key_vault_secret" "win_admin_pass" {
count = ${var.cnt} # either 0 and 1
name = "vm-winpw"
key_vault_id = data.azurerm_key_vault.keyvault.id
}
data "template_files" "wininv"{
count = ${var.cnt} # either 0 and 1
template = file(ansible/inventory.tpl)
var = {
winpw = data.azurerm_key_vault.keyvault.id[count.index]
}
}
resource "local_file" "wininv" {
count = ${var.cnt}
content = data.template_files.wininv[count.index]
filename = "ansible/inventory.cfg"
}
Here, I want fetch azure secret, if available on keyvault and generate template.
Without "count" code, its working well, but when secret is not available on azure that time getting error in Terraform. That stuff i have to control.
But with this code, getting below error:
Error: incorrect attributes value type
On test.tf in data template_files" "wininv":
66 var ={
inappropriate value for attribute string required vars: elements example : String required
Can you please suggest possible Syntex or any alternative solution for same.
Thanks

template_file requires string only attributes. It should be:
winpw = data.azurerm_key_vault.keyvault[count.index].id
Also these days its recommended to use templatefile over template_file.

Conditional Expressions may solve your problem.
data "template_files" "wininv"{
count = ${var.cnt} # either 0 and 1
template = file(ansible/inventory.tpl)
var = {
winpw = ${var.cnt} == 0 ? "" : data.azurerm_key_vault.keyvault[count.index].id
}
}
You need to use rendered attritube to get the rendered template. (doc)
resource "local_file" "wininv" {
count = ${var.cnt}
content = data.template_files.wininv[count.index].rendered # NOTICE rendered
filename = "ansible/inventory.cfg"
}

Related

how does terraform pass value of variable when it is not being referenced in main.tf

I don't understand the logic of the following terraform code, and not sure, but I guess it might be me, but would appreciate some help with this.
So there's the following module https://github.com/gettek/terraform-azurerm-policy-as-code/blob/main/modules/definition/main.tf
resource azurerm_policy_definition def {
name = local.policy_name
display_name = local.display_name
description = local.description
policy_type = "Custom"
mode = var.policy_mode
management_group_id = var.management_group_id
metadata = jsonencode(local.metadata)
parameters = length(local.parameters) > 0 ? jsonencode(local.parameters) : null
policy_rule = jsonencode(local.policy_rule)
lifecycle {
create_before_destroy = true
}
timeouts {
read = "10m"
}
}
and https://github.com/gettek/terraform-azurerm-policy-as-code/blob/main/modules/definition/variables.tf
variable management_group_id {
type = string
description = "The management group scope at which the policy will be defined. Defaults to current Subscription if omitted. Changing this forces a new resource to be created."
default = null
}
variable policy_name {
type = string
description = "Name to be used for this policy, when using the module library this should correspond to the correct category folder under /policies/policy_category/policy_name. Changing this forces a new resource to be created."
default = ""
validation {
condition = length(var.policy_name) <= 64
error_message = "Definition names have a maximum 64 character limit, ensure this matches the filename within the local policies library."
}
}
variable display_name {
type = string
description = "Display Name to be used for this policy"
default = ""
validation {
condition = length(var.display_name) <= 128
error_message = "Definition display names have a maximum 128 character limit."
}
}
variable policy_description {
type = string
description = "Policy definition description"
default = ""
validation {
condition = length(var.policy_description) <= 512
error_message = "Definition descriptions have a maximum 512 character limit."
}
}
variable policy_mode {
type = string
description = "The policy mode that allows you to specify which resource types will be evaluated, defaults to All. Possible values are All and Indexed"
default = "All"
validation {
condition = var.policy_mode == "All" || var.policy_mode == "Indexed" || var.policy_mode == "Microsoft.Kubernetes.Data"
error_message = "Policy mode possible values are: All, Indexed or Microsoft.Kubernetes.Data (In Preview). Other modes are only allowed in built-in policy definitions, these include Microsoft.ContainerService.Data, Microsoft.CustomerLockbox.Data, Microsoft.DataCatalog.Data, Microsoft.KeyVault.Data, Microsoft.MachineLearningServices.Data, Microsoft.Network.Data and Microsoft.Synapse.Data"
}
}
variable policy_category {
type = string
description = "The category of the policy, when using the module library this should correspond to the correct category folder under /policies/var.policy_category"
default = null
}
variable policy_version {
type = string
description = "The version for this policy, if different from the one stored in the definition metadata, defaults to 1.0.0"
default = null
}
variable policy_rule {
type = any
description = "The policy rule for the policy definition. This is a JSON object representing the rule that contains an if and a then block. Omitting this assumes the rules are located in /policies/var.policy_category/var.policy_name.json"
default = null
}
variable policy_parameters {
type = any
description = "Parameters for the policy definition. This field is a JSON object that allows you to parameterise your policy definition. Omitting this assumes the parameters are located in /policies/var.policy_category/var.policy_name.json"
default = null
}
variable policy_metadata {
type = any
description = "The metadata for the policy definition. This is a JSON object representing additional metadata that should be stored with the policy definition. Omitting this will fallback to meta in the definition or merge var.policy_category and var.policy_version"
default = null
}
variable file_path {
type = any
description = "The filepath to the custom policy. Omitting this assumes the policy is located in the module library"
default = null
}
locals {
# import the custom policy object from a library or specified file path
policy_object = jsondecode(coalesce(try(
file(var.file_path),
file("${path.cwd}/policies/${title(var.policy_category)}/${var.policy_name}.json"),
file("${path.root}/policies/${title(var.policy_category)}/${var.policy_name}.json"),
file("${path.root}/../policies/${title(var.policy_category)}/${var.policy_name}.json"),
file("${path.module}/../../policies/${title(var.policy_category)}/${var.policy_name}.json")
)))
# fallbacks
title = title(replace(local.policy_name, "/-|_|\\s/", " "))
category = coalesce(var.policy_category, try((local.policy_object).properties.metadata.category, "General"))
version = coalesce(var.policy_version, try((local.policy_object).properties.metadata.version, "1.0.0"))
# use local library attributes if runtime inputs are omitted
policy_name = coalesce(var.policy_name, try((local.policy_object).name, null))
display_name = coalesce(var.display_name, try((local.policy_object).properties.displayName, local.title))
description = coalesce(var.policy_description, try((local.policy_object).properties.description, local.title))
metadata = coalesce(var.policy_metadata, try((local.policy_object).properties.metadata, merge({ category = local.category },{ version = local.version })))
parameters = coalesce(var.policy_parameters, try((local.policy_object).properties.parameters, null))
policy_rule = coalesce(var.policy_rule, try((local.policy_object).properties.policyRule, null))
# manually generate the definition Id to prevent "Invalid for_each argument" on set_assignment plan/apply
definition_id = var.management_group_id != null ? "${var.management_group_id}/providers/Microsoft.Authorization/policyDefinitions/${local.policy_name}" : azurerm_policy_definition.def.id
}
and an example how to use the module https://github.com/gettek/terraform-azurerm-policy-as-code/blob/main/examples/definitions.tf
module "deny_resource_types" {
source = "..//modules/definition"
policy_name = "deny_resource_types"
display_name = "Deny Azure Resource types"
policy_category = "General"
management_group_id = data.azurerm_management_group.org.id
}
From how I see it (might be wrrong) a variable can be used as a default value to the local in a Terraform script. So how is the value for the variable policy_name used when main.tf references local.policy_name instead of var.policy_name. The variable policy_name is also not having a default value.
What am I missing ?
Thank you !
EDIT:
Thank you, very clear explanation. I think I understand it better now. So basically, when I’m calling the definition module Terraform essentially load and process that module’s configuration files (including variables.tf). I was confused to see name = local.policy_name instead of for example mode = var.policy_mode. But the way I understand it now, is that when calling this module, I can set the value for the variable policy_name, which is then further processed inside the locals section, and result is what is actual provided to the name argument required by azurerm_policy_definition https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/policy_definition. Could you please confirm that my understanding is correct?
Thank you !
policy_name does have default value, but the value is empty.
default = ""
Empty value can be default value. Terraform expects module inputs only when there is no default attribute set in the input field.
OK, so there are two scenarios at play here.
When the policy_name isn't provided to the module, it takes on its default behaviour of empty string
When a value is provided to the policy_name, the locals section transforms that value and then uses it in the code as local.policy_name for the resources. L103
policy_name = coalesce(var.policy_name, try((local.policy_object).name, null))
If you look for the coalesce function, its purpose is to return the first element that is not null/empty.
Although, I don't see the point of that logic since if both cases are null it is supposed to return null. Could've used a simple condition instead.
I hope this clarifies things more.
Ps: an empty string (""), Terraform consider as a value.

Terraform - ordered generation of resources which are related based on a list variable

I currently try to automate nested SumoLogic forder creation as part of my custom module. I have to use this resource. I need to create a folder path similar to:
parent_folder_path = "SRE/Test/Troubleshooting"
and due to the fact that this variable will change between environments I cannot hardcode creation of the underlying resources. The problematic part is that all shown folders (SRE, Test, Troubleshooting) need to be created in a sequence because the latter needs id of the former (eg. Test folder needs id of already created SRE folder) to be created.
The end result at which I am aiming is automatically generated code as below:
resource "sumologic_folder" "SRE" {
provider = sumologic
name = "SRE"
description = ""
parent_id = "0000000000XXXXX"
}
resource "sumologic_folder" "Test" {
provider = sumologic
name = "Test"
description = ""
parent_id = sumologic_folder.SRE.id
}
resource "sumologic_folder" "Troubleshooting" {
provider = sumologic
name = "Troubleshooting"
description = ""
parent_id = sumologic_folder.Test.id
}
I tried an approach which uses templatefile() and local_file:
parent_directories.tftpl
%{~ for index, path_part in parent_folder_path ~}
%{~ if index == 0 ~}
resource "sumologic_folder" "${replace(path_part, " ", "_")}" {
provider = sumologic
name = "${path_part}"
description = ""
parent_id = "${root_folder_id}"
}
%{~ else }
resource "sumologic_folder" "${replace(path_part, " ", "_")}" {
provider = sumologic
name = "${path_part}"
description = ""
parent_id = sumologic_folder.${replace(parent_folder_path[index - 1], " ", "_")}.id
}
%{~ endif ~}
%{~ endfor ~}
main.tf
resource "local_file" "parent_directories" {
content = templatefile("${path.module}/parent_directories.tftpl", { parent_folder_path = split("/", var.parent_folder_path), root_folder_id = var.root_folder_id })
filename = "${path.module}/parent_directories.tf"
}
and the file was correctly generated during terraform apply run but I was not able to include it in the scope of the run dynamically.
Does anyone know how to handle such usecase?
Thanks in advance for all help.
Best Regards,
Rafal.
I understand what you are trying to achieve - you want to create multiple resources of the same type, but relying on each one of them created before (previous one on the list), at the same time not knowing how many there would be (more folders in path). I am afraid it is not how Terraform works. You would create a cycle between the list or map of the same resources.
That said, however, I can offer you the ugly solution. If you can limit to some number of subdirectories, let's say up to five or ten levels, you can do that in the code that will create three folders if there are three dirs in the path, and four if there are four, and so on. You just stop creating resources if this level is empty.
Let's say you have a sumo module:
variable "parent_path" {}
variable "name" {}
data "sumologic_folder" "parent" {
path = var.parent_path
}
resource "sumologic_folder" "folder" {
provider = sumologic
name = var.name
description = ""
parent_id = data.sumologic_folder.parent.id
}
output "path" {
value = "${var.path}/${var.name}"
}
And then you can split the path to the list of folders and create as many resources as there are folders in the path, for example: AA/BB/CC/DD = 4 sumofolders.
locals {
desired_path = "SRE/Test/Troubleshooting" # example - 3 folders
regex = regexall("[^//]+", local.desired_path)
path0 = "/"
}
module "sumo" {
source = "./sumo"
name = local.regex[0]
parent_path = local.path0 # var.parent_path
}
module "sumo_child_1" {
source = "./sumo"
count = try(local.regex[1], null) == null ? 0 : 1
name = try(local.regex[1], "none")
parent_path = module.sumo.path
}
module "sumo_child_2" {
source = "./sumo"
count = try(local.regex[2], null) == null ? 0 : 1
name = try(local.regex[2], "none")
parent_path = module.sumo_child_1.path
}
module "sumo_child_3" { # this is NOT going to be even created in our example
source = "./sumo"
count = try(local.regex[3], null) == null ? 0 : 1
name = try(local.regex[3], "none")
parent_path = module.sumo_child_2.path
}
# and so on... if there are no more folders in the path, the resources won't be created anyway.
Now let me say that again, this is a very ugly solution... but it works. Cheers.

How to render terraform data when using a count

I was using a count for creating multiple AWS task_definitions that should be executed by an AWS step function.
The task_definition required a data "template_file" "task_definition" { section to be able to fill the template data.
Then I needed to render the template data for multiple definitions at a time and I was blocked by an error looking like this:
The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.
Here's the initial code:
data "template_file" "task_definition" {
count = length(var.task_container_command)
template = file("./configs/file.json")
vars = {
task = module.ecs[count.index].task_definition
}
}
module "step_function" {
count = length(var.task_container_command)
source = "path"
region = var.region
name = "${var.step_function_name}-${count.index}"
definition_file = data.template_file.task_definition.rendered
}
The point here is that I can't render task_definition because these are not known by terraform yet before the apply. I wasn't able to use the -target argument either because I wanted to make the change in code and not in my deployment pipeline. Meaning when you try to do a terraform plan on the definition_file, the error will pop up.
Solution is below.
What worked was to decouple the use of the count from the .rendered argument by doing this:
data "template_file" "task_definition" {
count = length(var.task_container_command)
template = file("./configs/file.json")
vars = {
task = module.ecs[count.index].task_definition
}
}
resource "local_file" "foo" {
count = length(var.task_container_command)
content = element(data.template_file.task_definition.*.rendered, count.index)
filename = "task-definition-${count.index}"
}
module "step_function" {
count = length(var.task_container_command)
source = "path"
region = var.region
name = "${var.step_function_name}-${count.index}"
definition_file = local_file.foo[count.index].filename
}
Now your data is rendered in the resource called "foo" here and then passed to the step_function module so the terraform plan already knows what's inside your variable. The content element of foo acts like a loop to render each task_definition that I've created using a different filename to avoid duplicates.
Hope this helped :)

How to reuse a terraform resource?

I'm pretty much new to terraform. I wanted to know is there a way to reuse a resource? Below is my code. Below is the main.tf, where I have a module declared.
module "deployments" {
source = "./modules/odo_deployments"
artifact_versions = local.artifact_versions
}
In the modules/odo_deployments folder, I have two resources which does exactly the same except for a different ad. Is there a way I can use just one resource and pass arguments (ad) like a function to this resource?
variable "artifact_versions" {
description = "What gets injected by terraform at the ET level"
}
resource "odo_deployment" "incident-management-service-dev" {
count = var.artifact_versions["incident-management-service"].version == "skip" ? 0 : 1
ad = "phx-ad-1"
alias = "cloud-incident-management-application"
artifact {
url = var.artifact_versions["incident-management-service"].uri
build_tag = var.artifact_versions["incident-management-service"].version
type = var.artifact_versions["incident-management-service"].type
}
flags = ["SKIP_UP_TO_DATE_NODES"]
}
resource "odo_deployment" "incident-management-service-dev-ad3" {
count = var.artifact_versions["incident-management-service"].version == "skip" ? 0 : 1
ad = "phx-ad-3"
alias = "cloud-incident-management-application"
artifact {
url = var.artifact_versions["incident-management-service"].uri
build_tag = var.artifact_versions["incident-management-service"].version
type = var.artifact_versions["incident-management-service"].type
}
flags = ["SKIP_UP_TO_DATE_NODES"]
}
What I did to solve this is,I added a locals in the main.tf and pass the local variable in the module like below
locals {
ad = ["phx-ad-1", "phx-ad3"]
}
module "deployments" {
source = "./modules/odo_deployments"
artifact_versions = local.artifact_versions
ad = local.ad
and in the resource instead of hard coding the ad value, I used it like below
count = length(var.ad)
ad = var.ad[count.index]

How to conditionally populate an argument value in terraform?

I am writing a Terraform script to spin up resources in Google Cloud Platform.
Some resources require one argument only if the other one set, how to populate one argument only if the other one is populated (or any other similar condition)?
For example:
resource "google_compute_router" "compute_router" {
name = "my-router"
network = "${google_compute_network.foobar.name}"
bgp {
asn = 64514
advertise_mode = "CUSTOM"
advertised_groups = ["ALL_SUBNETS"]
advertised_ip_ranges {
range = "1.2.3.4"
}
advertised_ip_ranges {
range = "6.7.0.0/16"
}
}
}
In the above resource (google_compute_router) the description for both advertised_groups and advertised_ip_ranges says This field can only be populated if advertise_mode is CUSTOM and is advertised to all peers of the router.
Now if I keep the value of advertise_mode as DEFAULT, my code looks something like below:
resource "google_compute_router" "compute_router" {
name = "my-router"
network = "${google_compute_network.foobar.name}"
bgp {
asn = 64514
#Changin only the value below
advertise_mode = "DEFAULT"
advertised_groups = ["ALL_SUBNETS"]
advertised_ip_ranges {
range = "1.2.3.4"
}
advertised_ip_ranges {
range = "6.7.0.0/16"
}
}
}
The above script however on running gives the following error:
* google_compute_router.compute_router_default: Error creating Router: googleapi: Error 400: Invalid value for field 'resource.bgp.advertiseMode': 'DEFAULT'. Router cannot have a custom advertisement configurati
on in default mode., invalid
As a workaround to the above, I have created two resources with different names doing almost the same thing. The script looks something like below:
resource "google_compute_router" "compute_router_default" {
count = "${var.advertise_mode == "DEFAULT" ? 1 : 0}"
name = "${var.router_name}"
region = "${var.region}"
network = "${var.network_name}"
bgp {
asn = "${var.asn}"
advertise_mode = "${var.advertise_mode}"
#Removed some codes from here
}
}
resource "google_compute_router" "compute_router_custom" {
count = "${var.advertise_mode == "CUSTOM" ? 1 : 0}"
name = "${var.router_name}"
region = "${var.region}"
network = "${var.network_name}"
bgp {
asn = "${var.asn}"
advertise_mode = "${var.advertise_mode}"
advertised_groups = ["${var.advertised_groups}"]
advertised_ip_ranges {
range = "${var.advertised_ip_range}"
description = "${var.advertised_ip_description}"
}
}
}
The above script works fine, however it seems like a lot of code repetition to me and a hack. Also, for two options (of dependent attributes) is fine, however, if there are more options say 5, the code repetition for such a small thing would be too much.
Is there a better way to do what I am trying to achieve?
This is pretty much what you are restricted to in Terraform < 0.12. Some resources allow you to use an empty string to omit basic values and the provider will interpret this as a null value, not passing it to the API endpoint so it won't complain about it not being set properly. But from my brief experience with the GCP provider this is not the case for most things there.
Terraform 0.12 introduces nullable arguments which would allow you to set these conditionally with something like the following:
variable "advertise_mode" {}
resource "google_compute_router" "compute_router" {
name = "my-router"
network = "${google_compute_network.foobar.name}"
bgp {
asn = 64514
advertise_mode = "${var.advertise_mode}"
advertised_groups = ["${var.advertise_mode == "DYNAMIC" ? ALL_SUBNETS : null}"]
advertised_ip_ranges {
range = "${var.advertise_mode == "DYNAMIC" ? 1.2.3.4 : null}"
}
advertised_ip_ranges {
range = "${var.advertise_mode == "DYNAMIC" ? 6.7.0.0/16 : null}"
}
}
}
It will also introduce dynamic blocks that you are able to loop over so you can also have a dynamic number of advertised_ip_ranges blocks.
The above answer is incorrect as 'advertised_ip_ranges' wont accept a null value; the solution to this is to leverage a dynamic block which can handle a null value for this resource and further enables the resource to accept a variable number of ip ranges.
variable custom_ranges {
default = ["172.16.31.0/24","172.16.32.0/24"]
}
resource "google_compute_router" "router_01" {
name = "cr-bgp-${var.gcp_bgp_asn}"
region = var.gcp_region
project = var.gcp_project
network = var.gcp_network
bgp {
asn = var.gcp_bgp_asn
advertise_mode = var.advertise_mode
advertised_groups = var.advertise_mode == "CUSTOM" ? ["ALL_SUBNETS"] : null
dynamic "advertised_ip_ranges" {
for_each = var.advertise_mode == "CUSTOM" ? var.custom_ranges : []
content {
range = advertised_ip_ranges.value
}
}
}
}
additional search keys: google_compute_router "bgp.0.advertised_ip_ranges.0.range" wont accept a null value.

Resources