Terraform 1.2.0: Referencing resources and object mapping - terraform

I have deployed a cloud run application for currently two domains with a load balancer, which is already running. Now this setup needs to be rolled out to other domains. Because the resource setup is always the same, I face some issues:
I want to prevent repeating code (which is managed through a for_each)
Still there are some domain-specific values to cover, which i tried through a mapping table
Referencing resources, which are created with for_each in another resource
The first issue I solved like this, which seems to work:
Old:
resource "google_cloud_run_service" "cr_domain1" {
name = "cr-domain1"
location = "europe-west6"
project = "my_project"
template {
...
}
}
resource "google_cloud_run_service" "cr_domain2" {
name = "cr-domain2"
location = "europe-southwest1"
project = "my_project"
template {
...
}
}
New:
resource "google_cloud_run_service" "cr" {
for_each = toset( ["domain1", "domain2"] )
name = "cr-${each_key}"
location = "tdb" # This is my second issue
project = "my_project"
template {
...
}
}
Regarding second issue I still need domain-specific location setup, which I tried to solve like this, but I am getting errors:
variable "cr_location" {
type = list(object({
domain1 = string
domain2 = string
}))
default = [{
domain1 = "europe-west6"
domain2 = "europe-southwest1"
}]
}
resource "google_cloud_run_service" "cr" {
for_each = toset( ["domain1", "domain2"] )
name = "cr-${each_key}"
location = "${var.cr_location[0]}.${each.key}"
project = "my_project"
template {
...
}
}
Error is "Cannot include the given value in a string template: string required". But I have already declared it as a string in my variable "cr_location". Any idea what's the issue here? The expected output should be:
location = "europe-west6" # For domain1
location = "europe-southwest1" # For domain2
Also regarding issue 3 I do not understand how to referencing resources, which are created with for_each in another resource. So before my for_each in the cloud run resource block (see issue 1) I had this 2 resources:
resource "google_cloud_run_service" "cr_domain1"
resource "google_cloud_run_service" "cr_domain2"
Now I only have resource "google_cloud_run_service" "cr". But in my loadbalancer.tf I still have to references to the old namings (last coderow within "service"):
resource "google_compute_region_network_endpoint_group" "backendneg" {
for_each = toset( ["domain1", "domain2"] )
name = "backendneg-${each.key}"
project = "my_project"
network_endpoint_type = "SERVERLESS"
region = "${var.cr_location[0]}.${each.key}" # Here same issues as issue 2
cloud_run {
service = google_cloud_run_service.cr_domain1.name # Old reference
}
}
So if there is no "cr_domain1" anymore how do I reference to this resource? My issue is that I have to create over 20 resources like that and I couldn't figure it out how to do it. I appreciate any guideline here.

What I would suggest here is to try and refactor the variable because it is making a lot of things harder than they should be. So I would go for this kind of a variable definition:
variable "cr_location" {
type = map(string)
default = {
domain1 = "europe-west6"
domain2 = "europe-southwest1"
}
}
Then, the rest should be easy to create:
resource "google_cloud_run_service" "cr" {
for_each = var.cr_location
name = "cr-${each.key}"
location = each.value
project = "my_project"
template {
...
}
}
And for the network endpoint resource:
resource "google_compute_region_network_endpoint_group" "backendneg" {
for_each = var.cr_location
name = "backendneg-${each.key}"
project = "my_project"
network_endpoint_type = "SERVERLESS"
region = each.value
cloud_run {
service = google_cloud_run_service.cr[each.key].name
}
}
You could even try resource chaining with for_each [1] to make sure you are doing this for all the Cloud Run resources created:
resource "google_compute_region_network_endpoint_group" "backendneg" {
for_each = google_cloud_run_service.cr
name = "backendneg-${each.key}"
project = "my_project"
network_endpoint_type = "SERVERLESS"
region = each.value.location
cloud_run {
service = each.value.name
}
}
[1] https://www.terraform.io/language/meta-arguments/for_each#chaining-for_each-between-resources

Related

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 ]
}

A resource with the ID already exists - to be managed via Terraform this resource needs to be imported into the State

