How to create a map in terraform - terraform

I'm confused on how to get this working, I have a sub-domian (module.foo.dev) and alternate domain name as *.foo.dev but it has to use the same zone_id as my root_domain.
I'm trying to use a local map something like
all_domains = {
["module.foo.dev","*.foo.dev"] = "foo.dev"
["bar.com"] = "bar.com"
}
My variables are as follows
primary_domain = "module.foo.dev"
sub_alternate_domain = ["*.foo.dev","bar.com"]
Eventually would be using that locals value in the below module
module:
resource "aws_route53_record" "record" {
count = var.validation_method == "DNS" ? local.all_domains : 0
name = aws_acm_certificate.certificate.domain_validation_options.0.resource_record_name
type = aws_acm_certificate.certificate.domain_validation_options.0.resource_record_type
zone_id = data.aws_route53_zone.selected[count.index].zone_id
ttl = "300"
records = [aws_acm_certificate.certificate.domain_validation_options.0.resource_record_value]
}
Can someone pls help me with this solution..

In Terraform a map can only have strings as keys (unquoted keys are still strings), so you need to swap your keys and values:
locals{
all_domains = {
"foo.dev" = ["module.foo.dev","*.foo.dev"]
"bar.com" = ["bar.com"]
}
}
Also, as above, your local variables need to be declared and assigned in a locals block.
The count argument on resources expects a whole non-negative number (0 or more) and will not accept a map as a value. You'll need to use for_each instead:
resource "aws_route53_record" "record" {
for_each = var.validation_method == "DNS" ? local.all_domains : {}
name = aws_acm_certificate.certificate.domain_validation_options.0.resource_record_name
type = aws_acm_certificate.certificate.domain_validation_options.0.resource_record_type
zone_id = data.aws_route53_zone.selected[count.index].zone_id
ttl = "300"
records = [aws_acm_certificate.certificate.domain_validation_options.0.resource_record_value]
}
The map type in the Expression Language doc provides some minimal additional guidance.

Related

How to iterate over list(string) and append to same records Terraform

I want to iterate over list of string and apply individual vale to same resource route53
I have list of ip in variable
variable "app_list" {
description = "List of ESAs to be allowed. (For instance, \[192.168.1.123, 10.1.1.11\] etc.)"
type = list(string)
default = \["192.168.1.123","10.1.1.11"\]
}
Creating route53 TXT record where I have to append this variable and create single record
resource "aws_route53_record" "spf_txt" {
zone_id = data.aws_route53_zone.public.zone_id
name = ""
type = "TXT"
ttl = 300
records = \["v=spf1 ip4:192.168.1.123 ip4:10.1.1.11 \~all"\]
}
Here i used for_each and count. it is trying to create two seperate TXT record. How can I iterate the list and pass it to record.
Please someone help me
Tried :
resource "aws_route53_record" "spf_txt" {
zone_id = data.aws_route53_zone.public.zone_id
name = ""
type = "TXT"
ttl = 300
count = length(var.app_list)
for_each = var.app_list
records = \["v=spf1 ip4:value \~all"\]
}
It errored as two elements with tuples
tried this as well
locals {
spf_record = "${formatlist("ip4:", var.app_list)}"
}
resource "aws_route53_record" "spf_txt" {
zone_id = data.aws_route53_zone.public.zone_id
name = ""
type = "TXT"
ttl = 300
records = \["v=spf1 ${local.spf_record} ip4:${data.aws_nat_gateway.nat_ip.public_ip} \~all"\]
}
It failed with this error
spf_record = "${formatlist("ip4:", var.app_list)}"
while calling formatlist(format, args...)
var.app_esas is list of string with 2 elements
Call to function "formatlist" failed: error on format
iteration 0: too many arguments; no verbs in format
string.
Even if you don't use count or for_each you would accomplish the purpose, I think.
resource "aws_route53_record" "spf_txt" {
...
records = ["v=spf1 ${join(" ", [for i in var.app_list : "ip4:${i}"])} ~all"]
}
Test:
variable "app_list" {
description = "List of ESAs to be allowed. (For instance, [192.168.1.123, 10.1.1.11] etc.)"
type = list(string)
default = ["192.168.1.123","10.1.1.11"]
}
output "spf_txt" {
value = ["v=spf1 ${join(" ", [for i in var.app_list : "ip4:${i}"])} ~all"]
}
$ terraform plan
Changes to Outputs:
+ spf_txt = [
+ "v=spf1 ip4:192.168.1.123 ip4:10.1.1.11 ~all",
]
If using formatlist:
resource "aws_route53_record" "spf_txt" {
...
records = ["v=spf1 ${join(" ", formatlist("ip4:%s", var.app_list))} ~all"]
}

