Earlier today I was looking at https://github.com/terraform-aws-modules/terraform-aws-vpc/blob/v2.77.0/main.tf to look deeper into how the VPC module for AWS works behind the scenes.
One thing that I am struggling with is the count conditional such as the one in the aws_internet_gateway resource.
Can someone explain and translate what the count defined in this resource is actually doing? It's very confusing to me at the moment.
resource "aws_internet_gateway" "this" {
count = var.create_vpc && var.create_igw && length(var.public_subnets) > 0 ? 1 : 0
vpc_id = local.vpc_id
tags = merge(
{
"Name" = format("%s", var.name)
},
var.tags,
var.igw_tags,
)
}
It uses ternary operation in the general form of:
CONDITION ? TRUEVAL : FALSEVAL
In the module, the
CONDITION is var.create_vpc && var.create_igw && length(var.public_subnets) > 0
TRUEVAL is 1
FALSEVAL is 0
This translates to the following: If both create_vpc and create_igw are true as well as public_subnets has been defined, then count will be 1 (TRUEVAL) and exactly one aws_internet_gateway.this will be created.
In contrast if the CONDITION is not satisfied, count will be 0 (FALSEVAL) and no aws_internet_gateway.this will be created.
In general, it is a common pattern to conditionally create resources in terraform:
resource "type" "name" {
count = CONDITION : 1 ? 0
}
Related
I want to define a list of maps inside a variable so I can use for_each on multiple resources with conditionals based on the key values.
For example I have this locals.tf file where I define the list of maps
locals {
networking = [
{
name = "first"
domain = "first.${local.dns_name}"
port = 8080
group = "eighties"
},
{
name = "second"
domain = "second.${local.dns_name}"
port = 8081
group = "eighties"
},
{
name = "third"
domain = "third.${local.dns_name}"
port = 9090
group = "nineties"
},
{
name = "fourth"
port = 9091
group = "nineties"
}
]
}
In my other file, I can loop through the list of maps with for_each and for arguments:
resource "google_dns_record_set" "dns_records" {
for_each = {
for k in local.networking: k.domain => k
if k.domain != null
}
name = each.value.domain
type = "A"
ttl = 300
managed_zone = var.managed_zone
project = var.dns_project
rrdatas = [google_compute_global_address.default-forwarding-address.address]
}
Given this setup, I have 2 different situations that I might discuss:
Since the domain key doesn't exists for the fourth map, Terraform just stops.
Is it possible to skip the resource creations if domain does not exist in the fourth map? Or simply skip the errors.
Error: Unsupported attribute on dns-record.tf line 4, in resource "google_dns_record_set" "dns_records": 4: if k.domain != null This object does not have an attribute named "domain".
If I change the k.domain values for k.group and the conditional to be something like k.group == "eighties" so I can target a specific group, I receive this duplication error.
Two different items produced the key "eighties" in this 'for' expression. If duplicates are expected, use the ellipsis (...) after the value expression to enable grouping by key.
Are this errors manageable with the current setup or should I drop the list of maps idea?
You can check if the domain exist as follows:
{
for k in local.networking: k.domain => k
if contains(keys(k), "domain")
}
// Terraform v0.14.9
# var.tf
variable "launch_zk" {
type = string
description = "Whether to launch zookeeper or not"
default = false
}
# main.tf
resource "aws_instance" "zk_ec2" {
count = var.launch_zk ? var.zk_instance_count : 0
...
}
# output.tf
output "zk_ips" {
description = "IPs of ZK instances"
value = {
for vm in aws_instance.zk_ec2 :
vm.tags.Name => vm.private_ip
}
}
resource "local_file" "AnsibleInventoryFile" {
content = templatefile("ansible_inventory.tpl",
{
zk-private-ip = var.zk_instance_count < 10 ? slice(aws_instance.zk_ec2.*.private_ip, 0, 3) : slice(aws_instance.zk_ec2.*.private_ip, 0, 5),
zk-private-dns = var.zk_instance_count < 10 ? slice(aws_instance.zk_ec2.*.private_dns, 0, 3) : slice(aws_instance.zk_ec2.*.private_dns, 0, 5),
}
)
filename = "ansible_inventory"
}
# ansible_inventory.tpl
[zk_servers]
%{ for index, dns in zk-private-dns ~}
${zk-private-ip[index]} server_name=${dns}
%{ endfor ~}
This is what I am using and now I want to conditionally generate output file including ansible inventory file. It should include IP and DNS of zookeeper only if I am passing boolean true parameter to my "launch_zk" variable otherwise it should not print anything. Here I am not able to perform conditional statement in my output file and ansible template tpl file. Can someone tell me how can I get it working?
Here I will have to use multiple conditional statement like this but I am getting error given below
resource "local_file" "AnsibleInventoryFile" {
content = templatefile("ansible_inventory.tpl",
{
zk-private-ip = var.launch_zk ? var.zk_instance_count < 10 ? slice(aws_instance.zk_ec2.*.private_ip, 0, 3) : slice(aws_instance.zk_ec2.*.private_ip, 0, 5) : "",
zk-private-dns = var.launch ? var.zk_instance_count < 10 ? slice(aws_instance.zk_ec2.*.private_dns, 0, 3) : slice(aws_instance.zk_ec2.*.private_dns, 0, 5) : "",
}
)
filename = "ansible_inventory"
}
# Error
Error: Inconsistent conditional result types
on output.tf line 67, in resource "local_file" "AnsibleInventoryFile":
67: zk-private-dns = var.launch_zk ? aws_instance.zk_ec2.*.private_dns : "",
|----------------
| aws_instance.zk_ec2 is empty tuple
| var.launch_zk is "false"
The true and false result expressions must have consistent types. The given
expressions are tuple and string, respectively.
As docs explain, your condition must have consistent types:
The two result values may be of any type, but they must both be of the same type so that Terraform can determine what type the whole conditional expression will return without knowing the condition value.
In your case you return a list, and string:
# ? list : string
zk-private-dns = var.launch_zk ? aws_instance.zk_ec2.*.private_dns : ""
The easiest way to ensure consistent types is by having an empty list:
zk-private-dns = var.launch_zk ? aws_instance.zk_ec2.*.private_dns : []
This change may require further changes in your code to account for the empty list
I have a module for service-accounts in GCP being used to populate kubernetes secrets
Here is my module
resource "google_service_account" "service_account" {
count = var.enabled ? 1 : 0
account_id = var.account_id
display_name = var.display_name
}
resource "google_project_iam_member" "service_account_roles" {
count = var.enabled ? length(var.roles) : 0
role = "roles/${element(var.roles, count.index)}"
member = "serviceAccount:${google_service_account.service_account[0].email}"
}
resource "google_service_account_key" "service_account_key" {
count = var.enabled ? 1 : 0
service_account_id = google_service_account.service_account[0].name
}
'output.tf' contains the following
output "private_decoded_key" {
value = base64decode(
element(
concat(
google_service_account_key.service_account_key.*.private_key,
[""],
),
0,
),
)
description = "The base 64 decoded version of the credentials"
}
Since there is a conditional that none of these resources are created without the enabled flag, I had to handle it in TF 0.11.14 this way, and the tf0.12 autoupgrade tool didnt do much changes here.
How can I simplify this in Terraform 0.12.24,
I tried modifying the output to simply
value = base64decode(google_service_account_key.service_account_key[0].private_key)
But the problem there is that if the corresponding kubernetes cluster gets deleted during a deletion, and there are errors midway because terraform, I will not be able to cleanup the terraform state of the rest of the resources using
`terraform destroy'
Attempts to convert the count to for_each as shown below gave me the following errors
resource "google_service_account" "service_account" {
# count = var.enabled ? 1 : 0
for_each = var.enabled ? 1 : 0
account_id = var.account_id
display_name = var.display_name
}
resource "google_project_iam_member" "service_account_roles" {
# count = var.enabled ? length(var.roles) : 0
for_each = var.enabled ? toset(var.roles) : 0
# role = "roles/${element(var.roles, count.index)}"
role = "roles/${each.value}"
member = "serviceAccount:${google_service_account.service_account[0].email}"
}
for_each = var.enabled ? toset(var.roles) : 0
The true and false result expressions must have consistent types. The given
expressions are set of dynamic and number, respectively.
What am I doing wrong above ?
In the terraform version you mentioned (0.12.24) you should be able to use try() in your outputs.tf:
value = try(base64decode(google_service_account_key.service_account_key[0].private_key), "")
This would default to "" if google_service_account_key.service_account_key[0].private_key is not resolvable for any reason; you can also default to null of course.
Edit/Update: To answer second (edited) part of the question:
To get rid of the error that both sides need to have the same type, you need to use [] as an empty set instead of 0 when converting to for_each:
for_each = var.enabled ? toset(var.roles) : []
Please pay attention with existing infrastructure as you need to manipulate the state file when converting from count to for_each or terraform will try to destroy and create resources.
(I will cover this in more detail in part 3 of a series of stories I am currently working on about how to write terraform modules. You can find part 1 on medium and part 2 will be released next week.)
I have a module for google_service_account, and just converted to Terraform-12 (0.12.24)
resource "google_service_account" "service_account" {
count = var.enabled ? 1 : 0
account_id = var.account_id
display_name = var.display_name
}
output.tf retrieves the email
output "email" {
value = try(google_service_account.service_account.*.email, null)
# value = element( --> Commented out part works fine
# concat(google_service_account.service_account.*.email, [""]),
# 0,
# )
description = "The e-mail address of the service account. Usually use this when constructing IAM policies."
When using this module in another resource as follows
resource "google_storage_bucket_iam_member" "registry_bucket_iam" {
bucket = "artifacts.${var.project}.appspot.com"
role = "roles/storage.objectViewer"
member = "serviceAccount:${module.k8s-node-service-account.email}"
}
I get the following error
48: member = "serviceAccount:${module.k8s-node-service-account.email}"
|----------------
| module.k8s-node-account.email is tuple with 1 element
Cannot include the given value in a string template: string required.
How can this be resolved ?
google_service_account.service_account.*.email evaluates to an array of 1 or 0 elements dependeing on var.enabled - while google_service_account.service_account[0].email evaluates to a string or to an error if var.enabled is false. So when using try() you want to evaluate to the string or in case of an error default to null
changing your output to the following should lead to the expected result of having an email output of type string.
output "email" {
value = try(google_service_account.service_account[0].email, null)
}
I'm using Terraform in order to build some AWS VPC components like the aws_route below.
I'm trying to scale the number of NAT gateways dynamically with the count parameter:
resource "aws_route" "my_nat_gw" {
route_table_id = "${var.rt_id}"
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = "${nat_gw_id}"
#I have an error here - on the "lookup" term
count = "${length(var.azs) * lookup(map(var.enable_nat_gateway, 1), "true", 0)}"
}
For the sake of brevity let's ignore the part of length(var.azs) in the count calculation.
I'm getting the following error on the lookup(map(var....) part:
Expected to be number, actual type is String more
The enable_nat_gateway variable is boolean.
I tried also the following:
lookup(map(true, 1), true, 0)}
lookup(map("true", 1), "true", 0)}
But still no good.
Any idea how to fix it?
Some calculations for those who are not familiar with the map and lookup syntax:
If the enable_nat_gateway is equal to true then 'map' is equal to{true=1} and the total lookup term should be equal to 1.
Else:
If the enable_nat_gateway is equal to false then 'map' is equal to{true=0} and the total lookup term should be equal to 0.
Notice that I'm using Terraform 0.11.11 so the map function is still supported.
If you're trying to conditionally add n route resources then you should be using a ternary statement here with something like:
resource "aws_route" "my_nat_gw" {
count = "${var.enable_nat_gateway ? length(var.azs) : 0}"
route_table_id = "${var.rt_id}"
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = "${var.nat_gw_id}"
}
This checks if the enable_nat_gateway variable evaluates to true and if so creates a resource for each element in the azs variable. If it's not true then it won't create any resources.