How to create a dynamic map in Terraform? - amazon-web-services

I have created 2 modules. One for SQS and another for SSM. My SQS creates 4 queues and i am trying to create corresponding entries in the parameter store for their url and arn. I am importing the SSM module inside my SQS module such that it creates the parameters right after SQS creation is done.
This is what my SQS module looks like :-
resource "aws_sqs_queue" "FirstStandardSQSQueue" {
name = "sqs-${var.stage}-one"
message_retention_seconds = var.SQSMsgRetention
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.FirstDeadLetterQueue.arn
maxReceiveCount = 2
})
}
resource "aws_sqs_queue" "FirstDeadLetterQueue" {
name = "sqs-dead-letter-${var.stage}-one"
receive_wait_time_seconds = var.SQSRecvMsgWaitTime
}
resource "aws_sqs_queue" "SecondStandardSQSQueue" {
name = "sqs-${var.stage}-two"
message_retention_seconds = var.SQSMsgRetention
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.SecondDeadLetterQueue.arn
maxReceiveCount = 3
})
}
resource "aws_sqs_queue" "SecondDeadLetterQueue" {
name = "sqs-dead-letter-${var.stage}-two"
receive_wait_time_seconds = var.SQSRecvMsgWaitTime
}
module "ssm" {
source = "../ssm/"
// How do i create a dynamic map???
for_each = var.Queues
name = "/new/${var.stage}/${each.key}"
type = "SecureString"
value = "${each.value}"
}
My SSM module looks like this :-
resource "aws_ssm_parameter" "main" {
name = var.name
type = var.type
value = var.value
}
I am trying to create either a map or somehow dynamically be able to pass values in my ssm module using for_each? I tried setting up something like this :-
variable "Queues" {
type = map
default = {
"FirstStandardSQSQueueUrl" = aws_sqs_queue.FirstStandardSQSQueue.url
"FirstStandardSQSQueueArn" = aws_sqs_queue.FirstStandardSQSQueue.arn
"SecondStandardSQSQueueUrl" = aws_sqs_queue.SecondStandardSQSQueue.url
"SecondStandardSQSQueueArn" = aws_sqs_queue.SecondStandardSQSQueue.arn
}
}
But this is invalid because i keep running into
Error: Variables not allowed line 40: Variables may not be used here.
Can someone suggest a better/right way to do it? Thank you.

As the error msg writes, you can't have dynamic variables. locals should be used instead.
locals {
Queues = {
"FirstStandardSQSQueueUrl" = aws_sqs_queue.FirstStandardSQSQueue.url
"FirstStandardSQSQueueArn" = aws_sqs_queue.FirstStandardSQSQueue.arn
"SecondStandardSQSQueueUrl" = aws_sqs_queue.SecondStandardSQSQueue.url
"SecondStandardSQSQueueArn" = aws_sqs_queue.SecondStandardSQSQueue.arn
}
}
Then you refer to the value as local.Queues in other parts of your code.

Related

Use attributes in a tfvars file

