A question about Terraform AWS Module EventBridge - amazon-web-services

This is an example of an official document,
https://registry.terraform.io/modules/terraform-aws-modules/eventbridge/aws/1.14.2
One of the use cases looks like this
EventBridge with schedule rule and Lambda target
module "eventbridge" {
source = "terraform-aws-modules/eventbridge/aws"
create_bus = false
rules = {
crons = {
description = "Trigger for a Lambda"
schedule_expression = "rate(5 minutes)"
}
}
targets = {
crons = [
{
name = "lambda-loves-cron"
arn = "arn:aws:lambda:ap-southeast-1:135367859851:function:resolved-penguin-lambda"
input = jsonencode({"job": "cron-by-rate"})
}
]
}
}
What does "input" above mean and what needs to be input?

The input value is passed to the Lambda function when it's invoked.
Its value is up to you (and could be empty); it depends what your Lambda function does.

Related

Terraform AWS: SQS destination for Lambda doesn't get added

I have a working AWS project that I'm trying to implement in Terraform.
One of the steps requires a lambda function to query athena and return results to SQS (I am using this module for lambda instead of the original resource). Here is the code:
data "archive_file" "go_package" {
type = "zip"
source_file = "./report_to_SQS_go/main"
output_path = "./report_to_SQS_go/main.zip"
}
resource "aws_sqs_queue" "emails_queue" {
name = "sendEmails_tf"
}
module "lambda_report_to_sqs" {
source = "terraform-aws-modules/lambda/aws"
function_name = "report_to_SQS_Go_tf"
handler = "main"
runtime = "go1.x"
create_package = false
local_existing_package = "./report_to_SQS_go/main.zip"
attach_policy_json = true
policy_json = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect : "Allow"
Action : [
"dynamodb:*",
"lambda:*",
"logs:*",
"athena:*",
"cloudwatch:*",
"s3:*",
"sqs:*"
]
Resource : ["*"]
}
]
})
destination_on_success = aws_sqs_queue.emails_queue.arn
timeout = 200
memory_size = 1024
}
The code works fine and produces the desired output; however, the problem is, SQS doesn't show up as a destination (although the Queue shows up in SQS normally and can send/recieve messages).
I don't think permissions are the problem because I can add SQS destinations manually from the console successfully.
The variable destination_on_success is only used if you also set create_async_event_config as true. Below is extracted from https://github.com/terraform-aws-modules/terraform-aws-lambda/blob/master
variables.tf
############################
# Lambda Async Event Config
############################
variable "create_async_event_config" {
description = "Controls whether async event configuration for Lambda Function/Alias should be created"
type = bool
default = false
}
variable "create_current_version_async_event_config" {
description = "Whether to allow async event configuration on current version of Lambda Function (this will revoke permissions from previous version because Terraform manages only current resources)"
type = bool
default = true
}
.....
variable "destination_on_failure" {
description = "Amazon Resource Name (ARN) of the destination resource for failed asynchronous invocations"
type = string
default = null
}
variable "destination_on_success" {
description = "Amazon Resource Name (ARN) of the destination resource for successful asynchronous invocations"
type = string
default = null
}
main.tf
resource "aws_lambda_function_event_invoke_config" "this" {
for_each = { for k, v in local.qualifiers : k => v if v != null && local.create && var.create_function && !var.create_layer && var.create_async_event_config }
function_name = aws_lambda_function.this[0].function_name
qualifier = each.key == "current_version" ? aws_lambda_function.this[0].version : null
maximum_event_age_in_seconds = var.maximum_event_age_in_seconds
maximum_retry_attempts = var.maximum_retry_attempts
dynamic "destination_config" {
for_each = var.destination_on_failure != null || var.destination_on_success != null ? [true] : []
content {
dynamic "on_failure" {
for_each = var.destination_on_failure != null ? [true] : []
content {
destination = var.destination_on_failure
}
}
dynamic "on_success" {
for_each = var.destination_on_success != null ? [true] : []
content {
destination = var.destination_on_success
}
}
}
}
}
So the destination_on_success is only used in this resource and this resources is only invoked if several conditions are met. The key one being var.create_async_event_config must be true.
You can see the example for this here https://github.com/terraform-aws-modules/terraform-aws-lambda/blob/be6cf9701071bf807cd7864fbcc751ed2552e434/examples/async/main.tf
module "lambda_function" {
source = "../../"
function_name = "${random_pet.this.id}-lambda-async"
handler = "index.lambda_handler"
runtime = "python3.8"
architectures = ["arm64"]
source_path = "${path.module}/../fixtures/python3.8-app1"
create_async_event_config = true
attach_async_event_policy = true
maximum_event_age_in_seconds = 100
maximum_retry_attempts = 1
destination_on_failure = aws_sns_topic.async.arn
destination_on_success = aws_sqs_queue.async.arn
}

Change eventbridge cron rule name in terraform

