Terraform expand map variable to independent parameters - amazon-web-services

I've the following variable:
variable "mymap" {
type = map(string)
default = {
"key1" = "val1"
"key2" = "val2"
}
}
I am trying to expand this to create individual parameters in this resource:
resource "aws_elasticache_parameter_group" "default" {
name = "cache-params"
family = "redis2.8"
parameter {
name = "activerehashing"
value = "yes"
}
parameter {
name = "min-slaves-to-write"
value = "2"
}
}
My desired state for this example would be:
resource "aws_elasticache_parameter_group" "default" {
name = "cache-params"
family = "redis2.8"
parameter {
name = "key1"
value = "val1"
}
parameter {
name = "key2"
value = "val2"
}
}
I don't see this supported explicitly in the docs; am I even taking the correct approach to doing this?
(I'm mainly looking at leveraging 'dynamic' and 'for_each' keywords, but haven't been able to have success)

To achieve the desired state, you would have to do a couple of things. One could be to use dynamic meta-argument [1] with for_each [2]. The code would have to be changed to the following:
resource "aws_elasticache_parameter_group" "default" {
name = "cache-params"
family = "redis2.8"
dynamic "parameter" {
for_each = var.mymap
content {
name = parameter.value.name
value = parameter.value.value
}
}
}
However, you would also have to adjust the variable:
variable "mymap" {
type = map(map(string))
description = "Map of parameters for Elasticache."
default = {
"parameter1" = {
"value" = "value1"
"name" = "name1"
}
}
}
Then, you can define the values for the variable mymap in a tfvars file (e.g., terraform.tfvars) like this:
mymap = {
"parameter1" = {
"name" = "activerehashing"
"value" = "yes"
}
"parameter2" = {
"name" = "min-slaves-to-write"
"value" = "2"
}
}
[1] https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks
[2] https://developer.hashicorp.com/terraform/language/meta-arguments/for_each

You can use a dynamic block to dynamically declare zero or more nested configuration blocks based on a collection.
resource "aws_elasticache_parameter_group" "default" {
name = "cache-params"
family = "redis2.8"
dynamic "parameter" {
for_each = var.mymap
content {
name = parameter.key
value = parameter.value
}
}
}
The above tells Terraform to generate one parameter block for each element of var.varmap, and to populate the name and value arguments of each generated block based on the key and value from each map element respectively.
The parameter symbol inside the content block represents the current element of the collection. This symbol is by default named after the block type being generated, which is why it was named parameter in this case. It's possible to override that generated name using an additional iterator argument in the dynamic block, but that's necessary only if you are generating multiple levels of nesting where a nested block type has the same name as its container.

Related

Terraform dynamic variable lookup

I am writing some terraform code that looks up the ami based on a variable called fortios_version. I seem to not understand how to have a map pass back a map value. Here is my code...
variable "fortios_version" {
type = string
}
variable "fortios_map" {
type = map
default = {
"7.2.0" = "fgtvmbyolami-7-2-0"
"6.4.8" = "fgtvmbyolami-6-4-8"
}
}
variable "fgtvmbyolami-7-2-0" {
type = map
default = {
us-east-1 = "ami-08a9244de2d3b3cfa"
us-east-2 = "ami-0b07d15df1781b3d8"
}
}
My aws instance code:
ami = lookup(lookup(var.fortios_map[var.fortios_version]), var.region)
My variable:
fortios_version: "7.2.0"
I hope I am making sense. I have played with different variations all day with no luck.
You can't dynamically refer to fgtvmbyolami-7-2-0 based on the output of fortios_map. It would be best to re-organize your variables:
variable "fortios_version" {
type = string
}
variable "fortios_map" {
type = map
default = {
"7.2.0" = "fgtvmbyolami-7-2-0"
"6.4.8" = "fgtvmbyolami-6-4-8"
}
}
variable "amis" {
type = map
default = {
"fgtvmbyolami-7-2-0" = {
us-east-1 = "ami-08a9244de2d3b3cfa"
us-east-2 = "ami-0b07d15df1781b3d8"
},
"fgtvmbyolami-6-4-8" = {
us-east-1 = "ami-08a92333cfa"
us-east-2 = "ami-0b07dgggg781b3d8"
}
}
}
then
ami = var.amis[var.fortios_map[var.fortios_version]][var.region]
You can expand this to ad lookup in to have some default values for each map.

