diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d5886a6..75deea3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,7 @@ repos: rev: v1.77.0 hooks: - id: terraform_fmt + - id: terraform_wrapper_module_for_each - id: terraform_validate - id: terraform_docs args: diff --git a/README.md b/README.md index d216e3d..2616e3b 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,132 @@ Terraform module which creates SNS resources on AWS +[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) + ## Usage +### Simple Topic + +```hcl +module "sns_topic" { + source = "terraform-aws-modules/sns/aws" + + name = "simple" + + tags = { + Environment = "dev" + Terraform = "true" + } +} +``` + +### Topic w/ SQS Subscription + +```hcl +module "sns_topic" { + source = "terraform-aws-modules/sns/aws" + + name = "pub-sub" + + topic_policy_statements = { + pub = { + actions = ["sns:Publish"] + principals = [{ + type = "AWS" + identifiers = ["arn:aws:iam::66666666666:role/publisher"] + }] + }, + + sub = { + actions = [ + "sns:Subscribe", + "sns:Receive", + ] + + principals = [{ + type = "AWS" + identifiers = ["*"] + }] + + conditions = [{ + test = "StringLike" + variable = "sns:Endpoint" + values = ["arn:aws:sqs:eu-west-1:11111111111:subscriber"] + }] + } + } + + subscriptions = { + sqs = { + protocol = "sqs" + endpoint = "arn:aws:sqs:eu-west-1:11111111111:subscriber" + } + } + + tags = { + Environment = "dev" + Terraform = "true" + } +} +``` + +### FIFO Topic w/ FIFO SQS Subscription + ```hcl module "sns_topic" { source = "terraform-aws-modules/sns/aws" - version = "~> 3.0" name = "my-topic" + + # SQS queue must be FIFO as well + fifo_topic = true + content_based_deduplication = true + + topic_policy_statements = { + pub = { + actions = ["sns:Publish"] + principals = [{ + type = "AWS" + identifiers = ["arn:aws:iam::66666666666:role/publisher"] + }] + }, + + sub = { + actions = [ + "sns:Subscribe", + "sns:Receive", + ] + + principals = [{ + type = "AWS" + identifiers = ["*"] + }] + + conditions = [{ + test = "StringLike" + variable = "sns:Endpoint" + values = ["arn:aws:sqs:eu-west-1:11111111111:subscriber.fifo"] + }] + } + } + + subscriptions = { + sqs = { + protocol = "sqs" + endpoint = "arn:aws:sqs:eu-west-1:11111111111:subscriber.fifo" + } + } + + tags = { + Environment = "dev" + Terraform = "true" + } } ``` ## Examples -- [Complete SNS topics](https://github.com/terraform-aws-modules/terraform-aws-sns/tree/master/examples/complete) +- [Complete](https://github.com/terraform-aws-modules/terraform-aws-sns/tree/master/examples/complete) ## Requirements @@ -23,13 +135,13 @@ module "sns_topic" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 4.0 | +| [aws](#requirement\_aws) | >= 4.40 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.0 | +| [aws](#provider\_aws) | >= 4.40 | ## Modules @@ -40,45 +152,47 @@ No modules. | Name | Type | |------|------| | [aws_sns_topic.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource | +| [aws_sns_topic_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_policy) | resource | +| [aws_sns_topic_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [application\_failure\_feedback\_role\_arn](#input\_application\_failure\_feedback\_role\_arn) | IAM role for failure feedback | `string` | `null` | no | -| [application\_success\_feedback\_role\_arn](#input\_application\_success\_feedback\_role\_arn) | The IAM role permitted to receive success feedback for this topic | `string` | `null` | no | -| [application\_success\_feedback\_sample\_rate](#input\_application\_success\_feedback\_sample\_rate) | Percentage of success to sample | `string` | `null` | no | +| [application\_feedback](#input\_application\_feedback) | Map of IAM role ARNs and sample rate for success and failure feedback | `map(string)` | `{}` | no | | [content\_based\_deduplication](#input\_content\_based\_deduplication) | Boolean indicating whether or not to enable content-based deduplication for FIFO topics. | `bool` | `false` | no | -| [create\_sns\_topic](#input\_create\_sns\_topic) | Whether to create the SNS topic | `bool` | `true` | no | +| [create](#input\_create) | Determines whether resources will be created (affects all resources) | `bool` | `true` | no | +| [create\_subscription](#input\_create\_subscription) | Determines whether an SNS subscription is created | `bool` | `true` | no | +| [create\_topic\_policy](#input\_create\_topic\_policy) | Determines whether an SNS topic policy is created | `bool` | `true` | no | | [delivery\_policy](#input\_delivery\_policy) | The SNS delivery policy | `string` | `null` | no | | [display\_name](#input\_display\_name) | The display name for the SNS topic | `string` | `null` | no | +| [enable\_default\_topic\_policy](#input\_enable\_default\_topic\_policy) | Specifies whether to enable the default topic policy. Defaults to `true` | `bool` | `true` | no | | [fifo\_topic](#input\_fifo\_topic) | Boolean indicating whether or not to create a FIFO (first-in-first-out) topic | `bool` | `false` | no | -| [firehose\_failure\_feedback\_role\_arn](#input\_firehose\_failure\_feedback\_role\_arn) | IAM role for failure feedback | `string` | `null` | no | -| [firehose\_success\_feedback\_role\_arn](#input\_firehose\_success\_feedback\_role\_arn) | The IAM role permitted to receive success feedback for this topic | `string` | `null` | no | -| [firehose\_success\_feedback\_sample\_rate](#input\_firehose\_success\_feedback\_sample\_rate) | Percentage of success to sample | `number` | `null` | no | -| [http\_failure\_feedback\_role\_arn](#input\_http\_failure\_feedback\_role\_arn) | IAM role for failure feedback | `string` | `null` | no | -| [http\_success\_feedback\_role\_arn](#input\_http\_success\_feedback\_role\_arn) | The IAM role permitted to receive success feedback for this topic | `string` | `null` | no | -| [http\_success\_feedback\_sample\_rate](#input\_http\_success\_feedback\_sample\_rate) | Percentage of success to sample | `string` | `null` | no | +| [firehose\_feedback](#input\_firehose\_feedback) | Map of IAM role ARNs and sample rate for success and failure feedback | `map(string)` | `{}` | no | +| [http\_feedback](#input\_http\_feedback) | Map of IAM role ARNs and sample rate for success and failure feedback | `map(string)` | `{}` | no | | [kms\_master\_key\_id](#input\_kms\_master\_key\_id) | The ID of an AWS-managed customer master key (CMK) for Amazon SNS or a custom CMK | `string` | `null` | no | -| [lambda\_failure\_feedback\_role\_arn](#input\_lambda\_failure\_feedback\_role\_arn) | IAM role for failure feedback | `string` | `null` | no | -| [lambda\_success\_feedback\_role\_arn](#input\_lambda\_success\_feedback\_role\_arn) | The IAM role permitted to receive success feedback for this topic | `string` | `null` | no | -| [lambda\_success\_feedback\_sample\_rate](#input\_lambda\_success\_feedback\_sample\_rate) | Percentage of success to sample | `string` | `null` | no | +| [lambda\_feedback](#input\_lambda\_feedback) | Map of IAM role ARNs and sample rate for success and failure feedback | `map(string)` | `{}` | no | | [name](#input\_name) | The name of the SNS topic to create | `string` | `null` | no | -| [name\_prefix](#input\_name\_prefix) | The prefix name of the SNS topic to create | `string` | `null` | no | -| [policy](#input\_policy) | The fully-formed AWS policy as JSON | `string` | `null` | no | -| [sqs\_failure\_feedback\_role\_arn](#input\_sqs\_failure\_feedback\_role\_arn) | IAM role for failure feedback | `string` | `null` | no | -| [sqs\_success\_feedback\_role\_arn](#input\_sqs\_success\_feedback\_role\_arn) | The IAM role permitted to receive success feedback for this topic | `string` | `null` | no | -| [sqs\_success\_feedback\_sample\_rate](#input\_sqs\_success\_feedback\_sample\_rate) | Percentage of success to sample | `string` | `null` | no | -| [tags](#input\_tags) | A mapping of tags to assign to all resources | `map(string)` | `{}` | no | +| [override\_topic\_policy\_documents](#input\_override\_topic\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid` | `list(string)` | `[]` | no | +| [source\_topic\_policy\_documents](#input\_source\_topic\_policy\_documents) | List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s | `list(string)` | `[]` | no | +| [sqs\_feedback](#input\_sqs\_feedback) | Map of IAM role ARNs and sample rate for success and failure feedback | `map(string)` | `{}` | no | +| [subscriptions](#input\_subscriptions) | A map of subscription definitions to create | `any` | `{}` | no | +| [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | +| [topic\_policy](#input\_topic\_policy) | An externally created fully-formed AWS policy as JSON | `string` | `null` | no | +| [topic\_policy\_statements](#input\_topic\_policy\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage | `any` | `{}` | no | +| [use\_name\_prefix](#input\_use\_name\_prefix) | Determines whether `name` is used as a prefix | `bool` | `false` | no | ## Outputs | Name | Description | |------|-------------| -| [sns\_topic\_arn](#output\_sns\_topic\_arn) | ARN of SNS topic | -| [sns\_topic\_id](#output\_sns\_topic\_id) | ID of SNS topic | -| [sns\_topic\_name](#output\_sns\_topic\_name) | NAME of SNS topic | -| [sns\_topic\_owner](#output\_sns\_topic\_owner) | OWNER of SNS topic | +| [subscriptions](#output\_subscriptions) | Map of subscriptions created and their attributes | +| [topic\_arn](#output\_topic\_arn) | The ARN of the SNS topic, as a more obvious property (clone of id) | +| [topic\_id](#output\_topic\_id) | The ARN of the SNS topic | +| [topic\_name](#output\_topic\_name) | The name of the topic | +| [topic\_owner](#output\_topic\_owner) | The AWS Account ID of the SNS topic owner | ## Authors diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md new file mode 100644 index 0000000..cce4c53 --- /dev/null +++ b/UPGRADE-5.0.md @@ -0,0 +1,142 @@ +# Upgrade from v4.x to v5.x + +If you have any questions regarding this upgrade process, please consult the [`examples`](https://github.com/terraform-aws-modules/terraform-aws-sns/tree/master/examples/complete) directory: + +If you find a bug, please open an issue with supporting configuration to reproduce. + +## List of backwards incompatible changes + +- `create_sns_topic` has been renamed to `create` +- `policy` has been renamed to `topic_policy` +- `name_prefix` has been replaced with the combination of `name` and `use_name_prefix = true` to ensure only one value is provided +- `*_failure_feedback_role_arn`, `*_success_feedback_role_arn`, `*_success_feedback_sample_rate` variables have been replaced with a respective top level variable that contains a map to the three attributes. See before and after below for further clarification. +- Outputs have had the `sns_` prefix stripped from their names + +## Additional changes + +### Added + +- Support for topic policy creation and subscriptions + +### Variable and output changes + +1. Removed variables: + + - None + +2. Renamed variables: + + - `create_sns_topic` -> `create` + - `policy` -> `topic_policy` + - `application_feedback_failure_role_arn`/`application_feedback_success_role_arn`/`application_feedback_success_feedback_sample_rate` -> `application_feedback` + - `firehose_feedback_failure_role_arn`/`firehose_feedback_success_role_arn`/`firehose_feedback_success_feedback_sample_rate` -> `firehose_feedback` + - `http_feedback_failure_role_arn`/`http_feedback_success_role_arn`/`http_feedback_success_feedback_sample_rate` -> `http_feedback` + - `lambda_feedback_failure_role_arn`/`lambda_feedback_success_role_arn`/`lambda_feedback_success_feedback_sample_rate` -> `lambda_feedback` + - `sqs_feedback_failure_role_arn`/`sqs_feedback_success_role_arn`/`sqs_feedback_success_feedback_sample_rate` -> `sqs_feedback` + +3. Added variables: + + - `use_name_prefix` + - `create_topic_policy` + - `source_topic_policy_documents` + - `override_topic_policy_documents` + - `enable_default_topic_policy` + - `topic_policy_statements` + - `create_subscription` + - `subscriptions` + +4. Removed outputs: + + - None + +5. Renamed outputs: + + - `sns_` prefix removed from all outputs + +6. Added outputs: + + - `subscriptions` + +## Upgrade Migrations + +Note: Only the affected attributes are shown below for brevity. + +### Before 4.x Example + +```hcl +module "sns" { + source = "terraform-aws-modules/sns/aws" + version = "~> 4.0" + + create_sns_topic = true + + name_prefix = "example-" + policy = "..." + + application_feedback_failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:application" + application_feedback_success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:application" + application_feedback_success_sample_rate = 100 + + firehose_feedback_failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:firehose" + firehose_feedback_success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:firehose" + firehose_feedback_success_sample_rate = 100 + + http_feedback_failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:http" + http_feedback_success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:http" + http_feedback_success_sample_rate = 100 + + lambda_feedback = { + lambda_feedback_failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:lambda" + lambda_feedback_success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:lambda" + lambda_feedback_success_sample_rate = 100 + + sqs_feedback_failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:sqs" + sqs_feedback_success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:sqs" + sqs_feedback_success_sample_rate = 100 +} +``` + +### After 5.x Example + +```hcl +module "sns" { + source = "terraform-aws-modules/sns/aws" + version = "~> 5.0" + + create = true + + name = "example-" + use_name_prefix = true + topic_policy = "..." + + application_feedback = { + failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:application" + success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:application" + success_sample_rate = 100 + } + firehose_feedback = { + failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:firehose" + success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:firehose" + success_sample_rate = 100 + } + http_feedback = { + failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:http" + success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:http" + success_sample_rate = 100 + } + lambda_feedback = { + failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:lambda" + success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:lambda" + success_sample_rate = 100 + } + sqs_feedback = { + failure_role_arn = "arn:aws:sqs:eu-west-1:11111111111:sqs" + success_role_arn = "arn:aws:sqs:eu-west-1:11111111111:sqs" + success_sample_rate = 100 + } +} +``` + +### State Changes + +No state changes required. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..f417c0a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,8 @@ +# Examples + +Please note - the examples provided serve two primary means: + +1. Show users working examples of the various ways in which the module can be configured and features supported +2. A means of testing/validating module changes + +Please do not mistake the examples provided as "best practices". It is up to users to consult the AWS service documentation for best practices, usage recommendations, etc. diff --git a/examples/complete/README.md b/examples/complete/README.md index b696131..9692aa6 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -1,6 +1,9 @@ # Complete SNS topic example -Configuration in this directory creates SNS topics. +Configuration in this directory creates: +- A simple, default SNS topic +- A FIFO SNS topic with FIFO SQS subscription; shows most of the supported arguments +- A disabled SNS topic ## Usage @@ -20,26 +23,30 @@ Note that this example may create resources which cost money. Run `terraform des | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 4.0 | +| [aws](#requirement\_aws) | >= 4.40 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.0 | +| [aws](#provider\_aws) | >= 4.40 | ## Modules | Name | Source | Version | |------|--------|---------| -| [users\_encrypted](#module\_users\_encrypted) | ../../ | n/a | -| [users\_unencrypted](#module\_users\_unencrypted) | ../../ | n/a | +| [complete\_sns](#module\_complete\_sns) | ../../ | n/a | +| [default\_sns](#module\_default\_sns) | ../../ | n/a | +| [disabled\_sns](#module\_disabled\_sns) | ../../ | n/a | +| [kms](#module\_kms) | terraform-aws-modules/kms/aws | ~> 1.0 | +| [sqs](#module\_sqs) | terraform-aws-modules/sqs/aws | ~> 4.0 | ## Resources | Name | Type | |------|------| -| [aws_kms_key.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | +| [aws_iam_role.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | ## Inputs @@ -49,6 +56,14 @@ No inputs. | Name | Description | |------|-------------| -| [users\_encrypted\_sns\_topic\_arn](#output\_users\_encrypted\_sns\_topic\_arn) | The ARN of the SNS topic | -| [users\_unencrypted\_sns\_topic\_arn](#output\_users\_unencrypted\_sns\_topic\_arn) | The ARN of the SNS topic | +| [complete\_sns\_subscriptions](#output\_complete\_sns\_subscriptions) | Map of subscriptions created and their attributes | +| [complete\_sns\_topic\_arn](#output\_complete\_sns\_topic\_arn) | The ARN of the SNS topic, as a more obvious property (clone of id) | +| [complete\_sns\_topic\_id](#output\_complete\_sns\_topic\_id) | The ARN of the SNS topic | +| [complete\_sns\_topic\_name](#output\_complete\_sns\_topic\_name) | The name of the topic | +| [complete\_sns\_topic\_owner](#output\_complete\_sns\_topic\_owner) | The AWS Account ID of the SNS topic owner | +| [default\_sns\_subscriptions](#output\_default\_sns\_subscriptions) | Map of subscriptions created and their attributes | +| [default\_sns\_topic\_arn](#output\_default\_sns\_topic\_arn) | The ARN of the SNS topic, as a more obvious property (clone of id) | +| [default\_sns\_topic\_id](#output\_default\_sns\_topic\_id) | The ARN of the SNS topic | +| [default\_sns\_topic\_name](#output\_default\_sns\_topic\_name) | The name of the topic | +| [default\_sns\_topic\_owner](#output\_default\_sns\_topic\_owner) | The AWS Account ID of the SNS topic owner | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index c418d1f..372114d 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -1,27 +1,233 @@ provider "aws" { + region = local.region +} + +data "aws_caller_identity" "current" {} + +locals { region = "eu-west-1" + name = "sns-ex-${basename(path.cwd)}" + + tags = { + Name = local.name + Example = "complete" + Repository = "github.com/terraform-aws-modules/terraform-aws-sns" + } } -resource "aws_kms_key" "this" {} +################################################################################ +# SNS Module +################################################################################ -module "users_unencrypted" { +module "default_sns" { source = "../../" - name = "users-unencrypted" + name = "${local.name}-default" - tags = { - Secure = "false" + tags = local.tags +} + +module "complete_sns" { + source = "../../" + + name = local.name + use_name_prefix = true + display_name = "complete" + kms_master_key_id = module.kms.key_id + + # SQS queue must be FIFO as well + fifo_topic = true + content_based_deduplication = true + + delivery_policy = jsonencode({ + "http" : { + "defaultHealthyRetryPolicy" : { + "minDelayTarget" : 20, + "maxDelayTarget" : 20, + "numRetries" : 3, + "numMaxDelayRetries" : 0, + "numNoDelayRetries" : 0, + "numMinDelayRetries" : 0, + "backoffFunction" : "linear" + }, + "disableSubscriptionOverrides" : false, + "defaultThrottlePolicy" : { + "maxReceivesPerSecond" : 1 + } + } + }) + + create_topic_policy = true + enable_default_topic_policy = true + topic_policy_statements = { + pub = { + actions = ["sns:Publish"] + principals = [{ + type = "AWS" + identifiers = [data.aws_caller_identity.current.arn] + }] + }, + + sub = { + actions = [ + "sns:Subscribe", + "sns:Receive", + ] + + principals = [{ + type = "AWS" + identifiers = ["*"] + }] + + conditions = [{ + test = "StringLike" + variable = "sns:Endpoint" + values = [module.sqs.queue_arn] + }] + } + } + + subscriptions = { + sqs = { + protocol = "sqs" + endpoint = module.sqs.queue_arn + } + } + + # Feedback + application_feedback = { + failure_role_arn = aws_iam_role.this.arn + success_role_arn = aws_iam_role.this.arn + success_sample_rate = 100 + } + firehose_feedback = { + failure_role_arn = aws_iam_role.this.arn + success_role_arn = aws_iam_role.this.arn + success_sample_rate = 100 + } + http_feedback = { + failure_role_arn = aws_iam_role.this.arn + success_role_arn = aws_iam_role.this.arn + success_sample_rate = 100 } + lambda_feedback = { + failure_role_arn = aws_iam_role.this.arn + success_role_arn = aws_iam_role.this.arn + success_sample_rate = 100 + } + sqs_feedback = { + failure_role_arn = aws_iam_role.this.arn + success_role_arn = aws_iam_role.this.arn + success_sample_rate = 100 + } + + tags = local.tags } -module "users_encrypted" { +module "disabled_sns" { source = "../../" - name_prefix = "users-encrypted-" - display_name = "users-encrypted" - kms_master_key_id = aws_kms_key.this.id + create = false +} - tags = { - Secure = "true" +################################################################################ +# Supporting Resources +################################################################################ + +module "kms" { + source = "terraform-aws-modules/kms/aws" + version = "~> 1.0" + + aliases = ["sns/${local.name}"] + description = "KMS key to encrypt topic" + + # Policy + key_statements = [ + { + sid = "SNS" + actions = [ + "kms:GenerateDataKey*", + "kms:Decrypt" + ] + resources = ["*"] + principals = [{ + type = "Service" + identifiers = ["sns.amazonaws.com"] + }] + } + ] + + tags = local.tags +} + +module "sqs" { + source = "terraform-aws-modules/sqs/aws" + version = "~> 4.0" + + name = local.name + fifo_queue = true + + create_queue_policy = true + queue_policy_statements = { + sns = { + sid = "SNS" + actions = ["sqs:SendMessage"] + + principals = [ + { + type = "Service" + identifiers = ["sns.amazonaws.com"] + } + ] + + condition = { + test = "ArnEquals" + variable = "aws:SourceArn" + values = [module.complete_sns.topic_arn] + } + } } + + tags = local.tags +} + +resource "aws_iam_role" "this" { + name = local.name + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "SnsAssume" + Principal = { + Service = "sns.amazonaws.com" + } + }, + ] + }) + + inline_policy { + name = local.name + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:PutMetricFilter", + "logs:PutRetentionPolicy", + ] + Effect = "Allow" + Resource = "*" + }, + ] + }) + } + + tags = local.tags } diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index 2568e14..a756ba3 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -1,9 +1,57 @@ -output "users_unencrypted_sns_topic_arn" { +################################################################################ +# Default +################################################################################ + +output "default_sns_topic_arn" { + description = "The ARN of the SNS topic, as a more obvious property (clone of id)" + value = module.default_sns.topic_arn +} + +output "default_sns_topic_id" { description = "The ARN of the SNS topic" - value = module.users_unencrypted.sns_topic_arn + value = module.default_sns.topic_id +} + +output "default_sns_topic_name" { + description = "The name of the topic" + value = module.default_sns.topic_name +} + +output "default_sns_topic_owner" { + description = "The AWS Account ID of the SNS topic owner" + value = module.default_sns.topic_owner +} + +output "default_sns_subscriptions" { + description = "Map of subscriptions created and their attributes" + value = module.default_sns.subscriptions } -output "users_encrypted_sns_topic_arn" { +################################################################################ +# Complete +################################################################################ + +output "complete_sns_topic_arn" { + description = "The ARN of the SNS topic, as a more obvious property (clone of id)" + value = module.complete_sns.topic_arn +} + +output "complete_sns_topic_id" { description = "The ARN of the SNS topic" - value = module.users_encrypted.sns_topic_arn + value = module.complete_sns.topic_id +} + +output "complete_sns_topic_name" { + description = "The name of the topic" + value = module.complete_sns.topic_name +} + +output "complete_sns_topic_owner" { + description = "The AWS Account ID of the SNS topic owner" + value = module.complete_sns.topic_owner +} + +output "complete_sns_subscriptions" { + description = "Map of subscriptions created and their attributes" + value = module.complete_sns.subscriptions } diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index d8dd1a4..fa875db 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.0" + version = ">= 4.40" } } } diff --git a/main.tf b/main.tf index ae17a9a..4ea2073 100644 --- a/main.tf +++ b/main.tf @@ -1,30 +1,155 @@ +data "aws_caller_identity" "current" {} + +################################################################################ +# Topic +################################################################################ + resource "aws_sns_topic" "this" { - count = var.create_sns_topic ? 1 : 0 - - name = var.name - name_prefix = var.name_prefix - - display_name = var.display_name - policy = var.policy - delivery_policy = var.delivery_policy - application_success_feedback_role_arn = var.application_success_feedback_role_arn - application_success_feedback_sample_rate = var.application_success_feedback_sample_rate - application_failure_feedback_role_arn = var.application_failure_feedback_role_arn - firehose_success_feedback_role_arn = var.firehose_success_feedback_role_arn - firehose_success_feedback_sample_rate = var.firehose_success_feedback_sample_rate - firehose_failure_feedback_role_arn = var.firehose_failure_feedback_role_arn - http_success_feedback_role_arn = var.http_success_feedback_role_arn - http_success_feedback_sample_rate = var.http_success_feedback_sample_rate - http_failure_feedback_role_arn = var.http_failure_feedback_role_arn - lambda_success_feedback_role_arn = var.lambda_success_feedback_role_arn - lambda_success_feedback_sample_rate = var.lambda_success_feedback_sample_rate - lambda_failure_feedback_role_arn = var.lambda_failure_feedback_role_arn - sqs_success_feedback_role_arn = var.sqs_success_feedback_role_arn - sqs_success_feedback_sample_rate = var.sqs_success_feedback_sample_rate - sqs_failure_feedback_role_arn = var.sqs_failure_feedback_role_arn - kms_master_key_id = var.kms_master_key_id - fifo_topic = var.fifo_topic - content_based_deduplication = var.content_based_deduplication + count = var.create ? 1 : 0 + + name = var.use_name_prefix ? null : var.name + name_prefix = var.use_name_prefix ? var.name : null + + application_failure_feedback_role_arn = try(var.application_feedback.failure_role_arn, null) + application_success_feedback_role_arn = try(var.application_feedback.success_role_arn, null) + application_success_feedback_sample_rate = try(var.application_feedback.success_sample_rate, null) + + content_based_deduplication = var.content_based_deduplication + delivery_policy = var.delivery_policy + display_name = var.display_name + fifo_topic = var.fifo_topic + + firehose_failure_feedback_role_arn = try(var.firehose_feedback.failure_role_arn, null) + firehose_success_feedback_role_arn = try(var.firehose_feedback.success_role_arn, null) + firehose_success_feedback_sample_rate = try(var.firehose_feedback.success_sample_rate, null) + + http_failure_feedback_role_arn = try(var.http_feedback.failure_role_arn, null) + http_success_feedback_role_arn = try(var.http_feedback.success_role_arn, null) + http_success_feedback_sample_rate = try(var.http_feedback.success_sample_rate, null) + + kms_master_key_id = var.kms_master_key_id + + lambda_failure_feedback_role_arn = try(var.lambda_feedback.failure_role_arn, null) + lambda_success_feedback_role_arn = try(var.lambda_feedback.success_role_arn, null) + lambda_success_feedback_sample_rate = try(var.lambda_feedback.success_sample_rate, null) + + policy = var.create_topic_policy ? var.topic_policy : null + + sqs_failure_feedback_role_arn = try(var.sqs_feedback.failure_role_arn, null) + sqs_success_feedback_role_arn = try(var.sqs_feedback.success_role_arn, null) + sqs_success_feedback_sample_rate = try(var.sqs_feedback.success_sample_rate, null) tags = var.tags } + +################################################################################ +# Topic Policy +################################################################################ + +data "aws_iam_policy_document" "this" { + count = var.create && var.create_topic_policy ? 1 : 0 + + source_policy_documents = var.source_topic_policy_documents + override_policy_documents = var.override_topic_policy_documents + + dynamic "statement" { + for_each = var.enable_default_topic_policy ? [1] : [] + + content { + sid = "__default_statement_ID" + actions = [ + "sns:Subscribe", + "sns:SetTopicAttributes", + "sns:RemovePermission", + "sns:Receive", + "sns:Publish", + "sns:ListSubscriptionsByTopic", + "sns:GetTopicAttributes", + "sns:DeleteTopic", + "sns:AddPermission", + ] + effect = "Allow" + resources = [aws_sns_topic.this[0].arn] + + principals { + type = "AWS" + identifiers = ["*"] + } + + condition { + test = "StringEquals" + values = [data.aws_caller_identity.current.account_id] + variable = "AWS:SourceOwner" + } + } + } + + dynamic "statement" { + for_each = var.topic_policy_statements + + content { + sid = try(statement.value.sid, statement.key) + actions = try(statement.value.actions, null) + not_actions = try(statement.value.not_actions, null) + effect = try(statement.value.effect, null) + # This avoids the chicken vs the egg scenario since its embedded and can reference the topic + resources = try(statement.value.resources, [aws_sns_topic.this[0].arn]) + not_resources = try(statement.value.not_resources, null) + + dynamic "principals" { + for_each = try(statement.value.principals, []) + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = try(statement.value.not_principals, []) + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = try(statement.value.conditions, []) + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +resource "aws_sns_topic_policy" "this" { + count = var.create && var.create_topic_policy ? 1 : 0 + + arn = aws_sns_topic.this[0].arn + policy = data.aws_iam_policy_document.this[0].json +} + +################################################################################ +# Subscription(s) +################################################################################ + +resource "aws_sns_topic_subscription" "this" { + for_each = { for k, v in var.subscriptions : k => v if var.create && var.create_subscription } + + confirmation_timeout_in_minutes = try(each.value.confirmation_timeout_in_minutes, null) + delivery_policy = try(each.value.delivery_policy, null) + endpoint = each.value.endpoint + endpoint_auto_confirms = try(each.value.endpoint_auto_confirms, null) + filter_policy = try(each.value.filter_policy, null) + filter_policy_scope = try(each.value.filter_policy_scope, null) + protocol = each.value.protocol + raw_message_delivery = try(each.value.raw_message_delivery, null) + redrive_policy = try(each.value.redrive_policy, null) + subscription_role_arn = try(each.value.subscription_role_arn, null) + topic_arn = aws_sns_topic.this[0].arn +} diff --git a/outputs.tf b/outputs.tf index 9c582f5..c7065c4 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,19 +1,32 @@ -output "sns_topic_arn" { - description = "ARN of SNS topic" - value = try(aws_sns_topic.this[0].arn, "") +################################################################################ +# Topic +################################################################################ + +output "topic_arn" { + description = "The ARN of the SNS topic, as a more obvious property (clone of id)" + value = try(aws_sns_topic.this[0].arn, null) +} + +output "topic_id" { + description = "The ARN of the SNS topic" + value = try(aws_sns_topic.this[0].id, null) } -output "sns_topic_name" { - description = "NAME of SNS topic" - value = try(aws_sns_topic.this[0].name, "") +output "topic_name" { + description = "The name of the topic" + value = try(aws_sns_topic.this[0].name, null) } -output "sns_topic_id" { - description = "ID of SNS topic" - value = try(aws_sns_topic.this[0].id, "") +output "topic_owner" { + description = "The AWS Account ID of the SNS topic owner" + value = try(aws_sns_topic.this[0].owner, null) } -output "sns_topic_owner" { - description = "OWNER of SNS topic" - value = try(aws_sns_topic.this[0].owner, "") +################################################################################ +# Subscription(s) +################################################################################ + +output "subscriptions" { + description = "Map of subscriptions created and their attributes" + value = aws_sns_topic_subscription.this } diff --git a/variables.tf b/variables.tf index b3885a2..97306c0 100644 --- a/variables.tf +++ b/variables.tf @@ -1,31 +1,47 @@ -variable "create_sns_topic" { - description = "Whether to create the SNS topic" +variable "create" { + description = "Determines whether resources will be created (affects all resources)" type = bool default = true } +variable "tags" { + description = "A map of tags to add to all resources" + type = map(string) + default = {} +} + +################################################################################ +# Topic +################################################################################ + variable "name" { description = "The name of the SNS topic to create" type = string default = null } -variable "name_prefix" { - description = "The prefix name of the SNS topic to create" - type = string - default = null +variable "use_name_prefix" { + description = "Determines whether `name` is used as a prefix" + type = bool + default = false } -variable "display_name" { - description = "The display name for the SNS topic" - type = string - default = null +variable "application_feedback" { + description = "Map of IAM role ARNs and sample rate for success and failure feedback" + type = map(string) + default = {} + # Example: + # application_feedback = { + # failure_role_arn = "arn:aws:iam::11111111111:role/failure" + # success_role_arn = "arn:aws:iam::11111111111:role/success" + # success_sample_rate = 75 + # } } -variable "policy" { - description = "The fully-formed AWS policy as JSON" - type = string - default = null +variable "content_based_deduplication" { + description = "Boolean indicating whether or not to enable content-based deduplication for FIFO topics." + type = bool + default = false } variable "delivery_policy" { @@ -34,116 +50,124 @@ variable "delivery_policy" { default = null } -variable "application_success_feedback_role_arn" { - description = "The IAM role permitted to receive success feedback for this topic" - type = string - default = null -} - -variable "application_success_feedback_sample_rate" { - description = "Percentage of success to sample" +variable "display_name" { + description = "The display name for the SNS topic" type = string default = null } -variable "application_failure_feedback_role_arn" { - description = "IAM role for failure feedback" - type = string - default = null +variable "fifo_topic" { + description = "Boolean indicating whether or not to create a FIFO (first-in-first-out) topic" + type = bool + default = false } -variable "firehose_success_feedback_role_arn" { - description = "The IAM role permitted to receive success feedback for this topic" - type = string - default = null +variable "firehose_feedback" { + description = "Map of IAM role ARNs and sample rate for success and failure feedback" + type = map(string) + default = {} + # Example: + # application_feedback = { + # failure_role_arn = "arn:aws:iam::11111111111:role/failure" + # success_role_arn = "arn:aws:iam::11111111111:role/success" + # success_sample_rate = 75 + # } } -variable "firehose_success_feedback_sample_rate" { - description = "Percentage of success to sample" - type = number - default = null +variable "http_feedback" { + description = "Map of IAM role ARNs and sample rate for success and failure feedback" + type = map(string) + default = {} + # Example: + # application_feedback = { + # failure_role_arn = "arn:aws:iam::11111111111:role/failure" + # success_role_arn = "arn:aws:iam::11111111111:role/success" + # success_sample_rate = 75 + # } } -variable "firehose_failure_feedback_role_arn" { - description = "IAM role for failure feedback" +variable "kms_master_key_id" { + description = "The ID of an AWS-managed customer master key (CMK) for Amazon SNS or a custom CMK" type = string default = null } -variable "http_success_feedback_role_arn" { - description = "The IAM role permitted to receive success feedback for this topic" - type = string - default = null +variable "lambda_feedback" { + description = "Map of IAM role ARNs and sample rate for success and failure feedback" + type = map(string) + default = {} + # Example: + # application_feedback = { + # failure_role_arn = "arn:aws:iam::11111111111:role/failure" + # success_role_arn = "arn:aws:iam::11111111111:role/success" + # success_sample_rate = 75 + # } } -variable "http_success_feedback_sample_rate" { - description = "Percentage of success to sample" +variable "topic_policy" { + description = "An externally created fully-formed AWS policy as JSON" type = string default = null } -variable "http_failure_feedback_role_arn" { - description = "IAM role for failure feedback" - type = string - default = null +variable "sqs_feedback" { + description = "Map of IAM role ARNs and sample rate for success and failure feedback" + type = map(string) + default = {} + # Example: + # application_feedback = { + # failure_role_arn = "arn:aws:iam::11111111111:role/failure" + # success_role_arn = "arn:aws:iam::11111111111:role/success" + # success_sample_rate = 75 + # } } -variable "lambda_success_feedback_role_arn" { - description = "The IAM role permitted to receive success feedback for this topic" - type = string - default = null -} +################################################################################ +# Topic Policy +################################################################################ -variable "lambda_success_feedback_sample_rate" { - description = "Percentage of success to sample" - type = string - default = null +variable "create_topic_policy" { + description = "Determines whether an SNS topic policy is created" + type = bool + default = true } -variable "lambda_failure_feedback_role_arn" { - description = "IAM role for failure feedback" - type = string - default = null +variable "source_topic_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s" + type = list(string) + default = [] } -variable "sqs_success_feedback_role_arn" { - description = "The IAM role permitted to receive success feedback for this topic" - type = string - default = null +variable "override_topic_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid`" + type = list(string) + default = [] } -variable "sqs_success_feedback_sample_rate" { - description = "Percentage of success to sample" - type = string - default = null +variable "enable_default_topic_policy" { + description = "Specifies whether to enable the default topic policy. Defaults to `true`" + type = bool + default = true } -variable "sqs_failure_feedback_role_arn" { - description = "IAM role for failure feedback" - type = string - default = null +variable "topic_policy_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = any + default = {} } -variable "kms_master_key_id" { - description = "The ID of an AWS-managed customer master key (CMK) for Amazon SNS or a custom CMK" - type = string - default = null -} +################################################################################ +# Subscription(s) +################################################################################ -variable "fifo_topic" { - description = "Boolean indicating whether or not to create a FIFO (first-in-first-out) topic" +variable "create_subscription" { + description = "Determines whether an SNS subscription is created" type = bool - default = false + default = true } -variable "tags" { - description = "A mapping of tags to assign to all resources" - type = map(string) +variable "subscriptions" { + description = "A map of subscription definitions to create" + type = any default = {} } - -variable "content_based_deduplication" { - description = "Boolean indicating whether or not to enable content-based deduplication for FIFO topics." - type = bool - default = false -} diff --git a/versions.tf b/versions.tf index d8dd1a4..fa875db 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.0" + version = ">= 4.40" } } } diff --git a/wrappers/README.md b/wrappers/README.md new file mode 100644 index 0000000..75a2a78 --- /dev/null +++ b/wrappers/README.md @@ -0,0 +1,100 @@ +# Wrapper for the root module + +The configuration in this directory contains an implementation of a single module wrapper pattern, which allows managing several copies of a module in places where using the native Terraform 0.13+ `for_each` feature is not feasible (e.g., with Terragrunt). + +You may want to use a single Terragrunt configuration file to manage multiple resources without duplicating `terragrunt.hcl` files for each copy of the same module. + +This wrapper does not implement any extra functionality. + +## Usage with Terragrunt + +`terragrunt.hcl`: + +```hcl +terraform { + source = "tfr:///terraform-aws-modules/sns/aws//wrappers" + # Alternative source: + # source = "git::git@github.com:terraform-aws-modules/terraform-aws-sns.git//wrappers?ref=master" +} + +inputs = { + defaults = { # Default values + create = true + tags = { + Terraform = "true" + Environment = "dev" + } + } + + items = { + my-item = { + # omitted... can be any argument supported by the module + } + my-second-item = { + # omitted... can be any argument supported by the module + } + # omitted... + } +} +``` + +## Usage with Terraform + +```hcl +module "wrapper" { + source = "terraform-aws-modules/sns/aws//wrappers" + + defaults = { # Default values + create = true + tags = { + Terraform = "true" + Environment = "dev" + } + } + + items = { + my-item = { + # omitted... can be any argument supported by the module + } + my-second-item = { + # omitted... can be any argument supported by the module + } + # omitted... + } +} +``` + +## Example: Manage multiple S3 buckets in one Terragrunt layer + +`eu-west-1/s3-buckets/terragrunt.hcl`: + +```hcl +terraform { + source = "tfr:///terraform-aws-modules/s3-bucket/aws//wrappers" + # Alternative source: + # source = "git::git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git//wrappers?ref=master" +} + +inputs = { + defaults = { + force_destroy = true + + attach_elb_log_delivery_policy = true + attach_lb_log_delivery_policy = true + attach_deny_insecure_transport_policy = true + attach_require_latest_tls_policy = true + } + + items = { + bucket1 = { + bucket = "my-random-bucket-1" + } + bucket2 = { + bucket = "my-random-bucket-2" + tags = { + Secure = "probably" + } + } + } +} +``` diff --git a/wrappers/main.tf b/wrappers/main.tf new file mode 100644 index 0000000..da5d84a --- /dev/null +++ b/wrappers/main.tf @@ -0,0 +1,28 @@ +module "wrapper" { + source = "../" + + for_each = var.items + + create = try(each.value.create, var.defaults.create, true) + tags = try(each.value.tags, var.defaults.tags, {}) + name = try(each.value.name, var.defaults.name, null) + use_name_prefix = try(each.value.use_name_prefix, var.defaults.use_name_prefix, false) + application_feedback = try(each.value.application_feedback, var.defaults.application_feedback, {}) + content_based_deduplication = try(each.value.content_based_deduplication, var.defaults.content_based_deduplication, false) + delivery_policy = try(each.value.delivery_policy, var.defaults.delivery_policy, null) + display_name = try(each.value.display_name, var.defaults.display_name, null) + fifo_topic = try(each.value.fifo_topic, var.defaults.fifo_topic, false) + firehose_feedback = try(each.value.firehose_feedback, var.defaults.firehose_feedback, {}) + http_feedback = try(each.value.http_feedback, var.defaults.http_feedback, {}) + kms_master_key_id = try(each.value.kms_master_key_id, var.defaults.kms_master_key_id, null) + lambda_feedback = try(each.value.lambda_feedback, var.defaults.lambda_feedback, {}) + topic_policy = try(each.value.topic_policy, var.defaults.topic_policy, null) + sqs_feedback = try(each.value.sqs_feedback, var.defaults.sqs_feedback, {}) + create_topic_policy = try(each.value.create_topic_policy, var.defaults.create_topic_policy, true) + source_topic_policy_documents = try(each.value.source_topic_policy_documents, var.defaults.source_topic_policy_documents, []) + override_topic_policy_documents = try(each.value.override_topic_policy_documents, var.defaults.override_topic_policy_documents, []) + enable_default_topic_policy = try(each.value.enable_default_topic_policy, var.defaults.enable_default_topic_policy, true) + topic_policy_statements = try(each.value.topic_policy_statements, var.defaults.topic_policy_statements, {}) + create_subscription = try(each.value.create_subscription, var.defaults.create_subscription, true) + subscriptions = try(each.value.subscriptions, var.defaults.subscriptions, {}) +} diff --git a/wrappers/outputs.tf b/wrappers/outputs.tf new file mode 100644 index 0000000..5da7c09 --- /dev/null +++ b/wrappers/outputs.tf @@ -0,0 +1,5 @@ +output "wrapper" { + description = "Map of outputs of a wrapper." + value = module.wrapper + # sensitive = false # No sensitive module output found +} diff --git a/wrappers/variables.tf b/wrappers/variables.tf new file mode 100644 index 0000000..a6ea096 --- /dev/null +++ b/wrappers/variables.tf @@ -0,0 +1,11 @@ +variable "defaults" { + description = "Map of default values which will be used for each item." + type = any + default = {} +} + +variable "items" { + description = "Maps of items to create a wrapper from. Values are passed through to the module." + type = any + default = {} +} diff --git a/wrappers/versions.tf b/wrappers/versions.tf new file mode 100644 index 0000000..51cad10 --- /dev/null +++ b/wrappers/versions.tf @@ -0,0 +1,3 @@ +terraform { + required_version = ">= 0.13.1" +}