From 3a38ef580677e36f92e48205c34372c0e9b8637b Mon Sep 17 00:00:00 2001 From: Gareth Allan <157592212+gareth-allan@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:22:20 +0100 Subject: [PATCH 1/2] CCM-16644: Add the eventsub module --- .../terraform/modules/eventsub/README.md | 52 ++++++ ...udwatch_log_group_kinesis_data_firehose.tf | 14 ++ ..._log_group_sns_delivery_logging_failure.tf | 9 + ..._log_group_sns_delivery_logging_success.tf | 9 + ...atch_metric_alarm_sns_delivery_failures.tf | 16 ++ ...udwatch_metric_alarm_subscriber_anomaly.tf | 40 ++++ ..._policy_sns_delivery_logging_cloudwatch.tf | 44 +++++ .../eventsub/iam_role_firehose_role.tf | 58 ++++++ .../modules/eventsub/iam_role_sns.tf | 51 ++++++ .../eventsub/iam_role_sns_delivery_logging.tf | 21 +++ .../kinesis_firehose_delivery_stream.tf | 25 +++ .../terraform/modules/eventsub/locals.tf | 34 ++++ .../eventsub/module_s3bucket_event_cache.tf | 173 ++++++++++++++++++ .../terraform/modules/eventsub/outputs.tf | 15 ++ .../terraform/modules/eventsub/sns_topic.tf | 24 +++ .../modules/eventsub/sns_topic_policy.tf | 63 +++++++ .../sns_topic_subscription_firehose.tf | 9 + .../terraform/modules/eventsub/variables.tf | 160 ++++++++++++++++ .../terraform/modules/eventsub/versions.tf | 9 + 19 files changed, 826 insertions(+) create mode 100644 infrastructure/terraform/modules/eventsub/README.md create mode 100644 infrastructure/terraform/modules/eventsub/cloudwatch_log_group_kinesis_data_firehose.tf create mode 100644 infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_failure.tf create mode 100644 infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_success.tf create mode 100644 infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf create mode 100644 infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_subscriber_anomaly.tf create mode 100644 infrastructure/terraform/modules/eventsub/iam_policy_sns_delivery_logging_cloudwatch.tf create mode 100644 infrastructure/terraform/modules/eventsub/iam_role_firehose_role.tf create mode 100644 infrastructure/terraform/modules/eventsub/iam_role_sns.tf create mode 100644 infrastructure/terraform/modules/eventsub/iam_role_sns_delivery_logging.tf create mode 100644 infrastructure/terraform/modules/eventsub/kinesis_firehose_delivery_stream.tf create mode 100644 infrastructure/terraform/modules/eventsub/locals.tf create mode 100644 infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf create mode 100644 infrastructure/terraform/modules/eventsub/outputs.tf create mode 100644 infrastructure/terraform/modules/eventsub/sns_topic.tf create mode 100644 infrastructure/terraform/modules/eventsub/sns_topic_policy.tf create mode 100644 infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf create mode 100644 infrastructure/terraform/modules/eventsub/variables.tf create mode 100644 infrastructure/terraform/modules/eventsub/versions.tf diff --git a/infrastructure/terraform/modules/eventsub/README.md b/infrastructure/terraform/modules/eventsub/README.md new file mode 100644 index 0000000..d00c4a7 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/README.md @@ -0,0 +1,52 @@ + + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.9.0 | +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [access\_logging\_bucket](#input\_access\_logging\_bucket) | Name of S3 bucket to use for access logging | `string` | `""` | no | +| [aws\_account\_id](#input\_aws\_account\_id) | The AWS Account ID (numeric) | `string` | n/a | yes | +| [component](#input\_component) | The name of the terraformscaffold component calling this module | `string` | n/a | yes | +| [default\_tags](#input\_default\_tags) | Default tag map for application to all taggable resources in the module | `map(string)` | `{}` | no | +| [enable\_event\_anomaly\_detection](#input\_enable\_event\_anomaly\_detection) | Enable CloudWatch anomaly detection alarm for SNS topic message publishing | `bool` | `true` | no | +| [enable\_event\_cache](#input\_enable\_event\_cache) | Enable caching of events to an S3 bucket | `bool` | `true` | no | +| [enable\_firehose\_raw\_message\_delivery](#input\_enable\_firehose\_raw\_message\_delivery) | Enables raw message delivery on firehose subscription | `bool` | `false` | no | +| [enable\_sns\_delivery\_logging](#input\_enable\_sns\_delivery\_logging) | Enable SNS Delivery Failure Notifications | `bool` | `true` | no | +| [environment](#input\_environment) | The name of the terraformscaffold environment the module is called for | `string` | n/a | yes | +| [event\_anomaly\_band\_width](#input\_event\_anomaly\_band\_width) | The width of the anomaly detection band. Higher values (e.g. 4-6) reduce sensitivity and noise, lower values (e.g. 2-3) increase sensitivity. Recommended: 2-4. | `number` | `3` | no | +| [event\_anomaly\_evaluation\_periods](#input\_event\_anomaly\_evaluation\_periods) | Number of evaluation periods for the anomaly alarm. Each period is defined by event\_anomaly\_period. | `number` | `2` | no | +| [event\_anomaly\_period](#input\_event\_anomaly\_period) | The period in seconds over which the specified statistic is applied for anomaly detection. Minimum 300 seconds (5 minutes). Recommended: 300-600. | `number` | `300` | no | +| [event\_cache\_buffer\_interval](#input\_event\_cache\_buffer\_interval) | The buffer interval for data firehose | `number` | `500` | no | +| [event\_cache\_expiry\_days](#input\_event\_cache\_expiry\_days) | s3 archiving expiry in days | `number` | `30` | no | +| [force\_destroy](#input\_force\_destroy) | When enabled will force destroy event-cache S3 bucket | `bool` | `false` | no | +| [glue\_role\_arn](#input\_glue\_role\_arn) | ARN of the Glue execution role from the parent | `string` | n/a | yes | +| [group](#input\_group) | The name of the tfscaffold group | `string` | `null` | no | +| [kms\_key\_arn](#input\_kms\_key\_arn) | KMS key arn to use for this function | `string` | n/a | yes | +| [log\_level](#input\_log\_level) | The log level to be used in lambda functions within the component. Any log with a lower severity than the configured value will not be logged: https://docs.python.org/3/library/logging.html#levels | `string` | `"WARN"` | no | +| [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | The retention period in days for the Cloudwatch Logs events generated by the lambda function | `number` | n/a | yes | +| [name](#input\_name) | A unique name to distinguish this module invocation from others within the same CSI scope | `string` | n/a | yes | +| [project](#input\_project) | The name of the terraformscaffold project calling the module | `string` | n/a | yes | +| [region](#input\_region) | The AWS Region | `string` | n/a | yes | +| [shared\_infra\_account\_id](#input\_shared\_infra\_account\_id) | The AWS Account ID of the shared infrastructure account | `string` | `"000000000000"` | no | +| [sns\_success\_logging\_sample\_percent](#input\_sns\_success\_logging\_sample\_percent) | Enable SNS Delivery Successful Sample Percentage | `number` | `0` | no | +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [s3bucket\_event\_cache](#module\_s3bucket\_event\_cache) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.2/terraform-s3bucket.zip | n/a | +## Outputs + +| Name | Description | +|------|-------------| +| [s3\_bucket\_event\_cache](#output\_s3\_bucket\_event\_cache) | S3 Bucket ARN and Name for event cache | +| [sns\_topic](#output\_sns\_topic) | SNS Topic ARN and Name | + + + diff --git a/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_kinesis_data_firehose.tf b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_kinesis_data_firehose.tf new file mode 100644 index 0000000..952fe8b --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_kinesis_data_firehose.tf @@ -0,0 +1,14 @@ +resource "aws_cloudwatch_log_group" "kinesis_data_firehose" { + count = var.enable_event_cache ? 1 : 0 + + name = "/aws/firehose/${local.csi}" + kms_key_id = var.kms_key_arn + retention_in_days = var.log_retention_in_days +} + +resource "aws_cloudwatch_log_stream" "kinesis_data_firehose_extended_s3" { + count = var.enable_event_cache ? 1 : 0 + + name = "extended_s3" + log_group_name = aws_cloudwatch_log_group.kinesis_data_firehose[0].name +} diff --git a/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_failure.tf b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_failure.tf new file mode 100644 index 0000000..28a7ecf --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_failure.tf @@ -0,0 +1,9 @@ +resource "aws_cloudwatch_log_group" "sns_delivery_logging_failure" { + count = var.enable_sns_delivery_logging ? 1 : 0 + + # SNS doesn't allow specifying a log group and is derived as: sns/${region}/${account_id}/${name_of_sns_topic}/Failure + # (for failure logs) + name = "sns/${var.region}/${var.aws_account_id}/${local.csi}/Failure" + kms_key_id = var.kms_key_arn + retention_in_days = var.log_retention_in_days +} diff --git a/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_success.tf b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_success.tf new file mode 100644 index 0000000..f760e85 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/cloudwatch_log_group_sns_delivery_logging_success.tf @@ -0,0 +1,9 @@ +resource "aws_cloudwatch_log_group" "sns_delivery_logging_success" { + count = var.enable_sns_delivery_logging ? 1 : 0 + + # SNS doesn't allow specifying a log group and is derived as: sns/${region}/${account_id}/${name_of_sns_topic} + # (for success logs) + name = "sns/${var.region}/${var.aws_account_id}/${local.csi}" + kms_key_id = var.kms_key_arn + retention_in_days = var.log_retention_in_days +} diff --git a/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf b/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf new file mode 100644 index 0000000..e8ef124 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf @@ -0,0 +1,16 @@ +resource "aws_cloudwatch_metric_alarm" "sns_delivery_failures" { + alarm_name = "${local.csi}-sns-delivery-failures" + alarm_description = "RELIABILITY: Alarm for SNS topic delivery failures" + comparison_operator = "GreaterThanThreshold" + evaluation_periods = 1 + metric_name = "NumberOfNotificationsFailed" + namespace = "AWS/SNS" + period = 300 + statistic = "Sum" + threshold = 0 + treat_missing_data = "notBreaching" + + dimensions = { + TopicName = aws_sns_topic.main.name + } +} diff --git a/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_subscriber_anomaly.tf b/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_subscriber_anomaly.tf new file mode 100644 index 0000000..012969b --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_subscriber_anomaly.tf @@ -0,0 +1,40 @@ +resource "aws_cloudwatch_metric_alarm" "subscriber_anomaly" { + count = var.enable_event_anomaly_detection ? 1 : 0 + + alarm_name = "${local.csi}-subscriber-anomaly" + alarm_description = "ANOMALY: Detects anomalous patterns in messages published to the SNS fanout topic" + comparison_operator = "LessThanLowerOrGreaterThanUpperThreshold" + evaluation_periods = var.event_anomaly_evaluation_periods + threshold_metric_id = "ad1" + treat_missing_data = "notBreaching" + + metric_query { + id = "m1" + return_data = true + + metric { + metric_name = "NumberOfNotificationsDelivered" + namespace = "AWS/SNS" + period = var.event_anomaly_period + stat = "Sum" + + dimensions = { + TopicName = aws_sns_topic.main.name + } + } + } + + metric_query { + id = "ad1" + expression = "ANOMALY_DETECTION_BAND(m1, ${var.event_anomaly_band_width})" + label = "NumberOfNotificationsDelivered (expected)" + return_data = true + } + + tags = merge( + var.default_tags, + { + Name = "${local.csi}-subscriber-anomaly" + } + ) +} diff --git a/infrastructure/terraform/modules/eventsub/iam_policy_sns_delivery_logging_cloudwatch.tf b/infrastructure/terraform/modules/eventsub/iam_policy_sns_delivery_logging_cloudwatch.tf new file mode 100644 index 0000000..d296da2 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/iam_policy_sns_delivery_logging_cloudwatch.tf @@ -0,0 +1,44 @@ +resource "aws_iam_policy" "sns_delivery_logging_cloudwatch" { + count = var.enable_sns_delivery_logging ? 1 : 0 + + name = "${local.csi}-${var.name}-sns-delivery" + description = "Policy for ${local.csi}-${var.name} SNS Delivery Logging" + policy = data.aws_iam_policy_document.sns_delivery_logging_cloudwatch[0].json +} + +data "aws_iam_policy_document" "sns_delivery_logging_cloudwatch" { + count = var.enable_sns_delivery_logging ? 1 : 0 + + statement { + sid = "KMSCloudwatchKeyAccess" + effect = "Allow" + + actions = [ + "kms:GenerateDataKey", + "kms:Decrypt", + ] + + resources = [ + var.kms_key_arn + ] + } + + statement { + sid = "AllowSNSDeliveryNotifications" + effect = "Allow" + + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:PutMetricFilter", + "logs:PutRetentionPolicy", + ] + + resources = [ + aws_cloudwatch_log_group.sns_delivery_logging_success[0].arn, + "${aws_cloudwatch_log_group.sns_delivery_logging_success[0].arn}:log-stream:*", + aws_cloudwatch_log_group.sns_delivery_logging_failure[0].arn, + "${aws_cloudwatch_log_group.sns_delivery_logging_failure[0].arn}:log-stream:*", + ] + } +} diff --git a/infrastructure/terraform/modules/eventsub/iam_role_firehose_role.tf b/infrastructure/terraform/modules/eventsub/iam_role_firehose_role.tf new file mode 100644 index 0000000..281bc98 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/iam_role_firehose_role.tf @@ -0,0 +1,58 @@ +resource "aws_iam_role" "firehose_role" { + count = var.enable_event_cache ? 1 : 0 + + name = "${local.csi}-firehose-role" + assume_role_policy = data.aws_iam_policy_document.firehose_assume_role[0].json +} + +data "aws_iam_policy_document" "firehose_assume_role" { + count = var.enable_event_cache ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["firehose.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role_policy_attachment" "s3_write_object" { + count = var.enable_event_cache ? 1 : 0 + + role = aws_iam_role.firehose_role[0].name + policy_arn = aws_iam_policy.s3_write_object[0].arn +} + +resource "aws_iam_policy" "s3_write_object" { + count = var.enable_event_cache ? 1 : 0 + + name = "${local.csi}-${var.name}-s3-write-object" + description = "S3 Put Object policy for ${local.csi}-${var.name} Firehose" + policy = data.aws_iam_policy_document.s3_write_object[0].json +} + +data "aws_iam_policy_document" "s3_write_object" { + count = var.enable_event_cache ? 1 : 0 + + statement { + sid = "AllowWriteObject" + effect = "Allow" + + actions = [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject", + ] + + resources = [ + "${module.s3bucket_event_cache[0].arn}/*", + ] + } +} diff --git a/infrastructure/terraform/modules/eventsub/iam_role_sns.tf b/infrastructure/terraform/modules/eventsub/iam_role_sns.tf new file mode 100644 index 0000000..97bdc99 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/iam_role_sns.tf @@ -0,0 +1,51 @@ +resource "aws_iam_role" "sns_role" { + name = "${local.csi}-sns-role" + assume_role_policy = data.aws_iam_policy_document.sns_assume_role.json +} + +resource "aws_iam_policy" "firehose_delivery" { + count = var.enable_event_cache ? 1 : 0 + + name = "${local.csi}-${var.name}-firehose-delivery" + description = "Delivery Policy for ${local.csi}-${var.name} Firehose" + policy = data.aws_iam_policy_document.firehose_delivery[0].json +} + +resource "aws_iam_role_policy_attachment" "firehose_delivery" { + count = var.enable_event_cache ? 1 : 0 + + role = aws_iam_role.sns_role.name + policy_arn = aws_iam_policy.firehose_delivery[0].arn +} + + +data "aws_iam_policy_document" "sns_assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["sns.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +data "aws_iam_policy_document" "firehose_delivery" { + count = var.enable_event_cache ? 1 : 0 + + statement { + sid = "AllowFirehoseDelivery" + effect = "Allow" + + actions = [ + "firehose:PutRecord", + "firehose:PutRecordBatch" + ] + + resources = [ + "${aws_kinesis_firehose_delivery_stream.main[0].arn}", + ] + } +} diff --git a/infrastructure/terraform/modules/eventsub/iam_role_sns_delivery_logging.tf b/infrastructure/terraform/modules/eventsub/iam_role_sns_delivery_logging.tf new file mode 100644 index 0000000..a952bfe --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/iam_role_sns_delivery_logging.tf @@ -0,0 +1,21 @@ +resource "aws_iam_role" "sns_delivery_logging_role" { + count = var.enable_sns_delivery_logging ? 1 : 0 + + name = "${local.csi}-sns-delivery-logging" + assume_role_policy = data.aws_iam_policy_document.sns_delivery_logging_assume_role[0].json +} + +data "aws_iam_policy_document" "sns_delivery_logging_assume_role" { + count = var.enable_sns_delivery_logging ? 1 : 0 + + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["sns.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} diff --git a/infrastructure/terraform/modules/eventsub/kinesis_firehose_delivery_stream.tf b/infrastructure/terraform/modules/eventsub/kinesis_firehose_delivery_stream.tf new file mode 100644 index 0000000..186372d --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/kinesis_firehose_delivery_stream.tf @@ -0,0 +1,25 @@ +resource "aws_kinesis_firehose_delivery_stream" "main" { + count = var.enable_event_cache ? 1 : 0 + + name = local.csi + destination = "extended_s3" + + + server_side_encryption { + enabled = true + key_type = "CUSTOMER_MANAGED_CMK" + key_arn = var.kms_key_arn + } + + extended_s3_configuration { + role_arn = aws_iam_role.firehose_role[0].arn + bucket_arn = module.s3bucket_event_cache[0].arn + buffering_interval = var.event_cache_buffer_interval + + cloudwatch_logging_options { + enabled = true + log_group_name = aws_cloudwatch_log_group.kinesis_data_firehose[0].name + log_stream_name = aws_cloudwatch_log_stream.kinesis_data_firehose_extended_s3[0].name + } + } +} diff --git a/infrastructure/terraform/modules/eventsub/locals.tf b/infrastructure/terraform/modules/eventsub/locals.tf new file mode 100644 index 0000000..1141f72 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/locals.tf @@ -0,0 +1,34 @@ +locals { + module = "eventsub" + + csi = replace( + format( + "%s-%s-%s-%s", + var.project, + var.environment, + var.component, + var.name, + ), + "_", + "", + ) + csi_global = replace( + format( + "%s-%s-%s-%s-%s", + var.project, + var.aws_account_id, + var.region, + var.environment, + var.component, + ), + "_", + "", + ) + default_tags = merge( + var.default_tags, + { + Module = local.module + Name = local.csi + }, + ) +} diff --git a/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf b/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf new file mode 100644 index 0000000..d095c5c --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf @@ -0,0 +1,173 @@ +module "s3bucket_event_cache" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.2/terraform-s3bucket.zip" + + count = var.enable_event_cache ? 1 : 0 + + name = "eventsub_event_cache" + + aws_account_id = var.aws_account_id + region = var.region + project = var.project + environment = var.environment + component = var.component + + acl = "private" + force_destroy = var.force_destroy + versioning = true + + lifecycle_rules = [ + { + enabled = true + + noncurrent_version_transition = [ + { + noncurrent_days = "30" + storage_class = "STANDARD_IA" + } + ] + + noncurrent_version_expiration = { + noncurrent_days = "90" + } + + abort_incomplete_multipart_upload = { + days = "1" + } + } + ] + + policy_documents = [ + data.aws_iam_policy_document.s3bucket_event_cache[0].json + ] + + bucket_logging_target = { + bucket = "${var.access_logging_bucket}" + } + + public_access = { + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true + } + + default_tags = { + Name = "Event Cache Storage" + NHSE-Enable-S3-Backup-Acct = "True" + } +} + +data "aws_iam_policy_document" "s3bucket_event_cache" { + count = var.enable_event_cache ? 1 : 0 + + statement { + sid = "DontAllowNonSecureConnection" + effect = "Deny" + + actions = [ + "s3:*", + ] + + resources = [ + module.s3bucket_event_cache[0].arn, + "${module.s3bucket_event_cache[0].arn}/*", + ] + + principals { + type = "AWS" + + identifiers = [ + "*", + ] + } + + condition { + test = "Bool" + variable = "aws:SecureTransport" + + values = [ + "false", + ] + } + } + + statement { + sid = "AllowManagedAccountsToList" + effect = "Allow" + + actions = [ + "s3:ListBucket", + ] + + resources = [ + module.s3bucket_event_cache[0].arn, + ] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${var.aws_account_id}:root" + ] + } + } + + statement { + sid = "AllowManagedAccountsToGet" + effect = "Allow" + + actions = [ + "s3:GetObject", + ] + + resources = [ + "${module.s3bucket_event_cache[0].arn}/*", + ] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${var.aws_account_id}:root" + ] + } + } + statement { + sid = "AllowGlueListBucketAndGetLocation" + effect = "Allow" + + principals { + type = "AWS" + identifiers = [var.glue_role_arn] + } + + actions = [ + "s3:ListBucket", + "s3:GetBucketLocation" + ] + + resources = [ + "arn:aws:s3:::${module.s3bucket_event_cache[0].bucket}" + ] + } + + # Object-level permissions: Get/Put/Delete objects + statement { + sid = "AllowGlueObjectAccess" + effect = "Allow" + + principals { + type = "AWS" + identifiers = [var.glue_role_arn] + } + + actions = [ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:PutObject", + "s3:DeleteObject" + ] + + resources = [ + "arn:aws:s3:::${module.s3bucket_event_cache[0].bucket}/*" + ] + } +} diff --git a/infrastructure/terraform/modules/eventsub/outputs.tf b/infrastructure/terraform/modules/eventsub/outputs.tf new file mode 100644 index 0000000..e2ff3b3 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/outputs.tf @@ -0,0 +1,15 @@ +output "sns_topic" { + description = "SNS Topic ARN and Name" + value = { + arn = aws_sns_topic.main.arn + name = aws_sns_topic.main.name + } +} + +output "s3_bucket_event_cache" { + description = "S3 Bucket ARN and Name for event cache" + value = var.enable_event_cache ? { + arn = module.s3bucket_event_cache[0].arn + bucket = module.s3bucket_event_cache[0].bucket + } : {} +} diff --git a/infrastructure/terraform/modules/eventsub/sns_topic.tf b/infrastructure/terraform/modules/eventsub/sns_topic.tf new file mode 100644 index 0000000..cc30db1 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/sns_topic.tf @@ -0,0 +1,24 @@ +resource "aws_sns_topic" "main" { + name = local.csi + kms_master_key_id = var.kms_key_arn + + application_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + application_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + application_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null + + firehose_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + firehose_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + firehose_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null + + http_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + http_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + http_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null + + lambda_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + lambda_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + lambda_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null + + sqs_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + sqs_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + sqs_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null +} diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf b/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf new file mode 100644 index 0000000..6ccd831 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf @@ -0,0 +1,63 @@ +resource "aws_sns_topic_policy" "main" { + arn = aws_sns_topic.main.arn + + policy = data.aws_iam_policy_document.sns_topic_policy.json +} + +data "aws_iam_policy_document" "sns_topic_policy" { + policy_id = "__default_policy_ID" + + statement { + sid = "AllowAllSNSActionsFromAccount" + effect = "Allow" + + principals { + type = "AWS" + identifiers = ["*"] + } + + actions = [ + "SNS:Subscribe", + "SNS:SetTopicAttributes", + "SNS:RemovePermission", + "SNS:Receive", + "SNS:Publish", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:DeleteTopic", + "SNS:AddPermission", + ] + + resources = [ + aws_sns_topic.main.arn, + ] + + condition { + test = "StringEquals" + variable = "AWS:SourceOwner" + + values = [ + var.aws_account_id, + ] + } + } + + statement { + sid = "AllowAllSNSActionsFromSharedAccount" + effect = "Allow" + actions = [ + "SNS:Publish", + ] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${var.shared_infra_account_id}:root" + ] + } + + resources = [ + aws_sns_topic.main.arn, + ] + } +} diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf b/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf new file mode 100644 index 0000000..42457f6 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf @@ -0,0 +1,9 @@ +resource "aws_sns_topic_subscription" "firehose" { + count = var.enable_event_cache ? 1 : 0 + + topic_arn = aws_sns_topic.main.arn + protocol = "firehose" + subscription_role_arn = aws_iam_role.sns_role.arn + endpoint = aws_kinesis_firehose_delivery_stream.main[0].arn + raw_message_delivery = var.enable_firehose_raw_message_delivery +} diff --git a/infrastructure/terraform/modules/eventsub/variables.tf b/infrastructure/terraform/modules/eventsub/variables.tf new file mode 100644 index 0000000..964e90b --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/variables.tf @@ -0,0 +1,160 @@ +## +# Basic inherited variables for terraformscaffold modules +## + +variable "project" { + type = string + description = "The name of the terraformscaffold project calling the module" +} + +variable "environment" { + type = string + description = "The name of the terraformscaffold environment the module is called for" +} + +variable "component" { + type = string + description = "The name of the terraformscaffold component calling this module" +} + +variable "aws_account_id" { + type = string + description = "The AWS Account ID (numeric)" +} + +variable "group" { + type = string + description = "The name of the tfscaffold group" + default = null +} + +## +# Variable specific to the module +## + +# We presume this will always be specified. The default of {} will cause an error if a valid map is not specified. +# If we ever want to define this but allow it to not be specified, then we must provide a default tag keypair will be applied +# as the true default. In any other case default_tags should be removed from the module. +variable "default_tags" { + type = map(string) + description = "Default tag map for application to all taggable resources in the module" + default = {} +} + +variable "region" { + type = string + description = "The AWS Region" +} + +variable "name" { + type = string + description = "A unique name to distinguish this module invocation from others within the same CSI scope" +} + +variable "kms_key_arn" { + type = string + description = "KMS key arn to use for this function" +} + +variable "log_retention_in_days" { + type = number + description = "The retention period in days for the Cloudwatch Logs events generated by the lambda function" +} + +variable "event_cache_buffer_interval" { + type = number + description = "The buffer interval for data firehose" + default = 500 +} + +variable "enable_sns_delivery_logging" { + type = bool + description = "Enable SNS Delivery Failure Notifications" + default = true +} + +variable "sns_success_logging_sample_percent" { + type = number + description = "Enable SNS Delivery Successful Sample Percentage" + default = 0 +} + +## +# CloudWatch Anomaly Detection Variables +## + +variable "enable_event_anomaly_detection" { + type = bool + description = "Enable CloudWatch anomaly detection alarm for SNS topic message publishing" + default = true +} + +variable "event_anomaly_evaluation_periods" { + type = number + description = "Number of evaluation periods for the anomaly alarm. Each period is defined by event_anomaly_period." + default = 2 +} + +variable "event_anomaly_period" { + type = number + description = "The period in seconds over which the specified statistic is applied for anomaly detection. Minimum 300 seconds (5 minutes). Recommended: 300-600." + default = 300 +} + +variable "event_anomaly_band_width" { + type = number + description = "The width of the anomaly detection band. Higher values (e.g. 4-6) reduce sensitivity and noise, lower values (e.g. 2-3) increase sensitivity. Recommended: 2-4." + default = 3 + + validation { + condition = var.event_anomaly_band_width >= 2 && var.event_anomaly_band_width <= 10 + error_message = "Band width must be between 2 and 10" + } +} + +variable "log_level" { + type = string + description = "The log level to be used in lambda functions within the component. Any log with a lower severity than the configured value will not be logged: https://docs.python.org/3/library/logging.html#levels" + default = "WARN" +} + +variable "event_cache_expiry_days" { + type = number + description = "s3 archiving expiry in days" + default = 30 +} + +variable "enable_event_cache" { + type = bool + description = "Enable caching of events to an S3 bucket" + default = true +} + +variable "enable_firehose_raw_message_delivery" { + type = bool + description = "Enables raw message delivery on firehose subscription" + default = false +} + +variable "force_destroy" { + type = bool + description = "When enabled will force destroy event-cache S3 bucket" + default = false +} + +variable "shared_infra_account_id" { + type = string + description = "The AWS Account ID of the shared infrastructure account" + default = "000000000000" +} + +variable "glue_role_arn" { + type = string + description = "ARN of the Glue execution role from the parent" +} + +variable "access_logging_bucket" { + type = string + description = "Name of S3 bucket to use for access logging" + default = "" +} diff --git a/infrastructure/terraform/modules/eventsub/versions.tf b/infrastructure/terraform/modules/eventsub/versions.tf new file mode 100644 index 0000000..f8dc86e --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/versions.tf @@ -0,0 +1,9 @@ + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + } + required_version = ">= 1.9.0" +} From 5a9c54211d5c08f3327abdd796ba47a82e4a86be Mon Sep 17 00:00:00 2001 From: Gareth Allan <157592212+gareth-allan@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:02:06 +0100 Subject: [PATCH 2/2] CCM-16644: Shorten s3 bucket name --- .../terraform/modules/eventsub/module_s3bucket_event_cache.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf b/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf index d095c5c..6ad0b92 100644 --- a/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf +++ b/infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf @@ -3,7 +3,7 @@ module "s3bucket_event_cache" { count = var.enable_event_cache ? 1 : 0 - name = "eventsub_event_cache" + name = "es_event_cache" aws_account_id = var.aws_account_id region = var.region