Terraform use variable based on another value passed during runtime - azure

During runtime I want to tell terraform which of those variables are to be used from my .tfvars file I have the following values:
website-review-author = "test1/fwsfds/34131"
website-dev-publish = "test2/fwsfds/34131"
website-other-dispatcher = "test3/fwsfds/34131"
So I want to data_source_id to use those variables based on a parameter during runtime:
resource "azurerm_managed_disk" "datadisk" {
name = "${var.default_prefix}-${var.environment}-datadisk"
location = "${var.default_region}"
resource_group_name = "${var.permanent_resources_rg}"
storage_account_type = "${var.disk_type}"
create_option = "Copy"
source_resource_id = "${var.website_review_publish.id}" ## I need to be a dynamic variable. So sometimes it will se website_review_author, other time website-dev-publish
disk_size_gb = "${var.data_disk_size_gb}"
tags = "${var.tags}"
depends_on = [
azurerm_storage_account.storage
#azurerm_managed_disk.osdisk
]
}
Is it possible in terrraform to switch between variables, so it gets the proper value.
The condition is :
If (var.environment == website-review-author) ## Can be
website-dev-publish or #website-other-dispathcer
then
source_resource_id = var.website-review-author #or other value based
on the parameter
I'm new to terraform, so still learning my ways around. I need something like if condition.

You could use the ternary operator [1]. If you decide to go down that route, the code would then look like:
resource "azurerm_managed_disk" "datadisk" {
name = "${var.default_prefix}-${var.environment}-datadisk"
location = "${var.default_region}"
resource_group_name = "${var.permanent_resources_rg}"
storage_account_type = "${var.disk_type}"
create_option = "Copy"
source_resource_id = var.environment == website-review-author ? var.website-review-author : var.environment == website-dev-publish ? var.website-dev-publish : var.environment == website-other-dispatcher ? var.website-other-dispatcher : null
disk_size_gb = "${var.data_disk_size_gb}"
tags = "${var.tags}"
depends_on = [
azurerm_storage_account.storage
]
}
It could be made more complex by using other logical operators [2] (e.g., or), however if you need all three different values, then I suggest using workspaces [3]. For example, in that case you could use the workspace name with the ternary operator to decide which value will be assigned:
source_resource_id = terraform.workspace == <someworkspacename> ? var.webiste-review-author : var.website-dev-publish
EDIT: I added all three conditions. It is really ugly but it should work. I suggest creating a different variable of type map and then using it for creating resources. Additionally, whenever the value for environment is changed, the resource will probably be destroyed and recreated, hence why it is better to use count or for_each.
[1] https://www.terraform.io/language/expressions/conditionals
[2] https://www.terraform.io/language/expressions/operators#arithmetic-and-logical-operators
[3] https://www.terraform.io/cli/workspaces#the-purpose-of-workspaces

Related

The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created