How to create a single dynamic block for a module from a map or object definition?

If I want to define a lambda function with a VPC config. I can do it like this:
resource "aws_lambda_function" "lambda" {
function_name = "..."
...
vpc_config {
subnet_ids = ["..."]
security_group_ids = ["..."]
}
}
I would like to create the lambda in a terraform module and define the vpc_config in the module definition. I can define the module like this:
resource "aws_lambda_function" "lambda" {
function_name = "..."
...
dynamic "vpc_config" {
for_each = var.vpc_configs
content {
subnet_ids = vpc_config.value["subnet_ids"]
security_group_ids = vpc_config.value["security_group_ids"]
}
}
}
variable "vpc_configs" {
type = list(object({
subnet_ids = list(string)
security_group_ids = list(string)
}))
default = []
}
And then use it:
module "my_lambda" {
source = "./lambda"
...
vpc_configs = [
{
subnet_ids = ["..."]
security_group_ids = ["..."]
}
]
}
However, since there is only one vpc_config block allowed there is no point in defining the variable as a list. I would prefer the following syntax:
module "my_lambda" {
source = "./lambda"
...
vpc_config = {
subnet_ids = ["..."]
security_group_ids = ["..."]
}
# or:
#vpc_config {
# subnet_ids = ["..."]
# security_group_ids = ["..."]
#}
}
However, I can't figure out if it is possible to define a variable like this and then use it in a dynamic block. I defined it as a list in the first place because I don't always need a VPC config and this way I can simply leave the list empty and no VPC config will be created. Is there a way to create an optional vpc_config block through a simple map or object definition?
dynamic blocks work by generating one block for each element in a collection, if any, whereas you want to define a variable that is an optional non-collection value. Therefore the key to this problem is to translate from a single value that might be null (representing absence) into a list of zero or one elements.
Due to how commonly this arises, Terraform has a concise way to represent that conversion using the splat operator, [*]. If you apply it to a value that isn't a list, then it will implicitly convert it into a list of zero or one elements, depending on whether the value is null.
The example in the documentation I just linked to shows a practical example of this pattern. The following is essentially the same approach, but adapted to use the resource type that you are using in your question:
variable "vpc_config" {
type = object({
subnet_ids = list(string)
security_group_ids = list(string)
})
default = null
}
resource "aws_lambda_function" "lambda" {
function_name = "..."
...
dynamic "vpc_config" {
for_each = var.vpc_config[*]
content {
subnet_ids = vpc_config.value.subnet_ids
security_group_ids = vpc_config.value.security_group_ids
}
}
}
The default value of var.vpc_config is null, so if the caller doesn't set it then that is the value it will take.
var.vpc_config[*] will either return an empty list or a list containing one vpc_config object, and so this dynamic block will generate either zero or one vpc_config blocks depending on the "null-ness" of var.vpc_config.
so you are wanting a conditional dynamic block
you could possibly get away with it by doing a check similar to the one on the object below
dynamic "vpc_config"{
for_each = length(var.vpc_config) > 0 ? {config=var.vpc_config}: {}
content{
...
}
}
if no vpc_config is passed in the module then the input variable should default to something like an empty object {}, that way the dynamic conditional check will still work if no config is passed
Turns out it doesn't seem to be possible what I want to do (building an optional type safe configuration through an object definition without having to nest it in a list).
Instead I now use the lambda module provided by Terraform:
module "email_lambda" {
source = "terraform-aws-modules/lambda/aws"
version = "3.3.1"
function_name = "${var.stack_name}-email"
handler = "pkg.email.App::handleRequest"
runtime = "java11"
architectures = ["x86_64"]
memory_size = 512
timeout = 30
layers = [aws_lambda_layer_version.lambda_layer.arn]
create_package = false
local_existing_package = "../email/target/email.jar"
environment_variables = {
# https://aws.amazon.com/blogs/compute/optimizing-aws-lambda-function-performance-for-java/
JAVA_TOOL_OPTIONS = "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
}
vpc_subnet_ids = module.vpc.private_subnets
vpc_security_group_ids = [aws_security_group.lambda_security_group.id]
attach_policies = true
policies = [
"arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole",
]
number_of_policies = 1
attach_policy_json = true
policy_json = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "SESBulkTemplatedPolicy"
Effect = "Allow"
Resource = [...]
Action = [
"ses:SendEmail",
"ses:SendRawEmail",
"ses:SendTemplatedEmail",
"ses:SendBulkTemplatedEmail",
]
}
]
})
}
As one can see in this configuration I had to set the VPC parameters individually and in case of the policy I had to specify a boolean parameter to tell Terraform that the configuration was set (I even had to specify the length of the provided list). Looking at the source code of the module reveals that there may not be a better way how to achieve this in the most up to date version of Terraform.