I have a lambda that I trigger with an EventBridge.
I have allowed_triggers in my lambda_function:
allowed_triggers = {
"RunDaily" = {
principal = "events.amazonaws.com"
source_arn = module.eventbridge.eventbridge_rule_arns["crons"]
}
}
And I have an eventbridge module:
module "eventbridge" {
source = "terraform-aws-modules/eventbridge/aws"
version = "1.14.0"
create_bus = false
create_role = false
create_rules = true
rules = {
crons = {
description = "deafault"
schedule_expression = "rate(1 day)"
}
}
targets = {
crons = [
{
arn = module.lambda_function.lambda_function_arn
input = jsonencode({ "job" : "crons" })
}
]
}
}
Now, this works great, as the rule is created and attached properly.
But when I want to change the name of the rule along with its description, terraform pickups only the description change:
module "eventbridge" {
...
rules = {
crons = {
description = "My custom cron rule"
schedule_expression = "rate(1 day)"
}
}
targets = {
crons = [
{
name = "my-custom-cron-rule-name"
arn = module.lambda_function.lambda_function_arn
input = jsonencode({ "job" : "crons" })
}
]
}
}
Plan:
Terraform will perform the following actions:
# module.eventbridge.aws_cloudwatch_event_rule.this["crons"] will be updated in-place
~ resource "aws_cloudwatch_event_rule" "this" {
~ description = "deafault" -> "My custom cron rule"
id = "crons-rule"
name = "crons-rule"
tags = {
"Name" = "crons-rule"
}
# (5 unchanged attributes hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Question: How do I change the name attribute for an eventbridge rule?
As per the module definition [1], the aws_cloudwatch_event_rule name is derived from value of the key of the rules block, i.e.:
rules = {
crons = {
description = "My custom cron rule"
schedule_expression = "rate(1 day)"
}
}
Based on the lines from the GitHub repo, the name is formed with:
locals {
eventbridge_rules = flatten([
for index, rule in var.rules :
merge(rule, {
"name" = index
"Name" = "${replace(index, "_", "-")}-rule"
})
])
... # rest of locals goes here
}
If you take a look at your definition and this part of code, you can see that the name will be crons-rule, which is visible in both the name and tags.Name arguments:
name = "crons-rule"
tags = {
"Name" = "crons-rule"
}
So in order to change the name of the rule, you would have to change the key of the rules block, i.e.:
rules = {
very-nice-new-crons = { # <----- here is where the change should be made
description = "My custom cron rule"
schedule_expression = "rate(1 day)"
}
}
You can verify this by looking at [2]:
resource "aws_cloudwatch_event_rule" "this" {
for_each = { for k, v in local.eventbridge_rules : v.name => v if var.create && var.create_rules }
name = each.value.Name
...
tags = merge(var.tags, {
Name = each.value.Name
})
}
EDIT: As pointed out, there are two more changes that need to be made after the name is changed:
The allowed_triggers of the Lambda function should now use the new key to reference the event rule that is allowed to trigger it. It has to be changed from
source_arn = module.eventbridge.eventbridge_rule_arns["crons"]
to
source_arn = module.eventbridge.eventbridge_rule_arns["very-nice-new-crons"]
The same name change has to be used in the targets block as well, i.e., the crons key in the targets has to be replaced with the same key name as in the rules block:
targets = {
very-nice-new-crons = [
{
name = "my-custom-cron-rule-name"
arn = module.lambda_function.lambda_function_arn
input = jsonencode({ "job" : "crons" })
}
]
}
[1] https://github.com/terraform-aws-modules/terraform-aws-eventbridge/blob/master/main.tf#L2-L6
[2] https://github.com/terraform-aws-modules/terraform-aws-eventbridge/blob/master/main.tf#L44

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

Why isn't my Event Bridge rule executing my Lambda function?

I am trying to create an Event Bridge rule that will run my Lambda function every 30 mins.
I based my code on this answer I found here on SO Use terraform to set up a lambda function triggered by a scheduled event source
Here is my terraform code:
monitoring/main.tf:
...
module "cloudwatch_event_rule" {
source = "./cloudwatch_event_rule"
extra_tags = local.extra_tags
}
module "lambda_function" {
source = "./lambda_functions"
extra_tags = local.extra_tags
alb_names = var.alb_names
slack_webhook_url = var.slack_webhook_url
environment_tag = local.environment_tag
}
module "cloudwatch_event_target" {
source = "./cloudwatch_event_target"
lambda_function_arn = module.lambda_function.detect_bad_rejects_on_alb_lambda_arn
cloudwatch_event_rule_name = module.cloudwatch_event_rule.cloudwatch_event_rule_name
extra_tags = local.extra_tags
}
monitoring/lambda_functions/main.tf:
resource "aws_lambda_function" "detect_bad_rejects_on_alb" {
filename = var.filename
function_name = var.function_name
role = aws_iam_role.detect_bad_reject_on_alb.arn
handler = var.handler
source_code_hash = filebase64sha256(var.filename)
runtime = var.runtime
timeout = var.timeout
environment {
...
}
}
monitoring/cloudwatch_event_rule/main.tf
resource "aws_cloudwatch_event_rule" "event_rule" {
name = var.rule_name
description = var.description
schedule_expression = var.schedule_expression
tags = ...
}
monitoring/cloudwatch_event_rule/variables.tf
...
variable "schedule_expression" {
type = string
default = "rate(30 minutes)"
}
...
monitoring/cloudwatch_event_target/main.tf
resource "aws_cloudwatch_event_target" "event_target" {
arn = var.lambda_function_arn
rule = var.cloudwatch_event_rule_name
input = var.input
}
This ends up creating the lambda function and the event bridge rule with my lambda function as its target with the schedule expression "rate(30 minutes)" but the lambda function is never executed? What am I doing wrong?
From what you posted is seems that you are not adding permissions for invocations. Your code does not show creation of aws_lambda_permission with proper rules. So you should add such permissions so that EventBridge can invoke your function (example):
resource "aws_lambda_permission" "event-invoke" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = var.function_name
principal = "events.amazonaws.com"
source_arn = module.cloudwatch_event_rule.cloudwatch_event_rule_arn
}
Make sure source_arn correctly points to the ARN of your event rule.

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