I want to exempt certain policies for an Azure VM. I have the following terraform code to exempt the policies.
It uses locals to identify the scope on which policies should be exempt.
locals {
exemption_scope = try({
mg = length(regexall("(\\/managementGroups\\/)", var.scope)) > 0 ? 1 : 0,
sub = length(split("/", var.scope)) == 3 ? 1 : 0,
rg = length(regexall("(\\/managementGroups\\/)", var.scope)) < 1 ? length(split("/", var.scope)) == 5 ? 1 : 0 : 0,
resource = length(split("/", var.scope)) >= 6 ? 1 : 0,
})
expires_on = var.expires_on != null ? "${var.expires_on}T23:00:00Z" : null
metadata = var.metadata != null ? jsonencode(var.metadata) : null
# generate reference Ids when unknown, assumes the set was created with the initiative module
policy_definition_reference_ids = length(var.member_definition_names) > 0 ? [for name in var.member_definition_names :
replace(substr(title(replace(name, "/-|_|\\s/", " ")), 0, 64), "/\\s/", "")
] : var.policy_definition_reference_ids
exemption_id = try(
azurerm_management_group_policy_exemption.management_group_exemption[0].id,
azurerm_subscription_policy_exemption.subscription_exemption[0].id,
azurerm_resource_group_policy_exemption.resource_group_exemption[0].id,
azurerm_resource_policy_exemption.resource_exemption[0].id,
"")
}
and the above local is used like mentioned below
resource "azurerm_management_group_policy_exemption" "management_group_exemption" {
count = local.exemption_scope.mg
name = var.name
display_name = var.display_name
description = var.description
management_group_id = var.scope
policy_assignment_id = var.policy_assignment_id
exemption_category = var.exemption_category
expires_on = local.expires_on
policy_definition_reference_ids = local.policy_definition_reference_ids
metadata = local.metadata
}
Both the locals and azurerm_management_group_policy_exemption are part of the same module file. And Policy exemption is applied like mentioned below
module exemption_jumpbox_sql_vulnerability_assessment {
count = var.enable_jumpbox == true ? 1 : 0
source = "../policy_exemption"
name = "Exemption - SQL servers on machines should have vulnerability"
display_name = "Exemption - SQL servers on machines should have vulnerability"
description = "Not required for Jumpbox"
scope = module.create_jumbox_vm[0].virtual_machine_id
policy_assignment_id = module.security_center.azurerm_subscription_policy_assignment_id
policy_definition_reference_ids = var.exemption_policy_definition_ids
exemption_category = "Waiver"
depends_on = [module.create_jumbox_vm,module.security_center]
}
It works for an existing Azure VM. However it throws the following error while trying to provision the Azure VM and apply the policy exemption on this Azure VM.
Ideally, module.exemption_jumpbox_sql_vulnerability_assessment should get executed only after [module.create_jumbox_vm as it is defined as a dependent. But not sure why it is throwing the error
│ 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 count depends on.
I tried to reproduce the scenario in my environment.
resource "azurerm_management_group_policy_exemption" "management_group_exemption" {
count = local.exemption_scope.mg
name = var.name
display_name = var.display_name
description = var.description
management_group_id = var.scope
policy_assignment_id = var.policy_assignment_id
exemption_category = var.exemption_category
expires_on = local.expires_on
policy_definition_reference_ids = local.policy_definition_reference_ids
metadata = local.metadata
}
locals {
exemption_scope = try({
...
})
Received the same error:
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 count depends on.
Referring to local values , the values will be known on the apply time only, and not during the apply time .So if it is not dependent on other sources , it will expmpt policies but it is dependent on the VM which may be still in process of creation.
So target only the resource that is dependent on first ,as only when vm is created is when the exemption policy can be assigned to that vm.
Check count:using-expressions-in-count | Terraform | HashiCorp Developer
Also note that while using terraform count argument with Azure Virtual Machines ,NIC resource also to be created for each Virtual Machine resource.
resource "azurerm_network_interface" "nic" {
count = var.vm_count
name = "${var.vm_name_pfx}-${count.index}-nic"
location = data.azurerm_resource_group.example.location
resource_group_name = data.azurerm_resource_group.example.name
//tags = var.tags
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.internal.id
private_ip_address_allocation = "Dynamic"
}
}
Reference: terraform-azurerm-policy-exemptions/examples/count at main · AnsumanBal-MT/terraform-azurerm-policy-exemptions · GitHub

Terraform Outputs: How do I create a (map?) and how do I use the map? Is a map even the right tool?

I am struggling with a few terraform concepts.
I am successfully using the aztfmod/azurecaf provider to name my resourcegroup, but this means I need to get that name as an output for the companynet.resource_group module, so that I can use that name again when calling the companynet.key_vault module.
# terraform.tfvars
resource_groups = {
rg1 = {
name = "resourcegroup1"
location = "eastus"
}
rg2 = {
name = "resourcegroup2"
location = "eastus"
}
}
# root main.tf
provider "azurerm" {
features {}
}
module "companynet" {
source = "./modules/companynet"
tenant_id = var.tenant_id
environment = var.environment
resource_groups = var.resource_groups
key_vaults = var.key_vaults
storage_accounts = var.storage_accounts
app_service_plans = var.app_service_plans
}
# modules/companynet/main.tf
module "resource_group" {
source = "../companynet.resource_group"
environment = var.environment
resource_groups = var.resource_groups
}
module "key_vault" {
source = "../companynet.key_vault"
tenant_id = var.tenant_id
environment = var.environment
resource_groups = "${module.resource_group.resource_groups.companynet}"
key_vaults = var.key_vaults
}
The module resource_group has the following main.tf:
# modules/companynet.resource_group/main.tf
resource "azurecaf_name" "resource_group" {
for_each = var.resource_groups
name = each.value.name
resource_type = "azurerm_resource_group"
suffixes = ["${var.environment}", "001"]
}
resource "azurerm_resource_group" "resource_group" {
for_each = var.resource_groups
name = azurecaf_name.resource_group[each.key].result
location = each.value.location
}
but I don't know how to get the output of that resource_group name.
I have tried a few different things that do not work
# modules/companynet.resource_group/outputs.tf
output "resource_groups" {
value = azurerm_resource_group.resource_group[*].name
}
value = azurerm_resource_group.resource_group.name
value = azurerm_resource_group.resource_group.companynet.name
value = azurerm_resource_group.resource_group[companynet].name
Each of these results in one error or another, all indicating a problem with modules/companynet.resource_group/outputs.tf
Ideally I would get an object that I can then iterate through in another module. I expect to be able to call something like to get access to those resource group names in other modules such as:
# modules/companynet.key_vault/main.tf
resource "azurerm_key_vault" "key_vault" {
for_each = var.key_vaults
name = azurecaf_name.key_vault[each.key].result
location = var.resource_groups.location
resource_groups = "${module.resource_group.resource_groups.[companynet]}"
sku_name = "standard"
tenant_id = var.tenant_id
}
azurerm_resource_group.resource_group is declared with for_each, and so that expression refers to a map of objects where the keys match the keys of the for_each expression and the values are the corresponding declared resource instances.
In References to Resource Attributes there are various examples of referring to resource attributes in different situations, including the following about resources using for_each:
When a resource has the for_each argument set, the resource itself becomes a map of instance objects rather than a single object, and attributes of instances must be specified by key, or can be accessed using a for expression.
aws_instance.example["a"].id returns the id of the "a"-keyed resource.
[for value in aws_instance.example: value.id] returns a list of all of the ids of each of the instances.
That second item shows how to use a for expression to produce a list of the ids of aws_instance.example, but it doesn't show exactly how to produce a map and instead expects you to refer to the linked documentation about for expressions to learn about that:
The type of brackets around the for expression decide what type of result it produces.
The above example uses [ and ], which produces a tuple. If you use { and } instead, the result is an object and you must provide two result expressions that are separated by the => symbol:
{for s in var.list : s => upper(s)}
This expression produces an object whose attributes are the original elements from var.list and their corresponding values are the uppercase versions. For example, the resulting value might be as follows:
{
foo = "FOO"
bar = "BAR"
baz = "BAZ"
}
A for expression alone can only produce either an object value or a tuple value, but Terraform's automatic type conversion rules mean that you can typically use the results in locations where lists, maps, and sets are expected.
This section describes how to produce an object and then notes that you can use the result in a location where a map is expected. In practice it's often possible to use object-typed values and mapped-type values interchangeably in Terraform, because they both have in common that they have elements identified by string keys. The difference is that an object type can have a separate type for each of its attributes, whereas a map must have the same type for all attributes.
Given all of this information, we can produce an object value describing the names for each resource group like this:
output "resource_groups" {
value = { for k, g in azurerm_resource_group.resource_group : k => g.name }
}
For most purposes it doesn't really matter that this is an object-typed result rather than specifically a map, but since we know that .name is always a string we can infer that all of the attributes of this object have string-typed values, and so it would also be valid to explicitly convert to a map of strings using the tomap function (which is a "location where [...] maps [...] are expected", per the above documentation):
output "resource_groups" {
value = tomap({
for k, g in azurerm_resource_group.resource_group : k => g.name
})
}

Terraform : How to loop over aws_instance N times as defined within object

I have the following variable
variable "instance_types" {
default = {
instances : [
{
count = 1
name = "control-plane"
ami = "ami-xxxxx"
instance_type = "t2.large"
iam_instance_profile = "xxx-user"
subnet_id = "subnet-xxxxx"
},
{
count = 3
name = "worker"
ami = "ami-xxxxx"
instance_type = "t2.large"
iam_instance_profile = "xxx-user"
subnet_id = "subnet-xxxxx"
}
]
}
}
With the following instance declaration (that I'm attempting to iterate)
resource "aws_instance" "k8s-node" {
# Problem here : How to turn an array of 2 objects into 4 (1 control_plane, 3 workers)
for_each = {for x in var.instance_types.instances: x.count => x}
ami = lookup(each.value, "ami")
instance_type = lookup(each.value, "instance_type")
iam_instance_profile = lookup(each.value, "iam_instance_profile")
subnet_id = lookup(each.value, "subnet_id")
tags = {
Name = lookup(each.value, "name")
Type = each.key
}
}
Goal: Get the aws_instance to iterate 4 times (1 control_plane + 3 workers) and populate the values the index of instance_types.
Problem : Cannot iterate the over the object array correctly with desired result. In a typical programming language this would be achieved in a double for loop.
This can be solved easier with a data type of map(object)) for your input variable. The transformed data structure appears like:
variable "instance_types" {
...
default = {
"control-plane" = {
count = 1
ami = "ami-xxxxx"
instance_type = "t2.large"
iam_instance_profile = "xxx-user"
subnet_id = "subnet-xxxxx"
},
"worker" = {
count = 3
ami = "ami-xxxxx"
instance_type = "t2.large"
iam_instance_profile = "xxx-user"
subnet_id = "subnet-xxxxx"
}
}
}
Note the name key in the object is subsumed into the map key for efficiency and cleanliness.
If the resources are split between the control plane and worker nodes, then we are finished and can immediately leverage this variable's value in a for_each meta-argument. However, combining the resources now requires a data transformation:
locals {
instance_types = flatten([ # need this for final structure type
for instance_key, instance in var.instance_types : [ # iterate over variable input objects
for type_count in range(1, instance.count + 1) : { # sub-iterate over objects by "count" value specified; use range function and begin at 1 for human readability
new_key = "${instance_key} ${type_count}" # for resource uniqueness
type = instance_key # for easier tag value later
ami = instance.ami # this and below retained from variable inputs
instance_type = instance.instance_type
iam_instance_profile = instance.iam_instance_profile
subnet_id = instance.subnet_id
}
]
])
}
Now we can iterate within the resource with the for_each meta-argument, and utilize the for expression to reconstruct the input for suitable usage within the resource.
resource "aws_instance" "k8s-node" {
# local.instance_types is a list of objects, and we need a map of objects with unique resource keys
for_each = { for instance_type in local.instance_types : instance_type.new_key => instance_type }
ami = each.value.ami
instance_type = each.value.instance_type
iam_instance_profile = each.value.iam_instance_profile
subnet_id = each.value.subnet_id
tags = {
Name = each.key
Type = each.value.type
}
}
This will give you the behavior you desire, and you can modify it for style preferences or different uses as the need arises.
Note the lookup functions are removed since they are only useful when default values are specified as a third argument, and that is not possible in object types within variable declarations except as an experimental feature in 0.14.
The absolute namespace for these resources' exported resource attributes would be:
(module?.<declared_module_name>?.)<resource_type>.<resource_name>[<resource_key>].<attribute>
For example, given an intra-module resource, first worker node, and private ip address exported attribute:
aws_instance.k8s-node["worker 1"].private_ip
Note you can also access all resources' exported attributes by terminating the namespace at <resource_name> (retaining the map of all resources instead of accessing a singular resource value). Then you could also use a for expression in an output declaration to create a custom aggregate output for all of the similar resources and their identical exported attribute(s).
{ for node_key, node in aws_instance.k8s-node : node_key => node.private_ip }

How do I assign unique "Name" tag to the EC2 instance(s)?

I am using Terraform 0.12. I am trying to build EC2 in bulk for a project and instead of sequentially naming the ec2's I will to name the instances by providing unique names.
I think of using dynamic tags, however, not quite sure how to incorporate in the code.
resource "aws_instance" "tf_server" {
count = var.instance_count
instance_type = var.instance_type
ami = data.aws_ami.server_ami.id
associate_public_ip_address = var.associate_public_ip_address
##This provides sequential name.
tags = {
Name = "tf_server-${count.index +1}"
}
key_name = "${aws_key_pair.tf_auth.id}"
vpc_security_group_ids = ["${var.security_group}"]
subnet_id = "${element(var.subnets, count.index)}"
}
If I understand your requirement correctly, you can pass the list of VM names as a terraform variable and use count.index to get the name from a specific position in the list based on the count.
# variables.tf
# Length of list should be the same as the count of instances being created
variable "instance_names" {
default = ["apple", "banana", "carrot"]
}
#main.tf
resource "aws_instance" "tf_server" {
count = var.instance_count
instance_type = var.instance_type
ami = data.aws_ami.server_ami.id
associate_public_ip_address = var.associate_public_ip_address
##This provides names as per requirement from the list.
tags = {
Name = "${element(var.instance_names, count.index)}"
}
key_name = "${aws_key_pair.tf_auth.id}"
vpc_security_group_ids = ["${var.security_group}"]
subnet_id = "${element(var.subnets, count.index)}"
}
Would the following be similar to what you are after?
Define a list of name prefixes as a variable and then cycle through the naming prefixes using the element function.
variable "name_prefixes" {
default = ["App", "Db", "Web"]
}
...
##This provides sequential name.
tags = {
Name = "${element(var.name_prefixes, count.index)}${count.index + 1}"
}
...
The result would be App1, Db2, Web3, App4, Db5... The numbering is not ideal, but at least you would have a distinct name per instance.
The only way I can think of naming them sequentially (e.g. App1, App2, Db1, Db2 etc.) would require an individual resource for each type of instance and then just use count.index on the name like your original code.

Terraform - reference instance value when calling module throwing errors

I have the block of code below that creates a bunch of subnets based on a list of names and a list of address_prefixes.
resource "azurerm_subnet" "subnet" {
count = "${length(var.subnet_names)}"
name = "${element(var.subnet_names, count.index)}"
resource_group_name = "${var.vnet_rg_name}"
virtual_network_name = "${data.azurerm_virtual_network.vnet.name}"
address_prefix = "${element(var.subnet_prefixes, count.index)}"
service_endpoints = ["Microsoft.Sql","Microsoft.Storage","Microsoft.AzureCosmosDB"]
network_security_group_id = "${data.azurerm_network_security_group.required_nsg.id}"
route_table_id = "${element(azurerm_route_table.routetable.*.id, count.index)}"
depends_on = ["azurerm_route_table.routetable"]
}
I am then trying to create some routes using a module but when I try to pass in values for variables using properties from a specific instance of the azurerm_subnet.subnet resource, it throws the error:
"module.insidedmzroutes.var.subnet_name: Resource 'azurerm_subnet.subnet' not found for variable 'azurerm_subnet.subnet.5.name'"
module "insidedmzroutes" {
source = "./modules/dmzroutes"
subnet_name = "${azurerm_subnet.subnet.5.name}"
vnet_rg = "${data.azurerm_resource_group.vnet_rg.name}"
route_table_name = "${azurerm_route_table.routetable.5.name}"
next_hop_ip = "${cidrhost(azurerm_subnet.subnet.5.address_prefix, 4)}"
subnet_names = ["${var.subnet_names}"]
subnet_prefixes = ["${var.subnet_prefixes}"]
}
Does this not work or do I have the reference constructed incorrectly?
Please have a look at the Terraform interpolation syntax documentation, look for interpolation syntax.
The following would work (as indicated by Adil B):
subnet_name = "${azurerm_subnet.subnet.*.name[5]}"
As with the splat syntax * you select all of the elements created using a count variable, which will then return a list, which you can select the correct element from [5].
However, why are you also passing along the entire list of subnets? Which subnets are these? It's not very clear from your code if these are the 5 subnets you created earlier or different ones. Are you creating an insidedmzroutes for every subnet? If so, I'd get rid of the subnet_name var and instead implement something like this in the resource inside the module:
count = "${length(var.subnet_names)}"
subnet_name = "${element(var.subnet_names, count.index)}"

Resources