From 82591c2a213aadf2c288a678e83a19b9be1a58fa Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 13 Nov 2025 17:14:12 -0600 Subject: [PATCH 01/11] feat!: Upgrade AWS provider and min required Terraform version to `6.21` and `1.11` respectively --- README.md | 11 ++--- docs/UPGRADE-6.0.md | 1 + docs/UPGRADE-7.0.md | 78 +++++++++++++++++++++++++++++++++++ examples/complete/README.md | 12 +++--- examples/complete/main.tf | 4 +- examples/complete/versions.tf | 4 +- main.tf | 16 +------ variables.tf | 12 ------ versions.tf | 9 +--- wrappers/main.tf | 2 - wrappers/versions.tf | 9 +--- 11 files changed, 97 insertions(+), 61 deletions(-) create mode 100644 docs/UPGRADE-7.0.md diff --git a/README.md b/README.md index 1977913..b060c31 100644 --- a/README.md +++ b/README.md @@ -177,16 +177,14 @@ module "redshift" { | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.3 | -| [aws](#requirement\_aws) | >= 5.45 | -| [random](#requirement\_random) | >= 3.0 | +| [terraform](#requirement\_terraform) | >= 1.11 | +| [aws](#requirement\_aws) | >= 6.21 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 5.45 | -| [random](#provider\_random) | >= 3.0 | +| [aws](#provider\_aws) | >= 6.21 | ## Modules @@ -212,7 +210,6 @@ No modules. | [aws_redshift_subnet_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/redshift_subnet_group) | resource | | [aws_redshift_usage_limit.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/redshift_usage_limit) | resource | | [aws_secretsmanager_secret_rotation.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_rotation) | resource | -| [random_password.master_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [aws_iam_policy_document.scheduled_action](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scheduled_action_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | @@ -239,7 +236,6 @@ No modules. | [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a CloudWatch log group is created for each `var.logging.log_exports` | `bool` | `false` | no | | [create\_endpoint\_access](#input\_create\_endpoint\_access) | Determines whether to create an endpoint access (managed VPC endpoint) | `bool` | `false` | no | | [create\_parameter\_group](#input\_create\_parameter\_group) | Determines whether to create a parameter group or use existing | `bool` | `true` | no | -| [create\_random\_password](#input\_create\_random\_password) | Determines whether to create random password for cluster `master_password` | `bool` | `true` | no | | [create\_scheduled\_action\_iam\_role](#input\_create\_scheduled\_action\_iam\_role) | Determines whether a scheduled action IAM role is created | `bool` | `false` | no | | [create\_snapshot\_schedule](#input\_create\_snapshot\_schedule) | Determines whether to create a snapshot schedule | `bool` | `false` | no | | [create\_subnet\_group](#input\_create\_subnet\_group) | Determines whether to create a subnet group or use existing | `bool` | `true` | no | @@ -285,7 +281,6 @@ No modules. | [port](#input\_port) | The port number on which the cluster accepts incoming connections. Default port is 5439 | `number` | `null` | no | | [preferred\_maintenance\_window](#input\_preferred\_maintenance\_window) | The weekly time range (in UTC) during which automated cluster maintenance can occur. Format: `ddd:hh24:mi-ddd:hh24:mi` | `string` | `"sat:10:00-sat:10:30"` | no | | [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `false` | no | -| [random\_password\_length](#input\_random\_password\_length) | Length of random password to create. Defaults to `16` | `number` | `16` | no | | [scheduled\_actions](#input\_scheduled\_actions) | Map of maps containing scheduled action definitions | `any` | `{}` | no | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Determines whether a final snapshot of the cluster is created before Redshift deletes the cluster. If true, a final cluster snapshot is not created. If false , a final cluster snapshot is created before the cluster is deleted | `bool` | `true` | no | | [snapshot\_cluster\_identifier](#input\_snapshot\_cluster\_identifier) | The name of the cluster the source snapshot was created from | `string` | `null` | no | diff --git a/docs/UPGRADE-6.0.md b/docs/UPGRADE-6.0.md index fc9408c..201bf77 100644 --- a/docs/UPGRADE-6.0.md +++ b/docs/UPGRADE-6.0.md @@ -115,6 +115,7 @@ module "redshift" { + snapshot_copy_grant_name = "ex-complete-us-east-1" } ``` + The `aws_redshift_logging` can be applied or imported. If setting the `log_destination_type`, an apply following an import will be required to clear the remaining diff. The `aws_redshift_snapshot_copy` resource requires importing if an existing snapshot_copy configuration exists. diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md new file mode 100644 index 0000000..c14fa70 --- /dev/null +++ b/docs/UPGRADE-7.0.md @@ -0,0 +1,78 @@ +# Upgrade from v6.x to v7.x + +Please consult the `examples` directory for reference example configurations. If you find a bug, please open an issue with supporting configuration to reproduce. + +## List of backwards incompatible changes + +- Terraform `v1.11` is now minimum supported version to support write-only (`wo_*`) attributes. +- AWS provider `v6.18` is now minimum supported version +- The ability for the module to create a random password has been removed in order to ensure passwords are not stored in plain text within the state file. Users must now provide their own password via the `master_password_wo` variable. + +## Additional changes + +### Added + +- + +### Modified + +- + +### Removed + +- + +### Variable and output changes + +1. Removed variables: + + - `create_random_password` + - `random_password_length` + +2. Renamed variables: + + - + +3. Added variables: + + - + +4. Removed outputs: + + - + +5. Renamed outputs: + + - + +6. Added outputs: + + - + +## Upgrade Migration + +### Before v6.x Example + +```hcl +module "redshift" { + source = "terraform-aws-modules/redshift/aws" + version = "~> 6.0" + + # Only the affected attributes are shown +} +``` + +### After v7.x Example + +```hcl +module "redshift" { + source = "terraform-aws-modules/redshift/aws" + version = "~> 7.0" + + # Only the affected attributes are shown +} +``` + +### State Move Commands + +TBD diff --git a/examples/complete/README.md b/examples/complete/README.md index f084b40..f9d30dd 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -23,15 +23,15 @@ Note that this example may create resources which cost money. Run `terraform des | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.3 | -| [aws](#requirement\_aws) | >= 5.45 | +| [terraform](#requirement\_terraform) | >= 1.11 | +| [aws](#requirement\_aws) | >= 6.21 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 5.45 | -| [aws.us\_east\_1](#provider\_aws.us\_east\_1) | >= 5.45 | +| [aws](#provider\_aws) | >= 6.21 | +| [aws.us\_east\_1](#provider\_aws.us\_east\_1) | >= 6.21 | ## Modules @@ -40,9 +40,9 @@ Note that this example may create resources which cost money. Run `terraform des | [default](#module\_default) | ../../ | n/a | | [disabled](#module\_disabled) | ../../ | n/a | | [redshift](#module\_redshift) | ../../ | n/a | -| [s3\_logs](#module\_s3\_logs) | terraform-aws-modules/s3-bucket/aws | ~> 3.0 | +| [s3\_logs](#module\_s3\_logs) | terraform-aws-modules/s3-bucket/aws | ~> 5.0 | | [security\_group](#module\_security\_group) | terraform-aws-modules/security-group/aws//modules/redshift | ~> 5.0 | -| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 5.0 | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 6.0 | | [with\_cloudwatch\_logging](#module\_with\_cloudwatch\_logging) | ../../ | n/a | ## Resources diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 6f7ce18..da22bab 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -259,7 +259,7 @@ module "disabled" { module "vpc" { source = "terraform-aws-modules/vpc/aws" - version = "~> 5.0" + version = "~> 6.0" name = local.name cidr = local.vpc_cidr @@ -341,7 +341,7 @@ data "aws_iam_policy_document" "s3_redshift" { module "s3_logs" { source = "terraform-aws-modules/s3-bucket/aws" - version = "~> 3.0" + version = "~> 5.0" bucket_prefix = local.name acl = "log-delivery-write" diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index 02f721b..49d3842 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 1.3" + required_version = ">= 1.11" required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.45" + version = ">= 6.21" } } } diff --git a/main.tf b/main.tf index 9ef436e..2f3eb2c 100644 --- a/main.tf +++ b/main.tf @@ -6,18 +6,6 @@ locals { dns_suffix = try(data.aws_partition.current[0].dns_suffix, "") } -resource "random_password" "master_password" { - count = var.create && var.create_random_password ? 1 : 0 - - length = var.random_password_length - min_lower = 1 - min_numeric = 1 - min_special = 1 - min_upper = 1 - special = true - override_special = "!#$%&*()-_=+[]{}<>:?" -} - ################################################################################ # Cluster ################################################################################ @@ -25,8 +13,6 @@ resource "random_password" "master_password" { locals { subnet_group_name = var.create && var.create_subnet_group ? aws_redshift_subnet_group.this[0].name : var.subnet_group_name parameter_group_name = var.create && var.create_parameter_group ? aws_redshift_parameter_group.this[0].id : var.parameter_group_name - - master_password = var.create && var.create_random_password ? random_password.master_password[0].result : var.master_password } resource "aws_redshift_cluster" "this" { @@ -55,7 +41,7 @@ resource "aws_redshift_cluster" "this" { maintenance_track_name = var.maintenance_track_name manual_snapshot_retention_period = var.manual_snapshot_retention_period manage_master_password = var.manage_master_password ? var.manage_master_password : null - master_password = var.snapshot_identifier == null && !var.manage_master_password ? local.master_password : null + master_password = var.snapshot_identifier == null && !var.manage_master_password ? var.master_password : null master_password_secret_kms_key_id = var.master_password_secret_kms_key_id master_username = var.master_username multi_az = var.multi_az diff --git a/variables.tf b/variables.tf index f9cfc55..320544f 100644 --- a/variables.tf +++ b/variables.tf @@ -149,18 +149,6 @@ variable "multi_az" { default = null } -variable "create_random_password" { - description = "Determines whether to create random password for cluster `master_password`" - type = bool - default = true -} - -variable "random_password_length" { - description = "Length of random password to create. Defaults to `16`" - type = number - default = 16 -} - variable "master_username" { description = "Username for the master DB user (Required unless a `snapshot_identifier` is provided). Defaults to `awsuser`" type = string diff --git a/versions.tf b/versions.tf index 512dd2c..49d3842 100644 --- a/versions.tf +++ b/versions.tf @@ -1,15 +1,10 @@ terraform { - required_version = ">= 1.3" + required_version = ">= 1.11" required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.45" - } - - random = { - source = "hashicorp/random" - version = ">= 3.0" + version = ">= 6.21" } } } diff --git a/wrappers/main.tf b/wrappers/main.tf index d04f14b..d91999f 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -21,7 +21,6 @@ module "wrapper" { create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, false) create_endpoint_access = try(each.value.create_endpoint_access, var.defaults.create_endpoint_access, false) create_parameter_group = try(each.value.create_parameter_group, var.defaults.create_parameter_group, true) - create_random_password = try(each.value.create_random_password, var.defaults.create_random_password, true) create_scheduled_action_iam_role = try(each.value.create_scheduled_action_iam_role, var.defaults.create_scheduled_action_iam_role, false) create_snapshot_schedule = try(each.value.create_snapshot_schedule, var.defaults.create_snapshot_schedule, false) create_subnet_group = try(each.value.create_subnet_group, var.defaults.create_subnet_group, true) @@ -67,7 +66,6 @@ module "wrapper" { port = try(each.value.port, var.defaults.port, null) preferred_maintenance_window = try(each.value.preferred_maintenance_window, var.defaults.preferred_maintenance_window, "sat:10:00-sat:10:30") publicly_accessible = try(each.value.publicly_accessible, var.defaults.publicly_accessible, false) - random_password_length = try(each.value.random_password_length, var.defaults.random_password_length, 16) scheduled_actions = try(each.value.scheduled_actions, var.defaults.scheduled_actions, {}) skip_final_snapshot = try(each.value.skip_final_snapshot, var.defaults.skip_final_snapshot, true) snapshot_cluster_identifier = try(each.value.snapshot_cluster_identifier, var.defaults.snapshot_cluster_identifier, null) diff --git a/wrappers/versions.tf b/wrappers/versions.tf index 512dd2c..49d3842 100644 --- a/wrappers/versions.tf +++ b/wrappers/versions.tf @@ -1,15 +1,10 @@ terraform { - required_version = ">= 1.3" + required_version = ">= 1.11" required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.45" - } - - random = { - source = "hashicorp/random" - version = ">= 3.0" + version = ">= 6.21" } } } From 975ba7cc038188e5c750a3a3d439d8dc45ab6d6c Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 13 Nov 2025 17:49:23 -0600 Subject: [PATCH 02/11] fix: Remove deprecated argument `aqua_configuration_status` --- README.md | 1 - docs/UPGRADE-7.0.md | 5 +++-- main.tf | 1 - variables.tf | 6 ------ wrappers/main.tf | 1 - 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b060c31..fa73f60 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,6 @@ No modules. |------|-------------|------|---------|:--------:| | [allow\_version\_upgrade](#input\_allow\_version\_upgrade) | If `true`, major version upgrades can be applied during the maintenance window to the Amazon Redshift engine that is running on the cluster. Default is `true` | `bool` | `null` | no | | [apply\_immediately](#input\_apply\_immediately) | Specifies whether any cluster modifications are applied immediately, or during the next maintenance window. Default is `false` | `bool` | `null` | no | -| [aqua\_configuration\_status](#input\_aqua\_configuration\_status) | The value represents how the cluster is configured to use AQUA (Advanced Query Accelerator) after the cluster is restored. Possible values are `enabled`, `disabled`, and `auto`. Requires Cluster reboot | `string` | `null` | no | | [authentication\_profiles](#input\_authentication\_profiles) | Map of authentication profiles to create | `any` | `{}` | no | | [automated\_snapshot\_retention\_period](#input\_automated\_snapshot\_retention\_period) | The number of days that automated snapshots are retained. If the value is 0, automated snapshots are disabled. Even if automated snapshots are disabled, you can still create manual snapshots when you want with create-cluster-snapshot. Default is 1 | `number` | `null` | no | | [availability\_zone](#input\_availability\_zone) | The EC2 Availability Zone (AZ) in which you want Amazon Redshift to provision the cluster. Can only be changed if `availability_zone_relocation_enabled` is `true` | `string` | `null` | no | diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md index c14fa70..8fa0190 100644 --- a/docs/UPGRADE-7.0.md +++ b/docs/UPGRADE-7.0.md @@ -26,8 +26,9 @@ Please consult the `examples` directory for reference example configurations. If 1. Removed variables: - - `create_random_password` - - `random_password_length` + - `create_random_password` removed along with support for generating a random password + - `random_password_length` removed along with support for generating a random password + - `aqua_configuration_status` argument was deprecated 2. Renamed variables: diff --git a/main.tf b/main.tf index 2f3eb2c..80f8720 100644 --- a/main.tf +++ b/main.tf @@ -20,7 +20,6 @@ resource "aws_redshift_cluster" "this" { allow_version_upgrade = var.allow_version_upgrade apply_immediately = var.apply_immediately - aqua_configuration_status = var.aqua_configuration_status automated_snapshot_retention_period = var.automated_snapshot_retention_period availability_zone = var.availability_zone availability_zone_relocation_enabled = var.availability_zone_relocation_enabled diff --git a/variables.tf b/variables.tf index 320544f..c63c952 100644 --- a/variables.tf +++ b/variables.tf @@ -26,12 +26,6 @@ variable "apply_immediately" { default = null } -variable "aqua_configuration_status" { - description = "The value represents how the cluster is configured to use AQUA (Advanced Query Accelerator) after the cluster is restored. Possible values are `enabled`, `disabled`, and `auto`. Requires Cluster reboot" - type = string - default = null -} - variable "automated_snapshot_retention_period" { description = "The number of days that automated snapshots are retained. If the value is 0, automated snapshots are disabled. Even if automated snapshots are disabled, you can still create manual snapshots when you want with create-cluster-snapshot. Default is 1" type = number diff --git a/wrappers/main.tf b/wrappers/main.tf index d91999f..96644ee 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -5,7 +5,6 @@ module "wrapper" { allow_version_upgrade = try(each.value.allow_version_upgrade, var.defaults.allow_version_upgrade, null) apply_immediately = try(each.value.apply_immediately, var.defaults.apply_immediately, null) - aqua_configuration_status = try(each.value.aqua_configuration_status, var.defaults.aqua_configuration_status, null) authentication_profiles = try(each.value.authentication_profiles, var.defaults.authentication_profiles, {}) automated_snapshot_retention_period = try(each.value.automated_snapshot_retention_period, var.defaults.automated_snapshot_retention_period, null) availability_zone = try(each.value.availability_zone, var.defaults.availability_zone, null) From 2aaee84c0a5e80500a63d7e7acefd69815899e43 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 13 Nov 2025 20:25:51 -0600 Subject: [PATCH 03/11] feat: Add variable optional attribute type definitions --- README.md | 30 +++++----- docs/UPGRADE-7.0.md | 3 +- examples/complete/main.tf | 54 ++++++++++-------- main.tf | 114 +++++++++++++++++++++----------------- variables.tf | 100 +++++++++++++++++++++++++-------- wrappers/main.tf | 10 ++-- 6 files changed, 190 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index fa73f60..08d14dc 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ No modules. | [aws_secretsmanager_secret_rotation.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_rotation) | resource | | [aws_iam_policy_document.scheduled_action](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scheduled_action_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | +| [aws_service_principal.scheduler_redshift](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/service_principal) | data source | ## Inputs @@ -220,7 +220,7 @@ No modules. |------|-------------|------|---------|:--------:| | [allow\_version\_upgrade](#input\_allow\_version\_upgrade) | If `true`, major version upgrades can be applied during the maintenance window to the Amazon Redshift engine that is running on the cluster. Default is `true` | `bool` | `null` | no | | [apply\_immediately](#input\_apply\_immediately) | Specifies whether any cluster modifications are applied immediately, or during the next maintenance window. Default is `false` | `bool` | `null` | no | -| [authentication\_profiles](#input\_authentication\_profiles) | Map of authentication profiles to create | `any` | `{}` | no | +| [authentication\_profiles](#input\_authentication\_profiles) | Map of authentication profiles to create |
map(object({
name = optional(string) # Will fall back to key if not set
content = any
}))
| `{}` | no | | [automated\_snapshot\_retention\_period](#input\_automated\_snapshot\_retention\_period) | The number of days that automated snapshots are retained. If the value is 0, automated snapshots are disabled. Even if automated snapshots are disabled, you can still create manual snapshots when you want with create-cluster-snapshot. Default is 1 | `number` | `null` | no | | [availability\_zone](#input\_availability\_zone) | The EC2 Availability Zone (AZ) in which you want Amazon Redshift to provision the cluster. Can only be changed if `availability_zone_relocation_enabled` is `true` | `string` | `null` | no | | [availability\_zone\_relocation\_enabled](#input\_availability\_zone\_relocation\_enabled) | If `true`, the cluster can be relocated to another availability zone, either automatically by AWS or when requested. Default is `false`. Available for use on clusters from the RA3 instance family | `bool` | `null` | no | @@ -229,7 +229,7 @@ No modules. | [cloudwatch\_log\_group\_skip\_destroy](#input\_cloudwatch\_log\_group\_skip\_destroy) | Set to true if you do not wish the log group (and any logs it may contain) to be deleted at destroy time, and instead just remove the log group from the Terraform state | `bool` | `null` | no | | [cloudwatch\_log\_group\_tags](#input\_cloudwatch\_log\_group\_tags) | Additional tags to add to cloudwatch log groups created | `map(string)` | `{}` | no | | [cluster\_identifier](#input\_cluster\_identifier) | The Cluster Identifier. Must be a lower case string | `string` | `""` | no | -| [cluster\_timeouts](#input\_cluster\_timeouts) | Create, update, and delete timeout configurations for the cluster | `map(string)` | `{}` | no | +| [cluster\_timeouts](#input\_cluster\_timeouts) | Create, update, and delete timeout configurations for the cluster |
object({
create = optional(string)
update = optional(string)
delete = optional(string)
})
| `null` | no | | [cluster\_version](#input\_cluster\_version) | The version of the Amazon Redshift engine software that you want to deploy on the cluster. The version selected runs on all the nodes in the cluster | `string` | `null` | no | | [create](#input\_create) | Determines whether to create Redshift cluster and resources (affects all resources) | `bool` | `true` | no | | [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a CloudWatch log group is created for each `var.logging.log_exports` | `bool` | `false` | no | @@ -256,16 +256,16 @@ No modules. | [iam\_role\_tags](#input\_iam\_role\_tags) | A map of additional tags to add to the scheduled action IAM role created | `map(string)` | `{}` | no | | [iam\_role\_use\_name\_prefix](#input\_iam\_role\_use\_name\_prefix) | Determines whether scheduled action the IAM role name (`iam_role_name`) is used as a prefix | `string` | `true` | no | | [kms\_key\_arn](#input\_kms\_key\_arn) | The ARN for the KMS encryption key. When specifying `kms_key_arn`, `encrypted` needs to be set to `true` | `string` | `null` | no | -| [logging](#input\_logging) | Logging configuration for the cluster | `any` | `{}` | no | +| [logging](#input\_logging) | Logging configuration for the cluster |
object({
bucket_name = optional(string)
log_destination_type = optional(string)
log_exports = optional(list(string))
s3_key_prefix = optional(string)
})
| `null` | no | | [maintenance\_track\_name](#input\_maintenance\_track\_name) | The name of the maintenance track for the restored cluster. When you take a snapshot, the snapshot inherits the MaintenanceTrack value from the cluster. The snapshot might be on a different track than the cluster that was the source for the snapshot. Default value is `current` | `string` | `null` | no | | [manage\_master\_password](#input\_manage\_master\_password) | Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password`. One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided | `bool` | `false` | no | -| [manage\_master\_password\_rotation](#input\_manage\_master\_password\_rotation) | Whether to manage the master user password rotation. Setting this value to false after previously having been set to true will disable automatic rotation. | `bool` | `false` | no | +| [manage\_master\_password\_rotation](#input\_manage\_master\_password\_rotation) | Whether to manage the master user password rotation. Setting this value to false after previously having been set to true will disable automatic rotation | `bool` | `false` | no | | [manual\_snapshot\_retention\_period](#input\_manual\_snapshot\_retention\_period) | The default number of days to retain a manual snapshot. If the value is -1, the snapshot is retained indefinitely. This setting doesn't change the retention period of existing snapshots. Valid values are between `-1` and `3653`. Default value is `-1` | `number` | `null` | no | | [master\_password](#input\_master\_password) | Password for the master DB user. (Required unless a `snapshot_identifier` is provided). Must contain at least 8 chars, one uppercase letter, one lowercase letter, and one number | `string` | `null` | no | -| [master\_password\_rotate\_immediately](#input\_master\_password\_rotate\_immediately) | Specifies whether to rotate the secret immediately or wait until the next scheduled rotation window. | `bool` | `null` | no | -| [master\_password\_rotation\_automatically\_after\_days](#input\_master\_password\_rotation\_automatically\_after\_days) | Specifies the number of days between automatic scheduled rotations of the secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified. | `number` | `null` | no | -| [master\_password\_rotation\_duration](#input\_master\_password\_rotation\_duration) | The length of the rotation window in hours. For example, 3h for a three hour window. | `string` | `null` | no | -| [master\_password\_rotation\_schedule\_expression](#input\_master\_password\_rotation\_schedule\_expression) | A cron() or rate() expression that defines the schedule for rotating your secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified. | `string` | `null` | no | +| [master\_password\_rotate\_immediately](#input\_master\_password\_rotate\_immediately) | Specifies whether to rotate the secret immediately or wait until the next scheduled rotation window | `bool` | `null` | no | +| [master\_password\_rotation\_automatically\_after\_days](#input\_master\_password\_rotation\_automatically\_after\_days) | Specifies the number of days between automatic scheduled rotations of the secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified | `number` | `null` | no | +| [master\_password\_rotation\_duration](#input\_master\_password\_rotation\_duration) | The length of the rotation window in hours. For example, 3h for a three hour window | `string` | `null` | no | +| [master\_password\_rotation\_schedule\_expression](#input\_master\_password\_rotation\_schedule\_expression) | A cron() or rate() expression that defines the schedule for rotating your secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified | `string` | `null` | no | | [master\_password\_secret\_kms\_key\_id](#input\_master\_password\_secret\_kms\_key\_id) | ID of the KMS key used to encrypt the cluster admin credentials secret | `string` | `null` | no | | [master\_username](#input\_master\_username) | Username for the master DB user (Required unless a `snapshot_identifier` is provided). Defaults to `awsuser` | `string` | `"awsuser"` | no | | [multi\_az](#input\_multi\_az) | Specifies if the Redshift cluster is multi-AZ | `bool` | `null` | no | @@ -273,17 +273,17 @@ No modules. | [number\_of\_nodes](#input\_number\_of\_nodes) | Number of nodes in the cluster. Defaults to 1. Note: values greater than 1 will trigger `cluster_type` to switch to `multi-node` | `number` | `1` | no | | [owner\_account](#input\_owner\_account) | The AWS customer account used to create or copy the snapshot. Required if you are restoring a snapshot you do not own, optional if you own the snapshot | `string` | `null` | no | | [parameter\_group\_description](#input\_parameter\_group\_description) | The description of the Redshift parameter group. Defaults to `Managed by Terraform` | `string` | `null` | no | -| [parameter\_group\_family](#input\_parameter\_group\_family) | The family of the Redshift parameter group | `string` | `"redshift-1.0"` | no | +| [parameter\_group\_family](#input\_parameter\_group\_family) | The family of the Redshift parameter group | `string` | `"redshift-2.0"` | no | | [parameter\_group\_name](#input\_parameter\_group\_name) | The name of the Redshift parameter group, existing or to be created | `string` | `null` | no | -| [parameter\_group\_parameters](#input\_parameter\_group\_parameters) | value | `map(any)` | `{}` | no | +| [parameter\_group\_parameters](#input\_parameter\_group\_parameters) | A list of Redshift parameters to apply |
list(object({
name = string
value = string
}))
| `null` | no | | [parameter\_group\_tags](#input\_parameter\_group\_tags) | Additional tags to add to the parameter group | `map(string)` | `{}` | no | -| [port](#input\_port) | The port number on which the cluster accepts incoming connections. Default port is 5439 | `number` | `null` | no | +| [port](#input\_port) | The port number on which the cluster accepts incoming connections. Default port is `5439` | `number` | `null` | no | | [preferred\_maintenance\_window](#input\_preferred\_maintenance\_window) | The weekly time range (in UTC) during which automated cluster maintenance can occur. Format: `ddd:hh24:mi-ddd:hh24:mi` | `string` | `"sat:10:00-sat:10:30"` | no | | [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `false` | no | -| [scheduled\_actions](#input\_scheduled\_actions) | Map of maps containing scheduled action definitions | `any` | `{}` | no | +| [scheduled\_actions](#input\_scheduled\_actions) | Map of scheduled action definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
description = optional(string)
enable = optional(bool)
start_time = optional(string)
end_time = optional(string)
schedule = string
iam_role = optional(string)
target_action = object({
pause_cluster = optional(object({}))
resize_cluster = optional(object({
classic = optional(bool)
cluster_type = optional(string)
node_type = optional(string)
number_of_nodes = optional(number)
}))
resume_cluster = optional(object({}))
})

}))
| `{}` | no | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Determines whether a final snapshot of the cluster is created before Redshift deletes the cluster. If true, a final cluster snapshot is not created. If false , a final cluster snapshot is created before the cluster is deleted | `bool` | `true` | no | | [snapshot\_cluster\_identifier](#input\_snapshot\_cluster\_identifier) | The name of the cluster the source snapshot was created from | `string` | `null` | no | -| [snapshot\_copy](#input\_snapshot\_copy) | Configuration of automatic copy of snapshots from one region to another | `any` | `{}` | no | +| [snapshot\_copy](#input\_snapshot\_copy) | Configuration of automatic copy of snapshots from one region to another |
object({
destination_region = string
manual_snapshot_retention_period = optional(number)
retention_period = optional(number)
grant_name = optional(string)
})
| `null` | no | | [snapshot\_identifier](#input\_snapshot\_identifier) | The name of the snapshot from which to create the new cluster | `string` | `null` | no | | [snapshot\_schedule\_definitions](#input\_snapshot\_schedule\_definitions) | The definition of the snapshot schedule. The definition is made up of schedule expressions, for example `cron(30 12 *)` or `rate(12 hours)` | `list(string)` | `[]` | no | | [snapshot\_schedule\_description](#input\_snapshot\_schedule\_description) | The description of the snapshot schedule | `string` | `null` | no | @@ -294,7 +294,7 @@ No modules. | [subnet\_group\_tags](#input\_subnet\_group\_tags) | Additional tags to add to the subnet group | `map(string)` | `{}` | no | | [subnet\_ids](#input\_subnet\_ids) | An array of VPC subnet IDs to use in the subnet group | `list(string)` | `[]` | no | | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | -| [usage\_limits](#input\_usage\_limits) | Map of usage limit definitions to create | `any` | `{}` | no | +| [usage\_limits](#input\_usage\_limits) | Map of usage limit definitions to create |
map(object({
amount = number
breach_action = optional(string)
feature_type = string
limit_type = optional(string) # Will fall back to key if not set
period = optional(string)
tags = optional(map(string), {})
}))
| `{}` | no | | [use\_snapshot\_identifier\_prefix](#input\_use\_snapshot\_identifier\_prefix) | Determines whether the identifier (`snapshot_schedule_identifier`) is used as a prefix | `bool` | `true` | no | | [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | A list of Virtual Private Cloud (VPC) security groups to be associated with the cluster | `list(string)` | `[]` | no | diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md index 8fa0190..272f35c 100644 --- a/docs/UPGRADE-7.0.md +++ b/docs/UPGRADE-7.0.md @@ -16,7 +16,8 @@ Please consult the `examples` directory for reference example configurations. If ### Modified -- +- Variable definitions now contain detailed `object` types in place of the previously used any type. +- Default value for `parameter_group_family` changed from `redshift-1.0` to `redshift-2.0` ### Removed diff --git a/examples/complete/main.tf b/examples/complete/main.tf index da22bab..006b569 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -71,36 +71,36 @@ module "redshift" { # Parameter group parameter_group_name = "${local.name}-custom" parameter_group_description = "Custom parameter group for ${local.name} cluster" - parameter_group_parameters = { - wlm_json_configuration = { + parameter_group_parameters = [ + { name = "wlm_json_configuration" value = jsonencode([ { query_concurrency = 15 } ]) - } - require_ssl = { + }, + { name = "require_ssl" value = true - } - use_fips_ssl = { + }, + { name = "use_fips_ssl" value = false - } - enable_user_activity_logging = { + }, + { name = "enable_user_activity_logging" value = true - } - max_concurrency_scaling_clusters = { + }, + { name = "max_concurrency_scaling_clusters" value = 3 - } - enable_case_sensitive_identifier = { + }, + { name = "enable_case_sensitive_identifier" value = true } - } + ] parameter_group_tags = { Additional = "CustomParameterGroup" } @@ -124,25 +124,31 @@ module "redshift" { create_scheduled_action_iam_role = true scheduled_actions = { pause = { - name = "${local.name}-pause" - description = "Pause cluster every night" - schedule = "cron(0 22 * * ? *)" - pause_cluster = true + name = "${local.name}-pause" + description = "Pause cluster every night" + schedule = "cron(0 22 * * ? *)" + target_action = { + pause_cluster = {} + } } resize = { name = "${local.name}-resize" description = "Resize cluster (demo only)" schedule = "cron(00 13 * * ? *)" - resize_cluster = { - node_type = "ds2.xlarge" - number_of_nodes = 5 + target_action = { + resize_cluster = { + node_type = "ds2.xlarge" + number_of_nodes = 5 + } } } resume = { - name = "${local.name}-resume" - description = "Resume cluster every morning" - schedule = "cron(0 12 * * ? *)" - resume_cluster = true + name = "${local.name}-resume" + description = "Resume cluster every morning" + schedule = "cron(0 12 * * ? *)" + target_action = { + resume_cluster = {} + } } } diff --git a/main.tf b/main.tf index 80f8720..6b52ff3 100644 --- a/main.tf +++ b/main.tf @@ -1,11 +1,3 @@ -data "aws_partition" "current" { - count = var.create && var.create_scheduled_action_iam_role ? 1 : 0 -} - -locals { - dns_suffix = try(data.aws_partition.current[0].dns_suffix, "") -} - ################################################################################ # Cluster ################################################################################ @@ -58,10 +50,14 @@ resource "aws_redshift_cluster" "this" { tags = var.tags - timeouts { - create = try(var.cluster_timeouts.create, null) - update = try(var.cluster_timeouts.update, null) - delete = try(var.cluster_timeouts.delete, null) + dynamic "timeouts" { + for_each = var.cluster_timeouts != null ? [1] : [] + + content { + create = var.cluster_timeouts.create + update = var.cluster_timeouts.update + delete = var.cluster_timeouts.delete + } } lifecycle { @@ -95,10 +91,10 @@ resource "aws_redshift_parameter_group" "this" { family = var.parameter_group_family dynamic "parameter" { - for_each = var.parameter_group_parameters + for_each = var.parameter_group_parameters != null ? var.parameter_group_parameters : [] content { - name = try(parameter.value.name, parameter.key) + name = parameter.value.name value = parameter.value.value } } @@ -154,45 +150,59 @@ locals { resource "aws_redshift_scheduled_action" "this" { for_each = { for k, v in var.scheduled_actions : k => v if var.create } - name = each.value.name - description = try(each.value.description, null) - enable = try(each.value.enable, null) - start_time = try(each.value.start_time, null) - end_time = try(each.value.end_time, null) + name = try(coalesce(each.value.name, each.key)) + description = each.value.description + enable = each.value.enable + start_time = each.value.start_time + end_time = each.value.end_time schedule = each.value.schedule iam_role = var.create_scheduled_action_iam_role ? aws_iam_role.scheduled_action[0].arn : each.value.iam_role - target_action { - dynamic "pause_cluster" { - for_each = try([each.value.pause_cluster], []) + dynamic "target_action" { + for_each = [each.value.target_action] + + content { + dynamic "pause_cluster" { + for_each = each.value.pause_cluster != null ? [each.value.pause_cluster] : [] - content { - cluster_identifier = aws_redshift_cluster.this[0].id + content { + cluster_identifier = aws_redshift_cluster.this[0].id + } } - } - dynamic "resize_cluster" { - for_each = try([each.value.resize_cluster], []) + dynamic "resize_cluster" { + for_each = each.value.resize_cluster != null ? [each.value.resize_cluster] : [] - content { - classic = try(resize_cluster.value.classic, null) - cluster_identifier = aws_redshift_cluster.this[0].id - cluster_type = try(resize_cluster.value.cluster_type, null) - node_type = try(resize_cluster.value.node_type, null) - number_of_nodes = try(resize_cluster.value.number_of_nodes, null) + content { + classic = resize_cluster.value.classic + cluster_identifier = aws_redshift_cluster.this[0].id + cluster_type = resize_cluster.value.cluster_type + node_type = resize_cluster.value.node_type + number_of_nodes = resize_cluster.value.number_of_nodes + } } - } - dynamic "resume_cluster" { - for_each = try([each.value.resume_cluster], []) + dynamic "resume_cluster" { + for_each = each.value.resume_cluster != null ? [each.value.resume_cluster] : [] - content { - cluster_identifier = aws_redshift_cluster.this[0].id + content { + cluster_identifier = aws_redshift_cluster.this[0].id + } } } } } +################################################################################ +# Scheduled Action IAM Role +################################################################################ + +data "aws_service_principal" "scheduler_redshift" { + count = var.create && var.create_scheduled_action_iam_role ? 1 : 0 + + service_name = "scheduler.redshift" +} + data "aws_iam_policy_document" "scheduled_action_assume" { count = var.create && var.create_scheduled_action_iam_role ? 1 : 0 @@ -202,7 +212,7 @@ data "aws_iam_policy_document" "scheduled_action_assume" { principals { type = "Service" - identifiers = ["scheduler.redshift.${local.dns_suffix}"] + identifiers = [data.aws_service_principal.scheduler_redshift[0].name] } } } @@ -273,12 +283,12 @@ resource "aws_redshift_usage_limit" "this" { cluster_identifier = aws_redshift_cluster.this[0].id amount = each.value.amount - breach_action = try(each.value.breach_action, null) + breach_action = each.value.breach_action feature_type = each.value.feature_type - limit_type = each.value.limit_type - period = try(each.value.period, null) + limit_type = try(coalesce(each.value.limit_type)) + period = each.value.period - tags = merge(var.tags, try(each.value.tags, {})) + tags = merge(var.tags, each.value.tags) } ################################################################################ @@ -297,13 +307,13 @@ resource "aws_redshift_authentication_profile" "this" { ################################################################################ resource "aws_redshift_logging" "this" { - count = var.create && length(var.logging) > 0 ? 1 : 0 + count = var.create && var.logging != null ? 1 : 0 cluster_identifier = aws_redshift_cluster.this[0].id - bucket_name = try(var.logging.bucket_name, null) - log_destination_type = try(var.logging.log_destination_type, null) - log_exports = try(var.logging.log_exports, null) - s3_key_prefix = try(var.logging.s3_key_prefix, null) + bucket_name = var.logging.bucket_name + log_destination_type = var.logging.log_destination_type + log_exports = var.logging.log_exports + s3_key_prefix = var.logging.s3_key_prefix } ################################################################################ @@ -311,13 +321,13 @@ resource "aws_redshift_logging" "this" { ################################################################################ resource "aws_redshift_snapshot_copy" "this" { - count = var.create && length(var.snapshot_copy) > 0 ? 1 : 0 + count = var.create && var.snapshot_copy != null ? 1 : 0 cluster_identifier = aws_redshift_cluster.this[0].id destination_region = var.snapshot_copy.destination_region - manual_snapshot_retention_period = try(var.snapshot_copy.manual_snapshot_retention_period, null) - retention_period = try(var.snapshot_copy.retention_period, null) - snapshot_copy_grant_name = try(var.snapshot_copy.grant_name, null) + manual_snapshot_retention_period = var.snapshot_copy.manual_snapshot_retention_period + retention_period = var.snapshot_copy.retention_period + snapshot_copy_grant_name = var.snapshot_copy.grant_name } ################################################################################ diff --git a/variables.tf b/variables.tf index c63c952..35a6e0b 100644 --- a/variables.tf +++ b/variables.tf @@ -101,8 +101,13 @@ variable "kms_key_arn" { variable "logging" { description = "Logging configuration for the cluster" - type = any - default = {} + type = object({ + bucket_name = optional(string) + log_destination_type = optional(string) + log_exports = optional(list(string)) + s3_key_prefix = optional(string) + }) + default = null } variable "maintenance_track_name" { @@ -117,7 +122,6 @@ variable "manual_snapshot_retention_period" { default = null } - variable "manage_master_password" { description = "Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password`. One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided" type = bool @@ -168,7 +172,7 @@ variable "owner_account" { } variable "port" { - description = "The port number on which the cluster accepts incoming connections. Default port is 5439" + description = "The port number on which the cluster accepts incoming connections. Default port is `5439`" type = number default = null } @@ -199,8 +203,13 @@ variable "snapshot_cluster_identifier" { variable "snapshot_copy" { description = "Configuration of automatic copy of snapshots from one region to another" - type = any - default = {} + type = object({ + destination_region = string + manual_snapshot_retention_period = optional(number) + retention_period = optional(number) + grant_name = optional(string) + }) + default = null } variable "snapshot_identifier" { @@ -217,8 +226,12 @@ variable "vpc_security_group_ids" { variable "cluster_timeouts" { description = "Create, update, and delete timeout configurations for the cluster" - type = map(string) - default = {} + type = object({ + create = optional(string) + update = optional(string) + delete = optional(string) + }) + default = null } ################################################################################ @@ -262,13 +275,16 @@ variable "parameter_group_description" { variable "parameter_group_family" { description = "The family of the Redshift parameter group" type = string - default = "redshift-1.0" + default = "redshift-2.0" } variable "parameter_group_parameters" { - description = "value" - type = map(any) - default = {} + description = "A list of Redshift parameters to apply" + type = list(object({ + name = string + value = string + })) + default = null } variable "parameter_group_tags" { @@ -356,11 +372,35 @@ variable "snapshot_schedule_force_destroy" { ################################################################################ variable "scheduled_actions" { - description = "Map of maps containing scheduled action definitions" - type = any - default = {} + description = "Map of scheduled action definitions to create" + type = map(object({ + name = optional(string) # Will fall back to key if not set + description = optional(string) + enable = optional(bool) + start_time = optional(string) + end_time = optional(string) + schedule = string + iam_role = optional(string) + target_action = object({ + pause_cluster = optional(object({})) + resize_cluster = optional(object({ + classic = optional(bool) + cluster_type = optional(string) + node_type = optional(string) + number_of_nodes = optional(number) + })) + resume_cluster = optional(object({})) + }) + + })) + default = {} + nullable = false } +################################################################################ +# Scheduled Action IAM Role +################################################################################ + variable "create_scheduled_action_iam_role" { description = "Determines whether a scheduled action IAM role is created" type = bool @@ -443,8 +483,16 @@ variable "endpoint_vpc_security_group_ids" { variable "usage_limits" { description = "Map of usage limit definitions to create" - type = any - default = {} + type = map(object({ + amount = number + breach_action = optional(string) + feature_type = string + limit_type = optional(string) # Will fall back to key if not set + period = optional(string) + tags = optional(map(string), {}) + })) + default = {} + nullable = false } ################################################################################ @@ -453,8 +501,12 @@ variable "usage_limits" { variable "authentication_profiles" { description = "Map of authentication profiles to create" - type = any - default = {} + type = map(object({ + name = optional(string) # Will fall back to key if not set + content = any + })) + default = {} + nullable = false } ################################################################################ @@ -496,31 +548,31 @@ variable "cloudwatch_log_group_tags" { ################################################################################ variable "manage_master_password_rotation" { - description = "Whether to manage the master user password rotation. Setting this value to false after previously having been set to true will disable automatic rotation." + description = "Whether to manage the master user password rotation. Setting this value to false after previously having been set to true will disable automatic rotation" type = bool default = false } variable "master_password_rotate_immediately" { - description = "Specifies whether to rotate the secret immediately or wait until the next scheduled rotation window." + description = "Specifies whether to rotate the secret immediately or wait until the next scheduled rotation window" type = bool default = null } variable "master_password_rotation_automatically_after_days" { - description = "Specifies the number of days between automatic scheduled rotations of the secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified." + description = "Specifies the number of days between automatic scheduled rotations of the secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified" type = number default = null } variable "master_password_rotation_duration" { - description = "The length of the rotation window in hours. For example, 3h for a three hour window." + description = "The length of the rotation window in hours. For example, 3h for a three hour window" type = string default = null } variable "master_password_rotation_schedule_expression" { - description = "A cron() or rate() expression that defines the schedule for rotating your secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified." + description = "A cron() or rate() expression that defines the schedule for rotating your secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified" type = string default = null } diff --git a/wrappers/main.tf b/wrappers/main.tf index 96644ee..c7f020e 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -14,7 +14,7 @@ module "wrapper" { cloudwatch_log_group_skip_destroy = try(each.value.cloudwatch_log_group_skip_destroy, var.defaults.cloudwatch_log_group_skip_destroy, null) cloudwatch_log_group_tags = try(each.value.cloudwatch_log_group_tags, var.defaults.cloudwatch_log_group_tags, {}) cluster_identifier = try(each.value.cluster_identifier, var.defaults.cluster_identifier, "") - cluster_timeouts = try(each.value.cluster_timeouts, var.defaults.cluster_timeouts, {}) + cluster_timeouts = try(each.value.cluster_timeouts, var.defaults.cluster_timeouts, null) cluster_version = try(each.value.cluster_version, var.defaults.cluster_version, null) create = try(each.value.create, var.defaults.create, true) create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, false) @@ -41,7 +41,7 @@ module "wrapper" { iam_role_tags = try(each.value.iam_role_tags, var.defaults.iam_role_tags, {}) iam_role_use_name_prefix = try(each.value.iam_role_use_name_prefix, var.defaults.iam_role_use_name_prefix, true) kms_key_arn = try(each.value.kms_key_arn, var.defaults.kms_key_arn, null) - logging = try(each.value.logging, var.defaults.logging, {}) + logging = try(each.value.logging, var.defaults.logging, null) maintenance_track_name = try(each.value.maintenance_track_name, var.defaults.maintenance_track_name, null) manage_master_password = try(each.value.manage_master_password, var.defaults.manage_master_password, false) manage_master_password_rotation = try(each.value.manage_master_password_rotation, var.defaults.manage_master_password_rotation, false) @@ -58,9 +58,9 @@ module "wrapper" { number_of_nodes = try(each.value.number_of_nodes, var.defaults.number_of_nodes, 1) owner_account = try(each.value.owner_account, var.defaults.owner_account, null) parameter_group_description = try(each.value.parameter_group_description, var.defaults.parameter_group_description, null) - parameter_group_family = try(each.value.parameter_group_family, var.defaults.parameter_group_family, "redshift-1.0") + parameter_group_family = try(each.value.parameter_group_family, var.defaults.parameter_group_family, "redshift-2.0") parameter_group_name = try(each.value.parameter_group_name, var.defaults.parameter_group_name, null) - parameter_group_parameters = try(each.value.parameter_group_parameters, var.defaults.parameter_group_parameters, {}) + parameter_group_parameters = try(each.value.parameter_group_parameters, var.defaults.parameter_group_parameters, null) parameter_group_tags = try(each.value.parameter_group_tags, var.defaults.parameter_group_tags, {}) port = try(each.value.port, var.defaults.port, null) preferred_maintenance_window = try(each.value.preferred_maintenance_window, var.defaults.preferred_maintenance_window, "sat:10:00-sat:10:30") @@ -68,7 +68,7 @@ module "wrapper" { scheduled_actions = try(each.value.scheduled_actions, var.defaults.scheduled_actions, {}) skip_final_snapshot = try(each.value.skip_final_snapshot, var.defaults.skip_final_snapshot, true) snapshot_cluster_identifier = try(each.value.snapshot_cluster_identifier, var.defaults.snapshot_cluster_identifier, null) - snapshot_copy = try(each.value.snapshot_copy, var.defaults.snapshot_copy, {}) + snapshot_copy = try(each.value.snapshot_copy, var.defaults.snapshot_copy, null) snapshot_identifier = try(each.value.snapshot_identifier, var.defaults.snapshot_identifier, null) snapshot_schedule_definitions = try(each.value.snapshot_schedule_definitions, var.defaults.snapshot_schedule_definitions, []) snapshot_schedule_description = try(each.value.snapshot_schedule_description, var.defaults.snapshot_schedule_description, null) From 25adcef9451dc302ad05278f49e5aab204bbcb7c Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 13 Nov 2025 20:37:45 -0600 Subject: [PATCH 04/11] feat: Add support for `region` resource level argument --- README.md | 1 + docs/UPGRADE-7.0.md | 2 +- examples/complete/README.md | 1 - examples/complete/main.tf | 15 +++++++------- main.tf | 40 ++++++++++++++++++++++++++++++------- variables.tf | 6 ++++++ wrappers/main.tf | 1 + 7 files changed, 50 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 08d14dc..d29c536 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,7 @@ No modules. | [port](#input\_port) | The port number on which the cluster accepts incoming connections. Default port is `5439` | `number` | `null` | no | | [preferred\_maintenance\_window](#input\_preferred\_maintenance\_window) | The weekly time range (in UTC) during which automated cluster maintenance can occur. Format: `ddd:hh24:mi-ddd:hh24:mi` | `string` | `"sat:10:00-sat:10:30"` | no | | [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `false` | no | +| [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | | [scheduled\_actions](#input\_scheduled\_actions) | Map of scheduled action definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
description = optional(string)
enable = optional(bool)
start_time = optional(string)
end_time = optional(string)
schedule = string
iam_role = optional(string)
target_action = object({
pause_cluster = optional(object({}))
resize_cluster = optional(object({
classic = optional(bool)
cluster_type = optional(string)
node_type = optional(string)
number_of_nodes = optional(number)
}))
resume_cluster = optional(object({}))
})

}))
| `{}` | no | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Determines whether a final snapshot of the cluster is created before Redshift deletes the cluster. If true, a final cluster snapshot is not created. If false , a final cluster snapshot is created before the cluster is deleted | `bool` | `true` | no | | [snapshot\_cluster\_identifier](#input\_snapshot\_cluster\_identifier) | The name of the cluster the source snapshot was created from | `string` | `null` | no | diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md index 272f35c..b2e0064 100644 --- a/docs/UPGRADE-7.0.md +++ b/docs/UPGRADE-7.0.md @@ -12,7 +12,7 @@ Please consult the `examples` directory for reference example configurations. If ### Added -- +- Support for `region` argument to specify the AWS region for the resources created if different from the provider region. ### Modified diff --git a/examples/complete/README.md b/examples/complete/README.md index f9d30dd..d65039a 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -31,7 +31,6 @@ Note that this example may create resources which cost money. Run `terraform des | Name | Version | |------|---------| | [aws](#provider\_aws) | >= 6.21 | -| [aws.us\_east\_1](#provider\_aws.us\_east\_1) | >= 6.21 | ## Modules diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 006b569..199d7d6 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -2,13 +2,14 @@ provider "aws" { region = local.region } -provider "aws" { - alias = "us_east_1" - region = "us-east-1" +data "aws_availability_zones" "available" { + # Exclude local zones + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } } -data "aws_availability_zones" "available" {} - locals { name = "ex-${basename(path.cwd)}" region = "eu-west-1" @@ -202,7 +203,7 @@ module "redshift" { resource "aws_redshift_snapshot_copy_grant" "useast1" { # Grants are declared outside of module because they are generally performed # in the destination region and we do not embed multiple providers in the root module - provider = aws.us_east_1 + region = "us-east-1" snapshot_copy_grant_name = "${local.name}-us-east-1" kms_key_id = aws_kms_key.redshift_us_east_1.arn @@ -307,7 +308,7 @@ resource "aws_kms_key" "redshift" { } resource "aws_kms_key" "redshift_us_east_1" { - provider = aws.us_east_1 + region = "us-east-1" description = "Customer managed key for encrypting Redshift snapshot cross-region" deletion_window_in_days = 7 diff --git a/main.tf b/main.tf index 6b52ff3..1bd8b2e 100644 --- a/main.tf +++ b/main.tf @@ -10,6 +10,8 @@ locals { resource "aws_redshift_cluster" "this" { count = var.create ? 1 : 0 + region = var.region + allow_version_upgrade = var.allow_version_upgrade apply_immediately = var.apply_immediately automated_snapshot_retention_period = var.automated_snapshot_retention_period @@ -74,6 +76,8 @@ resource "aws_redshift_cluster" "this" { resource "aws_redshift_cluster_iam_roles" "this" { count = var.create && length(var.iam_role_arns) > 0 ? 1 : 0 + region = var.region + cluster_identifier = aws_redshift_cluster.this[0].id iam_role_arns = var.iam_role_arns default_iam_role_arn = var.default_iam_role_arn @@ -86,6 +90,8 @@ resource "aws_redshift_cluster_iam_roles" "this" { resource "aws_redshift_parameter_group" "this" { count = var.create && var.create_parameter_group ? 1 : 0 + region = var.region + name = coalesce(var.parameter_group_name, replace(var.cluster_identifier, ".", "-")) description = var.parameter_group_description family = var.parameter_group_family @@ -109,6 +115,8 @@ resource "aws_redshift_parameter_group" "this" { resource "aws_redshift_subnet_group" "this" { count = var.create && var.create_subnet_group ? 1 : 0 + region = var.region + name = coalesce(var.subnet_group_name, var.cluster_identifier) description = var.subnet_group_description subnet_ids = var.subnet_ids @@ -123,6 +131,8 @@ resource "aws_redshift_subnet_group" "this" { resource "aws_redshift_snapshot_schedule" "this" { count = var.create && var.create_snapshot_schedule ? 1 : 0 + region = var.region + identifier = var.use_snapshot_identifier_prefix ? null : var.snapshot_schedule_identifier identifier_prefix = var.use_snapshot_identifier_prefix ? "${var.snapshot_schedule_identifier}-" : null description = var.snapshot_schedule_description @@ -135,6 +145,8 @@ resource "aws_redshift_snapshot_schedule" "this" { resource "aws_redshift_snapshot_schedule_association" "this" { count = var.create && var.create_snapshot_schedule ? 1 : 0 + region = var.region + cluster_identifier = aws_redshift_cluster.this[0].id schedule_identifier = aws_redshift_snapshot_schedule.this[0].id } @@ -150,6 +162,8 @@ locals { resource "aws_redshift_scheduled_action" "this" { for_each = { for k, v in var.scheduled_actions : k => v if var.create } + region = var.region + name = try(coalesce(each.value.name, each.key)) description = each.value.description enable = each.value.enable @@ -265,8 +279,9 @@ resource "aws_iam_role_policy" "scheduled_action" { resource "aws_redshift_endpoint_access" "this" { count = var.create && var.create_endpoint_access ? 1 : 0 - cluster_identifier = aws_redshift_cluster.this[0].id + region = var.region + cluster_identifier = aws_redshift_cluster.this[0].id endpoint_name = var.endpoint_name resource_owner = var.endpoint_resource_owner subnet_group_name = coalesce(var.endpoint_subnet_group_name, local.subnet_group_name) @@ -280,13 +295,14 @@ resource "aws_redshift_endpoint_access" "this" { resource "aws_redshift_usage_limit" "this" { for_each = { for k, v in var.usage_limits : k => v if var.create } - cluster_identifier = aws_redshift_cluster.this[0].id + region = var.region - amount = each.value.amount - breach_action = each.value.breach_action - feature_type = each.value.feature_type - limit_type = try(coalesce(each.value.limit_type)) - period = each.value.period + amount = each.value.amount + breach_action = each.value.breach_action + cluster_identifier = aws_redshift_cluster.this[0].id + feature_type = each.value.feature_type + limit_type = try(coalesce(each.value.limit_type)) + period = each.value.period tags = merge(var.tags, each.value.tags) } @@ -298,6 +314,8 @@ resource "aws_redshift_usage_limit" "this" { resource "aws_redshift_authentication_profile" "this" { for_each = { for k, v in var.authentication_profiles : k => v if var.create } + region = var.region + authentication_profile_name = try(each.value.name, each.key) authentication_profile_content = jsonencode(each.value.content) } @@ -309,6 +327,8 @@ resource "aws_redshift_authentication_profile" "this" { resource "aws_redshift_logging" "this" { count = var.create && var.logging != null ? 1 : 0 + region = var.region + cluster_identifier = aws_redshift_cluster.this[0].id bucket_name = var.logging.bucket_name log_destination_type = var.logging.log_destination_type @@ -323,6 +343,8 @@ resource "aws_redshift_logging" "this" { resource "aws_redshift_snapshot_copy" "this" { count = var.create && var.snapshot_copy != null ? 1 : 0 + region = var.region + cluster_identifier = aws_redshift_cluster.this[0].id destination_region = var.snapshot_copy.destination_region manual_snapshot_retention_period = var.snapshot_copy.manual_snapshot_retention_period @@ -337,6 +359,8 @@ resource "aws_redshift_snapshot_copy" "this" { resource "aws_cloudwatch_log_group" "this" { for_each = toset([for log in try(var.logging.log_exports, []) : log if var.create && var.create_cloudwatch_log_group]) + region = var.region + name = "/aws/redshift/cluster/${var.cluster_identifier}/${each.value}" retention_in_days = var.cloudwatch_log_group_retention_in_days kms_key_id = var.cloudwatch_log_group_kms_key_id @@ -352,6 +376,8 @@ resource "aws_cloudwatch_log_group" "this" { resource "aws_secretsmanager_secret_rotation" "this" { count = var.create && var.manage_master_password && var.manage_master_password_rotation ? 1 : 0 + region = var.region + secret_id = aws_redshift_cluster.this[0].master_password_secret_arn rotate_immediately = var.master_password_rotate_immediately diff --git a/variables.tf b/variables.tf index 35a6e0b..60313d4 100644 --- a/variables.tf +++ b/variables.tf @@ -4,6 +4,12 @@ variable "create" { default = true } +variable "region" { + description = "Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration" + type = string + default = null +} + variable "tags" { description = "A map of tags to add to all resources" type = map(string) diff --git a/wrappers/main.tf b/wrappers/main.tf index c7f020e..7ae298b 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -65,6 +65,7 @@ module "wrapper" { port = try(each.value.port, var.defaults.port, null) preferred_maintenance_window = try(each.value.preferred_maintenance_window, var.defaults.preferred_maintenance_window, "sat:10:00-sat:10:30") publicly_accessible = try(each.value.publicly_accessible, var.defaults.publicly_accessible, false) + region = try(each.value.region, var.defaults.region, null) scheduled_actions = try(each.value.scheduled_actions, var.defaults.scheduled_actions, {}) skip_final_snapshot = try(each.value.skip_final_snapshot, var.defaults.skip_final_snapshot, true) snapshot_cluster_identifier = try(each.value.snapshot_cluster_identifier, var.defaults.snapshot_cluster_identifier, null) From 2ed51aae59d80cae636fb9111dd4c3f5d68d1501 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Thu, 13 Nov 2025 20:46:29 -0600 Subject: [PATCH 05/11] feat: Add support for creating a security group by default for the cluster --- README.md | 13 ++++++- docs/UPGRADE-7.0.md | 10 ++++- examples/complete/main.tf | 11 ++---- main.tf | 74 ++++++++++++++++++++++++++++++++++- variables.tf | 82 ++++++++++++++++++++++++++++++++++++++- wrappers/main.tf | 10 ++++- 6 files changed, 188 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d29c536..a75fd1d 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,9 @@ No modules. | [aws_redshift_subnet_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/redshift_subnet_group) | resource | | [aws_redshift_usage_limit.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/redshift_usage_limit) | resource | | [aws_secretsmanager_secret_rotation.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_rotation) | resource | +| [aws_security_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_vpc_security_group_egress_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [aws_vpc_security_group_ingress_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | | [aws_iam_policy_document.scheduled_action](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.scheduled_action_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_service_principal.scheduler_redshift](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/service_principal) | data source | @@ -236,6 +239,7 @@ No modules. | [create\_endpoint\_access](#input\_create\_endpoint\_access) | Determines whether to create an endpoint access (managed VPC endpoint) | `bool` | `false` | no | | [create\_parameter\_group](#input\_create\_parameter\_group) | Determines whether to create a parameter group or use existing | `bool` | `true` | no | | [create\_scheduled\_action\_iam\_role](#input\_create\_scheduled\_action\_iam\_role) | Determines whether a scheduled action IAM role is created | `bool` | `false` | no | +| [create\_security\_group](#input\_create\_security\_group) | Determines whether to create security group for Redshift cluster | `bool` | `true` | no | | [create\_snapshot\_schedule](#input\_create\_snapshot\_schedule) | Determines whether to create a snapshot schedule | `bool` | `false` | no | | [create\_subnet\_group](#input\_create\_subnet\_group) | Determines whether to create a subnet group or use existing | `bool` | `true` | no | | [database\_name](#input\_database\_name) | The name of the first database to be created when the cluster is created. If you do not provide a name, Amazon Redshift will create a default database called `dev` | `string` | `null` | no | @@ -277,11 +281,17 @@ No modules. | [parameter\_group\_name](#input\_parameter\_group\_name) | The name of the Redshift parameter group, existing or to be created | `string` | `null` | no | | [parameter\_group\_parameters](#input\_parameter\_group\_parameters) | A list of Redshift parameters to apply |
list(object({
name = string
value = string
}))
| `null` | no | | [parameter\_group\_tags](#input\_parameter\_group\_tags) | Additional tags to add to the parameter group | `map(string)` | `{}` | no | -| [port](#input\_port) | The port number on which the cluster accepts incoming connections. Default port is `5439` | `number` | `null` | no | +| [port](#input\_port) | The port number on which the cluster accepts incoming connections. Default port is `5439` | `number` | `5439` | no | | [preferred\_maintenance\_window](#input\_preferred\_maintenance\_window) | The weekly time range (in UTC) during which automated cluster maintenance can occur. Format: `ddd:hh24:mi-ddd:hh24:mi` | `string` | `"sat:10:00-sat:10:30"` | no | | [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `false` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | | [scheduled\_actions](#input\_scheduled\_actions) | Map of scheduled action definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
description = optional(string)
enable = optional(bool)
start_time = optional(string)
end_time = optional(string)
schedule = string
iam_role = optional(string)
target_action = object({
pause_cluster = optional(object({}))
resize_cluster = optional(object({
classic = optional(bool)
cluster_type = optional(string)
node_type = optional(string)
number_of_nodes = optional(number)
}))
resume_cluster = optional(object({}))
})

}))
| `{}` | no | +| [security\_group\_description](#input\_security\_group\_description) | The description of the security group. If value is set to empty string it will contain cluster name in the description | `string` | `null` | no | +| [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Map of security group egress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
region = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
| `{}` | no | +| [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Map of security group ingress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
region = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
| `{}` | no | +| [security\_group\_name](#input\_security\_group\_name) | The security group name | `string` | `""` | no | +| [security\_group\_tags](#input\_security\_group\_tags) | Additional tags for the security group | `map(string)` | `{}` | no | +| [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name`) is used as a prefix | `bool` | `true` | no | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Determines whether a final snapshot of the cluster is created before Redshift deletes the cluster. If true, a final cluster snapshot is not created. If false , a final cluster snapshot is created before the cluster is deleted | `bool` | `true` | no | | [snapshot\_cluster\_identifier](#input\_snapshot\_cluster\_identifier) | The name of the cluster the source snapshot was created from | `string` | `null` | no | | [snapshot\_copy](#input\_snapshot\_copy) | Configuration of automatic copy of snapshots from one region to another |
object({
destination_region = string
manual_snapshot_retention_period = optional(number)
retention_period = optional(number)
grant_name = optional(string)
})
| `null` | no | @@ -297,6 +307,7 @@ No modules. | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | | [usage\_limits](#input\_usage\_limits) | Map of usage limit definitions to create |
map(object({
amount = number
breach_action = optional(string)
feature_type = string
limit_type = optional(string) # Will fall back to key if not set
period = optional(string)
tags = optional(map(string), {})
}))
| `{}` | no | | [use\_snapshot\_identifier\_prefix](#input\_use\_snapshot\_identifier\_prefix) | Determines whether the identifier (`snapshot_schedule_identifier`) is used as a prefix | `bool` | `true` | no | +| [vpc\_id](#input\_vpc\_id) | ID of the VPC where to create security group | `string` | `""` | no | | [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | A list of Virtual Private Cloud (VPC) security groups to be associated with the cluster | `list(string)` | `[]` | no | ## Outputs diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md index b2e0064..ea78c69 100644 --- a/docs/UPGRADE-7.0.md +++ b/docs/UPGRADE-7.0.md @@ -13,6 +13,7 @@ Please consult the `examples` directory for reference example configurations. If ### Added - Support for `region` argument to specify the AWS region for the resources created if different from the provider region. +- Support for creating security group ### Modified @@ -37,7 +38,14 @@ Please consult the `examples` directory for reference example configurations. If 3. Added variables: - - + - `region` + - `create_security_group` + - `security_group_name` + - `security_group_use_name_prefix` + - `security_group_description` + - `vpc_id` + - `security_group_ingress_rules` + - `security_group_egress_rules` 4. Removed outputs: diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 199d7d6..5f4bc5c 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -52,9 +52,8 @@ module "redshift" { encrypted = true kms_key_arn = aws_kms_key.redshift.arn - enhanced_vpc_routing = true - vpc_security_group_ids = [module.security_group.security_group_id] - subnet_ids = module.vpc.redshift_subnets + enhanced_vpc_routing = true + subnet_ids = module.vpc.redshift_subnets # Only available when using the ra3.x type availability_zone_relocation_enabled = true @@ -221,8 +220,7 @@ module "with_cloudwatch_logging" { cluster_identifier = "${local.name}-with-cloudwatch-logging" node_type = "dc2.large" - vpc_security_group_ids = [module.security_group.security_group_id] - subnet_ids = module.vpc.redshift_subnets + subnet_ids = module.vpc.redshift_subnets create_cloudwatch_log_group = true cloudwatch_log_group_retention_in_days = 7 @@ -244,8 +242,7 @@ module "default" { cluster_identifier = "${local.name}-default" node_type = "dc2.large" - vpc_security_group_ids = [module.security_group.security_group_id] - subnet_ids = module.vpc.redshift_subnets + subnet_ids = module.vpc.redshift_subnets tags = local.tags } diff --git a/main.tf b/main.tf index 1bd8b2e..9c0cc59 100644 --- a/main.tf +++ b/main.tf @@ -48,7 +48,7 @@ resource "aws_redshift_cluster" "this" { snapshot_cluster_identifier = var.snapshot_cluster_identifier snapshot_identifier = var.snapshot_identifier - vpc_security_group_ids = var.vpc_security_group_ids + vpc_security_group_ids = compact(concat(aws_security_group.this[*].id, var.vpc_security_group_ids)) tags = var.tags @@ -387,3 +387,75 @@ resource "aws_secretsmanager_secret_rotation" "this" { schedule_expression = var.master_password_rotation_schedule_expression } } + +################################################################################ +# Security Group +################################################################################ + +locals { + create_security_group = var.create && var.create_security_group + security_group_name = try(coalesce(var.security_group_name, var.cluster_identifier), "") +} + +resource "aws_security_group" "this" { + count = local.create_security_group ? 1 : 0 + + region = var.region + + name = var.security_group_use_name_prefix ? null : local.security_group_name + name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null + vpc_id = var.vpc_id + description = coalesce(var.security_group_description, "Control traffic to/from Redshift cluster ${var.security_group_name}") + + tags = merge( + var.tags, + var.security_group_tags, + { "Name" = local.security_group_name } + ) + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_vpc_security_group_ingress_rule" "this" { + for_each = { for k, v in var.security_group_ingress_rules : k => v if var.security_group_ingress_rules != null && local.create_security_group } + + region = var.region + + cidr_ipv4 = each.value.cidr_ipv4 + cidr_ipv6 = each.value.cidr_ipv6 + description = each.value.description + from_port = try(coalesce(each.value.from_port, var.port), null) + ip_protocol = each.value.ip_protocol + prefix_list_id = each.value.prefix_list_id + referenced_security_group_id = each.value.referenced_security_group_id == "self" ? aws_security_group.this[0].id : each.value.referenced_security_group_id + security_group_id = aws_security_group.this[0].id + tags = merge( + var.tags, + { "Name" = coalesce(each.value.name, "${local.security_group_name}-${each.key}") }, + each.value.tags + ) + to_port = try(coalesce(each.value.to_port, each.value.from_port, var.port), null) +} + +resource "aws_vpc_security_group_egress_rule" "this" { + for_each = { for k, v in var.security_group_egress_rules : k => v if var.security_group_egress_rules != null && local.create_security_group } + + region = var.region + + cidr_ipv4 = each.value.cidr_ipv4 + cidr_ipv6 = each.value.cidr_ipv6 + description = each.value.description + from_port = try(coalesce(each.value.from_port, each.value.to_port, var.port), null) + ip_protocol = each.value.ip_protocol + prefix_list_id = each.value.prefix_list_id + referenced_security_group_id = each.value.referenced_security_group_id == "self" ? aws_security_group.this[0].id : each.value.referenced_security_group_id + security_group_id = aws_security_group.this[0].id + tags = merge( + var.tags, + { "Name" = coalesce(each.value.name, "${local.security_group_name}-${each.key}") }, + each.value.tags + ) + to_port = try(coalesce(each.value.to_port, var.port), null) +} diff --git a/variables.tf b/variables.tf index 60313d4..baec77a 100644 --- a/variables.tf +++ b/variables.tf @@ -180,7 +180,7 @@ variable "owner_account" { variable "port" { description = "The port number on which the cluster accepts incoming connections. Default port is `5439`" type = number - default = null + default = 5439 } variable "preferred_maintenance_window" { @@ -582,3 +582,83 @@ variable "master_password_rotation_schedule_expression" { type = string default = null } + +################################################################################ +# Security Group +################################################################################ + +variable "create_security_group" { + description = "Determines whether to create security group for Redshift cluster" + type = bool + default = true +} + +variable "security_group_name" { + description = "The security group name" + type = string + default = "" +} + +variable "security_group_use_name_prefix" { + description = "Determines whether the security group name (`security_group_name`) is used as a prefix" + type = bool + default = true +} + +variable "security_group_description" { + description = "The description of the security group. If value is set to empty string it will contain cluster name in the description" + type = string + default = null +} + +variable "vpc_id" { + description = "ID of the VPC where to create security group" + type = string + default = "" +} + +variable "security_group_ingress_rules" { + description = "Map of security group ingress rules to add to the security group created" + type = map(object({ + name = optional(string) + + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(number) + ip_protocol = optional(string, "tcp") + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + region = optional(string) + tags = optional(map(string), {}) + to_port = optional(number) + })) + default = {} + nullable = false +} + +variable "security_group_egress_rules" { + description = "Map of security group egress rules to add to the security group created" + type = map(object({ + name = optional(string) + + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(number) + ip_protocol = optional(string, "tcp") + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + region = optional(string) + tags = optional(map(string), {}) + to_port = optional(number) + })) + default = {} + nullable = false +} + +variable "security_group_tags" { + description = "Additional tags for the security group" + type = map(string) + default = {} +} diff --git a/wrappers/main.tf b/wrappers/main.tf index 7ae298b..52bf914 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -21,6 +21,7 @@ module "wrapper" { create_endpoint_access = try(each.value.create_endpoint_access, var.defaults.create_endpoint_access, false) create_parameter_group = try(each.value.create_parameter_group, var.defaults.create_parameter_group, true) create_scheduled_action_iam_role = try(each.value.create_scheduled_action_iam_role, var.defaults.create_scheduled_action_iam_role, false) + create_security_group = try(each.value.create_security_group, var.defaults.create_security_group, true) create_snapshot_schedule = try(each.value.create_snapshot_schedule, var.defaults.create_snapshot_schedule, false) create_subnet_group = try(each.value.create_subnet_group, var.defaults.create_subnet_group, true) database_name = try(each.value.database_name, var.defaults.database_name, null) @@ -62,11 +63,17 @@ module "wrapper" { parameter_group_name = try(each.value.parameter_group_name, var.defaults.parameter_group_name, null) parameter_group_parameters = try(each.value.parameter_group_parameters, var.defaults.parameter_group_parameters, null) parameter_group_tags = try(each.value.parameter_group_tags, var.defaults.parameter_group_tags, {}) - port = try(each.value.port, var.defaults.port, null) + port = try(each.value.port, var.defaults.port, 5439) preferred_maintenance_window = try(each.value.preferred_maintenance_window, var.defaults.preferred_maintenance_window, "sat:10:00-sat:10:30") publicly_accessible = try(each.value.publicly_accessible, var.defaults.publicly_accessible, false) region = try(each.value.region, var.defaults.region, null) scheduled_actions = try(each.value.scheduled_actions, var.defaults.scheduled_actions, {}) + security_group_description = try(each.value.security_group_description, var.defaults.security_group_description, null) + security_group_egress_rules = try(each.value.security_group_egress_rules, var.defaults.security_group_egress_rules, {}) + security_group_ingress_rules = try(each.value.security_group_ingress_rules, var.defaults.security_group_ingress_rules, {}) + security_group_name = try(each.value.security_group_name, var.defaults.security_group_name, "") + security_group_tags = try(each.value.security_group_tags, var.defaults.security_group_tags, {}) + security_group_use_name_prefix = try(each.value.security_group_use_name_prefix, var.defaults.security_group_use_name_prefix, true) skip_final_snapshot = try(each.value.skip_final_snapshot, var.defaults.skip_final_snapshot, true) snapshot_cluster_identifier = try(each.value.snapshot_cluster_identifier, var.defaults.snapshot_cluster_identifier, null) snapshot_copy = try(each.value.snapshot_copy, var.defaults.snapshot_copy, null) @@ -82,5 +89,6 @@ module "wrapper" { tags = try(each.value.tags, var.defaults.tags, {}) usage_limits = try(each.value.usage_limits, var.defaults.usage_limits, {}) use_snapshot_identifier_prefix = try(each.value.use_snapshot_identifier_prefix, var.defaults.use_snapshot_identifier_prefix, true) + vpc_id = try(each.value.vpc_id, var.defaults.vpc_id, "") vpc_security_group_ids = try(each.value.vpc_security_group_ids, var.defaults.vpc_security_group_ids, []) } From c1c3f92ca8dce2feade8fa76e76975946495c9a5 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 14 Nov 2025 09:09:21 -0600 Subject: [PATCH 06/11] feat: Support creating multiple VPC access endpoints --- README.md | 8 ++------ docs/UPGRADE-7.0.md | 6 ++++++ examples/complete/main.tf | 11 +++++++---- main.tf | 10 +++++----- variables.tf | 39 ++++++++++----------------------------- wrappers/main.tf | 6 +----- 6 files changed, 31 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index a75fd1d..94ced2a 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,6 @@ No modules. | [cluster\_version](#input\_cluster\_version) | The version of the Amazon Redshift engine software that you want to deploy on the cluster. The version selected runs on all the nodes in the cluster | `string` | `null` | no | | [create](#input\_create) | Determines whether to create Redshift cluster and resources (affects all resources) | `bool` | `true` | no | | [create\_cloudwatch\_log\_group](#input\_create\_cloudwatch\_log\_group) | Determines whether a CloudWatch log group is created for each `var.logging.log_exports` | `bool` | `false` | no | -| [create\_endpoint\_access](#input\_create\_endpoint\_access) | Determines whether to create an endpoint access (managed VPC endpoint) | `bool` | `false` | no | | [create\_parameter\_group](#input\_create\_parameter\_group) | Determines whether to create a parameter group or use existing | `bool` | `true` | no | | [create\_scheduled\_action\_iam\_role](#input\_create\_scheduled\_action\_iam\_role) | Determines whether a scheduled action IAM role is created | `bool` | `false` | no | | [create\_security\_group](#input\_create\_security\_group) | Determines whether to create security group for Redshift cluster | `bool` | `true` | no | @@ -246,10 +245,7 @@ No modules. | [default\_iam\_role\_arn](#input\_default\_iam\_role\_arn) | The Amazon Resource Name (ARN) for the IAM role that was set as default for the cluster when the cluster was created | `string` | `null` | no | | [elastic\_ip](#input\_elastic\_ip) | The Elastic IP (EIP) address for the cluster | `string` | `null` | no | | [encrypted](#input\_encrypted) | If `true`, the data in the cluster is encrypted at rest | `bool` | `true` | no | -| [endpoint\_name](#input\_endpoint\_name) | The Redshift-managed VPC endpoint name | `string` | `""` | no | -| [endpoint\_resource\_owner](#input\_endpoint\_resource\_owner) | The Amazon Web Services account ID of the owner of the cluster. This is only required if the cluster is in another Amazon Web Services account | `string` | `null` | no | -| [endpoint\_subnet\_group\_name](#input\_endpoint\_subnet\_group\_name) | The subnet group from which Amazon Redshift chooses the subnet to deploy the endpoint | `string` | `""` | no | -| [endpoint\_vpc\_security\_group\_ids](#input\_endpoint\_vpc\_security\_group\_ids) | The security group IDs to use for the endpoint access (managed VPC endpoint) | `list(string)` | `[]` | no | +| [endpoint\_access](#input\_endpoint\_access) | Map of endpoint access (managed VPC endpoint) definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
resource_owner = optional(string)
subnet_group_name = string
vpc_security_group_ids = optional(list(string))
}))
| `{}` | no | | [enhanced\_vpc\_routing](#input\_enhanced\_vpc\_routing) | If `true`, enhanced VPC routing is enabled | `bool` | `null` | no | | [final\_snapshot\_identifier](#input\_final\_snapshot\_identifier) | The identifier of the final snapshot that is to be created immediately before deleting the cluster. If this parameter is provided, `skip_final_snapshot` must be `false` | `string` | `null` | no | | [iam\_role\_arns](#input\_iam\_role\_arns) | A list of IAM Role ARNs to associate with the cluster. A Maximum of 10 can be associated to the cluster at any time | `list(string)` | `[]` | no | @@ -285,7 +281,7 @@ No modules. | [preferred\_maintenance\_window](#input\_preferred\_maintenance\_window) | The weekly time range (in UTC) during which automated cluster maintenance can occur. Format: `ddd:hh24:mi-ddd:hh24:mi` | `string` | `"sat:10:00-sat:10:30"` | no | | [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `false` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | -| [scheduled\_actions](#input\_scheduled\_actions) | Map of scheduled action definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
description = optional(string)
enable = optional(bool)
start_time = optional(string)
end_time = optional(string)
schedule = string
iam_role = optional(string)
target_action = object({
pause_cluster = optional(object({}))
resize_cluster = optional(object({
classic = optional(bool)
cluster_type = optional(string)
node_type = optional(string)
number_of_nodes = optional(number)
}))
resume_cluster = optional(object({}))
})

}))
| `{}` | no | +| [scheduled\_actions](#input\_scheduled\_actions) | Map of scheduled action definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
description = optional(string)
enable = optional(bool)
start_time = optional(string)
end_time = optional(string)
schedule = string
iam_role = optional(string)
target_action = object({
pause_cluster = optional(object({}))
resize_cluster = optional(object({
classic = optional(bool)
cluster_type = optional(string)
node_type = optional(string)
number_of_nodes = optional(number)
}))
resume_cluster = optional(object({}))
})
}))
| `{}` | no | | [security\_group\_description](#input\_security\_group\_description) | The description of the security group. If value is set to empty string it will contain cluster name in the description | `string` | `null` | no | | [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Map of security group egress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
region = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
| `{}` | no | | [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Map of security group ingress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
region = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
| `{}` | no | diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md index ea78c69..56373ee 100644 --- a/docs/UPGRADE-7.0.md +++ b/docs/UPGRADE-7.0.md @@ -31,6 +31,12 @@ Please consult the `examples` directory for reference example configurations. If - `create_random_password` removed along with support for generating a random password - `random_password_length` removed along with support for generating a random password - `aqua_configuration_status` argument was deprecated + - The variables for endpoint access have been nested under a single, top-level `endpoint_access` variable: + - `create_endpoint_access` removed - set `endpoint_access` to `null` or omit to disable + - `endpoint_name` + - `endpoint_resource_owner` + - `endpoint_subnet_group_name` + - `endpoint_vpc_security_group_ids` 2. Renamed variables: diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 5f4bc5c..097e3ac 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -153,10 +153,13 @@ module "redshift" { } # Endpoint access - only available when using the ra3.x type - create_endpoint_access = true - endpoint_name = "${local.name}-example" - endpoint_subnet_group_name = aws_redshift_subnet_group.endpoint.id - endpoint_vpc_security_group_ids = [module.security_group.security_group_id] + endpoint_access = { + example = { + name = "${local.name}-example" + subnet_group_name = aws_redshift_subnet_group.endpoint.id + vpc_security_group_ids = [module.security_group.security_group_id] + } + } # Usage limits usage_limits = { diff --git a/main.tf b/main.tf index 9c0cc59..4cf5427 100644 --- a/main.tf +++ b/main.tf @@ -277,15 +277,15 @@ resource "aws_iam_role_policy" "scheduled_action" { ################################################################################ resource "aws_redshift_endpoint_access" "this" { - count = var.create && var.create_endpoint_access ? 1 : 0 + for_each = var.create && var.endpoint_access != null ? var.endpoint_access : {} region = var.region cluster_identifier = aws_redshift_cluster.this[0].id - endpoint_name = var.endpoint_name - resource_owner = var.endpoint_resource_owner - subnet_group_name = coalesce(var.endpoint_subnet_group_name, local.subnet_group_name) - vpc_security_group_ids = var.endpoint_vpc_security_group_ids + endpoint_name = try(coalesce(each.value.name, each.key)) + resource_owner = each.value.resource_owner + subnet_group_name = each.value.subnet_group_name + vpc_security_group_ids = each.value.vpc_security_group_ids } ################################################################################ diff --git a/variables.tf b/variables.tf index baec77a..07ef34c 100644 --- a/variables.tf +++ b/variables.tf @@ -397,7 +397,6 @@ variable "scheduled_actions" { })) resume_cluster = optional(object({})) }) - })) default = {} nullable = false @@ -453,34 +452,16 @@ variable "iam_role_tags" { # Endpoint Access ################################################################################ -variable "create_endpoint_access" { - description = "Determines whether to create an endpoint access (managed VPC endpoint)" - type = bool - default = false -} - -variable "endpoint_name" { - description = "The Redshift-managed VPC endpoint name" - type = string - default = "" -} - -variable "endpoint_resource_owner" { - description = "The Amazon Web Services account ID of the owner of the cluster. This is only required if the cluster is in another Amazon Web Services account" - type = string - default = null -} - -variable "endpoint_subnet_group_name" { - description = "The subnet group from which Amazon Redshift chooses the subnet to deploy the endpoint" - type = string - default = "" -} - -variable "endpoint_vpc_security_group_ids" { - description = "The security group IDs to use for the endpoint access (managed VPC endpoint)" - type = list(string) - default = [] +variable "endpoint_access" { + description = "Map of endpoint access (managed VPC endpoint) definitions to create" + type = map(object({ + name = optional(string) # Will fall back to key if not set + resource_owner = optional(string) + subnet_group_name = string + vpc_security_group_ids = optional(list(string)) + })) + default = {} + nullable = false } ################################################################################ diff --git a/wrappers/main.tf b/wrappers/main.tf index 52bf914..5ee8edf 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -18,7 +18,6 @@ module "wrapper" { cluster_version = try(each.value.cluster_version, var.defaults.cluster_version, null) create = try(each.value.create, var.defaults.create, true) create_cloudwatch_log_group = try(each.value.create_cloudwatch_log_group, var.defaults.create_cloudwatch_log_group, false) - create_endpoint_access = try(each.value.create_endpoint_access, var.defaults.create_endpoint_access, false) create_parameter_group = try(each.value.create_parameter_group, var.defaults.create_parameter_group, true) create_scheduled_action_iam_role = try(each.value.create_scheduled_action_iam_role, var.defaults.create_scheduled_action_iam_role, false) create_security_group = try(each.value.create_security_group, var.defaults.create_security_group, true) @@ -28,10 +27,7 @@ module "wrapper" { default_iam_role_arn = try(each.value.default_iam_role_arn, var.defaults.default_iam_role_arn, null) elastic_ip = try(each.value.elastic_ip, var.defaults.elastic_ip, null) encrypted = try(each.value.encrypted, var.defaults.encrypted, true) - endpoint_name = try(each.value.endpoint_name, var.defaults.endpoint_name, "") - endpoint_resource_owner = try(each.value.endpoint_resource_owner, var.defaults.endpoint_resource_owner, null) - endpoint_subnet_group_name = try(each.value.endpoint_subnet_group_name, var.defaults.endpoint_subnet_group_name, "") - endpoint_vpc_security_group_ids = try(each.value.endpoint_vpc_security_group_ids, var.defaults.endpoint_vpc_security_group_ids, []) + endpoint_access = try(each.value.endpoint_access, var.defaults.endpoint_access, {}) enhanced_vpc_routing = try(each.value.enhanced_vpc_routing, var.defaults.enhanced_vpc_routing, null) final_snapshot_identifier = try(each.value.final_snapshot_identifier, var.defaults.final_snapshot_identifier, null) iam_role_arns = try(each.value.iam_role_arns, var.defaults.iam_role_arns, []) From 3826756c8b6bb9354a2f9cfc022fdbd680e4d766 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 14 Nov 2025 09:45:29 -0600 Subject: [PATCH 07/11] feat: Consolidate variables under top level variables --- README.md | 65 ++++++++++++----------- docs/UPGRADE-7.0.md | 105 ++++++++++++++++++++++++++++++++++++-- examples/complete/main.tf | 28 +++++----- main.tf | 56 ++++++++++---------- variables.tf | 49 +++++------------- wrappers/main.tf | 7 +-- 6 files changed, 192 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index 94ced2a..f6b587c 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,17 @@ module "redshift" { node_type = "ra3.xlplus" number_of_nodes = 3 - database_name = "mydb" - master_username = "mydbuser" - create_random_password = false - master_password = "MySecretPassw0rd1!" # Do better! + database_name = "mydb" + master_username = "mydbuser" + + manage_master_password = true + manage_master_password_rotation = true + master_password_rotation_schedule_expression = "rate(90 days)" encrypted = true kms_key_arn = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" - enhanced_vpc_routing = true - vpc_security_group_ids = ["sg-12345678"] - subnet_ids = ["subnet-123456", "subnet-654321"] - + enhanced_vpc_routing = true availability_zone_relocation_enabled = true snapshot_copy = { @@ -79,17 +78,19 @@ module "redshift" { # Subnet group subnet_group_name = "example-custom" subnet_group_description = "Custom subnet group for example cluster" + subnet_ids = ["subnet-123456", "subnet-654321"] subnet_group_tags = { Additional = "CustomSubnetGroup" } # Snapshot schedule - create_snapshot_schedule = true - snapshot_schedule_identifier = local.name - use_snapshot_identifier_prefix = true - snapshot_schedule_description = "Example snapshot schedule" - snapshot_schedule_definitions = ["rate(12 hours)"] - snapshot_schedule_force_destroy = true + snapshot_schedule = { + identifier = "example" + use_prefix = true + description = "Example snapshot schedule" + definitions = ["rate(12 hours)"] + force_destroy = true + } # Scheduled actions create_scheduled_action_iam_role = true @@ -98,30 +99,39 @@ module "redshift" { name = "example-pause" description = "Pause cluster every night" schedule = "cron(0 22 * * ? *)" - pause_cluster = true + target_action = { + pause_cluster = true + } } resize = { name = "example-resize" description = "Resize cluster (demo only)" schedule = "cron(00 13 * * ? *)" - resize_cluster = { - node_type = "ds2.xlarge" - number_of_nodes = 5 + target_action = { + resize_cluster = { + node_type = "ds2.xlarge" + number_of_nodes = 5 + } } } resume = { name = "example-resume" description = "Resume cluster every morning" schedule = "cron(0 12 * * ? *)" - resume_cluster = true + target_action = { + resume_cluster = true + } } } # Endpoint access - create_endpoint_access = true - endpoint_name = "example-example" - endpoint_subnet_group_name = "example-subnet-group" - endpoint_vpc_security_group_ids = ["sg-12345678"] + endpoint_access = { + example = { + name = "example-example" + subnet_group_name = "example-subnet-group" + vpc_security_group_ids = ["sg-12345678"] + } + } # Usage limits usage_limits = { @@ -239,7 +249,6 @@ No modules. | [create\_parameter\_group](#input\_create\_parameter\_group) | Determines whether to create a parameter group or use existing | `bool` | `true` | no | | [create\_scheduled\_action\_iam\_role](#input\_create\_scheduled\_action\_iam\_role) | Determines whether a scheduled action IAM role is created | `bool` | `false` | no | | [create\_security\_group](#input\_create\_security\_group) | Determines whether to create security group for Redshift cluster | `bool` | `true` | no | -| [create\_snapshot\_schedule](#input\_create\_snapshot\_schedule) | Determines whether to create a snapshot schedule | `bool` | `false` | no | | [create\_subnet\_group](#input\_create\_subnet\_group) | Determines whether to create a subnet group or use existing | `bool` | `true` | no | | [database\_name](#input\_database\_name) | The name of the first database to be created when the cluster is created. If you do not provide a name, Amazon Redshift will create a default database called `dev` | `string` | `null` | no | | [default\_iam\_role\_arn](#input\_default\_iam\_role\_arn) | The Amazon Resource Name (ARN) for the IAM role that was set as default for the cluster when the cluster was created | `string` | `null` | no | @@ -281,7 +290,7 @@ No modules. | [preferred\_maintenance\_window](#input\_preferred\_maintenance\_window) | The weekly time range (in UTC) during which automated cluster maintenance can occur. Format: `ddd:hh24:mi-ddd:hh24:mi` | `string` | `"sat:10:00-sat:10:30"` | no | | [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `false` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | -| [scheduled\_actions](#input\_scheduled\_actions) | Map of scheduled action definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
description = optional(string)
enable = optional(bool)
start_time = optional(string)
end_time = optional(string)
schedule = string
iam_role = optional(string)
target_action = object({
pause_cluster = optional(object({}))
resize_cluster = optional(object({
classic = optional(bool)
cluster_type = optional(string)
node_type = optional(string)
number_of_nodes = optional(number)
}))
resume_cluster = optional(object({}))
})
}))
| `{}` | no | +| [scheduled\_actions](#input\_scheduled\_actions) | Map of scheduled action definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
description = optional(string)
enable = optional(bool)
start_time = optional(string)
end_time = optional(string)
schedule = string
iam_role = optional(string)
target_action = object({
pause_cluster = optional(bool, false)
resize_cluster = optional(object({
classic = optional(bool)
cluster_type = optional(string)
node_type = optional(string)
number_of_nodes = optional(number)
}))
resume_cluster = optional(bool, false)
})
}))
| `{}` | no | | [security\_group\_description](#input\_security\_group\_description) | The description of the security group. If value is set to empty string it will contain cluster name in the description | `string` | `null` | no | | [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Map of security group egress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
region = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
| `{}` | no | | [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Map of security group ingress rules to add to the security group created |
map(object({
name = optional(string)

cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
region = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
| `{}` | no | @@ -292,17 +301,13 @@ No modules. | [snapshot\_cluster\_identifier](#input\_snapshot\_cluster\_identifier) | The name of the cluster the source snapshot was created from | `string` | `null` | no | | [snapshot\_copy](#input\_snapshot\_copy) | Configuration of automatic copy of snapshots from one region to another |
object({
destination_region = string
manual_snapshot_retention_period = optional(number)
retention_period = optional(number)
grant_name = optional(string)
})
| `null` | no | | [snapshot\_identifier](#input\_snapshot\_identifier) | The name of the snapshot from which to create the new cluster | `string` | `null` | no | -| [snapshot\_schedule\_definitions](#input\_snapshot\_schedule\_definitions) | The definition of the snapshot schedule. The definition is made up of schedule expressions, for example `cron(30 12 *)` or `rate(12 hours)` | `list(string)` | `[]` | no | -| [snapshot\_schedule\_description](#input\_snapshot\_schedule\_description) | The description of the snapshot schedule | `string` | `null` | no | -| [snapshot\_schedule\_force\_destroy](#input\_snapshot\_schedule\_force\_destroy) | Whether to destroy all associated clusters with this snapshot schedule on deletion. Must be enabled and applied before attempting deletion | `bool` | `null` | no | -| [snapshot\_schedule\_identifier](#input\_snapshot\_schedule\_identifier) | The snapshot schedule identifier | `string` | `null` | no | +| [snapshot\_schedule](#input\_snapshot\_schedule) | Configuration for creating a snapshot schedule and associating it with the cluster |
object({
definitions = list(string)
description = optional(string)
force_destroy = optional(bool)
use_prefix = optional(bool, false)
identifier = optional(string)
tags = optional(map(string), {})
})
| `null` | no | | [subnet\_group\_description](#input\_subnet\_group\_description) | The description of the Redshift Subnet group. Defaults to `Managed by Terraform` | `string` | `null` | no | | [subnet\_group\_name](#input\_subnet\_group\_name) | The name of the Redshift subnet group, existing or to be created | `string` | `null` | no | | [subnet\_group\_tags](#input\_subnet\_group\_tags) | Additional tags to add to the subnet group | `map(string)` | `{}` | no | | [subnet\_ids](#input\_subnet\_ids) | An array of VPC subnet IDs to use in the subnet group | `list(string)` | `[]` | no | | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | | [usage\_limits](#input\_usage\_limits) | Map of usage limit definitions to create |
map(object({
amount = number
breach_action = optional(string)
feature_type = string
limit_type = optional(string) # Will fall back to key if not set
period = optional(string)
tags = optional(map(string), {})
}))
| `{}` | no | -| [use\_snapshot\_identifier\_prefix](#input\_use\_snapshot\_identifier\_prefix) | Determines whether the identifier (`snapshot_schedule_identifier`) is used as a prefix | `bool` | `true` | no | | [vpc\_id](#input\_vpc\_id) | ID of the VPC where to create security group | `string` | `""` | no | | [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | A list of Virtual Private Cloud (VPC) security groups to be associated with the cluster | `list(string)` | `[]` | no | diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md index 56373ee..4b07619 100644 --- a/docs/UPGRADE-7.0.md +++ b/docs/UPGRADE-7.0.md @@ -33,10 +33,17 @@ Please consult the `examples` directory for reference example configurations. If - `aqua_configuration_status` argument was deprecated - The variables for endpoint access have been nested under a single, top-level `endpoint_access` variable: - `create_endpoint_access` removed - set `endpoint_access` to `null` or omit to disable - - `endpoint_name` - - `endpoint_resource_owner` - - `endpoint_subnet_group_name` - - `endpoint_vpc_security_group_ids` + - `endpoint_name` -> `endpoint_access.name` + - `endpoint_resource_owner` -> `endpoint_access.resource_owner` + - `endpoint_subnet_group_name` -> `endpoint_access.subnet_group_name` + - `endpoint_vpc_security_group_ids` -> `endpoint_access.vpc_security_group_ids` + - The variables for snapshot schedule have been nested under a single, top-level `snapshot_schedule` variable: + - `create_snapshot_schedule` removed - set `snapshot_schedule` to `null` or omit to disable + - `snapshot_schedule_identifier` -> `snapshot_schedule.identifier` + - `use_snapshot_identifier_prefix` -> `snapshot_schedule.use_prefix` + - `snapshot_schedule_description` -> `snapshot_schedule.description` + - `snapshot_schedule_definitions` -> `snapshot_schedule.definitions` + - `snapshot_schedule_force_destroy` -> `snapshot_schedule.force_destroy` 2. Renamed variables: @@ -75,6 +82,46 @@ module "redshift" { version = "~> 6.0" # Only the affected attributes are shown + + # Snapshot schedule + create_snapshot_schedule = true + snapshot_schedule_identifier = "example" + use_snapshot_identifier_prefix = true + snapshot_schedule_description = "Example snapshot schedule" + snapshot_schedule_definitions = ["rate(12 hours)"] + snapshot_schedule_force_destroy = true + + # Scheduled actions + create_scheduled_action_iam_role = true + scheduled_actions = { + pause = { + name = "example-pause" + description = "Pause cluster every night" + schedule = "cron(0 22 * * ? *)" + pause_cluster = true + } + resize = { + name = "example-resize" + description = "Resize cluster (demo only)" + schedule = "cron(00 13 * * ? *)" + resize_cluster = { + node_type = "ds2.xlarge" + number_of_nodes = 5 + } + } + resume = { + name = "example-resume" + description = "Resume cluster every morning" + schedule = "cron(0 12 * * ? *)" + resume_cluster = true + } + } + + # Endpoint access - only available when using the ra3.x type + create_endpoint_access = true + endpoint_name = "example-example" + endpoint_subnet_group_name = aws_redshift_subnet_group.endpoint.id + endpoint_vpc_security_group_ids = [module.security_group.security_group_id] } ``` @@ -86,6 +133,56 @@ module "redshift" { version = "~> 7.0" # Only the affected attributes are shown + + # Snapshot schedule + snapshot_schedule = { + identifier = "example" + use_prefix = true + description = "Example snapshot schedule" + definitions = ["rate(12 hours)"] + force_destroy = true + } + + # Scheduled actions + create_scheduled_action_iam_role = true + scheduled_actions = { + pause = { + name = "example-pause" + description = "Pause cluster every night" + schedule = "cron(0 22 * * ? *)" + target_action = { + pause_cluster = true + } + } + resize = { + name = "example-resize" + description = "Resize cluster (demo only)" + schedule = "cron(00 13 * * ? *)" + target_action = { + resize_cluster = { + node_type = "ds2.xlarge" + number_of_nodes = 5 + } + } + } + resume = { + name = "example-resume" + description = "Resume cluster every morning" + schedule = "cron(0 12 * * ? *)" + target_action = { + resume_cluster = true + } + } + } + + # Endpoint access - only available when using the ra3.x type + endpoint_access = { + example = { + name = "example-example" + subnet_group_name = aws_redshift_subnet_group.endpoint.id + vpc_security_group_ids = [module.security_group.security_group_id] + } + } } ``` diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 097e3ac..c218343 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -40,23 +40,17 @@ module "redshift" { database_name = "mydb" master_username = "mydbuser" - # Either provide a good master password - # create_random_password = false - # master_password = "MySecretPassw0rd1!" # Do better! - # Or make Redshift manage it in secrets manager - manage_master_password = true + manage_master_password = true manage_master_password_rotation = true master_password_rotation_schedule_expression = "rate(90 days)" encrypted = true kms_key_arn = aws_kms_key.redshift.arn - enhanced_vpc_routing = true - subnet_ids = module.vpc.redshift_subnets - # Only available when using the ra3.x type availability_zone_relocation_enabled = true + enhanced_vpc_routing = true snapshot_copy = { destination_region = "us-east-1" @@ -108,17 +102,19 @@ module "redshift" { # Subnet group subnet_group_name = "${local.name}-custom" subnet_group_description = "Custom subnet group for ${local.name} cluster" + subnet_ids = module.vpc.redshift_subnets subnet_group_tags = { Additional = "CustomSubnetGroup" } # Snapshot schedule - create_snapshot_schedule = true - snapshot_schedule_identifier = local.name - use_snapshot_identifier_prefix = true - snapshot_schedule_description = "Example snapshot schedule" - snapshot_schedule_definitions = ["rate(12 hours)"] - snapshot_schedule_force_destroy = true + snapshot_schedule = { + identifier = local.name + use_prefix = true + description = "Example snapshot schedule" + definitions = ["rate(12 hours)"] + force_destroy = true + } # Scheduled actions create_scheduled_action_iam_role = true @@ -128,7 +124,7 @@ module "redshift" { description = "Pause cluster every night" schedule = "cron(0 22 * * ? *)" target_action = { - pause_cluster = {} + pause_cluster = true } } resize = { @@ -147,7 +143,7 @@ module "redshift" { description = "Resume cluster every morning" schedule = "cron(0 12 * * ? *)" target_action = { - resume_cluster = {} + resume_cluster = true } } } diff --git a/main.tf b/main.tf index 4cf5427..ce11d07 100644 --- a/main.tf +++ b/main.tf @@ -128,22 +128,26 @@ resource "aws_redshift_subnet_group" "this" { # Snapshot Schedule ################################################################################ +locals { + snapshot_schedule_identifier = try(coalesce(var.snapshot_schedule.identifier, var.cluster_identifier)) +} + resource "aws_redshift_snapshot_schedule" "this" { - count = var.create && var.create_snapshot_schedule ? 1 : 0 + count = var.create && var.snapshot_schedule != null ? 1 : 0 region = var.region - identifier = var.use_snapshot_identifier_prefix ? null : var.snapshot_schedule_identifier - identifier_prefix = var.use_snapshot_identifier_prefix ? "${var.snapshot_schedule_identifier}-" : null - description = var.snapshot_schedule_description - definitions = var.snapshot_schedule_definitions - force_destroy = var.snapshot_schedule_force_destroy + definitions = var.snapshot_schedule.definitions + description = var.snapshot_schedule.description + force_destroy = var.snapshot_schedule.force_destroy + identifier = var.snapshot_schedule.use_prefix ? null : local.snapshot_schedule_identifier + identifier_prefix = var.snapshot_schedule.use_prefix ? "${local.snapshot_schedule_identifier}-" : null - tags = var.tags + tags = merge(var.tags, var.snapshot_schedule.tags) } resource "aws_redshift_snapshot_schedule_association" "this" { - count = var.create && var.create_snapshot_schedule ? 1 : 0 + count = var.create && var.snapshot_schedule != null ? 1 : 0 region = var.region @@ -177,7 +181,7 @@ resource "aws_redshift_scheduled_action" "this" { content { dynamic "pause_cluster" { - for_each = each.value.pause_cluster != null ? [each.value.pause_cluster] : [] + for_each = each.value.pause_cluster != null ? [1] : [] content { cluster_identifier = aws_redshift_cluster.this[0].id @@ -185,7 +189,7 @@ resource "aws_redshift_scheduled_action" "this" { } dynamic "resize_cluster" { - for_each = each.value.resize_cluster != null ? [each.value.resize_cluster] : [] + for_each = each.value.resize_cluster != null ? [1] : [] content { classic = resize_cluster.value.classic @@ -336,22 +340,6 @@ resource "aws_redshift_logging" "this" { s3_key_prefix = var.logging.s3_key_prefix } -################################################################################ -# Snapshot Copy -################################################################################ - -resource "aws_redshift_snapshot_copy" "this" { - count = var.create && var.snapshot_copy != null ? 1 : 0 - - region = var.region - - cluster_identifier = aws_redshift_cluster.this[0].id - destination_region = var.snapshot_copy.destination_region - manual_snapshot_retention_period = var.snapshot_copy.manual_snapshot_retention_period - retention_period = var.snapshot_copy.retention_period - snapshot_copy_grant_name = var.snapshot_copy.grant_name -} - ################################################################################ # CloudWatch Log Group ################################################################################ @@ -369,6 +357,22 @@ resource "aws_cloudwatch_log_group" "this" { tags = merge(var.tags, var.cloudwatch_log_group_tags) } +################################################################################ +# Snapshot Copy +################################################################################ + +resource "aws_redshift_snapshot_copy" "this" { + count = var.create && var.snapshot_copy != null ? 1 : 0 + + region = var.region + + cluster_identifier = aws_redshift_cluster.this[0].id + destination_region = var.snapshot_copy.destination_region + manual_snapshot_retention_period = var.snapshot_copy.manual_snapshot_retention_period + retention_period = var.snapshot_copy.retention_period + snapshot_copy_grant_name = var.snapshot_copy.grant_name +} + ################################################################################ # Managed Secret Rotation ################################################################################ diff --git a/variables.tf b/variables.tf index 07ef34c..a7f1d7c 100644 --- a/variables.tf +++ b/variables.tf @@ -337,40 +337,17 @@ variable "subnet_group_tags" { # Snapshot Schedule ################################################################################ -variable "create_snapshot_schedule" { - description = "Determines whether to create a snapshot schedule" - type = bool - default = false -} - -variable "snapshot_schedule_identifier" { - description = "The snapshot schedule identifier" - type = string - default = null -} - -variable "use_snapshot_identifier_prefix" { - description = "Determines whether the identifier (`snapshot_schedule_identifier`) is used as a prefix" - type = bool - default = true -} - -variable "snapshot_schedule_description" { - description = "The description of the snapshot schedule" - type = string - default = null -} - -variable "snapshot_schedule_definitions" { - description = "The definition of the snapshot schedule. The definition is made up of schedule expressions, for example `cron(30 12 *)` or `rate(12 hours)`" - type = list(string) - default = [] -} - -variable "snapshot_schedule_force_destroy" { - description = "Whether to destroy all associated clusters with this snapshot schedule on deletion. Must be enabled and applied before attempting deletion" - type = bool - default = null +variable "snapshot_schedule" { + description = "Configuration for creating a snapshot schedule and associating it with the cluster" + type = object({ + definitions = list(string) + description = optional(string) + force_destroy = optional(bool) + use_prefix = optional(bool, false) + identifier = optional(string) + tags = optional(map(string), {}) + }) + default = null } ################################################################################ @@ -388,14 +365,14 @@ variable "scheduled_actions" { schedule = string iam_role = optional(string) target_action = object({ - pause_cluster = optional(object({})) + pause_cluster = optional(bool, false) resize_cluster = optional(object({ classic = optional(bool) cluster_type = optional(string) node_type = optional(string) number_of_nodes = optional(number) })) - resume_cluster = optional(object({})) + resume_cluster = optional(bool, false) }) })) default = {} diff --git a/wrappers/main.tf b/wrappers/main.tf index 5ee8edf..dd1ecb6 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -21,7 +21,6 @@ module "wrapper" { create_parameter_group = try(each.value.create_parameter_group, var.defaults.create_parameter_group, true) create_scheduled_action_iam_role = try(each.value.create_scheduled_action_iam_role, var.defaults.create_scheduled_action_iam_role, false) create_security_group = try(each.value.create_security_group, var.defaults.create_security_group, true) - create_snapshot_schedule = try(each.value.create_snapshot_schedule, var.defaults.create_snapshot_schedule, false) create_subnet_group = try(each.value.create_subnet_group, var.defaults.create_subnet_group, true) database_name = try(each.value.database_name, var.defaults.database_name, null) default_iam_role_arn = try(each.value.default_iam_role_arn, var.defaults.default_iam_role_arn, null) @@ -74,17 +73,13 @@ module "wrapper" { snapshot_cluster_identifier = try(each.value.snapshot_cluster_identifier, var.defaults.snapshot_cluster_identifier, null) snapshot_copy = try(each.value.snapshot_copy, var.defaults.snapshot_copy, null) snapshot_identifier = try(each.value.snapshot_identifier, var.defaults.snapshot_identifier, null) - snapshot_schedule_definitions = try(each.value.snapshot_schedule_definitions, var.defaults.snapshot_schedule_definitions, []) - snapshot_schedule_description = try(each.value.snapshot_schedule_description, var.defaults.snapshot_schedule_description, null) - snapshot_schedule_force_destroy = try(each.value.snapshot_schedule_force_destroy, var.defaults.snapshot_schedule_force_destroy, null) - snapshot_schedule_identifier = try(each.value.snapshot_schedule_identifier, var.defaults.snapshot_schedule_identifier, null) + snapshot_schedule = try(each.value.snapshot_schedule, var.defaults.snapshot_schedule, null) subnet_group_description = try(each.value.subnet_group_description, var.defaults.subnet_group_description, null) subnet_group_name = try(each.value.subnet_group_name, var.defaults.subnet_group_name, null) subnet_group_tags = try(each.value.subnet_group_tags, var.defaults.subnet_group_tags, {}) subnet_ids = try(each.value.subnet_ids, var.defaults.subnet_ids, []) tags = try(each.value.tags, var.defaults.tags, {}) usage_limits = try(each.value.usage_limits, var.defaults.usage_limits, {}) - use_snapshot_identifier_prefix = try(each.value.use_snapshot_identifier_prefix, var.defaults.use_snapshot_identifier_prefix, true) vpc_id = try(each.value.vpc_id, var.defaults.vpc_id, "") vpc_security_group_ids = try(each.value.vpc_security_group_ids, var.defaults.vpc_security_group_ids, []) } From 6b63603fa5dd766317e357982e30edb4b1eba623 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 14 Nov 2025 10:10:12 -0600 Subject: [PATCH 08/11] feat: Replace `master_password` with write only argument equivalent `master_password_wo` --- README.md | 12 +++--- docs/UPGRADE-7.0.md | 4 +- main.tf | 51 +++++++++++------------- variables.tf | 97 +++++++++++++++++++++++++-------------------- wrappers/main.tf | 8 ++-- 5 files changed, 94 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index f6b587c..12c4fd4 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,7 @@ No modules. | [database\_name](#input\_database\_name) | The name of the first database to be created when the cluster is created. If you do not provide a name, Amazon Redshift will create a default database called `dev` | `string` | `null` | no | | [default\_iam\_role\_arn](#input\_default\_iam\_role\_arn) | The Amazon Resource Name (ARN) for the IAM role that was set as default for the cluster when the cluster was created | `string` | `null` | no | | [elastic\_ip](#input\_elastic\_ip) | The Elastic IP (EIP) address for the cluster | `string` | `null` | no | -| [encrypted](#input\_encrypted) | If `true`, the data in the cluster is encrypted at rest | `bool` | `true` | no | +| [encrypted](#input\_encrypted) | If `true`, the data in the cluster is encrypted at rest | `bool` | `null` | no | | [endpoint\_access](#input\_endpoint\_access) | Map of endpoint access (managed VPC endpoint) definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
resource_owner = optional(string)
subnet_group_name = string
vpc_security_group_ids = optional(list(string))
}))
| `{}` | no | | [enhanced\_vpc\_routing](#input\_enhanced\_vpc\_routing) | If `true`, enhanced VPC routing is enabled | `bool` | `null` | no | | [final\_snapshot\_identifier](#input\_final\_snapshot\_identifier) | The identifier of the final snapshot that is to be created immediately before deleting the cluster. If this parameter is provided, `skip_final_snapshot` must be `false` | `string` | `null` | no | @@ -270,13 +270,14 @@ No modules. | [manage\_master\_password](#input\_manage\_master\_password) | Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password`. One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided | `bool` | `false` | no | | [manage\_master\_password\_rotation](#input\_manage\_master\_password\_rotation) | Whether to manage the master user password rotation. Setting this value to false after previously having been set to true will disable automatic rotation | `bool` | `false` | no | | [manual\_snapshot\_retention\_period](#input\_manual\_snapshot\_retention\_period) | The default number of days to retain a manual snapshot. If the value is -1, the snapshot is retained indefinitely. This setting doesn't change the retention period of existing snapshots. Valid values are between `-1` and `3653`. Default value is `-1` | `number` | `null` | no | -| [master\_password](#input\_master\_password) | Password for the master DB user. (Required unless a `snapshot_identifier` is provided). Must contain at least 8 chars, one uppercase letter, one lowercase letter, and one number | `string` | `null` | no | | [master\_password\_rotate\_immediately](#input\_master\_password\_rotate\_immediately) | Specifies whether to rotate the secret immediately or wait until the next scheduled rotation window | `bool` | `null` | no | | [master\_password\_rotation\_automatically\_after\_days](#input\_master\_password\_rotation\_automatically\_after\_days) | Specifies the number of days between automatic scheduled rotations of the secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified | `number` | `null` | no | | [master\_password\_rotation\_duration](#input\_master\_password\_rotation\_duration) | The length of the rotation window in hours. For example, 3h for a three hour window | `string` | `null` | no | | [master\_password\_rotation\_schedule\_expression](#input\_master\_password\_rotation\_schedule\_expression) | A cron() or rate() expression that defines the schedule for rotating your secret. Either `master_user_password_rotation_automatically_after_days` or `master_user_password_rotation_schedule_expression` must be specified | `string` | `null` | no | | [master\_password\_secret\_kms\_key\_id](#input\_master\_password\_secret\_kms\_key\_id) | ID of the KMS key used to encrypt the cluster admin credentials secret | `string` | `null` | no | -| [master\_username](#input\_master\_username) | Username for the master DB user (Required unless a `snapshot_identifier` is provided). Defaults to `awsuser` | `string` | `"awsuser"` | no | +| [master\_password\_wo](#input\_master\_password\_wo) | Password for the master DB user. Must contain at least 8 chars, one uppercase letter, one lowercase letter, and one number | `string` | `null` | no | +| [master\_password\_wo\_version](#input\_master\_password\_wo\_version) | Used together with `master_password_wo` to trigger an update. Increment this value when an update to the `master_password_wo` is required | `string` | `null` | no | +| [master\_username](#input\_master\_username) | Username for the master DB user. Defaults to `awsuser` | `string` | `"awsuser"` | no | | [multi\_az](#input\_multi\_az) | Specifies if the Redshift cluster is multi-AZ | `bool` | `null` | no | | [node\_type](#input\_node\_type) | The node type to be provisioned for the cluster | `string` | `""` | no | | [number\_of\_nodes](#input\_number\_of\_nodes) | Number of nodes in the cluster. Defaults to 1. Note: values greater than 1 will trigger `cluster_type` to switch to `multi-node` | `number` | `1` | no | @@ -288,7 +289,7 @@ No modules. | [parameter\_group\_tags](#input\_parameter\_group\_tags) | Additional tags to add to the parameter group | `map(string)` | `{}` | no | | [port](#input\_port) | The port number on which the cluster accepts incoming connections. Default port is `5439` | `number` | `5439` | no | | [preferred\_maintenance\_window](#input\_preferred\_maintenance\_window) | The weekly time range (in UTC) during which automated cluster maintenance can occur. Format: `ddd:hh24:mi-ddd:hh24:mi` | `string` | `"sat:10:00-sat:10:30"` | no | -| [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `false` | no | +| [publicly\_accessible](#input\_publicly\_accessible) | If true, the cluster can be accessed from a public network | `bool` | `null` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | | [scheduled\_actions](#input\_scheduled\_actions) | Map of scheduled action definitions to create |
map(object({
name = optional(string) # Will fall back to key if not set
description = optional(string)
enable = optional(bool)
start_time = optional(string)
end_time = optional(string)
schedule = string
iam_role = optional(string)
target_action = object({
pause_cluster = optional(bool, false)
resize_cluster = optional(object({
classic = optional(bool)
cluster_type = optional(string)
node_type = optional(string)
number_of_nodes = optional(number)
}))
resume_cluster = optional(bool, false)
})
}))
| `{}` | no | | [security\_group\_description](#input\_security\_group\_description) | The description of the security group. If value is set to empty string it will contain cluster name in the description | `string` | `null` | no | @@ -298,9 +299,10 @@ No modules. | [security\_group\_tags](#input\_security\_group\_tags) | Additional tags for the security group | `map(string)` | `{}` | no | | [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name`) is used as a prefix | `bool` | `true` | no | | [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | Determines whether a final snapshot of the cluster is created before Redshift deletes the cluster. If true, a final cluster snapshot is not created. If false , a final cluster snapshot is created before the cluster is deleted | `bool` | `true` | no | +| [snapshot\_arn](#input\_snapshot\_arn) | The ARN of the snapshot from which to create the new cluster. Conflicts with `snapshot_identifier` | `string` | `null` | no | | [snapshot\_cluster\_identifier](#input\_snapshot\_cluster\_identifier) | The name of the cluster the source snapshot was created from | `string` | `null` | no | | [snapshot\_copy](#input\_snapshot\_copy) | Configuration of automatic copy of snapshots from one region to another |
object({
destination_region = string
manual_snapshot_retention_period = optional(number)
retention_period = optional(number)
grant_name = optional(string)
})
| `null` | no | -| [snapshot\_identifier](#input\_snapshot\_identifier) | The name of the snapshot from which to create the new cluster | `string` | `null` | no | +| [snapshot\_identifier](#input\_snapshot\_identifier) | The name of the snapshot from which to create the new cluster. Conflicts with `snapshot_arn` | `string` | `null` | no | | [snapshot\_schedule](#input\_snapshot\_schedule) | Configuration for creating a snapshot schedule and associating it with the cluster |
object({
definitions = list(string)
description = optional(string)
force_destroy = optional(bool)
use_prefix = optional(bool, false)
identifier = optional(string)
tags = optional(map(string), {})
})
| `null` | no | | [subnet\_group\_description](#input\_subnet\_group\_description) | The description of the Redshift Subnet group. Defaults to `Managed by Terraform` | `string` | `null` | no | | [subnet\_group\_name](#input\_subnet\_group\_name) | The name of the Redshift subnet group, existing or to be created | `string` | `null` | no | diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md index 4b07619..9934912 100644 --- a/docs/UPGRADE-7.0.md +++ b/docs/UPGRADE-7.0.md @@ -7,6 +7,7 @@ Please consult the `examples` directory for reference example configurations. If - Terraform `v1.11` is now minimum supported version to support write-only (`wo_*`) attributes. - AWS provider `v6.18` is now minimum supported version - The ability for the module to create a random password has been removed in order to ensure passwords are not stored in plain text within the state file. Users must now provide their own password via the `master_password_wo` variable. + - `master_password` is no longer supported and only the write-only equivalent is supported (`master_password_wo` and `master_password_wo_version`) ## Additional changes @@ -47,7 +48,7 @@ Please consult the `examples` directory for reference example configurations. If 2. Renamed variables: - - + - `master_password` -> `master_password_wo` 3. Added variables: @@ -59,6 +60,7 @@ Please consult the `examples` directory for reference example configurations. If - `vpc_id` - `security_group_ingress_rules` - `security_group_egress_rules` + - `master_password_wo_version` 4. Removed outputs: diff --git a/main.tf b/main.tf index ce11d07..c93e3e9 100644 --- a/main.tf +++ b/main.tf @@ -28,37 +28,34 @@ resource "aws_redshift_cluster" "this" { enhanced_vpc_routing = var.enhanced_vpc_routing final_snapshot_identifier = var.skip_final_snapshot ? null : var.final_snapshot_identifier kms_key_id = var.kms_key_arn - - # iam_roles and default_iam_roles are managed in the aws_redshift_cluster_iam_roles resource below - - maintenance_track_name = var.maintenance_track_name - manual_snapshot_retention_period = var.manual_snapshot_retention_period - manage_master_password = var.manage_master_password ? var.manage_master_password : null - master_password = var.snapshot_identifier == null && !var.manage_master_password ? var.master_password : null - master_password_secret_kms_key_id = var.master_password_secret_kms_key_id - master_username = var.master_username - multi_az = var.multi_az - node_type = var.node_type - number_of_nodes = var.number_of_nodes - owner_account = var.owner_account - port = var.port - preferred_maintenance_window = var.preferred_maintenance_window - publicly_accessible = var.publicly_accessible - skip_final_snapshot = var.skip_final_snapshot - snapshot_cluster_identifier = var.snapshot_cluster_identifier - - snapshot_identifier = var.snapshot_identifier - vpc_security_group_ids = compact(concat(aws_security_group.this[*].id, var.vpc_security_group_ids)) - - tags = var.tags + maintenance_track_name = var.maintenance_track_name + manage_master_password = var.manage_master_password ? var.manage_master_password : null + manual_snapshot_retention_period = var.manual_snapshot_retention_period + master_password_wo = var.snapshot_identifier == null && !var.manage_master_password ? var.master_password_wo : null + master_password_wo_version = var.snapshot_identifier == null && !var.manage_master_password ? var.master_password_wo_version : null + master_password_secret_kms_key_id = var.master_password_secret_kms_key_id + master_username = var.master_username + multi_az = var.multi_az + node_type = var.node_type + number_of_nodes = var.number_of_nodes + owner_account = var.owner_account + port = var.port + preferred_maintenance_window = var.preferred_maintenance_window + publicly_accessible = var.publicly_accessible + skip_final_snapshot = var.skip_final_snapshot + snapshot_arn = var.snapshot_arn + snapshot_cluster_identifier = var.snapshot_cluster_identifier + snapshot_identifier = var.snapshot_identifier + tags = var.tags + vpc_security_group_ids = compact(concat(aws_security_group.this[*].id, var.vpc_security_group_ids)) dynamic "timeouts" { - for_each = var.cluster_timeouts != null ? [1] : [] + for_each = var.cluster_timeouts != null ? [var.cluster_timeouts] : [] content { - create = var.cluster_timeouts.create - update = var.cluster_timeouts.update - delete = var.cluster_timeouts.delete + create = each.value.create + update = each.value.update + delete = each.value.delete } } diff --git a/variables.tf b/variables.tf index a7f1d7c..9c5d923 100644 --- a/variables.tf +++ b/variables.tf @@ -56,9 +56,6 @@ variable "cluster_identifier" { default = "" } -# cluster_parameter_group_name -> see parameter group section -# cluster_subnet_group_name -> see subnet group section - variable "cluster_version" { description = "The version of the Amazon Redshift engine software that you want to deploy on the cluster. The version selected runs on all the nodes in the cluster" type = string @@ -71,8 +68,6 @@ variable "database_name" { default = null } -# default_iam_role_arn -> see iam roles section - variable "elastic_ip" { description = "The Elastic IP (EIP) address for the cluster" type = string @@ -82,7 +77,7 @@ variable "elastic_ip" { variable "encrypted" { description = "If `true`, the data in the cluster is encrypted at rest" type = bool - default = true + default = null } variable "enhanced_vpc_routing" { @@ -97,54 +92,47 @@ variable "final_snapshot_identifier" { default = null } -# iam_roles -> see iam roles section - variable "kms_key_arn" { description = "The ARN for the KMS encryption key. When specifying `kms_key_arn`, `encrypted` needs to be set to `true`" type = string default = null } -variable "logging" { - description = "Logging configuration for the cluster" - type = object({ - bucket_name = optional(string) - log_destination_type = optional(string) - log_exports = optional(list(string)) - s3_key_prefix = optional(string) - }) - default = null -} - variable "maintenance_track_name" { description = "The name of the maintenance track for the restored cluster. When you take a snapshot, the snapshot inherits the MaintenanceTrack value from the cluster. The snapshot might be on a different track than the cluster that was the source for the snapshot. Default value is `current`" type = string default = null } +variable "manage_master_password" { + description = "Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password`. One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided" + type = bool + default = false +} + variable "manual_snapshot_retention_period" { description = "The default number of days to retain a manual snapshot. If the value is -1, the snapshot is retained indefinitely. This setting doesn't change the retention period of existing snapshots. Valid values are between `-1` and `3653`. Default value is `-1`" type = number default = null } -variable "manage_master_password" { - description = "Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password`. One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided" - type = bool - default = false +variable "master_password_wo" { + description = "Password for the master DB user. Must contain at least 8 chars, one uppercase letter, one lowercase letter, and one number" + type = string + default = null + sensitive = true } -variable "master_password_secret_kms_key_id" { - description = "ID of the KMS key used to encrypt the cluster admin credentials secret" +variable "master_password_wo_version" { + description = "Used together with `master_password_wo` to trigger an update. Increment this value when an update to the `master_password_wo` is required" type = string default = null } -variable "master_password" { - description = "Password for the master DB user. (Required unless a `snapshot_identifier` is provided). Must contain at least 8 chars, one uppercase letter, one lowercase letter, and one number" +variable "master_password_secret_kms_key_id" { + description = "ID of the KMS key used to encrypt the cluster admin credentials secret" type = string default = null - sensitive = true } variable "multi_az" { @@ -154,7 +142,7 @@ variable "multi_az" { } variable "master_username" { - description = "Username for the master DB user (Required unless a `snapshot_identifier` is provided). Defaults to `awsuser`" + description = "Username for the master DB user. Defaults to `awsuser`" type = string default = "awsuser" } @@ -192,7 +180,7 @@ variable "preferred_maintenance_window" { variable "publicly_accessible" { description = "If true, the cluster can be accessed from a public network" type = bool - default = false + default = null } variable "skip_final_snapshot" { @@ -201,25 +189,20 @@ variable "skip_final_snapshot" { default = true } -variable "snapshot_cluster_identifier" { - description = "The name of the cluster the source snapshot was created from" +variable "snapshot_arn" { + description = "The ARN of the snapshot from which to create the new cluster. Conflicts with `snapshot_identifier`" type = string default = null } -variable "snapshot_copy" { - description = "Configuration of automatic copy of snapshots from one region to another" - type = object({ - destination_region = string - manual_snapshot_retention_period = optional(number) - retention_period = optional(number) - grant_name = optional(string) - }) - default = null +variable "snapshot_cluster_identifier" { + description = "The name of the cluster the source snapshot was created from" + type = string + default = null } variable "snapshot_identifier" { - description = "The name of the snapshot from which to create the new cluster" + description = "The name of the snapshot from which to create the new cluster. Conflicts with `snapshot_arn`" type = string default = null } @@ -473,6 +456,21 @@ variable "authentication_profiles" { nullable = false } +################################################################################ +# Logging +################################################################################ + +variable "logging" { + description = "Logging configuration for the cluster" + type = object({ + bucket_name = optional(string) + log_destination_type = optional(string) + log_exports = optional(list(string)) + s3_key_prefix = optional(string) + }) + default = null +} + ################################################################################ # CloudWatch Log Group ################################################################################ @@ -507,6 +505,21 @@ variable "cloudwatch_log_group_tags" { default = {} } +################################################################################ +# Snapshot Copy +################################################################################ + +variable "snapshot_copy" { + description = "Configuration of automatic copy of snapshots from one region to another" + type = object({ + destination_region = string + manual_snapshot_retention_period = optional(number) + retention_period = optional(number) + grant_name = optional(string) + }) + default = null +} + ################################################################################ # Managed Secret Rotation ################################################################################ diff --git a/wrappers/main.tf b/wrappers/main.tf index dd1ecb6..4a11e9d 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -25,7 +25,7 @@ module "wrapper" { database_name = try(each.value.database_name, var.defaults.database_name, null) default_iam_role_arn = try(each.value.default_iam_role_arn, var.defaults.default_iam_role_arn, null) elastic_ip = try(each.value.elastic_ip, var.defaults.elastic_ip, null) - encrypted = try(each.value.encrypted, var.defaults.encrypted, true) + encrypted = try(each.value.encrypted, var.defaults.encrypted, null) endpoint_access = try(each.value.endpoint_access, var.defaults.endpoint_access, {}) enhanced_vpc_routing = try(each.value.enhanced_vpc_routing, var.defaults.enhanced_vpc_routing, null) final_snapshot_identifier = try(each.value.final_snapshot_identifier, var.defaults.final_snapshot_identifier, null) @@ -42,12 +42,13 @@ module "wrapper" { manage_master_password = try(each.value.manage_master_password, var.defaults.manage_master_password, false) manage_master_password_rotation = try(each.value.manage_master_password_rotation, var.defaults.manage_master_password_rotation, false) manual_snapshot_retention_period = try(each.value.manual_snapshot_retention_period, var.defaults.manual_snapshot_retention_period, null) - master_password = try(each.value.master_password, var.defaults.master_password, null) master_password_rotate_immediately = try(each.value.master_password_rotate_immediately, var.defaults.master_password_rotate_immediately, null) master_password_rotation_automatically_after_days = try(each.value.master_password_rotation_automatically_after_days, var.defaults.master_password_rotation_automatically_after_days, null) master_password_rotation_duration = try(each.value.master_password_rotation_duration, var.defaults.master_password_rotation_duration, null) master_password_rotation_schedule_expression = try(each.value.master_password_rotation_schedule_expression, var.defaults.master_password_rotation_schedule_expression, null) master_password_secret_kms_key_id = try(each.value.master_password_secret_kms_key_id, var.defaults.master_password_secret_kms_key_id, null) + master_password_wo = try(each.value.master_password_wo, var.defaults.master_password_wo, null) + master_password_wo_version = try(each.value.master_password_wo_version, var.defaults.master_password_wo_version, null) master_username = try(each.value.master_username, var.defaults.master_username, "awsuser") multi_az = try(each.value.multi_az, var.defaults.multi_az, null) node_type = try(each.value.node_type, var.defaults.node_type, "") @@ -60,7 +61,7 @@ module "wrapper" { parameter_group_tags = try(each.value.parameter_group_tags, var.defaults.parameter_group_tags, {}) port = try(each.value.port, var.defaults.port, 5439) preferred_maintenance_window = try(each.value.preferred_maintenance_window, var.defaults.preferred_maintenance_window, "sat:10:00-sat:10:30") - publicly_accessible = try(each.value.publicly_accessible, var.defaults.publicly_accessible, false) + publicly_accessible = try(each.value.publicly_accessible, var.defaults.publicly_accessible, null) region = try(each.value.region, var.defaults.region, null) scheduled_actions = try(each.value.scheduled_actions, var.defaults.scheduled_actions, {}) security_group_description = try(each.value.security_group_description, var.defaults.security_group_description, null) @@ -70,6 +71,7 @@ module "wrapper" { security_group_tags = try(each.value.security_group_tags, var.defaults.security_group_tags, {}) security_group_use_name_prefix = try(each.value.security_group_use_name_prefix, var.defaults.security_group_use_name_prefix, true) skip_final_snapshot = try(each.value.skip_final_snapshot, var.defaults.skip_final_snapshot, true) + snapshot_arn = try(each.value.snapshot_arn, var.defaults.snapshot_arn, null) snapshot_cluster_identifier = try(each.value.snapshot_cluster_identifier, var.defaults.snapshot_cluster_identifier, null) snapshot_copy = try(each.value.snapshot_copy, var.defaults.snapshot_copy, null) snapshot_identifier = try(each.value.snapshot_identifier, var.defaults.snapshot_identifier, null) From b751a59536740494782591cd594fdd7da4522855 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Fri, 14 Nov 2025 11:06:48 -0600 Subject: [PATCH 09/11] chore: Updates from testing and validating upgrade guide --- README.md | 8 +++----- docs/UPGRADE-7.0.md | 36 ++++++++++++++++++++++++------------ examples/complete/README.md | 5 +---- examples/complete/main.tf | 7 +++++-- examples/complete/outputs.tf | 22 ++++------------------ main.tf | 18 +++++++----------- outputs.tf | 21 +++------------------ variables.tf | 2 +- 8 files changed, 48 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 12c4fd4..f6be62f 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ module "redshift" { encrypted = true kms_key_arn = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" + vpc_id = "vpc-1234556abcdef" enhanced_vpc_routing = true availability_zone_relocation_enabled = true @@ -265,7 +266,7 @@ No modules. | [iam\_role\_tags](#input\_iam\_role\_tags) | A map of additional tags to add to the scheduled action IAM role created | `map(string)` | `{}` | no | | [iam\_role\_use\_name\_prefix](#input\_iam\_role\_use\_name\_prefix) | Determines whether scheduled action the IAM role name (`iam_role_name`) is used as a prefix | `string` | `true` | no | | [kms\_key\_arn](#input\_kms\_key\_arn) | The ARN for the KMS encryption key. When specifying `kms_key_arn`, `encrypted` needs to be set to `true` | `string` | `null` | no | -| [logging](#input\_logging) | Logging configuration for the cluster |
object({
bucket_name = optional(string)
log_destination_type = optional(string)
log_exports = optional(list(string))
s3_key_prefix = optional(string)
})
| `null` | no | +| [logging](#input\_logging) | Logging configuration for the cluster |
object({
bucket_name = optional(string)
log_destination_type = optional(string)
log_exports = optional(list(string), [])
s3_key_prefix = optional(string)
})
| `null` | no | | [maintenance\_track\_name](#input\_maintenance\_track\_name) | The name of the maintenance track for the restored cluster. When you take a snapshot, the snapshot inherits the MaintenanceTrack value from the cluster. The snapshot might be on a different track than the cluster that was the source for the snapshot. Default value is `current` | `string` | `null` | no | | [manage\_master\_password](#input\_manage\_master\_password) | Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password`. One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided | `bool` | `false` | no | | [manage\_master\_password\_rotation](#input\_manage\_master\_password\_rotation) | Whether to manage the master user password rotation. Setting this value to false after previously having been set to true will disable automatic rotation | `bool` | `false` | no | @@ -343,10 +344,7 @@ No modules. | [cluster\_type](#output\_cluster\_type) | The Redshift cluster type | | [cluster\_version](#output\_cluster\_version) | The version of Redshift engine software | | [cluster\_vpc\_security\_group\_ids](#output\_cluster\_vpc\_security\_group\_ids) | The VPC security group ids associated with the cluster | -| [endpoint\_access\_address](#output\_endpoint\_access\_address) | The DNS address of the endpoint | -| [endpoint\_access\_id](#output\_endpoint\_access\_id) | The Redshift-managed VPC endpoint name | -| [endpoint\_access\_port](#output\_endpoint\_access\_port) | The port number on which the cluster accepts incoming connections | -| [endpoint\_access\_vpc\_endpoint](#output\_endpoint\_access\_vpc\_endpoint) | The connection endpoint for connecting to an Amazon Redshift cluster through the proxy. See details below | +| [endpoint\_access](#output\_endpoint\_access) | A map of access endpoints created and their attributes | | [master\_password\_secret\_arn](#output\_master\_password\_secret\_arn) | ARN of managed master password secret | | [parameter\_group\_arn](#output\_parameter\_group\_arn) | Amazon Resource Name (ARN) of the parameter group created | | [parameter\_group\_id](#output\_parameter\_group\_id) | The name of the Redshift parameter group created | diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md index 9934912..4774d24 100644 --- a/docs/UPGRADE-7.0.md +++ b/docs/UPGRADE-7.0.md @@ -8,13 +8,14 @@ Please consult the `examples` directory for reference example configurations. If - AWS provider `v6.18` is now minimum supported version - The ability for the module to create a random password has been removed in order to ensure passwords are not stored in plain text within the state file. Users must now provide their own password via the `master_password_wo` variable. - `master_password` is no longer supported and only the write-only equivalent is supported (`master_password_wo` and `master_password_wo_version`) +- The variable(s) used to create access endpoints has changed from creating a single endpoint to n-number of endpoints ## Additional changes ### Added - Support for `region` argument to specify the AWS region for the resources created if different from the provider region. -- Support for creating security group +- Support for creating a security group used by the cluster ### Modified @@ -23,7 +24,7 @@ Please consult the `examples` directory for reference example configurations. If ### Removed -- +- Support for generating random passwords has been removed. ### Variable and output changes @@ -64,15 +65,18 @@ Please consult the `examples` directory for reference example configurations. If 4. Removed outputs: - - + - `endpoint_access_address` -> see `endpoint_access` output + - `endpoint_access_port` -> see `endpoint_access` output + - `endpoint_access_id` -> see `endpoint_access` output + - `endpoint_access_vpc_endpoint` -> see `endpoint_access` output 5. Renamed outputs: - - + - None 6. Added outputs: - - + - None ## Upgrade Migration @@ -121,9 +125,9 @@ module "redshift" { # Endpoint access - only available when using the ra3.x type create_endpoint_access = true - endpoint_name = "example-example" - endpoint_subnet_group_name = aws_redshift_subnet_group.endpoint.id - endpoint_vpc_security_group_ids = [module.security_group.security_group_id] + endpoint_name = "example" + endpoint_subnet_group_name = "example" + endpoint_vpc_security_group_ids = ["sg-12345678"] } ``` @@ -136,6 +140,9 @@ module "redshift" { # Only the affected attributes are shown + # Security group + vpc_id = "vpc-1234556abcdef" + # Snapshot schedule snapshot_schedule = { identifier = "example" @@ -180,14 +187,19 @@ module "redshift" { # Endpoint access - only available when using the ra3.x type endpoint_access = { example = { - name = "example-example" - subnet_group_name = aws_redshift_subnet_group.endpoint.id - vpc_security_group_ids = [module.security_group.security_group_id] + name = "example" + subnet_group_name = "example" + vpc_security_group_ids = ["sg-12345678"] } } + + # Maintains backward compatibility, as needed + parameter_group_family = "redshift-1.0" } ``` ### State Move Commands -TBD +```sh +terraform state mv 'module.redshift.aws_redshift_endpoint_access.this[0]' 'module.redshift.aws_redshift_endpoint_access.this["example"]' +``` diff --git a/examples/complete/README.md b/examples/complete/README.md index d65039a..4a4ad91 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -88,10 +88,7 @@ No inputs. | [cluster\_type](#output\_cluster\_type) | The Redshift cluster type | | [cluster\_version](#output\_cluster\_version) | The version of Redshift engine software | | [cluster\_vpc\_security\_group\_ids](#output\_cluster\_vpc\_security\_group\_ids) | The VPC security group ids associated with the cluster | -| [endpoint\_access\_address](#output\_endpoint\_access\_address) | The DNS address of the endpoint | -| [endpoint\_access\_id](#output\_endpoint\_access\_id) | The Redshift-managed VPC endpoint name | -| [endpoint\_access\_port](#output\_endpoint\_access\_port) | The port number on which the cluster accepts incoming connections | -| [endpoint\_access\_vpc\_endpoint](#output\_endpoint\_access\_vpc\_endpoint) | The connection endpoint for connecting to an Amazon Redshift cluster through the proxy. See details below | +| [endpoint\_access](#output\_endpoint\_access) | A map of access endpoints created and their attributes | | [master\_password\_secret\_arn](#output\_master\_password\_secret\_arn) | ARN of managed master password secret | | [master\_password\_secretsmanager\_secret\_rotation\_enabled](#output\_master\_password\_secretsmanager\_secret\_rotation\_enabled) | Specifies whether automatic rotation is enabled for the secret | | [parameter\_group\_arn](#output\_parameter\_group\_arn) | Amazon Resource Name (ARN) of the parameter group created | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index c218343..9b50812 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -51,6 +51,7 @@ module "redshift" { # Only available when using the ra3.x type availability_zone_relocation_enabled = true enhanced_vpc_routing = true + vpc_id = module.vpc.vpc_id snapshot_copy = { destination_region = "us-east-1" @@ -217,8 +218,9 @@ module "with_cloudwatch_logging" { source = "../../" cluster_identifier = "${local.name}-with-cloudwatch-logging" - node_type = "dc2.large" + node_type = "ra3.large" + vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.redshift_subnets create_cloudwatch_log_group = true @@ -239,8 +241,9 @@ module "default" { source = "../../" cluster_identifier = "${local.name}-default" - node_type = "dc2.large" + node_type = "ra3.large" + vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.redshift_subnets tags = local.tags diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index a8ea7ec..f378b9c 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -115,6 +115,7 @@ output "cluster_namespace_arn" { output "cluster_master_password" { description = "The Redshift cluster master password" value = module.redshift.cluster_master_password + sensitive = true } output "cluster_master_username" { @@ -187,24 +188,9 @@ output "scheduled_action_iam_role_unique_id" { # Endpoint Access ################################################################################ -output "endpoint_access_address" { - description = "The DNS address of the endpoint" - value = module.redshift.endpoint_access_address -} - -output "endpoint_access_id" { - description = "The Redshift-managed VPC endpoint name" - value = module.redshift.endpoint_access_id -} - -output "endpoint_access_port" { - description = "The port number on which the cluster accepts incoming connections" - value = module.redshift.endpoint_access_port -} - -output "endpoint_access_vpc_endpoint" { - description = "The connection endpoint for connecting to an Amazon Redshift cluster through the proxy. See details below" - value = module.redshift.endpoint_access_vpc_endpoint +output "endpoint_access" { + description = "A map of access endpoints created and their attributes" + value = module.redshift.endpoint_access } ################################################################################ diff --git a/main.tf b/main.tf index c93e3e9..e1d9fe1 100644 --- a/main.tf +++ b/main.tf @@ -125,10 +125,6 @@ resource "aws_redshift_subnet_group" "this" { # Snapshot Schedule ################################################################################ -locals { - snapshot_schedule_identifier = try(coalesce(var.snapshot_schedule.identifier, var.cluster_identifier)) -} - resource "aws_redshift_snapshot_schedule" "this" { count = var.create && var.snapshot_schedule != null ? 1 : 0 @@ -137,8 +133,8 @@ resource "aws_redshift_snapshot_schedule" "this" { definitions = var.snapshot_schedule.definitions description = var.snapshot_schedule.description force_destroy = var.snapshot_schedule.force_destroy - identifier = var.snapshot_schedule.use_prefix ? null : local.snapshot_schedule_identifier - identifier_prefix = var.snapshot_schedule.use_prefix ? "${local.snapshot_schedule_identifier}-" : null + identifier = var.snapshot_schedule.use_prefix ? null : try(coalesce(var.snapshot_schedule.identifier, var.cluster_identifier), "") + identifier_prefix = var.snapshot_schedule.use_prefix ? "${try(coalesce(var.snapshot_schedule.identifier, var.cluster_identifier), "")}-" : null tags = merge(var.tags, var.snapshot_schedule.tags) } @@ -178,7 +174,7 @@ resource "aws_redshift_scheduled_action" "this" { content { dynamic "pause_cluster" { - for_each = each.value.pause_cluster != null ? [1] : [] + for_each = target_action.value.pause_cluster != null && target_action.value.pause_cluster ? [1] : [] content { cluster_identifier = aws_redshift_cluster.this[0].id @@ -186,7 +182,7 @@ resource "aws_redshift_scheduled_action" "this" { } dynamic "resize_cluster" { - for_each = each.value.resize_cluster != null ? [1] : [] + for_each = target_action.value.resize_cluster != null ? [target_action.value.resize_cluster] : [] content { classic = resize_cluster.value.classic @@ -198,7 +194,7 @@ resource "aws_redshift_scheduled_action" "this" { } dynamic "resume_cluster" { - for_each = each.value.resume_cluster != null ? [each.value.resume_cluster] : [] + for_each = target_action.value.resume_cluster != null && target_action.value.resume_cluster ? [target_action.value.resume_cluster] : [] content { cluster_identifier = aws_redshift_cluster.this[0].id @@ -317,7 +313,7 @@ resource "aws_redshift_authentication_profile" "this" { region = var.region - authentication_profile_name = try(each.value.name, each.key) + authentication_profile_name = try(coalesce(each.value.name, each.key)) authentication_profile_content = jsonencode(each.value.content) } @@ -342,7 +338,7 @@ resource "aws_redshift_logging" "this" { ################################################################################ resource "aws_cloudwatch_log_group" "this" { - for_each = toset([for log in try(var.logging.log_exports, []) : log if var.create && var.create_cloudwatch_log_group]) + for_each = var.create && var.create_cloudwatch_log_group && var.logging != null ? toset([for log in try(var.logging.log_exports, []) : log]) : toset([]) region = var.region diff --git a/outputs.tf b/outputs.tf index 0c60927..53cf63f 100644 --- a/outputs.tf +++ b/outputs.tf @@ -193,24 +193,9 @@ output "scheduled_action_iam_role_unique_id" { # Endpoint Access ################################################################################ -output "endpoint_access_address" { - description = "The DNS address of the endpoint" - value = try(aws_redshift_endpoint_access.this[0].address, null) -} - -output "endpoint_access_id" { - description = "The Redshift-managed VPC endpoint name" - value = try(aws_redshift_endpoint_access.this[0].id, null) -} - -output "endpoint_access_port" { - description = "The port number on which the cluster accepts incoming connections" - value = try(aws_redshift_endpoint_access.this[0].port, null) -} - -output "endpoint_access_vpc_endpoint" { - description = "The connection endpoint for connecting to an Amazon Redshift cluster through the proxy. See details below" - value = try(aws_redshift_endpoint_access.this[0].vpc_endpoint, null) +output "endpoint_access" { + description = "A map of access endpoints created and their attributes" + value = aws_redshift_endpoint_access.this } ################################################################################ diff --git a/variables.tf b/variables.tf index 9c5d923..d3d604e 100644 --- a/variables.tf +++ b/variables.tf @@ -465,7 +465,7 @@ variable "logging" { type = object({ bucket_name = optional(string) log_destination_type = optional(string) - log_exports = optional(list(string)) + log_exports = optional(list(string), []) s3_key_prefix = optional(string) }) default = null From 912284fac36141ad2495e8c0a5c8cd3e70f01cdc Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Sat, 15 Nov 2025 07:43:50 -0600 Subject: [PATCH 10/11] fix: Correct timeouts --- main.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.tf b/main.tf index e1d9fe1..9dc4493 100644 --- a/main.tf +++ b/main.tf @@ -53,9 +53,9 @@ resource "aws_redshift_cluster" "this" { for_each = var.cluster_timeouts != null ? [var.cluster_timeouts] : [] content { - create = each.value.create - update = each.value.update - delete = each.value.delete + create = timeouts.value.create + update = timeouts.value.update + delete = timeouts.value.delete } } From 2f27803a1088c3f5f250bbb8df8cb6999e26fd7c Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Sun, 16 Nov 2025 09:53:28 -0600 Subject: [PATCH 11/11] fix: Remove empty logging list, add default password management to replace prior default password generation --- README.md | 4 ++-- docs/UPGRADE-7.0.md | 2 ++ variables.tf | 6 +++--- wrappers/main.tf | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f6be62f..911b6cb 100644 --- a/README.md +++ b/README.md @@ -266,9 +266,9 @@ No modules. | [iam\_role\_tags](#input\_iam\_role\_tags) | A map of additional tags to add to the scheduled action IAM role created | `map(string)` | `{}` | no | | [iam\_role\_use\_name\_prefix](#input\_iam\_role\_use\_name\_prefix) | Determines whether scheduled action the IAM role name (`iam_role_name`) is used as a prefix | `string` | `true` | no | | [kms\_key\_arn](#input\_kms\_key\_arn) | The ARN for the KMS encryption key. When specifying `kms_key_arn`, `encrypted` needs to be set to `true` | `string` | `null` | no | -| [logging](#input\_logging) | Logging configuration for the cluster |
object({
bucket_name = optional(string)
log_destination_type = optional(string)
log_exports = optional(list(string), [])
s3_key_prefix = optional(string)
})
| `null` | no | +| [logging](#input\_logging) | Logging configuration for the cluster |
object({
bucket_name = optional(string)
log_destination_type = optional(string)
log_exports = optional(list(string))
s3_key_prefix = optional(string)
})
| `null` | no | | [maintenance\_track\_name](#input\_maintenance\_track\_name) | The name of the maintenance track for the restored cluster. When you take a snapshot, the snapshot inherits the MaintenanceTrack value from the cluster. The snapshot might be on a different track than the cluster that was the source for the snapshot. Default value is `current` | `string` | `null` | no | -| [manage\_master\_password](#input\_manage\_master\_password) | Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password`. One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided | `bool` | `false` | no | +| [manage\_master\_password](#input\_manage\_master\_password) | Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password_wo`. One of `master_password_wo` or `manage_master_password` is required unless `snapshot_identifier` is provided | `bool` | `true` | no | | [manage\_master\_password\_rotation](#input\_manage\_master\_password\_rotation) | Whether to manage the master user password rotation. Setting this value to false after previously having been set to true will disable automatic rotation | `bool` | `false` | no | | [manual\_snapshot\_retention\_period](#input\_manual\_snapshot\_retention\_period) | The default number of days to retain a manual snapshot. If the value is -1, the snapshot is retained indefinitely. This setting doesn't change the retention period of existing snapshots. Valid values are between `-1` and `3653`. Default value is `-1` | `number` | `null` | no | | [master\_password\_rotate\_immediately](#input\_master\_password\_rotate\_immediately) | Specifies whether to rotate the secret immediately or wait until the next scheduled rotation window | `bool` | `null` | no | diff --git a/docs/UPGRADE-7.0.md b/docs/UPGRADE-7.0.md index 4774d24..0e96b55 100644 --- a/docs/UPGRADE-7.0.md +++ b/docs/UPGRADE-7.0.md @@ -8,6 +8,7 @@ Please consult the `examples` directory for reference example configurations. If - AWS provider `v6.18` is now minimum supported version - The ability for the module to create a random password has been removed in order to ensure passwords are not stored in plain text within the state file. Users must now provide their own password via the `master_password_wo` variable. - `master_password` is no longer supported and only the write-only equivalent is supported (`master_password_wo` and `master_password_wo_version`) + - `manage_master_password` default changed from `false` to `true` to ensure password rotation is managed by default. - The variable(s) used to create access endpoints has changed from creating a single endpoint to n-number of endpoints ## Additional changes @@ -21,6 +22,7 @@ Please consult the `examples` directory for reference example configurations. If - Variable definitions now contain detailed `object` types in place of the previously used any type. - Default value for `parameter_group_family` changed from `redshift-1.0` to `redshift-2.0` +- `manage_master_password` default changed from `false` to `true` ### Removed diff --git a/variables.tf b/variables.tf index d3d604e..b3f1c54 100644 --- a/variables.tf +++ b/variables.tf @@ -105,9 +105,9 @@ variable "maintenance_track_name" { } variable "manage_master_password" { - description = "Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password`. One of `master_password` or `manage_master_password` is required unless `snapshot_identifier` is provided" + description = "Whether to use AWS SecretsManager to manage the cluster admin credentials. Conflicts with `master_password_wo`. One of `master_password_wo` or `manage_master_password` is required unless `snapshot_identifier` is provided" type = bool - default = false + default = true } variable "manual_snapshot_retention_period" { @@ -465,7 +465,7 @@ variable "logging" { type = object({ bucket_name = optional(string) log_destination_type = optional(string) - log_exports = optional(list(string), []) + log_exports = optional(list(string)) s3_key_prefix = optional(string) }) default = null diff --git a/wrappers/main.tf b/wrappers/main.tf index 4a11e9d..907995f 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -39,7 +39,7 @@ module "wrapper" { kms_key_arn = try(each.value.kms_key_arn, var.defaults.kms_key_arn, null) logging = try(each.value.logging, var.defaults.logging, null) maintenance_track_name = try(each.value.maintenance_track_name, var.defaults.maintenance_track_name, null) - manage_master_password = try(each.value.manage_master_password, var.defaults.manage_master_password, false) + manage_master_password = try(each.value.manage_master_password, var.defaults.manage_master_password, true) manage_master_password_rotation = try(each.value.manage_master_password_rotation, var.defaults.manage_master_password_rotation, false) manual_snapshot_retention_period = try(each.value.manual_snapshot_retention_period, var.defaults.manual_snapshot_retention_period, null) master_password_rotate_immediately = try(each.value.master_password_rotate_immediately, var.defaults.master_password_rotate_immediately, null)