using count.index in terraform? - terraform

I am trying to generate a bunch of files from templates. I need to replace the hardcoded 1 with the count.index, not sure what format terraform will allow me to use.
resource "local_file" "foo" {
count = "${length(var.files)}"
content = "${data.template_file.tenant_repo_multi.1.rendered}"
#TODO: Replace 1 with count index.
filename = "${element(var.files, count.index)}"
}
data "template_file" "tenant_repo_multi" {
count = "${length(var.files)}"
template = "${file("templates/${element(var.files, count.index)}")}"
}
variable "files" {
type = "list"
default = ["filebeat-config_filebeat.yml",...]
}
I am running with:
Terraform v0.11.7
+ provider.gitlab v1.0.0
+ provider.local v1.1.0
+ provider.template v1.0.0

You can iterate through the tenant_repo_multi data source like so -
resource "local_file" "foo" {
count = "${length(var.files)}"
content = "${element(data.template_file.tenant_repo_multi.*.rendered, count.index)}"
filename = "${element(var.files, count.index)}"
}
However, have you considered using the template_dir resource in the Terraform Template provider. An example below -
resource "template_dir" "config" {
source_dir = "./unrendered"
destination_dir = "./rendered"
vars = {
message = "world"
}
}

Related

Removing region from Resource Group name

