Curently I'm trying to build dev and production environment without duplicating resource blocks. I have found that I can crate map of objects and use for loop for this.
For this I have created this piece of code that was
variable "sqlserver" {
type = map(object({
name = string
username = string
password = string
}))
}
sqlserver = {
"dev" = {
name = "devsonovasqlserver"
username = "dev_username"
password = "biaJB8wQJb4n!RwG"
}
"prd" = {
name = "testexamplesqlsonova"
username = "prd_username"
password = "biaJB8wQJb4asdan!RwG"
}
}
resource "azurerm_sql_server" "sql_server" {
for_each = var.sqlserver
name = each.value["name"]
resource_group_name = var.dev_main_rg
location = var.location
version = "12.0"
administrator_login = each.value["username"]
administrator_login_password = each.value["password"]
}
This sadly raise Error like
╷
│ Error: Incorrect attribute value type
│
│ on main.tf line 56, in resource "azurerm_sql_server" "dev_sql_server":
│ 56: name = var.sqlserver.name
│ ├────────────────
│ │ var.sqlserver.name is a object, known only after apply
│
│ Inappropriate value for attribute "name": string required.
╵
Your code is valid. When I copy it to a project of my own it works fine. I guess you have something else in your files that make it work different from what is shown here.
Related
I can't figure out how to dynamically connect disks and networks to virtual machines if they are counted
I expect i will pass variables like this:
module "vps-test" {
source = "../module"
count = 3
server_name = "vpstest"
server_image = "debian-11"
server_type = "cx21"
server_datacenter = "fsn1-dc14"
labels = { groups = "test_offline.test_vps" }
server_network = {
backend_network = {
subnet_id = (data.terraform_remote_state.htz_network.outputs.main-subnet-id)
ip = "" #DHCP
}
custom_network = {
subnet_id = "1867414"
ip = ""
}
}
volumes = {
firts_volume = {
name = "volume1"
size = "20"
}
second_volume = {
name = "volume1"
size = "20"
}
}
hetzner_cloud_token = var.hetzner_cloud_offline_main_api_token
cloud_init_file_path = "../module/scripts/user.yaml"
}
and the module will create 3 identical VMs, where each will have 2 disks and 2 networks
It's Hetznere cloud provider, here is my simple code:
resource "hcloud_server" "vps" {
count = var.server_count
name = var.server_count > 1 ? "${var.server_name}-${count.index}" : var.server_name
image = var.server_image
server_type = var.server_type
datacenter = var.server_datacenter
user_data = data.template_file.ansible_user_data.rendered
labels = var.labels
}
resource "hcloud_volume" "volume" {
for_each = var.volumes
name = tostring(each.value["name"])
size = tonumber(each.value["size"])
server_id = hcloud_server.vps.id
automount = true
format = var.volume_filesystem
}
resource "hcloud_server_network" "network" {
for_each = var.server_network
server_id = hcloud_server.vps.id
subnet_id = each.value["subnet_id"]
ip = tostring(each.value["ip"])
}
Errors:
│ Error: Missing resource instance key
│
│ on ../module/resource.tf line 15, in resource "hcloud_volume" "volume":
│ 15: server_id = hcloud_server.vps.id
│
│ Because hcloud_server.vps has "count" set, its attributes must be accessed on specific instances.
│
│ For example, to correlate with indices of a referring resource, use:
│ hcloud_server.vps[count.index]
╵
╷
│ Error: Missing resource instance key
│
│ on ../module/resource.tf line 22, in resource "hcloud_server_network" "network":
│ 22: server_id = hcloud_server.vps.id
│
│ Because hcloud_server.vps has "count" set, its attributes must be accessed on specific instances.
│
│ For example, to correlate with indices of a referring resource, use:
│ hcloud_server.vps[count.index]
if using recommends from error log
│ Error: Reference to "count" in non-counted context
│
│ on ../module/resource.tf line 15, in resource "hcloud_volume" "volume":
│ 15: server_id = hcloud_server.vps[count.index].id
│
│ The "count" object can only be used in "module", "resource", and "data" blocks, and only when the "count" argument is set.
╵
╷
│ Error: Reference to "count" in non-counted context
│
│ on ../module/resource.tf line 22, in resource "hcloud_server_network" "network":
│ 22: server_id = hcloud_server.vps[count.index].id
│
│ The "count" object can only be used in "module", "resource", and "data" blocks, and only when the "count" argument is set.
but if server_id = hcloud_server.vps[0].id (or any specific index) - working
where is the correct way
Since you are using both count and for_each you need to iterate over both of them. Basically it means you need a double for loop. One way to do it in TF is with the help of setproduct:
resource "hcloud_volume" "volume" {
for_each = {for idx, val in setproduct(range(var.server_count), keys(var.volumes)): idx => val}
name = tostring(var.volume[each.value[1]].name)
size = tonumber(var.volume[each.value[1]].size)
server_id = hcloud_server.vps[each.value[0]].id
automount = true
format = var.volume_filesystem
}
resource "hcloud_server_network" "network" {
for_each = {for idx, val in setproduct(range(var.server_count), keys(var.server_network)): idx => val}
server_id = hcloud_server.vps[each.value[0]].id
subnet_id = var.server_network[each.value[1]].subnet_id
ip = tostring(var.server_network[each.value[1]].ip)
}
So my goal is to have write a terraform code to deploy 3 resource groups in AZ dev, uat and prod with each having the following resources.
SQL Database
Key Vault
variable.tf
variable "resource_group_name" {
description = "deafault resource group"
type = list (string)
default = ["Test-dev","Test-uat","Test-prod"]
}
variable "storage_account_name" {
description = "name for storage account"
default = "test-storageact"
}
main.tf
resource "azurerm_resource_group" "resgrp" {
for_each = toset(var.resource_group_name)
name = "${var.resource_group_name}-rg-${each.value}"
location = var.location
tags = {
"Environment" = "${var.env}-${each.value}"
}
}
resource "azurerm_storage_account" "storageact" {
for_each = toset(var.resource_group_name)
name = "${var.storage_account_name}-${each.value}"
resource_group_name = azurerm_resource_group.resgrp["${each.value}"].location
location = azurerm_resource_group.resgrp["${each.value}"].name
account_tier = "Standard"
account_replication_type = "LRS"
tags = {
"Environment" = "${var.env}-${each.value}"
}
}
error message
│ Error: Invalid template interpolation value
│
│ on main.tf line 3, in resource "azurerm_resource_group" "resgrp":
│ 3: name = "${var.resource_group_name}-rg-${each.value}"
│ ├────────────────
│ │ var.resource_group_name is list of string with 3 elements
│
│ on main.tf line 6, in resource "azurerm_resource_group" "resgrp":
│ 6: "Environment" = "${var.env}-${each.value}"
│ ├────────────────
│ │ var.env is map of string with 3 elements
│
│ Cannot include the given value in a string template: string required.
╵
Operation failed: failed running terraform plan (exit 1)
please any help will be greatly appreciated.
i tried using type = map(string) in the variable but still gave me an error.
There are a few problems with the code.
var.resource_group_name is a list containing 3 string elements (by default), and thus the error: var.resource_group_name is list of string with 3 elements explains the problem, and is illustrated below.
The var.env is a map consisting of 3 elements and also being used as a string and fails for the same reason.
resource "azurerm_resource_group" "resgrp" {
for_each = toset(var.resource_group_name)
# var.resource_group_name is actually a list, but it's being
# used as a string here, which will fail.
name = "${var.resource_group_name}-rg-${each.value}"
location = var.location
tags = {
"Environment" = "${var.env}-${each.value}"
}
}
You probably instead want:
resource "azurerm_resource_group" "resgrp" {
for_each = toset(var.resource_group_name)
name = "${each.value}-rg"
location = var.location
tags = {
"Environment" = "${each.value}"
}
}
Additionally, as an alternative to the approach of putting multiple environments in a single resource construct, consider using modules to create reusable infrastructure for your resources and then calling each module for the environment that you're using, this is a best practice when implementing duplicate or near-duplicate infrastructure across multiple environments and allows you some flexibility with naming conventions and other parameters that would differ based upon the environment.
Rough example:
module "test-dev" {
source = "../modules/infrastructure"
environment = "Test-dev"
vm_count = 1
}
module "test-uat" {
source = "../modules/infrastructure"
environment = "Test-dev"
vm_count = 3
}
module "test-prod" {
source = "../modules/infrastructure"
environment = "Test-prod"
account_tier = "Premium"
vm_count = 6
}
Azure Loadbalancer Rule Module code is like below:
main.tf.
resource "azurerm_lb_rule" "lb_rule" {
count = length(var.lb_rule_specs)
name = var.lb_rule_specs[count.index]["name"]
resource_group_name = var.resource_group_name
loadbalancer_id = var.loadbalancer_id
frontend_ip_configuration_name = var.lb_rule_specs[count.index]["frontend_ip_configuration_name"]
protocol = var.lb_rule_specs[count.index]["protocol"]
frontend_port = var.lb_rule_specs[count.index]["frontend_port"]
backend_port = var.lb_rule_specs[count.index]["backend_port"]
probe_id = var.probe_id
load_distribution = var.load_distribution
backend_address_pool_id = var.backend_address_pool_id
}
variables.tf
variable "lb_rule_specs" {
description = "Load balancer rules specifications"
type = list(map(string))
}
variable "resource_group_name" {
description = "Name of the resource group"
type = string
}
variable "loadbalancer_id" {
description = "ID of the load balancer"
type = string
}
variable "backend_address_pool_id" {
description = "Backend address pool id for the load balancer"
type = string
}
variable "probe_id" {
description = "ID of the loadbalancer probe"
type = string
default = ""
}
variable "load_distribution" {
description = "Specifies the load balancing distribution type to be used by the Load Balancer. Possible values are: Default – The load balancer is configured to use a 5 tuple hash to map traffic to available servers. SourceIP – The load balancer is configured to use a 2 tuple hash to map traffic to available servers. SourceIPProtocol – The load balancer is configured to use a 3 tuple hash to map traffic to available servers. Also known as Session Persistence, where the options are called None, Client IP and Client IP and Protocol respectively."
type = string
default = ""
}
Calling module as below:
variable "loadbalancer_rule" {
description = "Map of loadbalancer-rule objects"
type = any
default = null
}
module "loadbalancer_rule" {
for_each = coalesce(var.loadbalancer_rule, {})
source = "company.com.au/tfmodules/loadbalancer-rule/azurerm"
version = "7.0.0-2-1.0"
backend_address_pool_id = try(each.value.backend_address_pool_id, null)
load_distribution = try(each.value.load_distribution, "")
loadbalancer_id = each.value.loadbalancer_ref != null ? module.loadbalancer[each.value.loadbalancer_ref].id : null
probe_id = each.value.probe_ref != null ? module.loadbalancer_probe[each.value.probe_ref].id : null
resource_group_name = var.__ngc.environment_resource_groups
lb_rule_specs = [
for lb_rule_spec in each.value.lb_rule_specs :
{
frontend_ip_configuration_name = try(for_each.lb_rule_spec.frontend_ip_configuration_name, null)
protocol = try(for_each.lb_rule_spec.protocol, null)
frontend_port = try(for_each.lb_rule_spec.frontend_port, null)
backend_port = try(for_each.lb_rule_spec.backend_port, null)
}
]
}
lbrule.auto.tfvars.json file like below:
{
"loadbalancer_rule": {
"patterns_default_loadbalancer_rule": {
"backend_address_pool_id": null,
"lb_rule_specs" : {
"name" : "test2",
"protocol": "tcp",
"frontend_port": "8080",
"backend_port": "8081",
"frontend_ip_configuration_name": "LBFrontendIPConfig_1"
},
"name" : "test2",
"protocol": "tcp",
"frontend_port": "8100",
"backend_port": "9100",
"frontend_ip_configuration_name": "LBFrontendIPConfig_2"
},
"load_distribution": "",
"loadbalancer_ref": "patterns_default_loadbalancer",
"probe_ref": "patterns_default_loadbalancer_probe"
}
}
Unfortunately, I get error as like below:
│ Error: Reference to undeclared resource
│
│ on loadbalancer_rule.tf line 20, in module "loadbalancer_rule":
│ 20: frontend_ip_configuration_name = try(for_each.lb_rule_spec.frontend_ip_configuration_name, null)
│
│ A managed resource "for_each" "lb_rule_spec" has not been declared in the
│ root module.
╵
╷
│ Error: Reference to undeclared resource
│
│ on loadbalancer_rule.tf line 21, in module "loadbalancer_rule":
│ 21: protocol = try(for_each.lb_rule_spec.protocol, null)
│
│ A managed resource "for_each" "lb_rule_spec" has not been declared in the
│ root module.
╵
╷
│ Error: Reference to undeclared resource
│
│ on loadbalancer_rule.tf line 22, in module "loadbalancer_rule":
│ 22: frontend_port = try(for_each.lb_rule_spec.frontend_port, null)
│
│ A managed resource "for_each" "lb_rule_spec" has not been declared in the
│ root module.
╵
╷
│ Error: Reference to undeclared resource
│
│ on loadbalancer_rule.tf line 23, in module "loadbalancer_rule":
│ 23: backend_port = try(for_each.lb_rule_spec.backend_port, null)
│
│ A managed resource "for_each" "lb_rule_spec" has not been declared in the
│ root module.
I am guessing that I am not writing outer OR inner loop properly? or perhaps the definition file (variable file) is not right?
There could be one or more lb rules and for each of those there could be 1 or more front end ip, protocol, front end port and backend port.
I have recently separated terraform files from it's variable file as per below structure
(root)
| main.tf
| users.tf
| roles.tf
├── Configuration (folder)
├──── azure-pipelines.yml
├──── .gitignore
├──── variables.tf
and since then I am getting bellow error messages
│ Error: Unsupported attribute
│
│ on main.tf line 77, in resource "azurerm_key_vault_secret" "primary_account_storage_access_key":
│ 77: name = "${module.variables.storage-account_name}-access-key"
│ ├────────────────
│ │ module.variables is a object, known only after apply
│
│ This object does not have an attribute named "storage-account_name".
╵
╷
│ Error: Unsupported attribute
│
│ on main.tf line 78, in resource "azurerm_key_vault_secret" "primary_account_storage_access_key":
│ 78: value = module.variables.storage-access_key
│ ├────────────────
│ │ module.variables is a object, known only after apply
│
│ This object does not have an attribute named "storage-access_key".
╵
This is how problematic resource "primary_account_storage_access_key" is defined
resource "azurerm_key_vault_secret" "primary_account_storage_access_key" {
depends_on = [azurerm_key_vault_access_policy.terraform_sp_access]
key_vault_id = data.azurerm_key_vault.azvault.id
name = "${module.variables.storage-account_name}-access-key"
value = module.variables.storage-access_key
}
Module is defined as below:
module "variables" {
source = "./Configuration"
}
I have no issue with utilizing the same module in other resources placed in the same file (main.tf)
terraform {
backend "azurerm" {
resource_group_name = module.variables.storage-resource_group_name
storage_account_name = module.variables.storage-storage_account_name
container_name = module.variables.storage-container_name
key = module.variables.storage-key
}
}
Found articles here on the site focusing on the error suggesting
"splat" operator with "toset" / "one" function, however that did not help:
name = "${toset(module.variables[*].storage-account_name)}-access-key"
value = toset(module.variables[*].storage-access_key)
name = "${one(module.variables[*].storage-account_name)}-access-key"
value = one(module.variables[*].storage-access_key)
Content of variables.tf what child module variables uses:
variable "storage-resource_group_name" {
type = string
default = "Reporting-HFM-integration-rg"
}
variable "storage-account_name" {
type = string
default = "reportinghfmintegration"
}
variable "storage-container_name" {
type = string
default = "tfstate-blob"
}
variable "storage-key" {
type = string
default = "terraform.tfstate"
}
variable "storage-access_key" {
type = string
default = "u3K..."
}
variable "keyVault-name" {
type = string
default = "se-dataplat-dwvault-prod"
}
variable "keyVault-resource_group_name" {
type = string
default = "AzureDataPlatform-dwtools-prod-rg"
}
variable "keyVault-id" {
type = string
default = "/subscriptions/23a89ca1-9743-4b3b-b5ff-41cea9985deb/resourceGroups/..."
}
I have a formatting issue that I cannot figure out. The soa_record seems to fail on the email. Maybe the soa_record block is not being defined correctly?
Code and error below.
variable "private_dns_zones" {
description = "A list of Private DNS Zones and their properties."
type = list(object({
name = string
resource_group_name = string
tags = map(string)
soa_record = object({
email = string
expire_time = number
minimum_ttl = number
refresh_time = number
retry_time = number
ttl = number
})
}))
}
resource "azurerm_private_dns_zone" "this" {
for_each = { for n in var.private_dns_zones : "${n.name}" => n } #name of private dns zone is unique
name = each.value.name
resource_group_name = each.value.resource_group_name
tags = merge(data.azurerm_resource_group.this[each.value.resource_group_name].tags, each.value.tags)
soa_record = {
email = each.value.soa_record.email
expire_time = each.value.soa_record.expire_time
minimum_ttl = each.value.soa_record.minimum_ttl
refresh_time = each.value.soa_record.refresh_time
retry_time = each.value.soa_record.retry_time
ttl = each.value.soa_record.ttl
}
}
ERROR
╷
│ Error: "soa_record.0.email" only contains letters, numbers, underscores, dashes and periods
│
│ with module.private_dns_zones.azurerm_private_dns_zone.this["zone1.local"],
│ on Modules/privatednszone/main.tf line 22, in resource "azurerm_private_dns_zone" "this":
│ 22: email = each.value.soa_record.email
│
╵
╷
│ Error: "soa_record.0.email" only contains letters, numbers, underscores, dashes and periods
│
│ with module.private_dns_zones.azurerm_private_dns_zone.this["zone2.local"],
│ on Modules/privatednszone/main.tf line 22, in resource "azurerm_private_dns_zone" "this":
│ 22: email = each.value.soa_record.email
If you check Azure docs you will find that they use email format without #. For example:
--email myhostmaster.mysite.com
Thus I guess, that you have to follow same format in TF.