How to check, if Terraform will create resource - amazon-web-services

I use Terraform + AWS Lambda in my workflow, and i want to call some lambda with ARNs of my existent instances during every plan or apply action. I use this structure:
locals {
...
centralized_nodes = {...}
tags_map = {...}
backup_map = {
for node in keys(local.centralized_nodes):
node => aws_instance.centralized_node[node].arn
if can(local.tags_map[node]["backup"]) && !(can(aws_instance.centralized_node[node].root_block_device.0.tags["backup"]))
}
...
}
resource "aws_instance" "centralized_node" {
...
for_each = local.centralized_nodes
...
}
data "aws_lambda_invocation" "lambda_backup" {
for_each = local.backup_map
function_name = "lambdafunc"
input = jsonencode({
"resources" = [each.value]
})
}
it worked fine until i try to add node description object to centralized_nodes map and create new instance. When i add this, terraform shows me error during planning:
Error: Invalid for_each argument
│
│ on resources.tf line 223, in data "aws_lambda_invocation" "lambda_backup":
│ 223: for_each = local.backup_map
│ ├────────────────
│ │ local.backup_map will be known only after apply
│
│ The "for_each" 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 for_each depends on.
Now i'm looking for terraform check if resource exists or will be created. i've tried adding arguments to map generator to exclude uncreated instances (it's OK in my workflow) like that:
if can(local.tags_map[node]["backup"]) && !(can(aws_instance.centralized_node[node].root_block_device.0.tags["backup"])) && can(aws_instance.centralized_node[node].arn)
But it doesn't work, terraform thinks it can reach ARN value for uncreated instance, but later.
Please, help me find the way to avoid such error.

Seems that your local.centralized_nodes is not pre-defined by rather generated automatically. If so, you can't use it with for_each.
The keys of the map (or all the values in the case of a set of strings) must be known values, or you will get an error message that for_each has dependencies that cannot be determined before apply, and a -target may be needed.
You can try converting your code to use count, as count does not have such a limitation. But then, count has its own issues which may prevent you from achieving your goals anyway.

I've found the only one way to get check if resources like instances and volumes already exist. It is using aws_instance and aws_instances datasources.
This is my solution:
locals {
...
centralized_nodes = {...}
tags_map = {...}
instance_name_root_volume_tags_map = {
for name, value in {for id, value in data.aws_instance.instance_id_arn_map : value.tags["Name"] => value}:
name => data.aws_ebs_volume.volume_instance_id_map[value.id].tags
}
backup_map = {
for name, value in {for id, value in data.aws_instance.instance_id_arn_map : value.tags["Name"] => value}:
name => value.arn
if !can(local.instance_name_root_volume_tags_map[name]["backup"]) && can({for node, value in local.tags_map: value["Name"] => value}[name]["backup"]) )
}
...
}
resource "aws_instance" "centralized_node" {
...
for_each = local.centralized_nodes
...
}
data "aws_lambda_invocation" "lambda_backup" {
for_each = local.backup_map
function_name = "lambdafunc"
input = jsonencode({
"resources" = [each.value]
})
}
data "aws_instances" "existant_instances_array" {
filter {
name = "tag:Name"
values = [for tagmap in local.hiera_tags_map : tagmap["Name"]]
}
}
data "aws_instance" "instance_id_arn_map" {
for_each = toset(data.aws_instances.existant_instances_array.ids)
instance_id = each.value
}
data "aws_ebs_volume" "volume_instance_id_map" {
for_each = data.aws_instance.instance_id_arn_map
filter {
name = "attachment.instance-id"
values = [each.value.id]
}
filter {
name = "attachment.device"
values = ["/dev/sda1"]
}
}

Related

Terraform local variables dependancy in for_each statement