Understanding Terraform for_each loop iteration

I am learning terraform and trying to understand the for_each loop iteration in terraform.
I am iterating through a loop for creating RGs in Azure cloud and what I want to understand is the difference between accessing the value of an instance using . or [""].
So for example, below is my tfvar file:
resource_groups = {
resource_group_1 = {
name = "terraform-apply-1"
location = "eastus2"
tags = {
created_by = "vivek89#test.com"
}
},
resource_group_2 = {
name = "terraform-apply-2"
location = "eastus2"
tags = {
created_by = "vivek89#test.com"
}
},
resource_group_3 = {
name = "terraform-apply-3"
location = "eastus2"
tags = {
created_by = "vivek89#test.com"
contact_dl = "vivek89#test.com"
}
}
}
and below is my terraform main.tf file:
resource "azurerm_resource_group" "terraformRG" {
for_each = var.resource_groups
name = each.value.name
location = each.value.location
tags = each.value.tags
}
I am confused with the expression in for_each in RG creation block. Both the below codes works and create RGs:
name = each.value.name
name = each.value["name"]
I want to understand the difference between the two and which one is correct.
They are equivalent as explained in the docs:
Map/object attributes with names that are valid identifiers can also be accessed using the dot-separated attribute notation, like local.object.attrname. In cases where a map might contain arbitrary user-specified keys, we recommend using only the square-bracket index notation (local.map["keyname"]).
The main difference is that dot notation requires key attributes to be valid identifiers. In contrast, the square-bracket notation works with any identifiers.

Conditionals in for_each loop around elastic IPs

I have the following code and I would like to have a conditional that will only create an elastic IP if the instance is part of a public subnet (or based off of a boolean value if needed). This is the code that I currently have that works, but I want it to not create elastic IPs for resources on the private subnets:
locals {
instances_beta = {
my-ec2 = {
name = "myec2",
ami = "ami-029e27fb2fc8ce9d8",
instancetype = "t3.xlarge"
environment = "Beta",
securitygroups = [var.mysg],
subnetid = var.public-a,
elasticip = true
}
}
}
resource "aws_instance" "beta-instance" {
for_each = local.instances_beta
ami = each.value.ami
instance_type = each.value.instancetype
subnet_id = each.value.subnetid
key_name = "mykey"
vpc_security_group_ids = each.value.securitygroups
tags = {
Name = each.value.name
Environment = each.value.environment
}
}
resource "aws_eip" "beta-eip" {
for_each = local.instances_beta
instance = aws_instance.beta-instance[each.key].id
vpc = true
}
It sounds like count is the best way to do something like that, but I cannot do that as I am already using a for_each to achieve the resource creation. I was trying to do this with a nested for loop, but I cannot quite figure out how to get the syntax correct or if this is the best way to do it. For reference , the best resource I found on it was here around for_each conditionals: https://blog.gruntwork.io/terraform-tips-tricks-loops-if-statements-and-gotchas-f739bbae55f9
You can use for loop to create filtered map, for example:
for_each = {
for key, value in local.instances_beta: key => value if value.subnetid == var.public-a
}
It will filter local.instances_beta and leave items where subnetid equals var.public-a. You can adjust condition according to your needs.
More details in terraform documentation.

Conditional attributes in Terraform

