diff --git a/.github/workflows/commit-to-pr.yaml b/.github/workflows/commit-to-pr.yaml index 2bd9dc0..aaf2a09 100644 --- a/.github/workflows/commit-to-pr.yaml +++ b/.github/workflows/commit-to-pr.yaml @@ -20,7 +20,7 @@ jobs: validation: strategy: matrix: - folder: ["add", "folders", "here"] + folder: ["./", "./examples/lambda_target", "./examples/state_machine_target"] name: Terraform validate for ${{ matrix.folder }} runs-on: ubuntu-20.04 steps: @@ -41,7 +41,7 @@ jobs: linting: strategy: matrix: - folder: ["add", "folders", "here"] + folder: ["./", "./examples/lambda_target", "./examples/state_machine_target"] name: Terraform lint for ${{ matrix.folder }} runs-on: ubuntu-20.04 steps: @@ -59,7 +59,7 @@ jobs: plan: strategy: matrix: - folder: ["add", "folders", "here"] + folder: ["./examples/lambda_target", "./examples/state_machine_target"] name: Terraform plan for ${{ matrix.folder }} runs-on: ubuntu-20.04 needs: [validation, linting] diff --git a/.terraform-version b/.terraform-version new file mode 100644 index 0000000..8e03717 --- /dev/null +++ b/.terraform-version @@ -0,0 +1 @@ +1.5.1 \ No newline at end of file diff --git a/README.md b/README.md index ad4a304..185c6af 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,92 @@ The below documentation is intended to assist users in utilising the module, the the module itself, and the [examples](#examples) section which has examples of how to utilise the module. +## Requirements +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | ~> 1.5.0 | +| [aws](#requirement\_aws) | ~> 5.27.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | 5.67.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [rule](#module\_rule) | ./modules/rule | n/a | +| [target](#module\_target) | ./modules/target | n/a | + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_policy.invoke_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.invoke_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.invoke_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_permission.allow_lambda_execution_from_event_bridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [aws_iam_policy_document.allow_event_bridge_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.event_bridge_target_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [application\_name](#input\_application\_name) | Name of the application utilising resource. | `string` | n/a | yes | +| [environment](#input\_environment) | Which environment this is being instantiated in. | `string` | n/a | yes | +| [raw\_event\_bridge\_rules](#input\_raw\_event\_bridge\_rules) | Data structure
---------------
A list of dictionaries, where each dictionary has the following attributes:

REQUIRED
---------
- suffix : Friendly name for the rule in Event Bridge
- description : A friendly description of what the Event Bridge rule does
- targets : A list of dictionaries with the following attributes, defining what target this event triggers:
-- name : A friendly name for the target, if lambda this should be the lambda name
-- arn : The ARN of the resource being targeted
MUTUALLY EXCLUSIVE TARGETS INPUTS:
-- input : OPTIONAL JSON string of input to pass to target, defaults to null
-- input\_path : OPTIONAL value of the JSONPath that is used for extracting part of the matched event when passing it to the target, defaults to null.
-- input\_transformer : OPTIONAL parameters used when you are providing a custom input to a target based on certain event data, defaults to null.

One of the following, but not both:
- schedule : The scheduling expression. For example, cron(0 20 * * ? *) or rate(5 minutes)
- pattern : Pattern for the event to match on, should be jsonencoded dictionary

OPTIONAL
---------
By default we deploy event bridge rules as disabled, and ignore state on apply, such that
enabling/disabling event bridge rules is always a manual affair rather than doing via Terraform. But via the below
optional values this may be changed on a per-rule basis.

- state : By default DISABLED, can set to ENABLED or ENABLED\_WITH\_ALL\_CLOUDTRAIL\_MANAGEMENT\_EVENTS
- ignore\_state : By default true, can set to false.


IAM role Statement and Role Suffix to be used for this target when the rule is triggered.
Required if ecs\_target is used or target in arn is EC2 instance, Kinesis data stream, Step Functions state machine,
or Event Bus in different account or region.
- iam\_role\_suffix : IAM role suffix for the event bridge Role having permission to invoke target AWS Service
- iam\_policy\_statements : A list of dictionaries where each dictionary is an IAM statement defining Event Bridge permissions
-- conditions : An OPTIONAL list of dictionaries, which each defines:
--- test : Test condition for limiting the action
--- variable : Value to test
--- values : A list of strings, denoting what to test for |
list(
object({
suffix = string,
description = string,
targets = optional(list(
object({
name = string,
arn = string,
input = optional(string, null)
input_path = optional(string, null)
input_transformer = optional(object({
input_template = string,
input_paths = optional(map(any), null)
}), null)
})), null),
schedule = optional(string, null),
pattern = optional(string, null),
iam_role_suffix = optional(string, ""),
iam_policy_statements = optional(list(
object({
sid = string,
actions = list(string),
resources = list(string),
conditions = optional(list(
object({
test : string,
variable : string,
values = list(string)
})
), [])
})), []),
state = optional(string, "DISABLED"),
ignore_state = optional(bool, true)
})
)
| n/a | yes | + +## Outputs + +No outputs. ## Data structure - +``` +Data structure +--------------- +A list of dictionaries, where each dictionary has the following attributes: + +REQUIRED +--------- +- suffix : Friendly name for the rule in Event Bridge +- description : A friendly description of what the Event Bridge rule does +- targets : A list of dictionaries with the following attributes, defining what target this event triggers: +-- name : A friendly name for the target, if lambda this should be the lambda name +-- arn : The ARN of the resource being targeted +MUTUALLY EXCLUSIVE TARGETS INPUTS: +-- input : OPTIONAL JSON string of input to pass to target, defaults to null +-- input_path : OPTIONAL value of the JSONPath that is used for extracting part of the matched event when passing it to the target, defaults to null. +-- input_transformer : OPTIONAL parameters used when you are providing a custom input to a target based on certain event data, defaults to null. + +One of the following, but not both: +- schedule : The scheduling expression. For example, cron(0 20 * * ? *) or rate(5 minutes) +- pattern : Pattern for the event to match on, should be jsonencoded dictionary + +OPTIONAL +--------- +By default we deploy event bridge rules as disabled, and ignore state on apply, such that +enabling/disabling event bridge rules is always a manual affair rather than doing via Terraform. But via the below +optional values this may be changed on a per-rule basis. + +- state : By default DISABLED, can set to ENABLED or ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS +- ignore_state : By default true, can set to false. + + +IAM role Statement and Role Suffix to be used for this target when the rule is triggered. +Required if ecs_target is used or target in arn is EC2 instance, Kinesis data stream, Step Functions state machine, +or Event Bus in different account or region. +- iam_role_suffix : IAM role suffix for the event bridge Role having permission to invoke target AWS Service +- iam_policy_statements : A list of dictionaries where each dictionary is an IAM statement defining Event Bridge permissions +-- conditions : An OPTIONAL list of dictionaries, which each defines: +--- test : Test condition for limiting the action +--- variable : Value to test +--- values : A list of strings, denoting what to test for +``` ## Examples See `examples` folder for an example setup. diff --git a/aws_iam_policy_document.tf b/aws_iam_policy_document.tf new file mode 100644 index 0000000..0996e7f --- /dev/null +++ b/aws_iam_policy_document.tf @@ -0,0 +1,42 @@ +locals { + actual_iam_policy_documents = { + for rule in var.raw_event_bridge_rules : + rule.suffix => { + statements = rule.iam_policy_statements + } if length(rule.iam_policy_statements) > 0 + } +} + +data "aws_iam_policy_document" "event_bridge_target_policy" { + for_each = local.actual_iam_policy_documents + + dynamic "statement" { + for_each = each.value["statements"] + + content { + sid = statement.value["sid"] + actions = statement.value["actions"] + resources = statement.value["resources"] + + dynamic "condition" { + for_each = statement.value["conditions"] + + content { + test = condition.value["test"] + variable = condition.value["variable"] + values = condition.value["values"] + } + } + } + } +} + +data "aws_iam_policy_document" "allow_event_bridge_assume" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["events.amazonaws.com"] + } + } +} \ No newline at end of file diff --git a/event_bridge_rules.tf b/event_bridge_rules.tf new file mode 100644 index 0000000..b0e94d2 --- /dev/null +++ b/event_bridge_rules.tf @@ -0,0 +1,13 @@ +module "rule" { + source = "./modules/rule" + for_each = { for rule in var.raw_event_bridge_rules : rule.suffix => rule } + + environment = var.environment + application_name = var.application_name + event_name_suffix = each.value["suffix"] + event_description = each.value["description"] + event_schedule = each.value["schedule"] + event_pattern = each.value["pattern"] + state = each.value["state"] + ignore_state = each.value["ignore_state"] +} \ No newline at end of file diff --git a/event_bridge_target.tf b/event_bridge_target.tf new file mode 100644 index 0000000..a5e74d8 --- /dev/null +++ b/event_bridge_target.tf @@ -0,0 +1,37 @@ +locals { + event_bridge_targets = flatten([ + for rule in var.raw_event_bridge_rules : [ + for target in rule.targets : { + identifier = format("%s/%s", rule.suffix, target.name), + event_rule : module.rule[rule.suffix].name + event_target : target.name + event_target_arn : target.arn + event_target_role_arn : try(aws_iam_role.invoke_role[rule.suffix].arn, null) + event_target_input : target.input + event_target_input_path : target.input_path + event_target_input_transformer : target.input_transformer + } + ] + ]) +} + +module "target" { + source = "./modules/target" + + for_each = { for target in local.event_bridge_targets : target.identifier => target } + + event_rule = each.value["event_rule"] + event_target = each.value["event_target"] + event_target_arn = each.value["event_target_arn"] + event_target_role_arn = each.value["event_target_role_arn"] + event_target_input = each.value["event_target_input"] + event_target_input_path = each.value["event_target_input_path"] + event_target_input_transformer = each.value["event_target_input_transformer"] + + + depends_on = [ + module.rule, + data.aws_iam_policy_document.event_bridge_target_policy, + data.aws_iam_policy_document.allow_event_bridge_assume + ] +} \ No newline at end of file diff --git a/examples/lambda_target/.terraform-version b/examples/lambda_target/.terraform-version new file mode 100644 index 0000000..8e03717 --- /dev/null +++ b/examples/lambda_target/.terraform-version @@ -0,0 +1 @@ +1.5.1 \ No newline at end of file diff --git a/examples/lambda_target/data.tf b/examples/lambda_target/data.tf new file mode 100644 index 0000000..7ae4bae --- /dev/null +++ b/examples/lambda_target/data.tf @@ -0,0 +1,5 @@ +# Get current region +data "aws_region" "current_region" {} + +# Retrieve the current AWS Account info +data "aws_caller_identity" "current_account" {} \ No newline at end of file diff --git a/examples/lambda_target/locals.tf b/examples/lambda_target/locals.tf new file mode 100644 index 0000000..a90d921 --- /dev/null +++ b/examples/lambda_target/locals.tf @@ -0,0 +1,84 @@ +/* +Data structure +--------------- +A list of dictionaries, where each dictionary has the following attributes: + +REQUIRED +--------- +- suffix : Friendly name for the rule in Event Bridge +- description : A friendly description of what the Event Bridge rule does +- targets : A list of dictionaries with the following attributes, defining what target this event triggers: +-- name : A friendly name for the target, if lambda this should be the lambda name +-- arn : The ARN of the resource being targeted +MUTUALLY EXCLUSIVE TARGETS INPUTS: +-- input : OPTIONAL JSON string of input to pass to target, defaults to null +-- input_path : OPTIONAL value of the JSONPath that is used for extracting part of the matched event when passing it to the target, defaults to null. +-- input_transformer : OPTIONAL parameters used when you are providing a custom input to a target based on certain event data, defaults to null. + +One of the following, but not both: +- schedule : The scheduling expression. For example, cron(0 20 * * ? *) or rate(5 minutes) +- pattern : Pattern for the event to match on, should be jsonencoded dictionary + +OPTIONAL +--------- +By default we deploy event bridge rules as disabled, and ignore state on apply, such that +enabling/disabling event bridge rules is always a manual affair rather than doing via Terraform. But via the below +optional values this may be changed on a per-rule basis. + +- state : By default DISABLED, can set to ENABLED or ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS +- ignore_state : By default true, can set to false. + + +IAM role Statement and Role Suffix to be used for this target when the rule is triggered. +Required if ecs_target is used or target in arn is EC2 instance, Kinesis data stream, Step Functions state machine, +or Event Bus in different account or region. +- iam_role_suffix : IAM role suffix for the event bridge Role having permission to invoke target AWS Service +- iam_policy_statements : A list of dictionaries where each dictionary is an IAM statement defining Event Bridge permissions +-- conditions : An OPTIONAL list of dictionaries, which each defines: +--- test : Test condition for limiting the action +--- variable : Value to test +--- values : A list of strings, denoting what to test for +*/ + +locals { + raw_event_bridge_rules = [ + # Rule is enabled and its state is managed with Terraform + { + suffix = "sagemaker-promotion" + description = "Trigger SageMaker model promotion when package state changes" + state = "ENABLED" + ignore_state = "false" + targets = [ + { + name = "my-promotion-lambda" + arn = "arn:aws:lambda:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:function:my-promotion-lambda" + } + ] + pattern = jsonencode({ + source = ["aws.sagemaker"] + detail-type = ["SageMaker Model Package State Change"] + detail = { + "ModelPackageGroupName" : [ + { + "exists" : true + } + ] + } + }) + }, + # Rule is disabled, by state is not managed by Terraform, thus it may be enabled/disabled in the account + # manually by individuals + { + suffix = "hourly-healthcheck" + description = "EventBridge Schedule Rule to trigger hourly healthcheck lambda" + schedule = "cron(0 0 * * ? *)" + targets = [ + { + name = "hourly-healthcheck-lambda" + arn = "arn:aws:lambda:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:function:hourly-healthcheck-lambda" + } + ] + iam_role_suffix = "healthcheck" + }, + ] +} \ No newline at end of file diff --git a/examples/lambda_target/main.tf b/examples/lambda_target/main.tf new file mode 100644 index 0000000..6c70eaf --- /dev/null +++ b/examples/lambda_target/main.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.27.0" + } + } + required_version = "~> 1.5.0" +} + +provider "aws" { + region = "eu-west-2" +} + +module "event_bridge" { + source = "github.com/sudoblark/sudoblark.terraform.module.aws.event_bridge_rule?ref=1.0.0" + + application_name = var.application_name + environment = var.environment + raw_event_bridge_rules = local.raw_event_bridge_rules +} \ No newline at end of file diff --git a/examples/lambda_target/variables.tf b/examples/lambda_target/variables.tf new file mode 100644 index 0000000..9ccf4b8 --- /dev/null +++ b/examples/lambda_target/variables.tf @@ -0,0 +1,15 @@ +variable "environment" { + description = "Which environment this is being instantiated in." + type = string + validation { + condition = contains(["dev", "test", "prod"], var.environment) + error_message = "Must be either dev, test or prod" + } + default = "prod" +} + +variable "application_name" { + description = "Name of the application utilising the resource resource." + type = string + default = "demo-app" +} \ No newline at end of file diff --git a/examples/state_machine_target/.terraform-version b/examples/state_machine_target/.terraform-version new file mode 100644 index 0000000..8e03717 --- /dev/null +++ b/examples/state_machine_target/.terraform-version @@ -0,0 +1 @@ +1.5.1 \ No newline at end of file diff --git a/examples/state_machine_target/data.tf b/examples/state_machine_target/data.tf new file mode 100644 index 0000000..7ae4bae --- /dev/null +++ b/examples/state_machine_target/data.tf @@ -0,0 +1,5 @@ +# Get current region +data "aws_region" "current_region" {} + +# Retrieve the current AWS Account info +data "aws_caller_identity" "current_account" {} \ No newline at end of file diff --git a/examples/state_machine_target/locals.tf b/examples/state_machine_target/locals.tf new file mode 100644 index 0000000..3f14266 --- /dev/null +++ b/examples/state_machine_target/locals.tf @@ -0,0 +1,94 @@ +/* +Data structure +--------------- +A list of dictionaries, where each dictionary has the following attributes: + +REQUIRED +--------- +- suffix : Friendly name for the rule in Event Bridge +- description : A friendly description of what the Event Bridge rule does +- targets : A list of dictionaries with the following attributes, defining what target this event triggers: +-- name : A friendly name for the target, if lambda this should be the lambda name +-- arn : The ARN of the resource being targeted +MUTUALLY EXCLUSIVE TARGETS INPUTS: +-- input : OPTIONAL JSON string of input to pass to target, defaults to null +-- input_path : OPTIONAL value of the JSONPath that is used for extracting part of the matched event when passing it to the target, defaults to null. +-- input_transformer : OPTIONAL parameters used when you are providing a custom input to a target based on certain event data, defaults to null. + +One of the following, but not both: +- schedule : The scheduling expression. For example, cron(0 20 * * ? *) or rate(5 minutes) +- pattern : Pattern for the event to match on, should be jsonencoded dictionary + +OPTIONAL +--------- +By default we deploy event bridge rules as disabled, and ignore state on apply, such that +enabling/disabling event bridge rules is always a manual affair rather than doing via Terraform. But via the below +optional values this may be changed on a per-rule basis. + +- state : By default DISABLED, can set to ENABLED or ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS +- ignore_state : By default true, can set to false. + + +IAM role Statement and Role Suffix to be used for this target when the rule is triggered. +Required if ecs_target is used or target in arn is EC2 instance, Kinesis data stream, Step Functions state machine, +or Event Bus in different account or region. +- iam_role_suffix : IAM role suffix for the event bridge Role having permission to invoke target AWS Service +- iam_policy_statements : A list of dictionaries where each dictionary is an IAM statement defining Event Bridge permissions +-- conditions : An OPTIONAL list of dictionaries, which each defines: +--- test : Test condition for limiting the action +--- variable : Value to test +--- values : A list of strings, denoting what to test for +*/ + +locals { + raw_event_bridge_rules = [ + # Rule is enabled and its state is managed with Terraform + { + suffix = "etl-load" + description = "EventBridge Schedule Rule to trigger StateMachine based on a schedule." + state = "ENABLED" + ignore_state = "false" + schedule = "cron(0 0 * * ? *)" + targets = [ + { + name : "StateMachine" + arn : "arn:aws:states:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:stateMachine:etl-load" + } + ] + iam_role_suffix = "etl-load" + iam_policy_statements = [ + { + sid = "EventBridgeInvokeStateMachine", + actions = [ + "states:StartExecution" + ] + resources = [ + "arn:aws:states:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:stateMachine:etl-load" + ] + } + ] + }, + # Rule is disabled, by state is not managed by Terraform, thus it may be enabled/disabled in the account + # manually by individuals + { + suffix = "daily-load-alert" + description = "Tracks and monitors status in daily step function and send SNS notification for any status other than success." + pattern = jsonencode( + { + "source" : ["aws.states"], + "detail-type" : ["Step Functions Execution Status Change"], + "detail" : { + "status" : ["SUCCEEDED", "FAILED", "TIMED_OUT", "ABORTED"], + "stateMachineArn" : ["arn:aws:states:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:stateMachine:etl-load"] + } + } + ) + targets = [ + { + name : "SNSTopic" + arn : "arn:aws:sns:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:etl-alert-topic" + } + ] + }, + ] +} \ No newline at end of file diff --git a/examples/state_machine_target/main.tf b/examples/state_machine_target/main.tf new file mode 100644 index 0000000..6c70eaf --- /dev/null +++ b/examples/state_machine_target/main.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.27.0" + } + } + required_version = "~> 1.5.0" +} + +provider "aws" { + region = "eu-west-2" +} + +module "event_bridge" { + source = "github.com/sudoblark/sudoblark.terraform.module.aws.event_bridge_rule?ref=1.0.0" + + application_name = var.application_name + environment = var.environment + raw_event_bridge_rules = local.raw_event_bridge_rules +} \ No newline at end of file diff --git a/examples/state_machine_target/variables.tf b/examples/state_machine_target/variables.tf new file mode 100644 index 0000000..9ccf4b8 --- /dev/null +++ b/examples/state_machine_target/variables.tf @@ -0,0 +1,15 @@ +variable "environment" { + description = "Which environment this is being instantiated in." + type = string + validation { + condition = contains(["dev", "test", "prod"], var.environment) + error_message = "Must be either dev, test or prod" + } + default = "prod" +} + +variable "application_name" { + description = "Name of the application utilising the resource resource." + type = string + default = "demo-app" +} \ No newline at end of file diff --git a/iam_policy.tf b/iam_policy.tf new file mode 100644 index 0000000..f9f8435 --- /dev/null +++ b/iam_policy.tf @@ -0,0 +1,21 @@ +locals { + event_bridge_policies = { for idx, policy in flatten([ + for rule in var.raw_event_bridge_rules : [ + for i, policy_statement in rule.iam_policy_statements : { + policy_name_suffix = rule.suffix + policy_content = data.aws_iam_policy_document.event_bridge_target_policy[rule.suffix].json + } + ] if length(rule.iam_policy_statements) > 0 + ]) : policy.policy_name_suffix => policy } +} + +resource "aws_iam_policy" "invoke_policy" { + for_each = local.event_bridge_policies + name = lower(lower("aws-${var.environment}-${var.application_name}-${each.value["policy_name_suffix"]}-policy")) + policy = each.value["policy_content"] + + depends_on = [ + data.aws_iam_policy_document.event_bridge_target_policy, + data.aws_iam_policy_document.allow_event_bridge_assume + ] +} \ No newline at end of file diff --git a/iam_role.tf b/iam_role.tf new file mode 100644 index 0000000..d208172 --- /dev/null +++ b/iam_role.tf @@ -0,0 +1,38 @@ +locals { + event_bridge_roles = { for idx, policy in flatten([ + for rule in var.raw_event_bridge_rules : [ + for i, policy_statement in rule.iam_policy_statements : { + index : rule.suffix, + role_name_suffix = rule.iam_role_suffix, + assume_role_policy = data.aws_iam_policy_document.allow_event_bridge_assume.json + policy_arn = aws_iam_policy.invoke_policy[rule.suffix].arn + + } + ] if length(rule.iam_policy_statements) > 0 + ]) : policy.index => policy } +} + + +resource "aws_iam_role" "invoke_role" { + for_each = local.event_bridge_roles + + name_prefix = lower("${var.environment}-${var.application_name}-${each.value["role_name_suffix"]}-role") + assume_role_policy = each.value["assume_role_policy"] + + depends_on = [ + aws_iam_policy.invoke_policy, + data.aws_iam_policy_document.allow_event_bridge_assume, + data.aws_iam_policy_document.event_bridge_target_policy + ] +} + +resource "aws_iam_role_policy_attachment" "invoke_role_policy" { + for_each = local.event_bridge_roles + + role = aws_iam_role.invoke_role[each.key].id + policy_arn = each.value["policy_arn"] + + depends_on = [ + aws_iam_role.invoke_role + ] +} \ No newline at end of file diff --git a/lambda_permission.tf b/lambda_permission.tf new file mode 100644 index 0000000..7e06641 --- /dev/null +++ b/lambda_permission.tf @@ -0,0 +1,23 @@ +locals { + target_permissions = { for identifier, rule in flatten([ + for rule in var.raw_event_bridge_rules : [ + for i, target in rule.targets : { + index : format("%s/%s", rule.suffix, target.name), + lambda_name : target.name, + rule_arn : module.rule[rule.suffix].arn, + event_bridge_rule_suffix : rule.suffix + } + ] if length(rule.targets) > 0 + ]) : rule.index => rule + } +} + +resource "aws_lambda_permission" "allow_lambda_execution_from_event_bridge" { + for_each = { for permission in local.target_permissions : permission.index => permission if can(regex("lambda", permission.lambda_name)) } + + statement_id = format("AllowExecutionFromEventBridgeRule-%s", each.value["event_bridge_rule_suffix"]) + action = "lambda:InvokeFunction" + function_name = each.value["lambda_name"] + principal = "events.amazonaws.com" + source_arn = each.value["rule_arn"] +} \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..2166122 --- /dev/null +++ b/main.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.27.0" + } + } + required_version = "~> 1.5.0" +} \ No newline at end of file diff --git a/modules/rule/outputs.tf b/modules/rule/outputs.tf new file mode 100644 index 0000000..58d3710 --- /dev/null +++ b/modules/rule/outputs.tf @@ -0,0 +1,7 @@ +output "arn" { + value = try(aws_cloudwatch_event_rule.event_rule_ignore_state[0].arn, aws_cloudwatch_event_rule.event_rule_update_state[0].arn) +} + +output "name" { + value = try(aws_cloudwatch_event_rule.event_rule_ignore_state[0].name, aws_cloudwatch_event_rule.event_rule_update_state[0].name) +} \ No newline at end of file diff --git a/modules/rule/rule.tf b/modules/rule/rule.tf new file mode 100644 index 0000000..87b92af --- /dev/null +++ b/modules/rule/rule.tf @@ -0,0 +1,29 @@ +resource "aws_cloudwatch_event_rule" "event_rule_ignore_state" { + count = var.ignore_state == true ? 1 : 0 + + name = lower("aws-${var.environment}-${var.application_name}-${var.event_name_suffix}") + event_bus_name = var.event_bus + event_pattern = var.event_pattern + description = var.event_description + state = var.state + tags = var.resource_tags + schedule_expression = var.event_schedule + + lifecycle { + ignore_changes = [ + state + ] + } +} + +resource "aws_cloudwatch_event_rule" "event_rule_update_state" { + count = var.ignore_state == false ? 1 : 0 + + name = lower("aws-${var.environment}-${var.application_name}-${var.event_name_suffix}") + event_bus_name = var.event_bus + event_pattern = var.event_pattern + description = var.event_description + state = var.state + tags = var.resource_tags + schedule_expression = var.event_schedule +} \ No newline at end of file diff --git a/modules/rule/variables.tf b/modules/rule/variables.tf new file mode 100644 index 0000000..040a5b0 --- /dev/null +++ b/modules/rule/variables.tf @@ -0,0 +1,62 @@ +variable "application_name" { + description = "Name of the application the rule relates to." + type = string +} + +variable "environment" { + description = "Which environment this is being instantiated in." + type = string + validation { + condition = contains(["dev", "test", "prod"], var.environment) + error_message = "Must be either dev, test or prod" + } +} + +variable "event_bus" { + description = "The name of the event bus" + type = string + default = "default" +} + +variable "event_pattern" { + description = "The pattern for the event" + type = string + default = null +} + +variable "event_description" { + description = "The description of the event rule" + type = string +} + +variable "state" { + description = "Initial state of the resource" + type = string + default = "ENABLED" + validation { + condition = alltrue([ + contains(["ENABLED", "DISABLED", "ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS"], var.state) + ]) + error_message = "state must be one of the following: ENABLED, DISABLED, ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS" + } +} + +variable "event_name_suffix" { + description = "Additional suffix to for event bridge rule." + type = string +} + +variable "resource_tags" { + description = "Additional tags for the resource." + default = null +} + +variable "event_schedule" { + description = "The scheduling expression. For example, cron(0 20 * * ? *) or rate(5 minutes)." + default = null +} + +variable "ignore_state" { + description = "Ignore state when deploying the resource." + default = false +} \ No newline at end of file diff --git a/modules/target/target.tf b/modules/target/target.tf new file mode 100644 index 0000000..4109b0e --- /dev/null +++ b/modules/target/target.tf @@ -0,0 +1,29 @@ +resource "aws_cloudwatch_event_target" "event_target" { + rule = var.event_rule + target_id = var.event_target + arn = var.event_target_arn + event_bus_name = var.event_bus + role_arn = var.event_target_role_arn + input = var.event_target_input + input_path = var.event_target_input_path + + dynamic "input_transformer" { + // i.e. only have an input_transformer block if event_target_input_transformer is not null + for_each = var.event_target_input_transformer == null ? [] : [var.event_target_input_transformer] + content { + input_paths = var.event_target_input_transformer.input_paths + input_template = var.event_target_input_transformer.input_template + } + } + lifecycle { + precondition { + condition = alltrue([ + (var.event_target_input != null && var.event_target_input_path == null && var.event_target_input_transformer == null) || + (var.event_target_input == null && var.event_target_input_path != null && var.event_target_input_transformer == null) || + (var.event_target_input == null && var.event_target_input_path == null && var.event_target_input_transformer != null) || + (var.event_target_input == null && var.event_target_input_path == null && var.event_target_input_transformer == null) + ]) + error_message = "event_target_input, event_target_input_path and event_target_input_transformer are mutually exclusive" + } + } +} diff --git a/modules/target/variables.tf b/modules/target/variables.tf new file mode 100644 index 0000000..8159a4c --- /dev/null +++ b/modules/target/variables.tf @@ -0,0 +1,46 @@ +variable "event_rule" { + description = "Name of the event rule" + type = string +} + +variable "event_target" { + description = "The resource target of the event" + type = string +} + +variable "event_target_arn" { + description = "The arn of the resource being targeted, for cross account use the arn of the event bus" + type = string +} + +variable "event_bus" { + description = "Name of the event bus. Do not use if using a cross account event bus" + type = string + default = null +} + +variable "event_target_role_arn" { + description = "The Amazon Resource Name (ARN) of the IAM role to be used for this target when the rule is triggered. Required if ecs_target is used or target in arn is EC2 instance, Kinesis data stream, Step Functions state machine, or Event Bus in different account or region." + type = string + default = null +} + +variable "event_target_input" { + description = "Valid JSON text passed to the target" + type = string + default = null +} + +variable "event_target_input_path" { + description = "The value of the JSONPath that is used for extracting part of the matched event when passing it to the target." + type = string + default = null +} + +variable "event_target_input_transformer" { + description = "Parameters used when you are providing a custom input to a target based on certain event data" + type = object({ + input_template = string, + input_paths = optional(map(any), null) + }) +} \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..292e96b --- /dev/null +++ b/variables.tf @@ -0,0 +1,211 @@ +# Input variable definitions +variable "environment" { + description = "Which environment this is being instantiated in." + type = string + validation { + condition = contains(["dev", "test", "prod"], var.environment) + error_message = "Must be either dev, test or prod" + } +} + +variable "application_name" { + description = "Name of the application utilising resource." + type = string +} + +variable "raw_event_bridge_rules" { + description = <