I'm using following local variables to pass map of account_id and account_type to my child module in order to do to create patch manager resources based on account_type from my child module.
locals {
org_sub_accounts_map = zipmap(module.accounts.account_id, module.accounts.tags_all.*.AccountType)
org_sub_accounts = [for k, v in local.org_sub_accounts_map : {
id = k
type = v
}
]
}
module "ssm_patch_manager" {
source = "../../../../modules/aws/cloudformation/stacksets"
accounts = local.org_sub_accounts
account_exception_list = var.account_exception_list
regions = var.region_list
stackset_name = "SSM-PatchManager"
template = "ssm_patch_manager"
parameters = var.patch_manager_default_params
parameter_overrides = var.patch_manager_params_overrides
stackset_admin_role_arn = module.stackset_admin_role.role_arn
depends_on = [module.accounts]
}
local.org_sub_accounts is something like this:
org_sub_accounts = [
{
"id" = "111111111111"
"type" = "Dev"
},
{
"id" = "222222222222"
"type" = "Prod"
},
{
"id" = "33333333333"
"type" = "Dev"
}
]
This works fine with all the existing AWS accounts as terraform aware of the accounts IDs. Now the problem is, when I'm creating a new AWS account from module.accounts, and running the terraform plan, I get below error:
Error: Invalid for_each argument
on ../../../../modules/aws/cloudformation/stacksets/main.tf line 25, in resource "aws_cloudformation_stack_set_instance" "stack":
25: for_each = {
26: for stack_instance in local.instance_data : "${stack_instance.account}.${stack_instance.region}" => stack_instance if contains(var.account_exception_list, stack_instance.account) == false
27: }
├────────────────
│ local.instance_data will be known only after apply
│ var.account_exception_list is list of string with 1 element
The "for_each" 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 for_each depends on.
I understand this is clearly because terraform doesn't know the account_id when evaluating the locals variables. Can anyone suggest something here to resolve this issue?
Please note, this is already implemented solution. But we came to know after sometime when we try to create a new account. Therefore, any suggestion without major structure changes in the code would be really helpful.
Update
instance_data is a resource block in child module.
locals {
instance_data = flatten([
for account in var.accounts : [
for region in var.regions : {
account = account.id
type = try(length(account.type), 0) > 0 ? account.type : "default" # To support legacy var.account input, which account 'type' key is not passed.
region = region
}
]
])
resource "aws_cloudformation_stack_set_instance" "stack" {
for_each = {
for stack_instance in local.instance_data : "${stack_instance.account}.${stack_instance.region}" => stack_instance if contains(var.account_exception_list, stack_instance.account) == false
}
account_id = each.value.account
region = each.value.region
parameter_overrides = lookup(var.parameter_overrides, each.value.type, null) # To handle different parameters based on 'AccountType' Tag in sub accounts
stack_set_name = aws_cloudformation_stack_set.stackset.name
}
Finally came up with below solution. Just posting this here if anyone needs to reference in future.
data "aws_organizations_resource_tags" "account" {
count = length(module.organization.non_master_accounts.*.id)
resource_id = module.organization.non_master_accounts.*.id[count.index]
}
# “for_each” value in child module depends on resource attributes (AWS account_id) that cannot be determined until terraform apply, so Terraform cannot predict how many instances will be created.
# Because of this, we use `aws_organizations_resource_tags` data source to create stacksets, instead of module outputs.
locals {
org_sub_accounts = [for account in data.aws_organizations_resource_tags.account : {
id = account.id
type = try(length(account.tags.AccountType), 0) > 0 ? account.tags.AccountType : "default" # In case AccountType tag missing in existing/invited accounts.
}
]
}
# Output in Organization module
output "non_master_accounts" {
value = aws_organizations_organization.root.non_master_accounts
}

terraform combine data template file and each.key in a for_each block

Using a module to create multiple IAM roles with a for_each block I am trying to pass into the policy a rendered data output and the key from the for_each loop. The name of this policy will be slightly different for each role
module "sso_roles" {
source = "git::ssh://git#gitlab.com/iam/role?ref=1.1.0"
for_each = local.roles
policy = "${data.template_file}.${each.key}_policy".rendered
role_name = each.key
assume_role_policy_def = data.template_file.testing_role.rendered
}
These are the locals its looping through:
locals {
roles = {
"test_Read_Only" = ["arn:aws:iam::*:role/testReadOnly", "]
"test_OS_Only" = ["arn:aws:iam::*:role/testSigninOSOnly"]
}
}
what I need terraform to see when its running are these two:
${data.template_file.test_Read_Only_policy.rendered}
${data.template_file.test_OS_Only_policy.rendered}
But there is something not right with the syntax I have. The error I get says "The "data" object must be followed by two attribute names: the data source type and the resource name."
I don't know how to combine the each.key into the rendered data template file
What I would suggest is:
To use the data source with for_each and use the same variable
To switch to the templatefile built-in function and pass the value as a variable.
To achieve the first, you would do something like:
module "sso_roles" {
source = "git::ssh://git#gitlab.com/iam/role?ref=1.1.0"
for_each = local.roles
policy = data.template_file.policy[each.key].rendered
role_name = each.key
assume_role_policy_def = data.template_file.testing_role.rendered
}
data "template_file" "policy" {
for_each = local.roles
...
}
The second one is probably a bit more convenient and it's using a newer and better templatefile function [1]:
module "sso_roles" {
source = "git::ssh://git#gitlab.com/iam/role?ref=1.1.0"
for_each = local.roles
policy = templatefile("${path.module}/path/to/template/file.tpl", {
iam_role = each.value
})
role_name = each.key
assume_role_policy_def = data.template_file.testing_role.rendered
}
With more information about the template file you are using I would be able to adjust the second example.
[1] https://www.terraform.io/language/functions/templatefile

How to grab two arbitrary subnet IDs from Terraform

Looking to deploy a Fargate application where the invoking function runs the ECS container in two arbitrarily selected subnets from the default VPC.
So far my template looks like this:
data "aws_subnets" "subnets" {
filter {
name = "vpc-id"
values = [var.vpc_id]
}
}
data "aws_subnet" "subnet" {
for_each = toset(data.aws_subnets.subnets.ids)
id = each.value
}
resource "aws_lambda_function" "ecs_invoker" {
function_name = "ecs_invoker"
...
environment {
variables = {
SUBNET_PRIMARY = data.aws_subnet.subnet[0]
SUBNET_SECONDARY = data.aws_subnet.subnet[1]
}
}
}
However, this produces the following error:
│ Error: Invalid index
│
│ on lambda.tf line 16, in resource "aws_lambda_function" "ecs_invoker":
│ 16: SUBNET_PRIMARY = data.aws_subnet.subnet[0]
│ ├────────────────
│ │ data.aws_subnet.subnet is object with 6 attributes
│
│ The given key does not identify an element in this collection value. An object only supports looking up attributes by name, not by numeric index.
╵
So how exactly should I grab two arbitrary subnet IDs from the default VPC?
Since you used for_each, data.aws_subnet.subnet will be a map, not a list. So to get two first subnet ids, you can do:
SUBNET_PRIMARY = values(data.aws_subnet.subnet)[0].id
SUBNET_SECONDARY = values(data.aws_subnet.subnet)[1].id
To get two random ids, you can do:
resource "random_shuffle" "subnets" {
input = values(data.aws_subnet.subnet)[*].id
result_count = 2
}
and then
SUBNET_PRIMARY = random_shuffle.subnets.result[0]
SUBNET_SECONDARY = random_shuffle.subnets.result[1]

How to use nested count in terraform for creating VPN endpoint routes?

I want to create VPN client endpoint in AWS using terraform.
My current block of code is:
resource "aws_ec2_client_vpn_route" "vpn_route" {
depends_on = [
aws_ec2_client_vpn_network_association.vpn_subnets
]
count = length(var.rule)
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.vpn.id
destination_cidr_block = element(var.rule, count.index)
target_vpc_subnet_id = element(var.subnets_id, count.index)
}
Here rule & subnet_id variables are as below:
rule = ["172.16.0.0/16", "172.18.0.0/16", "172.19.0.0/16"]
subnets_id = ["subnet-123", "subnet-456"]
I want to associate each rule CIDR with both subnets. But my current code is only associating 1 subnet with 1 CIDR. I am not able to figure out how to solve it.
Update:
I modified code according to #Ervin's answer but getting following error.
Error: error creating client VPN route "cvpn-endpoint-0e72bbde5,subnet-0fefd,172.19.0.0/16": ConcurrentMutationLimitExceeded: Cannot initiate another change for this endpoint at this time. Please try again later.
│ status code: 400, request id: 2663f630-54a1-4a22-a093-d04425204cf5
│
│ with module.VPN-Endpoint.aws_ec2_client_vpn_route.vpn_route["5"],
│ on modules\VPN-Endpoint\rule_route.tf line 14, in resource "aws_ec2_client_vpn_route" "vpn_route":
│ 14: resource "aws_ec2_client_vpn_route" "vpn_route" {
I guess it is because each route should be created one by one. So I modified my code as below by adding time sleep:
resource "time_sleep" "wait_30_seconds" {
create_duration = "30s"
}
resource "aws_ec2_client_vpn_route" "vpn_route" {
depends_on = [
aws_ec2_client_vpn_network_association.vpn_subnets,
time_sleep.wait_30_seconds
]
for_each = { for index, pair in setproduct(var.rule, var.subnets_id) : index => pair }
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.vpn.id
destination_cidr_block = each.value[0]
target_vpc_subnet_id = each.value[1]
}
But it is still not working. Is there any workaround for this?
You can accomplish this by using setproduct. This function computes the Cartesian-product for the elements of the two lists.
resource "aws_ec2_client_vpn_route" "vpn_route" {
depends_on = [
aws_ec2_client_vpn_network_association.vpn_subnets
]
for_each = { for index, pair in setproduct(var.rule, var.subnets_id) : index => pair }
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.vpn.id
destination_cidr_block = each.value[0]
target_vpc_subnet_id = each.value[1]
}

WAF for all ALBs in an account using terraform

This is my waf.tf terraform file:
resource "aws_wafv2_web_acl" "waf_acl-dev" {
name = "waf_log4j_Protections-dev"
description = "WAFv2 for dev"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "AWSManagedRulesKnownBadInputsRule"
priority = 1
override_action {
count {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesKnownBadInputsRuleSet"
vendor_name = "AWS"
# Excluding all these leaves only Log4JRCE
excluded_rule {
name = "Host_localhost_HEADER"
}
excluded_rule {
name = "PROPFIND_METHOD"
}
excluded_rule {
name = "ExploitablePaths_URIPATH"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSManagedRulesKnownBadInputsRule"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSManagedKnownBadInputsRule"
sampled_requests_enabled = true
}
}
variable "lb_arn" {
type = string
default = ""
}
data "aws_lb" "all_alb" {
tags = {
Environment = "Dev"
}
# arn = var.lb_arn
# for_each = data.aws_lb.all_alb
# id = each.value
}
output "all_alb" {
value = data.aws_lb.all_alb
}
resource "aws_wafv2_web_acl_association" "waf_acl-association-dev" {
for_each = data.aws_lb.all_alb.arn
resource_arn = each.value
web_acl_arn = aws_wafv2_web_acl.waf_acl-dev.arn
}
My objective is to create a WAF with rules (that portion works fine), and attach multiple ALBs to the WAF using tags on each ALB. Im running it inside a teamcity loop that loops into multiple AWS accounts (That is out of scope). Each account can have one or multiple ALBs with the tag provided. When I run this code I get the following error:
Error: Search returned 2 results, please revise so only one is returned
17:39:18 │
17:39:18 │ with data.aws_lb.all_alb,
17:39:18 │ on xa-waf-inow.tf line 49, in data "aws_lb" "all_alb":
17:39:18 │ 49: data "aws_lb" "all_alb" {
I also tried a few other alternatives like putting [0] at the end of line 49 but Im still getting hit with some syntax error or the other. Can someone please help?Thanks
Edit:
I also tried for_each in data block:
data "aws_lb" "all_alb" {
for_each = {
tags = {
Environment = "Dev"
}
}
arn = each.key
}
output "all_alb" {
value = data.aws_lb.all_alb
}
resource "aws_wafv2_web_acl_association" "waf_acl-association-dev" {
# for_each = data.aws_lb.all_alb.arn
resource_arn = data.aws_lb.all_alb[each.key]
web_acl_arn = aws_wafv2_web_acl.waf_acl-dev.arn
}
But got this error:
Error: Reference to "each" in context without for_each
18:24:01 │
18:24:01 │ on xa-waf-inow.tf line 65, in resource "aws_wafv2_web_acl_association" "waf_acl-association-dev":
18:24:01 │ 65: resource_arn = data.aws_lb.all_alb[each.key]
18:24:01 │
18:24:01 │ The "each" object can be used only in "module" or "resource" blocks, and
18:24:01 │ only when the "for_each" argument is set.
I was able to get this to work for myself. My issue was a mixture of lists and sets. I believe I can change my variable to a set, and not have to deal with any sets here, but I know the below works as is.
VARIABLES.TF
variable "list_of_alb" {
type = list(string)
}
MAIN.TF
list_of_alb = [
"${terraform.workspace}-unique-1",
"${terraform.workspace}-unique-2"
]
DATA.TF - the problem child
data "aws_lb" "main" {
for_each = toset( var.list_of_alb )
name = each.value
}
WAF.TF
resource "aws_wafv2_web_acl_association" "main" {
for_each = toset(var.list_of_alb)
resource_arn = data.aws_lb.main[each.value].arn
web_acl_arn = aws_wafv2_web_acl.main.arn
}
The aws_lb data source must return only one result. You can't change that, as this is how it was destined.
If you want to return multiple ALBs you have two choices:
Use for_each with the data source. This way your data source will run for each id of your alb. This means that you have to provide the alb ids as an input variable.
Or, create your own custom data source. Since this is fully custom code that you have to write, it can overcome any limitations of TF's build in data sources.