I would not normally ask a question like this, however I feel stuck and I do not want to hack things, but rather take the time to understand. I am new to terraform and trying to learn it, a simple task that I have set myself is to create a SQL server.
My Environment
I have some resource groups created before, anytime I try to use the same name, I get the error.
Error: A resource with the ID "/subscriptions/000000-0000-0000-0000-00000000005/resourceGroups/tf_learning" already exists - to be managed via Terraform this resource needs to be imported into the State. Please see the resource documentation for "azurerm_resource_group" for more information.
Now, you look at the error and after 2 days of google research, i followed the steps here.
Using Terraform to import existing resources on Azure
and https://gmusumeci.medium.com/how-to-import-an-existing-azure-resource-in-terraform-6d585f93ea02 and Terraform resource with the ID already exists
I create a file called existing_statee.tf with the content below.
resource "azurerm_resource_group" "tf_learning" {
}
Ran
terraform import azurerm_resource_group.tf_learning /subscriptions/000000-0000-0000-0000-00000000005/resourceGroups/tf_learningterraform import azurerm_resource_group.tf_learning /subscriptions/000000-0000-0000-0000-00000000005/resourceGroups/tf_learning
I edited the file again and saved it.
resource "azurerm_resource_group" "tf_learning" {
# (resource arguments)
name = "tf_learning"
location = "UK South"
}
Then ran the following.
terraform init
terraform plan
terraform apply
To my surprise I am still getting the error.
Error: A resource with the ID "/subscriptions/00000-00000-0000-0000-00000000000/resourceGroups/tf_learning" already exists - to be managed via Terraform this resource needs to be imported into the State. Please see the resource documentation for "azurerm_resource_group" for more information.
│
│ with azurerm_resource_group.RG-Terraform,
│ on main.tf line 1, in resource "azurerm_resource_group" "RG-Terraform":
│ 1: resource "azurerm_resource_group" "RG-Terraform" {
My tf.main file.
resource "azurerm_resource_group" "RG-Terraform" {
name = var.resource-group-name
location = var.my_location
}
resource "azurerm_sql_server" "test" {
name = var.my_dev_server
resource_group_name = azurerm_resource_group.RG-Terraform.name
location = azurerm_resource_group.RG-Terraform.location
version = var.my_dev_sql_version
administrator_login = var.my_dev_sql_login
administrator_login_password = "change_me"
}
resource "azurerm_sql_database" "test" {
name = var.my_dev_sql_database
resource_group_name = azurerm_resource_group.RG-Terraform.name
location = azurerm_resource_group.RG-Terraform.location
server_name = azurerm_sql_server.test.name
edition = var.my_dev_sql_database_sku
requested_service_objective_name = var.my_dev_sql_database_objective
tags = {
environment = "dev_database_build"
}
}
variables.tf file
variable "resource-group-name" {
default = "tf_learning"
description = "The prefix used for all resources in this example"
}
variable "app-service-name" {
default = "terraform-app-service"
description = "The name of the Web App"
}
variable "location" {
default = "UK South"
description = "The Azure location where all resources in this example should be created"
}
variable "my_location" {
type = string
default = "UK South"
}
variable "my_dev_server"{
type = string
default = "test-server-test"
}
variable "my_dev_sql_login" {
type = string
default = "mylogin"
}
variable "my_dev_sql_version" {
type = string
default = "12.0"
}
variable "my_dev_sql_database" {
type = string
default = "dev_db"
}
variable "my_dev_sql_database_tag" {
type = string
default = "dev sql database from TF"
}
variable "my_dev_sql_database_sku" {
type = string
default = "Basic"
}
variable "my_dev_sql_database_objective" {
type = string
default = "Basic"
}
I am lost as to what to do next, for now I will carry on researching.
I would like to point out this, you have configured in existing_statee.tf after importing the state
resource "azurerm_resource_group" "tf_learning" {
name = "tf_learning"
location = "UK South"
}
But in main.tf you also define again
resource "azurerm_resource_group" "RG-Terraform" {
name = var.resource-group-name
location = var.my_location
}
variable "resource-group-name" {
default = "tf_learning"
description = "The prefix used for all resources in this example"
}
variable "my_location" {
type = string
default = "UK South"
}
Since you have already declared this resource in existing_statee.tf, maybe you should remove it from main.tf

Disable a resource using count for specific environment with terraform

I'm trying to disable a resource for a specific environment in this case its the qa environment, I only want the resource to be created for production and staging.
I have the same terraform code that runs for qa,staging and production via a pipeline.
I have some code used to create an ECS cluster. I don't want it to create for the qa environment.
This is the code that creates the cluster on ECS:
resource "aws_ecs_cluster" "main" {
name = "${terraform.workspace}-main"
tags = {
App = var.app_name
Environment = terraform.workspace
}
}
output "main_ecs_id" {
value = aws_ecs_cluster.main.id
}
I tried using count to disable the qa environment, and this works:
resource "aws_ecs_cluster" "main" {
count = terraform.workspace != "qa" ? 1 : 0
name = "${terraform.workspace}-main"
tags = {
App = var.app_name
Environment = aws_ecs_cluster.main[count.index].name
}
}
output "main_ecs_id" {
value = aws_ecs_cluster.main.*.id
}
Only issue is it doesn't work for the environment (production and staging) where I want the resource to create.
When it tries to create the resource with that code on production and staging then I get this error:
Error: Self-referential block
on ecs.tf line 8, in resource "aws_ecs_cluster" "main":
8: Environment = aws_ecs_cluster.main[count.index].name
Configuration for aws_ecs_cluster.main may not refer to itself.
Any idea what I could be doing wrong?
Thanks
Your issue isn't to do with disabling the resource with count, just that you can't refer to a resource inside itself (provisioners using the self keyword being the exception here but they're extra to the resource).
Instead you'd need to either build the name again or extract the name string into a local:
resource "aws_ecs_cluster" "main" {
count = terraform.workspace != "qa" ? 1 : 0
name = "${terraform.workspace}-main"
tags = {
App = var.app_name
Environment = "${terraform.workspace}-main"
}
}
or with a local:
locals {
cluster_name = "${terraform.workspace}-main"
}
resource "aws_ecs_cluster" "main" {
count = terraform.workspace != "qa" ? 1 : 0
name = local.cluster_name
tags = {
App = var.app_name
Environment = local.cluster_name
}
}
The use of locals only allows you to handle things that are known outside of the resource block (eg your string concatenation). If you wanted something that relied on the count or each in the resource block then you wouldn't be able to use that. There's an existing issue that covers resource scoped locals as an outstanding feature request.

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

Error: azurerm_app_service.ci_rg: resource repeated multiple times

I am trying to deploy 2 different Azure Apps in the same resource group.
These Azure Apps are defined as docker images stored in an Azure Container Registry (where I previously pushed those docker images).
I am not able to deploy both of them at the same time because I think there is something wrong in the way I am defining them as Terraform is expecting to find only one azurerm_app_service, but I am not sure how I can work around this?
When I run this command: terraform plan -var-file test.tfvars, then I see this message in the output:
Error: azurerm_app_service.ci_rg: resource repeated multiple times
How do I define "2 different resources of the same type"?
This is the content of the main.tf file (where I inject the variables defined in variables.tf with the values defined in test.tfvars):
// the resource group definition
resource "azurerm_resource_group" "ci_rg" {
name = "${var.resource_group_name}"
location = "${var.azure_location}"
}
// the app service plan definition
resource "azurerm_app_service_plan" "ci_rg" {
name = "${var.app_service_plan}"
location = "${azurerm_resource_group.ci_rg.location}"
resource_group_name = "${azurerm_resource_group.ci_rg.name}"
kind = "Linux"
sku {
tier = "Standard"
size = "S1"
capacity = 2 // for both the docker containers
}
properties {
reserved = true
}
}
// the first azure app
resource "azurerm_app_service" "ci_rg" {
name = "${var.first_app_name}"
location = "${azurerm_resource_group.ci_rg.location}"
resource_group_name = "${azurerm_resource_group.ci_rg.name}"
app_service_plan_id = "${azurerm_app_service_plan.ci_rg.id}"
site_config {
linux_fx_version = "DOCKER|${var.first_app_docker_image_name}"
}
app_settings {
"CONF_ENV" = "${var.conf_env}"
"DOCKER_REGISTRY_SERVER_URL" = "${var.docker_registry_url}",
"DOCKER_REGISTRY_SERVER_USERNAME" = "${var.docker_registry_username}",
"DOCKER_REGISTRY_SERVER_PASSWORD" = "${var.docker_registry_password}",
}
}
// the second azure app
resource "azurerm_app_service" "ci_rg" {
name = "${var.second_app_name}"
location = "${azurerm_resource_group.ci_rg.location}"
resource_group_name = "${azurerm_resource_group.ci_rg.name}"
app_service_plan_id = "${azurerm_app_service_plan.ci_rg.id}"
site_config {
linux_fx_version = "DOCKER|${var.second_app_docker_image_name}"
}
app_settings {
"CONF_ENV" = "${var.conf_env}"
"DOCKER_REGISTRY_SERVER_URL" = "${var.docker_registry_url}",
"DOCKER_REGISTRY_SERVER_USERNAME" = "${var.docker_registry_username}",
"DOCKER_REGISTRY_SERVER_PASSWORD" = "${var.docker_registry_password}",
}
}
Edit:
I am not entirely sure about how this Terraform thing works, but I think the label azurerm_app_service is taken by the "syntax of Terraform". See the docs here: https://www.terraform.io/docs/providers/azurerm/r/app_service.html
where the title is azurerm_app_service. So I don't think I can change that.
My guess would be you need to rename the second one to something else. Like this: resource "azurerm_app_service" "ci_rg_second". It obviously doesnt like the fact that it has the same name.

Resources