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
}
Related
I'm trying to add the tag "Project" to the resource "aws_ec2_transit_gateway_vpc_attachment" to make it match the rest of the resources in this project, but when I do so Terraform throws an error.
Relevant Terraform code:
resource "aws_ec2_transit_gateway" "tgw" {
description = "TGW for inter-region connectivity"
auto_accept_shared_attachments = "disable"
default_route_table_association = "disable"
default_route_table_propagation = "disable"
dns_support = "enable"
tags = {
"Name" = "labs tgw",
"Terraform" = "true",
"Project" = var.project_id
}
}
## Create new TGW Attachments
resource "aws_ec2_transit_gateway_vpc_attachment" "tgw_attachments" {
subnet_ids = aws_subnet.tgw[*].id
transit_gateway_id = aws_ec2_transit_gateway.tgw.id
vpc_id = aws_vpc.vpc.id
dns_support = "enable"
transit_gateway_default_route_table_association = "false"
transit_gateway_default_route_table_propagation = "false"
tags = {
"Name" = "labs_tgw_attachment",
"Terraform" = "true",
#"Project" = var.project_id
}
}
## Accept attachment if it hasn't already been accepted
data "aws_ec2_transit_gateway_attachment" "attachment_data" {
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.tgw_attachments.id
}
resource "aws_ec2_transit_gateway_vpc_attachment_accepter" "accept" {
count = (data.aws_ec2_transit_gateway_attachment.attachment_data.state == "pendingAcceptance") ? 1 : 0
transit_gateway_attachment_id = aws_ec2_transit_gateway_vpc_attachment.tgw_attachments.id
depends_on = [
aws_ec2_transit_gateway_vpc_attachment.tgw_attachments
]
}
The purpose of the data block and count is due to terraform throwing an error that the attachment is in an inconsistent state after it's already been accepted, so it checks to see if the attachment is in "pendingAcceptance" state, and builds the accepter only if that's true.
Currently, the attachment is accepted, everything else is working as expected, but if I uncomment "Project" = var.project_id, then I get this error:
Error: Invalid count argument
│
│ on modules/main.tf line 74, in resource "aws_ec2_transit_gateway_vpc_attachment_accepter" "accept":
│ 74: count = (data.aws_ec2_transit_gateway_attachment.attachment_data.state == "pendingAcceptance") ? 1 : 0
│
│ 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've tried adding the tags with merge, like so:
tags = merge({
"Name" = "labs tgw attachment",
"Terraform" = "true"
},
{
"Project" = var.project_id
}
)
}
But got the same error, presumably since that formatting isn't required and doesn't really change the way the tags are evaluated.
I have a terraform code like below.
locals {
org_sub_accounts = [
"111111111111",
"222222222222,
"333333333333",
]
role_arns = [
"arn:aws:iam::111111111111:role/DataConnector1",
"arn:aws:iam::222222222222:role/DataConnector2",
"arn:aws:iam::333333333333:role/DataConnector3",
]
}
resource "aws_cloudformation_stack_set_instance" "stack" {
count = length(local.org_sub_accounts)
account_id = local.org_sub_accounts[count.index]
region = "ap-east-1"
parameter_overrides = {
RoleName = local.role_arns[count.index]
}
stack_set_name = aws_cloudformation_stack_set.stackset.name
}
My problem is my RoleName should be DataConnector potion (after /) but not the entire ARN in the aws_cloudformation_stack_set_instance. How can I pass the RoleName DataConnector* within each index?
Note, here I defined the variables in the locals to show my use case. But actually those comes from other resource outputs.
This can be achieved by using the split built-in function:
locals {
role_names = [for arn in local.role_arns : split("/", arn)[1]]
}
With this part split("/", arn)[1] you are splitting the IAM role ARN into two parts (before and after the /) and with the index [1] you are effectively getting the second part of that list. Then, you would have to change the code to reflect that with:
resource "aws_cloudformation_stack_set_instance" "stack" {
count = length(local.org_sub_accounts)
account_id = local.org_sub_accounts[count.index]
region = "ap-east-1"
parameter_overrides = {
RoleName = local.role_names[count.index]
}
stack_set_name = aws_cloudformation_stack_set.stackset.name
}
[1] https://developer.hashicorp.com/terraform/language/functions/split
I want to create an object through which I will iterate using for_each to create some:
google_service_account
google_project_iam_member
resources
So my object is more or less like this
service_accounts = {
"gha_storage_admin_sa" = {
create = true
project = var.project_id
account_id = "id1"
display_name = "GHA service account 1"
role = "roles/storage.admin"
},
"gha_cluster_scaling_sa" = {
create = false
project = var.project_id
account_id = "id2"
display_name = "GHA service account 2"
role = google_organization_iam_custom_role.my_custom_role.id
},
}
resource "google_service_account" "service_account" {
for_each = {
for k, v in local.service_accounts: k => v
if v.create
}
project = each.value.project
account_id = each.value.account_id
display_name = each.value.display_name
}
resource "google_project_iam_member" "member" {
for_each = local.service_accounts
project = var.project_id
role = each.value.role
member = "serviceAccount:${google_service_account.service_account[each.key].email}"
}
This works fine if the above is a local variable.
I want however to expose it as a regular variable.
My question is whether the referenced resource (google_organization_iam_custom_role.my_custom_role.id) in the second element can be somehow exposed as a variable.
My question is whether the referenced resource (google_organization_iam_custom_role.my_custom_role.id) in the second element can be somehow exposed as a variable.
Sadly its not possible. TF does not support dynamic variables. All variables' values must be know at plan time. You have to pass the actual value of google_organization_iam_custom_role.my_custom_role.id as input variable to your code.
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"]
}
}
Trying to lunch 1 or 2 instances based on a condition ( "single-target" )
ERROR im getting :
132: target_id = var.ec2-2
var.ec2-2 is empty tuple
Inappropriate value for attribute "target_id": string required.
the use of that variable :
resource "aws_lb_target_group_attachment" "b" {
target_id = var.ec2-2
}
outputs :
output "ec2-2_ot" {
value = aws_instance.ec2V2[*].id
description = "the value of the network module ec2-2 id"
}
variable :
variable "single-target" {
type=bool
default=false
}
main.tf :
module "network_module" {
ec2-2 = module.compute_module.ec2-2_ot
}
ec2 launch :
resource "aws_instance" "ec2V2" {
count = var.single-target ? 0 : 1
ami = var.ec2_ami
instance_type = var.ec2_type
associate_public_ip_address = true
key_name = var.key_pair
vpc_security_group_ids = [ var.vpc-sg ]
subnet_id = var.subnet-2
user_data = file("PATH/user-data.sh")
tags = {
Name = var.ec2-2_name
}
}
As the count has been already set to the resource you are trying to create, the returning result list would need to be accessed via Splat Expression mentioned in this terraform documentation.
If you need to choose specific index from the list of the result , then you can use the element function to do so
So in your case, if you need to output EC2 id it would be as follows.
output "thisisoutput" {
value = aws_instance.ec2V2[*].arn
}