How to add and reference a map of map as a local variable in terraform?

I want to create a map of map as a local variable. I am using this
locals {
region_map = {
mumbai = {
is_second_execution = true
cg_ip_address = "ip.add.re.ss"
}
}
}
Now I am referencing it as
module "mumbai" {
source = "./site-to-site-vpn-setup"
providers = { aws = aws.mumbai }
is_second_execution = lookup(local.region_map, local.region_map["mumbai"]["is_second_execution"], false)
cg_ip_address = lookup(local.region_map, local.region_map["mumbai"]["cg_ip_address"], "")
}
but upon doing terrafrom plan the cg_ip_address is being set to null.
Also If I add another module say "saopaulo" and I need to pass default values of is_second_execution and cg_ip_address for it without adding saopaulo in the map, how do I do that?
The lookup built-in function [1] has the following syntax:
lookup(map, key, default)
Since you have a map of maps, that means that the first argument is the map (local.region_map.mumbai), the second is the key you are looking for (cg_ip_address) and the third argument is the default value. So in your case you have to change the lookup to this:
module "mumbai" {
source = "./site-to-site-vpn-setup"
providers = { aws = aws.mumbai }
is_second_execution = lookup(local.region_map.mumbai, "is_second_execution", false)
cg_ip_address = lookup(local.region_map.mumbai, "cg_ip_address", "")
}
[1] https://www.terraform.io/language/functions/lookup

What is the workaround of using for each and count together in Terraform?

I have two conditions need to be fulfilled:
Grant users permission to specific project-id based on env. For example: my-project-{env} (env: stg/prd)
I want to loop over the variables, instead of writing down repetitive resource for each user.
Example:
variable some_ext_users {
type = map(any)
default = {
user_1 = { email_id = "user_1#gmail.com" }
user_2 = { email_id = "user_2#gmail.com" }
}
}
To avoid repetitive resource made on each user (imagine 100++ users), I decided to list them in variable as written above.
Then, I'd like to assign these user GCS permission, e.g:
resource "google_storage_bucket_iam_member" "user_email_access" {
for_each = var.some_ext_users
count = var.env == "stg" ? 1 : 0
provider = google-beta
bucket = "my-bucketttt"
role = "roles/storage.objectViewer"
member = "user:${each.value.email_id}"
}
The error I'm getting is clear :
Error: Invalid combination of "count" and "for_each" on
../../../modules/my-tf.tf line 54, in resource
"google_storage_bucket_iam_member" "user_email_access": 54:
for_each = var.some_ext_users The "count" and "for_each"
meta-arguments are mutually-exclusive, only one should be used to be
explicit about the number of resources to be created.
My question is, what is the workaround in order to satisfy the requirements above if count and for_each can't be used together?
You could control the user list according to the environment, rather than trying to control the resource. So, something like this:
resource "google_storage_bucket_iam_member" "user_email_access" {
for_each = var.env == "stg" ? var.some_ext_users : {}
provider = google-beta
bucket = "my-bucketttt"
role = "roles/storage.objectViewer"
member = "user:${each.value.email_id}"
}
The rule for for_each is to assign it a map that has one element per instance you want to declare, so the best way to think about your requirement here is that you need to write an expression that produces a map with zero elements when your condition doesn't hold.
The usual way to project and filter collections in Terraform is for expressions, and indeed we can use a for expression with an if clause to conditionally filter out unwanted elements, which in this particular case will be all of the elements:
resource "google_storage_bucket_iam_member" "user_email_access" {
for_each = {
for name, user in var.some_ext_users : name => user
if var.env == "stg"
}
# ...
}
Another possible way to structure this would be to include the environment keywords as part of the data structure, which would keep all of the information in one spot and potentially allow you to have entries that apply to more than one environment at once:
variable "some_ext_users" {
type = map(object({
email_id = string
environments = set(string)
}))
default = {
user_1 = {
email_id = "user_1#gmail.com"
environments = ["stg"]
}
user_2 = {
email_id = "user_2#gmail.com"
environments = ["stg", "prd"]
}
}
}
resource "google_storage_bucket_iam_member" "user_email_access" {
for_each = {
for name, user in var.some_ext_users : name => user
if contains(user.environments, var.env)
}
# ...
}
This is a variation of the example in the "Filtering Elements" documentation I linked above, which uses an is_admin flag in order to declare different resources for admin users vs. non-admin users. In this case, notice that the if clause refers to the symbols declared in the for expression, which means we can now get a different result for each element of the map, whereas the first example either kept all elements or no elements.