I have a file fsx.tf with 2 resource blocks and a file prod.tfvars:
My fsx.tf looks like:
resource "aws_fsx_ontap_storage_virtual_machine" "fsx_svm" {
for_each = var.fsx_svm
file_system_id = aws_fsx_ontap_file_system.fsx.id
name = each.value.name
}
resource "aws_fsx_ontap_volume" "fsx_volumes" {
for_each = var.fsx_volumes
name = var.name
storage_virtual_machine_id = each.value.storage_virtual_machine_id
My prod.tfvars looks like:
fsx_svm = {
svm01 = {
name = "svm01-single-az"
}
}
fsx_volumes = {
vol01 = {
name = "FS"
storage_virtual_machine_id = fsx_svm.svm01.id
}
}
I get the following error:
Error: expected length of storage_virtual_machine_id to be in the range (21 - 21), got fsx_svm.svm01.id
OR
Variables not allowed here
How to set the attribute id of resource aws_fsx_ontap_storage_virtual_machine in the variable of fsx_volumes ? My goal is to be able to reuse the resource block for other .tfvars files.
In this case the best way to do this may be resource chaining with for_each [1]. This means instead of having to rely on variables you can just do this:
resource "aws_fsx_ontap_storage_virtual_machine" "fsx_svm" {
for_each = var.fsx_svm
file_system_id = aws_fsx_ontap_file_system.fsx.id
name = each.value.name
}
resource "aws_fsx_ontap_volume" "fsx_volumes" {
for_each = aws_fsx_ontap_storage_virtual_machine.fsx_svm
name = "${each.key}-${var.name}"
storage_virtual_machine_id = each.value.id
}
[1] https://developer.hashicorp.com/terraform/language/meta-arguments/for_each#chaining-for_each-between-resources

Local variables inside map variables

I am trying to use a map variable(which has 2 lambda names). Also, I want to pass a local variable inside the key values, as shown in the example below.However, I get an error as variable not allowed here. Any suggestions/advice?
variables.tf:
variable "lambdas" {
type = map(string)
default = {
"lambda1_name-${local.global_suffix}" = "lambda_function1",
"lambda2_name-${local.global_suffix}" = "lambda_function2"
}
}
locals{
global_suffix = "${var.env}-${var.project}${var.branch_hash}"
}
main.tf:
resource "aws_lambda_function" "main" {
for_each = var.lambdas
function_name = each.key
handler = "${each.value}.${var.handler}"
filename = "${path.module}/modules/lambda-main/${each.value}.zip"
source_code_hash = data.archive_file.init[each.key].output_base64sha256
role = module.lambda_iam_role.arn
runtime = "python3.6"
memory_size = "2048"
timeout = "900"
tags = local.tags
description = "${var.project} Lambda Function"
}
I am trying to use one lambda resource block to create 2 lambda functions(hence using the map variable)
Your variable default values can't by dynamic. They must be static values. Thus, instead of having var.lambdas, in your case it would be better to use locals:
variable "lambdas" {
type = map(string)
default = {
"lambda1_name" = "lambda_function1",
"lambda2_name" = "lambda_function2"
}
}
locals {
lambdas = {for key, value in var.lambdas: "${key}-${local.global_suffix}" => value}
}
Them you would:'
resource "aws_lambda_function" "main" {
for_each = local.lambdas
....
}

AWS Terraform passing of multiple arn for Cloudfront function_association

I like to do a dynamic function_assocation to an AWS CloudFront resource. Instead of defining each function, I did something like the below.
resource "aws_cloudfront_function" "functions" {
for_each = var.cf_lambda_functions
name = each.value.name
comment = each.value.description
runtime = each.value.runtime
publish = each.value.publish
code = each.value.code
}
and for the function_association, I did something like the below.
dynamic "function_association" {
for_each = aws_cloudfront_function.functions
content {
event_type = "viewer-request"
function_arn = each.value.arn
}
}
this gives me an error: each.value cannot be used in this context. How do you do this by passing multiple ARN of functions?
In dynamic blocks you can't use each. Instead it should be function_association in your case:
dynamic "function_association" {
for_each = aws_cloudfront_function.functions
content {
event_type = "viewer-request"
function_arn = function_association.value.arn
}
}

How to define aws_cloudwatch_metric_alarm with multiple actions with terraform?

I used the AWS Console to set up a Cloud Watch alarm that will send my team a slack notification and reboot the affected EC2 instance, when the EC2 instance fails a StatusCheck fails.
Here's what the actions look like in the AWS console:
Now I want to write a terraform module that will set this up for me. Here is what I have so far:
cloudwatch_metric_alarm/main.tf
locals {
name = format("%s_ec2-instance-down-alarm", var.name_prefix)
}
resource "aws_cloudwatch_metric_alarm" "ec2-instance-alarm" {
name = local.name
description = var.alarm_description
schedule_expression = var.schedule_expression
tags = merge(map("Name", local.name), var.extra_tags)
alarm_name = local.name
comparison_operator = var.comparison_operator
evaluation_periods = var.evaluation_periods
namespace = var.namespace
metric_name = var.metric_name
period = var.period
statistic = var.statistic
threshold = var.threshold
dimensions = {
InstanceId = var.instance_id
}
alarm_actions = [var.alarm_actions]
}
cloudwatch_metric_alarm/variables.tf
variable "extra_tags" {
type = map
default = {}
}
variable "name_prefix" {
type = string
}
variable "comparison_operator" {
type = string
default = "GreaterThanOrEqualToThreshold"
}
variable "evaluation_periods" {
type = number
default = 1
}
variable "namespace" {
type = string
default = "AWS/EC2"
}
variable "metric_name" {
type = string
default = "StatusCheckFailed"
}
variable "period" {
type = string
default = "60"
}
variable "statistic" {
type = string
default = "Average"
}
variable "threshold" {
type = string
default = "1"
}
variable "instance_id" {
type = string
}
variable "alarm_actions" {
type = list(string)
}
variable "alarm_description" {
type = string
default = "This alarm will send a slack notification if the bastion host because unresponsive."
}
My question what do I pass in for alarm_actions in my parent module. Here's the pertinent of the parent main.tf:
... other stuff ...
module "my-cloudwatch-metric-alarm" {
source = "./cloudwatch_metric_alarm"
alarm_actions = [???]
}
... other stuff ...
I have no idea what I am suppose to pass in as the alarm_actions.
What am I suppose to pass in?
Thanks!
UPDATE and NOTE to Self.
I was getting the error message
Error: Creating metric alarm failed: ValidationError: Invalid use of EC2 action. An EC2 action can only be used if the alarm is monitoring an EC2 instance.
The cause of this error was this line were I misspelled InstanceId:
InstatnceId = var.instance_id
First, your var.alarm_actions is already a list, so adding brackets around it like [var.alarm_actions] is going to create a list of lists, which is going to cause errors. You just need to pass it directly to the resource like:
alarm_actions = var.alarm_actions
For the first value you need to pass in that list, you need the ARN of the SNS topic you want to send the notifications to. You would find that in the AWS SNS console. If Terraform is managing the SNS topic for you, then you should have access to the topic ARN in Terraform already. Alternatively you could look it up via a datasource by topic name.
For the second value, it is a special ARN that indicates to CloudWatch to reboot the instance being monitored. It looks like this: arn:aws:automate:<region>:ec2:reboot. For example if your infrastructure is in us-east-1 then the ARN would be arn:aws:automate:us-east-1:ec2:reboot. You could construct that dynamically in your Terraform code based on the region Terraform is deploying to by using the aws_region datasource in the Terraform AWS provider.
Your final code may look something like this:
data "aws_sns_topic" "alerts" {
name = "Your Topic Name"
}
data "aws_region" "current" {}
module "my-cloudwatch-metric-alarm" {
source = "./cloudwatch_metric_alarm"
alarm_actions = [
data.aws_sns_topic.alerts.arn,
"arn:aws:automate:${data.aws_region.current.name}:ec2:reboot"
]
}

Terraform create a Cloudwatch rule with multiple targets

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"
}
}