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 = <