Terraform: How to add a filter to a data source conditionally

Given the data source definition:
data "aws_ami" "my_ami" {
filter {
name = "name"
values = ["my_ami_name"]
}
}
How does one add a second filter only if a condition is true?
Example pseudo code of what I want:
data "aws_ami" "my_ami" {
filter {
name = "name"
values = ["my_ami_name"]
}
var.state ? filter {
name = "state"
values = [var.state]
} : pass
}
The second filter would only be used if the state variable has content.
Note that I don't want to use a 'N/A' value to always use the second filter, regardless if it's needed or not.
You can use dynamic blocks. The condition depends exactly on what is your condition (var.state is not shown, so I don't know what it is), but in general you can do:
data "aws_ami" "my_ami" {
filter {
name = "name"
values = ["my_ami_name"]
}
dynamic "filter" {
for_each = var.state ? [1] : []
content {
name = "state"
values = [var.state]
}
}
}

How do I populate sourceInfo in SSM Association using TerraForm

I am building a very basic Systems Manager Association in TerraForm but I do not understand what the sourceInfo field is asking for. It requires a string but even simple strings like "test" cause it to reject the input.
resource "aws_ssm_association" "sslscanssm" {
name = "AWS-RunInspecChecks"
association_name = "test"
targets = {
key = "tag:os"
values = ["linux"]
}
parameters {
sourceType = "GitHub"
sourceInfo = "{"owner":"awslabs","repository":"amazon-ssm","path":"Compliance/InSpec/PortCheck","getOptions":"branch:master"}"
#^this line doesn't work
#sourceInfo = "test"
#^this line doesn't work either
}
}
Instead of escaping all of your strings you could also use the jsonencode function to turn a map into the JSON you want:
locals {
source_info = {
owner = "awslabs"
repository = "amazon-ssm"
path = "Compliance/InSpec/PortCheck"
getOptions = "branch:master"
}
}
resource "aws_ssm_association" "sslscanssm" {
name = "AWS-RunInspecChecks"
association_name = "test"
targets = {
key = "tag:os"
values = ["linux"]
}
parameters {
sourceType = "GitHub"
sourceInfo = "${jsonencode(local.source_info)}"
}
}
I wasn't aware sourceInfo expects parentheses and all inner double quotes to be escaped or it won't work.
resource "aws_ssm_association" "sslscanssm" {
name = "AWS-RunInspecChecks"
association_name = "test"
targets = {
key = "tag:os"
values = ["linux"]
}
parameters {
sourceType = "GitHub"
sourceInfo = "{\"owner\":\"awslabs\",\"repository\":\"amazon-ssm\",\"path\":\"Compliance/InSpec/PortCheck\",\"getOptions\":\"branch:master\"}"
}
}
There is a mistake in the code shared (no equal sign after targets but after parameters). The correct syntax of the resource is :
resource "aws_ssm_association" "sslscanssm" {
name = "AWS-RunInspecChecks"
association_name = "test"
targets {
key = "tag:os"
values = ["linux"]
}
parameters = {
sourceType = "GitHub"
sourceInfo = "${jsonencode(local.source_info)}"
}
}