How would I go about removing the location code (aus) from the Resource Group creation.
from:
rg-d-lxr-aus-app1
to
rg-d-lxr-app1
I only want this to affect the only RG and not any other resources eg vnets/keyvaults etc
app1
locals.tf
locals {
full_env_code = format("%s-%s-%s", lower(var.environment_code), lower(var.deployment_code), lower(var.location_code))
}
resource_groups.tf
module "rg-app1" {
source = "git::ssh://git#ssh.dev.azure.com/*"
resource_group_name = format("rg-%s-%s", local.full_env_code, lower(var.name_suffix))
location = var.location
}
variables.tf
variable "location" {
description = "Location in which to deploy resources"
# default = "Australia Southeast"
}
variable "environment_code" {
description = "Environment code"
# default = "d"
}
variable "environment" {
description = "Environment"
# default = "d"
}
variable "deployment_code" {
description = "Deployment code"
# default = "d"
}
variable "location_code" {
description = "Location code"
# default = "d"
}
dev.tfvars
environment = "non-prod"
environment_code = "d"
deployment_code = "my org"
location_code = "aus"
location = "Australia Southeast"
name_suffix = "app1"
Output of TF Plan
Terraform will perform the following actions:
# module.rg-sharegate.azurerm_resource_group.rg will be created
+ resource "azurerm_resource_group" "rg" {
+ id = (known after apply)
+ location = "australiasoutheast"
+ name = "rg-d-lxr-aus-app1"
}
Plan: 1 to add, 0 to change, 0 to destroy.
I am thinking we need to modify the RG module and use the split command, but i'm not sure how to go about it
module "rg-app1" {
resource_group_name = format("rg-%s-%s", local.full_env_code, lower(var.name_suffix))
You can use a mixture of built-in Terraform functions to achieve what you're after.
Local Code to recreate a MRE for you:
locals {
rg_string = "rg-d-lxr-aus-app1"
// Use split by "-" to create an array so we can use contains()
is_rg = contains(split("-", local.rg_string), "rg")
final_rg_string = local.is_rg ? replace(local.rg_string, "aus-", "") : local.rg_string
not_rg_string = "blah-d-lxr-aus-app1"
is_not_rg = contains(split("-", local.not_rg_string), "rg")
final_not_rg_string = local.is_not_rg ? replace(local.not_rg_string, "aus-", "") : local.not_rg_string
}
Result using Terraform Console:
> local.is_rg
true
> local.final_rg_string
"rg-d-lxr-app1"
> local.is_not_rg
false
> local.final_not_rg_string
"blah-d-lxr-aus-app1"
Links:
https://www.terraform.io/language/functions/replace
https://www.terraform.io/language/functions/contains
https://www.terraform.io/language/functions/split
You'll likely not need to duplicate the logic but I did just for demonstrative purposes. For example you can just use your resource_group_name and create your true/false conditions. If you need more help with that please let me know.

Terraform - azurerm_frontdoor_custom_https_configuration - 'the given key does not identify an element in this collection value'

this code has worked before, all I'm trying to do is add new frontend endpoints, routing rules, backend pools
I've tried only sharing the code snippets that I think are relevant but let me know if there's some key info you need missing
This one has stumped me for a couple days now and no matter what I've tried I cannot seem to make sense of the error. Its like its indexing out of the variable or searching for something that isn't there but there are something like 6 already there and now I'm adding another.
I'm worried that this front door code has not been ran in awhile and something has gotten screwed up in state. Especially given all the alerts on the accompanying TF docs for this resource - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/frontdoor_custom_https_configuration
Its been quite awhile but the AzureRM version has gone through several updates - possibly from previous to 2.58 to now past 2.58. I guess I also don't know how to verify/look at the state file and ensure its correct - even looking at the 2.58 upgrade notes its just confusing.
Ideas?
The error
on ..\modules\frontdoor\main.tf line 129, in resource "azurerm_frontdoor_custom_https_configuration" "https_config":
129: frontend_endpoint_id = azurerm_frontdoor.main.frontend_endpoints[each.value]
|----------------
| azurerm_frontdoor.main.frontend_endpoints is map of string with 8 elements
| each.value is "www-sell-dev-contoso-com"
The given key does not identify an element in this collection value.
main.tf
provider "azurerm" {
features {}
}
terraform {
backend "azurerm" {
}
}
#the outputs.tf on this module output things like the frontdoor_endpoints
#the outputs.tf with main.tf also output similar values
module "coreInfraFrontDoor" {
source = "../modules/frontdoor"
resource_group_name = module.coreInfraResourceGroup.resource_group_name
frontdoor_name = "fd-infra-${terraform.workspace}-001"
enforce_backend_pools_certificate_name_check = lookup(var.enforce_backend_pools_certificate_name_check, terraform.workspace)
log_analytics_workspace_id = module.coreInfraLogAnalytics.log_analytics_workspace_id
tags = local.common_tags
health_probes = lookup(var.health_probes, terraform.workspace)
routing_rules = lookup(var.routing_rules, terraform.workspace)
backend_pools = lookup(var.backend_pools, terraform.workspace)
frontend_endpoints = lookup(var.frontend_endpoints, terraform.workspace)
prestage_frontend_endpoints = lookup(var.prestage_frontend_endpoints, terraform.workspace)
frontdoor_firewall_policy_name = "fdfwp${terraform.workspace}001"
frontdoor_firewall_prestage_policy_name = "fdfwp${terraform.workspace}prestage"
mode = lookup(var.mode, terraform.workspace)
ip_whitelist_enable = lookup(var.ip_whitelist_enable, terraform.workspace)
ip_whitelist = lookup(var.ip_whitelist, terraform.workspace)
key_vault_id = module.coreInfraKeyVault.id
}
module main.tf
resource "azurerm_frontdoor" "main" {
name = var.frontdoor_name
location = "global"
resource_group_name = var.resource_group_name
enforce_backend_pools_certificate_name_check = var.enforce_backend_pools_certificate_name_check
tags = var.tags
dynamic "routing_rule {#stuff is here obv}
dynamic "backend_pool {#also here}
#i think this is because there was an issue/needs to be some default value for the first endpoint?
frontend_endpoint {
name = var.frontdoor_name
host_name = "${var.frontdoor_name}.azurefd.net"
web_application_firewall_policy_link_id = azurerm_frontdoor_firewall_policy.main.id
}
#now the dynamic ones from vars
dynamic "frontend_endpoint" {
for_each = var.frontend_endpoints
content {
name = frontend_endpoint.value.name
host_name = frontend_endpoint.value.host_name
session_affinity_enabled = lookup(frontend_endpoint.value, "session_affinity_enabled", false)
web_application_firewall_policy_link_id = azurerm_frontdoor_firewall_policy.main.id
}
}
versions.tf
terraform {
required_version = "~> 0.14.7"
required_providers {
azurerm = "~>2.72.0"
}
}
variables.tf
variable "frontend_endpoints" {
type = map(any)
description = "List of frontend (custom) endpoints. This is in addition to the <frontend_name>.azurefd.net endpoint that this module creates by default."
default = {
dev = [
{
name = "dev-search-contoso-com"
host_name = "dev.search.contoso.com"
},
{
name = "dev-cool-contoso-com"
host_name = "dev.cool.contoso.com"
},
########################
#this is new below
########################
{
name = "dev-sell-contoso-com"
host_name = "dev.sell.contoso.com"
}
]
prod = [ #you get the idea ]
}

How to handle empty list for dynamic block

This is regarding an Azure resource, app_service, but I think it’s a more general HCL question…
You can specify IP restrictions to an app_service using a dynamic block e.g.
locals {
ip_addresses = [ "192.168.250.1" ]
}
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "West Europe"
}
resource "azurerm_app_service_plan" "example" {
name = "example-appserviceplan"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
sku {
tier = "Standard"
size = "S1"
}
}
resource "azurerm_app_service" "example" {
name = "example-app-service"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
app_service_plan_id = azurerm_app_service_plan.example.id
site_config {
dotnet_framework_version = "v4.0"
scm_type = "LocalGit"
}
app_settings = {
"SOME_KEY" = "some-value"
}
connection_string {
name = "Database"
type = "SQLServer"
value = "Server=some-server.mydomain.com;Integrated Security=SSPI"
}
dynamic "ip_restriction" {
for_each = toset(local.ip_addresses)
content {
ip_address = each.value
}
}
}
However, to remove the restrictions you need to explicit assign ip_restriction to the empty list, i.e.
resource "azurerm_app_service" "example" {
...
ip_restriction = []
}
What I don’t see is how to do this conditionally - if I make two resources and have those conditional my app_service will be created/destroyed whereas I need it updated in place.
I'm afraid that the dynamic block does not support an empty list when using a conditional expression. Read more reference here.
After my validation, the conditional expression like for_each = var.some_variable == "" ? [] : [1] does not work when var.some_variable set to null but this could work seperately when for_each = var.some_variable and var.some_variable set to null.
So, in this case, as the answer from #rkm, you can use the for loop like this working sample for me.
variable "ip_restrictions" {
default = [
# {
# ip_address = "1.1.1.1/32"
# virtual_network_subnet_id = null
# subnet_id = null
# name = "aaa"
# priority = 110
# action = "Allow"
# },
# {
# ip_address = "2.2.2.2/32"
# virtual_network_subnet_id = null
# subnet_id = null
# name = "bbb"
# priority = 112
# action = "Allow"
# },
]
}
resource "azurerm_app_service" "example" {
name = "nn-example-app-service"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
app_service_plan_id = azurerm_app_service_plan.example.id
site_config {
ip_restriction = [
for s in var.ip_restrictions :
{
ip_address = s.ip_address
virtual_network_subnet_id = s.virtual_network_subnet_id
subnet_id = s.subnet_id
name = s.name
priority = s.priority
action = s.action
}
]
}
}
This is special terraform syntax called Attributes as Blocks. Resource arguments defined using nested block syntax implicitly define a fixed collection of objects and thus in order to specify zero objects, we should explicitly set empty list. And these two forms cannot be mixed.
With that said, terraform supports an argument syntax too (even though they recommend using block syntax for simple cases for readability):
example = [
for name in var.names: {
foo = name
}
]
Just in case anyone lands here looking for how to return a blank dynamic configuration block (instead of an input parameter like the OP), you can make the iteration list used by the for_each to be blank, and use conditionals in the content to set everything to null.
For example, if you have a dynamic inline_policy for AWS iam_role and you pass your inline json with a variable name policy_documents, you can first combine them into a single json document, then set a local variable to either a blank or what was passed in:
data "aws_iam_policy_document" "combined_policy" {
source_policy_documents = var.policy_documents
}
locals {
policy_documents = length(var.policy_documents) == 0 ? [""] : [data.aws_iam_policy_document.combined_policy.json]
}
Now you can use local.policy_documents as the iterator for the dynamic block and the blank ensures the dynamic block will always generate something. To produce a blank inline_policy, you can test if the iterator is blank and set everything to null.
resource "aws_iam_role" "default" {
... #(other parameters ommitted)
dynamic "inline_policy" {
for_each = local.policy_documents
content {
name = length(inline_policy.value) == 0 ? null : "custom_inline_policy"
policy = length(inline_policy.value) == 0 ? null : inline_policy.value
}
}
}
In the case where no inline policy is passed in, the dynamic block will generate a blank inline_policy {}:
+ resource "aws_iam_role" "default" {
+ arn = (known after apply)
+ assume_role_policy = jsonencode(
{
+ Statement = [
+ {
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Principal = {
+ Service = "ec2.amazonaws.com"
}
+ Sid = ""
},
]
+ Version = "2012-10-17"
}
)
+ create_date = (known after apply)
+ id = (known after apply)
+ inline_policy {}
}
I discovered how important a blank inline_policy is by accident when someone manually added an inline policy in the console, but the dynamic inline_policy did not notice it when I ran a terraform plan. If there was no inline_policy to start with, then the dynamic block never runs and so terraform doesn't check it. If you force the dynamic block to run even if the inline_policy is blank, terraform will ensure it is blank and delete the manually added inline policy. (Look here and here for terraform documentation and here for a good discussion at hashicorp about blank dyanamic blocks.)

Using "count" in aws_route53_record terrafrom resource

I'm starting to use (and learn) terraform, for now, I need to create multiple DO droplets and attach them to the aws route53 zone, what I'm trying to do:
My DO terraform file:
# Configure the DigitalOcean Provider
provider "digitalocean" {
token = var.do_token
}
# Create a new tag
resource "digitalocean_tag" "victor" {
name = "victor-fee1good22"
}
resource "digitalocean_droplet" "web" {
count = 2
image = var.do_config["image"]
name = "web-${count.index}"
region = var.do_config["region"]
size = var.do_config["size"]
ssh_keys = [var.public_ssh_key, var.pv_ssh_key]
tags = [digitalocean_tag.victor.name]
}
My route53 file:
provider "aws" {
version = "~> 2.0"
region = "us-east-1"
access_key = var.aws_a_key
secret_key = var.aws_s_key
}
data "aws_route53_zone" "selected" {
name = "devops.rebrain.srwx.net"
}
resource "aws_route53_record" "www" {
сount = length(digitalocean_droplet.web)
zone_id = data.aws_route53_zone.selected.zone_id
name = "web_${count.index}"
type = "A"
ttl = "300"
records = [digitalocean_droplet.web[count.index].ipv4_address]
}
But I always get The "count" object can be used only in "resource" and "data" blocks, and only
when the "count" argument is set. error, what did I wrong?
Thanks!
UPDATE:
I've resolved this one like — add сount = 2 instead of сount = length(digitalocean_droplet.web)
It works but would be better to have the dynamic variable instead of constant count. :)
you want to get number of services, that not yet created. Terraform couldn't do that.
As I think simplest way use common var with the number of droplets.
resource "digitalocean_droplet" "test" {
count = var.number_of_vps
image = "ubuntu-18-04-x64"
name = "test-1"
region = data.digitalocean_regions.available.regions[0].slug
size = "s-1vcpu-1gb"
}
resource "aws_route53_record" "test" {
count = var.number_of_vps
zone_id = data.aws_route53_zone.primary.zone_id
name = "${local.login}-${count.index}.${data.aws_route53_zone.primary.name}"
type = "A"
ttl = "300"
records = [digitalocean_droplet.test[count.index].ipv4_address]
}
This trick helped - https://github.com/hashicorp/terraform/issues/12570#issuecomment-291517239
resource "aws_route53_record" "dns" {
count = "${length(var.ips) > 0 ? length(var.domains) : 0}"
// ...
}

How do I make terraform skip that block while creating multiple resources in loop from a CSV file?

Hi I am trying to create a Terraform script which will take inputs from the user in the form of a CSV file and create multiple Azure resources.
For example if the user wants to create: ResourceGroup>Vnet>Subnet in bulk, he will provide input in CSV format as below:
resourcegroup,RG_location,RG_tag,domainname,DNS_Zone_tag,virtualnetwork,VNET_location,addressspace
csvrg1,eastus2,Terraform RG,test.sd,Terraform RG,csvvnet1,eastus2,10.0.0.0/16,Terraform VNET,subnet1,10.0.0.0/24
csvrg2,westus,Terraform RG2,test2.sd,Terraform RG2,csvvnet2,westus,172.0.0.0/8,Terraform VNET2,subnet1,171.0.0.0/24
I have written the following working main.tf file:
# Configure the Microsoft Azure Provider
provider "azurerm" {
version = "=1.43.0"
subscription_id = var.subscription
tenant_id = var.tenant
client_id = var.client
client_secret = var.secret
}
#Decoding the csv file
locals {
vmcsv = csvdecode(file("${path.module}/computelanding.csv"))
}
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = [azurerm_resource_group.myterraformgroup]
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
To be continued....
The issue I am facing here what is in the second resource group, the user don't want a resource type, suppose the user want to skip the DNS zone in the resource group csvrg2. How do I make terraform skip that block ?
Edit: What I am trying to achieve is "based on some condition in the CSV file, not to create azurerm_dns_zone resource for the resource group csvrg2"
I have provided an example of the CSV file, how it may look like below:
resourcegroup,RG_location,RG_tag,DNS_required,domainname,DNS_Zone_tag,virtualnetwork,VNET_location,addressspace
csvrg1,eastus2,Terraform RG,1,test.sd,Terraform RG,csvvnet1,eastus2,10.0.0.0/16,Terraform VNET,subnet1,10.0.0.0/24
csvrg2,westus,Terraform RG2,0,test2.sd,Terraform RG2,csvvnet2,westus,172.0.0.0/8,Terraform VNET2,subnet1,171.0.0.0/24
you had already the right thought in your mind using the depends_on function. Although, you're using a count inside, which causes from my understanding, that once the first resource[0] is created, Terraform sees the dependency as solved and goes ahead as well.
I found this post with a workaround which you might be able to try:
https://github.com/hashicorp/terraform/issues/15285#issuecomment-447971852
That basically tells us to create a null_resource like in that example:
variable "instance_count" {
default = 0
}
resource "null_resource" "a" {
count = var.instance_count
}
resource "null_resource" "b" {
depends_on = [null_resource.a]
}
In your example, it might look like this:
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = null_resource.example
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
resource "null_resource" "example" {
...
depends_on = [azurerm_resource_group.myterraformgroup[length(local.vmcsv)]]
}
or depending on your Terraform version (0.12+ which you're using guessing your syntax)
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = [azurerm_resource_group.myterraformgroup[length(local.vmcsv)]]
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
I hope that helps.
Greetings

Resources