Does Terraform support conditional attributes? I only want to use an attribute depending on a variable's value.
Example:
resource "aws_ebs_volume" "my_volume" {
availability_zone = "xyz"
size = 30
if ${var.staging_mode} == true:
snapshot_id = "a_specific_snapshot_id"
endif
}
The above if statement enclosing the attribute snapshot_id is what I'm looking for. Does Terraform support such attribute inclusion based on a variable's value.
Terraform 0.12 (yet to be released) will also bring support for HCL2 which allows you to use nullable arguments with something like this:
resource "aws_ebs_volume" "my_volume" {
availability_zone = "xyz"
size = 30
snapshot_id = var.staging_mode ? local.a_specific_snapshot_id : null
}
Nullable arguments are covered in this 0.12 preview guide.
For version of Terraform before 0.12, Markus's answer is probably your best bet although I'd be more explicit with the count with something like this:
resource "aws_ebs_volume" "staging_volume" {
count = "${var.staging_mode ? 1 : 0}"
availability_zone = "xyz"
size = 30
snapshot_id = "a_specific_snapshot_id"
}
resource "aws_ebs_volume" "non_staging_volume" {
count = "${var.staging_mode ? 0 : 1}"
availability_zone = "xyz"
size = 30
}
Note that the resource names must be unique or Terraform will complain. This then causes issues if you need to refer to the EBS volume such as with an aws_volume_attachment as in pre 0.12 the ternary expression is not lazy so something like this doesn't work:
resource "aws_volume_attachment" "ebs_att" {
device_name = "/dev/sdh"
volume_id = "${var.staging_mode ? aws_ebs_volume.staging_volume.id : aws_ebs_volume.non_staging_volume.id}"
instance_id = "${aws_instance.web.id}"
}
Because it will attempt to evaluate both sides of the ternary where only one can be valid at any point. In Terraform 0.12 this will no longer be the case but obviously you could solve it more easily with the nullable arguments.
I'm not aware of such a feature, however, you can model around this, if your cases are not too complicated. Since the Boolean values true and false are considered to be 1 and 0, you can use them within a count. So you may use
provider "null" {}
resource "null_resource" "test1" {
count= ${var.condition ? 1 : 0}
}
resource "null_resource" "test2" {
count = ${var.condition ? 0 : 1}
}
output "out" {
value = "${var.condition ? join(",",null_resource.test1.*.id) : join(",",null_resource.test2.*.id) }"
}
Only one of the two resources is created due to the count attribute.
You have to use join for the values, because this seems to handle the inexistence of one of the two values gracefully.
Thanks ydaetskcor for pointing out in their answer the improvements for variable handling.
Now that Terraform v0.12 and respective HCL2 were released, you can achieve this by just setting the default variable value to "null". Look at this example from Terraform website:
variable "override_private_ip" {
type = string
default = null
}
resource "aws_instance" "example" {
# ... (other aws_instance arguments) ...
private_ip = var.override_private_ip
}
More info here:
https://www.hashicorp.com/blog/terraform-0-12-conditional-operator-improvements
There is a new experimental feature with Terraform 0.15 : defaults which works with optional.
The defaults function is a specialized function intended for use with input variables whose type constraints are object types or collections of object types that include optional attributes.
From documentation :
terraform {
# Optional attributes and the defaults function are
# both experimental, so we must opt in to the experiment.
experiments = [module_variable_optional_attrs]
}
variable "storage" {
type = object({
name = string
enabled = optional(bool)
website = object({
index_document = optional(string)
error_document = optional(string)
})
documents = map(
object({
source_file = string
content_type = optional(string)
})
)
})
}
locals {
storage = defaults(var.storage, {
# If "enabled" isn't set then it will default
# to true.
enabled = true
# The "website" attribute is required, but
# it's here to provide defaults for the
# optional attributes inside.
website = {
index_document = "index.html"
error_document = "error.html"
}
# The "documents" attribute has a map type,
# so the default value represents defaults
# to be applied to all of the elements in
# the map, not for the map itself. Therefore
# it's a single object matching the map
# element type, not a map itself.
documents = {
# If _any_ of the map elements omit
# content_type then this default will be
# used instead.
content_type = "application/octet-stream"
}
})
}
just for the help, a more complex example:
data "aws_subnet" "private_subnet" {
count = var.sk_count
vpc_id = data.aws_vpc.vpc.id
availability_zone = element(sort(data.aws_availability_zones.available.names), count.index)
tags = {
Name = var.old_cluster_fqdn != "" ? "${var.old_cluster_fqdn}-prv-subnet-${count.index}" : "${var.cluster_fqdn}-prv-subnet-${count.index}"
}
}

Resources