I would like to create a dynamic resource with for_each as well I would like to implement a resource creation condition, however by some reason which I don't understand, the resource creation condition is not working as expected.
The logic is very simple: I've a list of objects, each object is resource, therefore in each object a have a bool variable that enables creation of a resource.
resource "aws_autoscaling_group" "aws_asg" {
for_each = { for key, value in var.parameters : key => value if flatten([ for x in var.parameters : x.init ]) }
}
The problem in flatten([ for x in var.parameters : x.init ]), the resource takes first element, and ignores next element:
+ x = [
+ true,
+ false,
]
Here is var.parameters
parameters = [
# Runner 1
{
init = true
name = "test-runner-1"
scaling = {
desired = 3
maximum = 9
minimum = 3
}
},
# Runner 2
{
init = false
name = "test-runner-2"
scaling = {
desired = 3
maximum = 9
minimum = 3
}
}
]
}
Any idea how to map each boolean to resource creation?
If you want to conditionally loop through your parameters, then it should be:
resource "aws_autoscaling_group" "aws_asg" {
for_each = { for key, value in var.parameters : key => value if value.init }
}
Related
I need an if statement with for_each.
If it is non-prod create resource in a single subnet, if production create resources in both subnets:
locals {
zone_data = {
a = {
subnet_id = data.aws_cloudformation_export.subnet1.value
}
b = {
subnet_id = data.aws_cloudformation_export.subnet2.value
}
}
}
module "batch_server" {
for_each = var.env_name == "non-prod" ? local.zone_data.a : local.zone_data
...
I get an error:
|----------------
| local.zone_data is object with 2 attributes
| local.zone_data.a is object with 1 attribute "subnet_id"
The true and false result expressions must have consistent types.
The given expressions are object and object, respectively.
I tried this:
for_each = var.env_name == "non-prod" ? [local.zone_data.a] : [local.zone_data]
and am getting similar error:
|----------------
| local.zone_data is object with 2 attributes
| local.zone_data.a is object with 1 attribute "subnet_id"
The true and false result expressions must have consistent types.
The given expressions are tuple and tuple, respectively.
Tried changing types but nothing seems to work
for_each expects a map with one element for each instance you want to declare. That means that if you want to declare only one instance then you need to produce a one-element map.
Here's one way to do that:
for_each = var.env_name == "non-prod" ? tomap({a = local.zone_data.a}) : local.zone_data
This can work because both arms of the conditional produce a map of objects. The true arm produces a single-element map while the second produces the whole original map.
Another option to avoid the inline map construction would be to factor out a per-environment lookup table into a separate local value:
locals {
all_zones = tomap({
a = {
subnet_id = data.aws_cloudformation_export.subnet1.value
}
b = {
subnet_id = data.aws_cloudformation_export.subnet2.value
}
})
env_zones = tomap({
non-prod = tomap({
for k, z in local.all_zones : k => z
if k == "a"
})
})
}
module "batch_server" {
for_each = try(local.env_zones[var.env_name], local.all_zones)
# ...
}
The try function call here means that local.all_zones is the fallback to use if var.env_name doesn't match one of the keys in local.env_zones.
How about a lookup with var.env_name as the key?
variable "env_name" {
type = string
default = "non_prod"
}
locals {
zone_data = {
prod = {
a = {
subnet_id = data.aws_cloudformation_export.subnet1.value
}
b = {
subnet_id = data.aws_cloudformation_export.subnet2.value
}
},
non_prod = {
a = {
subnet_id = data.aws_cloudformation_export.subnet1.value
}
}
}
}
module "batch_server" {
for_each = lookup(local.zone_data, var.env_name)
...
I have a number of Terraform data sources and a locals block created as such
data "aws_subnets" "subs" {
for_each = toset(["a", "b", "c"])
filter {
name = "vpc-id"
values = [data.aws_vpc.vpc.id]
}
filter {
name = "availability-zone"
values = ["${data.aws_region.region.name}${each.key}"]
}
}
data "aws_vpc" "vpc" {
default = false
}
data "aws_region" "region" {}
locals {
ids = [for az in data.aws_subnets.subs : az.ids[1]]
}
And an output block
output "main" {
value = local.ids
}
But when I run a terraform apply I get the error
The given key does not identify an element in this collection value: the given index is greater than or equal to the length of the collection
When I take out the index value [1] from my locals block, I can see the output as
+ main = [
+ [
+ "subnet-1234567f3f5d95987",
+ "subnet-123456797f61f831e",
+ "subnet-123456791ec481316",
],
+ [
+ "subnet-12345674da33e8064",
+ "subnet-12345676030bc7040",
],
+ [],
]
How can I extract a particular subnet ID from this list?
You have to consider two things here:
You are using for_each for the data source, so that means the return result will have key value pairs
The return result for each key will be a list
In order to achieve what you want, you need to change to the following code:
data "aws_subnets" "subs" {
for_each = toset(["a", "b", "c"])
filter {
name = "vpc-id"
values = [data.aws_vpc.vpc.id]
}
filter {
name = "availability-zone"
values = ["${data.aws_region.region.name}${each.key}"]
}
}
data "aws_vpc" "vpc" {
default = false
}
data "aws_region" "region" {}
locals {
ids = values(data.aws_subnets.subs)[*].ids
}
Here, the built-in values [1] function is used to get all the values for all the keys. The return result is also a list, so this will be a list of lists.
[1] https://developer.hashicorp.com/terraform/language/functions/values
I working on a module, provided below, to manage AWS KMS keys via Terraform and I'm using the flatten function but the output I'm getting is empty when I call this module.
Any thought why I'm getting empty output?
module
main.tf
locals {
kms_keys = flatten([
for key, kms_key in var.kms_key_list : [
for index in range(kms_key.key_id) : {
key_id = index
aws_kms_alias = kms_key.alias
is_rotating = kms_key.enable_key_rotation
deletion_window_in_days = kms_key.deletion_window_in_days
is_enabled = kms_key.is_enabled
description = kms_key.description
policy = kms_key.policy
}
]
])
}
resource "aws_kms_key" "main" {
for_each = {
for k, v in local.kms_keys: k => v if v.key_id > 0
}
deletion_window_in_days = each.value.deletion_window_in_days
is_enabled = each.value.is_enabled
enable_key_rotation = each.value.enable_key_rotation
description = each.value.description
policy = each.value.policy
tags = merge({
Name = each.value.aws_kms_alias
}, var.common_tags)
}
resource "aws_kms_alias" "alias" {
for_each = aws_kms_key.main
name = "alias/${each.value.tags.Name}"
target_key_id = each.value.key_id
}
variables.tf
variable "kms_key_list" {
type = map(object({
key_id = number
deletion_window_in_days = number
is_enabled = bool
enable_key_rotation = bool
description = string
policy = string
key_usage = string
customer_master_key_spec = string
alias = string
}))
}
calling the module in main.tf
module "kms_keys" {
source = "../module/kms"
kms_key_list = local.kms_keys
}
kms_keys.tf
locals {
kms_keys = {
name_1 = {
key_id = 1
deletion_window_in_days = 7
is_enabled = true
enable_key_rotation = true
description = "description_1"
policy = ""
key_usage = "ENCRYPT_DECRYPT"
customer_master_key_spec = "SYMMETRIC_DEFAULT"
alias = "alias_1"
}
}
}
TF Plan Output looks like this:
Changes to Outputs:
+ kms_info = {
+ kms_key = {}
}
This seems odd:
for index in range(kms_key.key_id)
This is going to loop through all values from 0 to the key_id value; is that really what you want? To add an entry into kms_keys for each value from 0 to key_id?
I doubt it, because the way you have this coded, if your var.kms_key_list contains a key config with key_id = 10, it's going to create 10 different KMS keys, all with the same configuration values.
Essentially, I'm not understanding the purpose of the nested for loop.
If you can provide samples of:
The input variable, but with a key_id > 1
The output that you expect to see
Then we might be able to help. Also, I don't see any output declared either in the module or in the parent file, so those must be missing; please include them.
I have created Maintenance window and task to start EC2 instance using terraform below is the code:
resource "aws_ssm_maintenance_window_task" "StopTask" {
count = var.Enabled == "true" ? 1 : 0
max_concurrency = "100%"
max_errors = "100%"
name = "StopJumpEc2"
priority = 1
service_role_arn = aws_iam_role.EC2_start_stop.0.arn
task_arn = "AWS-StopEC2Instance"
task_type = "AUTOMATION"
window_id = aws_ssm_maintenance_window.StopWindow.0.id
targets {
key = "WindowTargetIds"
values = [
aws_ssm_maintenance_window_target.WinOsPatches_StopServer_Target.0.id
]
}
task_invocation_parameters {
automation_parameters {
document_version = "$DEFAULT"
parameter { // same result if you remove or add this :(
name = "AutomationAssumeRole"
values = [
"",
]
}
parameter {
name = "InstanceId"
values = [
"{{RESOURCE_ID}}",
]
}
}
}
}
While AWS console shows this task as :
This is ending with error below
the supplied parameters for invoking the specified automation document are incorrect.
What can be issue ?
From the Terraform docs - https://www.terraform.io/docs/providers/aws/r/cloudwatch_event_target.html
I don't see an option to map multiple targets to the same Cloudwatch rule. It only takes an arn field which accepts one resource. I'm trying to map 5 Lambdas to the same Cloudwatch rule. Does Terraform support this?
EDIT: How can I attach only 5 lambdas? If I've created 15 lambdas, I want to attach 5 each to 3 cloudwatch rules.
Got it working! I had to divide the count of the rules by 5 when I assigned targets to rules. This is roughly what it looks like:
resource "aws_cloudwatch_event_rule" "arule" {
count = "${ceil(length(var.lambda_arns) / 5.0)}" // Needs to be 5.0 to force float computation
name = "${var.rule_name}${format("-%d", count.index)}"
is_enabled = true
}
resource "aws_cloudwatch_event_target" "atarget" {
depends_on = ["aws_cloudwatch_event_rule.arule"]
count = "${length(var.lambda_arns)}"
rule = "${aws_cloudwatch_event_rule.arule.*.name[count.index / 5]}"
arn = "${var.lambda_arns[count.index]}"
}
I created the event rules based on the number of lambdas (i.e., if there are 10 lambdas, 2 rules are created).
I created the targets based on number of lambdas (i.e., if there are 10 lambdas, 10 targets are created).
I assigned the targets proportionally among the rules by dividing the count.index by 5 (the same logic used to determine the count of rules).
Assuming you created all your lambas using the same terraform resource with count, you can use count on this as well:
resource "aws_cloudwatch_event_target" "cw_target" {
count = length(aws_lambda_function.my_lambdas)
rule = "${aws_cloudwatch_event_rule.my_rule.name}"
arn = "${aws_lambda_function.my_lambdas.*.arn[count.index]}"
}
Here is what I did. I ignore the "target_id" of resource "aws_cloudwatch_event_target" (very important), and use local variables (define your local vars, this example: "targets"), and looping for the local vars, and creating multiple aws_cloudwatch_event_target, and multiple assessment templates.
locals {
stack_name_prefix = "Inspector"
rules_package_arn_cis = "arn:aws:inspector:ap-southeast-2:454640832652:rulespackage/0-Vkd2Vxjq"
default_target = {
rules : [local.rules_package_arn_cis],
duration : 3600
}
targets = [
merge(local.default_target, {
name : "data_indexer",
tags : {
Function = "LR - DX"
},
}),
merge(local.default_target, {
name : "ai_engine",
tags : {
Function = "LR - AIE"
},
}),
merge(local.default_target, {
name : "data_processor",
tags : {
Function = "LR - Data Processor"
},
}),
merge(local.default_target, {
name : "platform_manager",
tags : {
Function = "LR - PM"
},
})
]
}
resource "aws_inspector_assessment_template" "assessment_template" {
count = length(local.targets)
name = "${local.stack_name_prefix}_${local.targets[count.index]["name"]}_assessment_template"
target_arn = aws_inspector_assessment_target.assessment[count.index].arn
duration = local.default_target.duration
rules_package_arns = [local.rules_package_arn_cis]
}
resource "aws_cloudwatch_event_target" "event_target_for_inspector_assessment_template" {
count = length(local.targets)
rule = aws_cloudwatch_event_rule.event_rule_for_inspector_assessment_template.name
// target_id = "amazon_inspector_assessment" ## Don't USE target_id, it will mess up the cloudwatch event target, and only generated one target instead of 4
arn = aws_inspector_assessment_template.assessment_template[count.index].arn
role_arn = aws_iam_role.inspector_assessment_template.arn
}
module "eventbridgetarget" {
for_each = var.rule_and_target_details
source = "git::ssh://git#bitbucket.int.ally.com/tf/terraform-modules-aws-eventbridge.git//modules/target?ref=v1"
rule_arn = module.eventbridgerule.rule.arn
name = each.value.name
namespace = module.namespace.lower_short_name
tags = module.namespace.tags
#arn = module.lambda.arn
arn = each.value.arn
}
Now in the tfvars pass the value like below:
rule_and_target_details = {
"firsttarget" = {
name = "getentities"
arn = "arn:aws:execute-api:us-east-1:2XXXXXXX:92mchkioeh/api/GET/getEntities"
}
"secondtarget" = {
name = "getactivitylog"
arn = "arn:aws:execute-api:us-east-1:2XXXXXX:92mchkioeh/api/GET/getActivityLog"
}
"thirdtarget" = {
name = "searchactivitylog"
arn = "arn:aws:execute-api:us-east-1:XXXXXX:92mchkioeh/api/GET/searchActivityLog"
}
}