diff --git a/CHANGELOG.md b/CHANGELOG.md index b72321d..4836354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [9.2.0](https://github.com/terraform-aws-modules/terraform-aws-rds-aurora/compare/v9.1.0...v9.2.0) (2024-03-03) + + +### Features + +* Add `global_upgradable` variable to support major version upgrades to global clusters. ([#425](https://github.com/terraform-aws-modules/terraform-aws-rds-aurora/issues/425)) + ## [9.1.0](https://github.com/terraform-aws-modules/terraform-aws-rds-aurora/compare/v9.0.2...v9.1.0) (2024-02-16) diff --git a/README.md b/README.md index abe8dc9..62fdf74 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,7 @@ No modules. | [aws_db_subnet_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_subnet_group) | resource | | [aws_iam_role.rds_enhanced_monitoring](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.rds_enhanced_monitoring](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_rds_cluster.global_upgradable](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster) | resource | | [aws_rds_cluster.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster) | resource | | [aws_rds_cluster_activity_stream.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_activity_stream) | resource | | [aws_rds_cluster_endpoint.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_endpoint) | resource | @@ -323,6 +324,7 @@ No modules. | [engine\_version](#input\_engine\_version) | The database engine version. Updating this argument results in an outage | `string` | `null` | no | | [final\_snapshot\_identifier](#input\_final\_snapshot\_identifier) | The name of your final DB snapshot when this DB cluster is deleted. If omitted, no final snapshot will be made | `string` | `null` | no | | [global\_cluster\_identifier](#input\_global\_cluster\_identifier) | The global cluster identifier specified on `aws_rds_global_cluster` | `string` | `null` | no | +| [global\_upgradable](#input\_global\_upgradable) | True if `engine_version` should be ignored for the cluster. This is only relevant if you want to be able to upgrade a member cluster of a global cluster. If this is enabled after creation, you'll need to `terraform mv` to move the resource to this new resource address. | `bool` | `false` | no | | [iam\_database\_authentication\_enabled](#input\_iam\_database\_authentication\_enabled) | Specifies whether or mappings of AWS Identity and Access Management (IAM) accounts to database accounts is enabled | `bool` | `null` | no | | [iam\_role\_description](#input\_iam\_role\_description) | Description of the monitoring role | `string` | `null` | no | | [iam\_role\_force\_detach\_policies](#input\_iam\_role\_force\_detach\_policies) | Whether to force detaching any policies the monitoring role has before destroying it | `bool` | `null` | no | diff --git a/examples/global-cluster/README.md b/examples/global-cluster/README.md index c6fcc87..8e044cf 100644 --- a/examples/global-cluster/README.md +++ b/examples/global-cluster/README.md @@ -14,6 +14,37 @@ $ terraform apply Note that this example may create resources which cost money. Run `terraform destroy` when you don't need these resources. +## Upgrading major version of global clusters + +Upgrading the major version of global clusters is possible, but due to a limitation in terraform, it requires some special consideration. As [documented in the provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_global_cluster#upgrading-engine-versions): + +> When you upgrade the version of an aws_rds_global_cluster, Terraform will attempt to in-place upgrade the engine versions of all associated clusters. Since the aws_rds_cluster resource is being updated through the aws_rds_global_cluster, you are likely to get an error (Provider produced inconsistent final plan). To avoid this, use the lifecycle ignore_changes meta argument as shown below on the aws_rds_cluster. + +In order to accomplish this in a module that is otherwise used for non-global clusters, we must duplicate the cluster resource. The limitation that requires this is, terraform [lifecycle meta-arguments](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#literal-values-only) can contain only literal values: + +> The lifecycle settings all affect how Terraform constructs and traverses the dependency graph. As a result, only literal values can be used because the processing happens too early for arbitrary expression evaluation. + +That means, that to ignore the `engine_version` in some cases but not in others, we need another resource. So, if you intend to upgrade your global cluster in the future, you must set the new variable `global_upgradable` to `true`. + +### Migrating the resource + +If you already have a global cluster created with this module, and would like to make use of this feature, you'll need to move the cluster resource. That can be done with the cli: + +```sh +terraform state mv 'module.this.aws_rds_cluster.this[0]' 'module.this.aws_rds_cluster.global_upgradable[0]' +``` + +Or via a new [moved block](https://developer.hashicorp.com/terraform/language/modules/develop/refactoring#moved-block-syntax): + +```tf +moved { + from = module.this.aws_rds_cluster.this[0] + to = module.this.aws_rds_cluster.global_upgradable[0] +} +``` + +After that, changing the major version should work without issue. + ## Requirements diff --git a/main.tf b/main.tf index 6bb295b..536ab12 100644 --- a/main.tf +++ b/main.tf @@ -37,7 +37,7 @@ resource "aws_db_subnet_group" "this" { ################################################################################ resource "aws_rds_cluster" "this" { - count = local.create ? 1 : 0 + count = local.create && !var.global_upgradable ? 1 : 0 allocated_storage = var.allocated_storage allow_major_version_upgrade = var.allow_major_version_upgrade @@ -152,6 +152,122 @@ resource "aws_rds_cluster" "this" { depends_on = [aws_cloudwatch_log_group.this] } +resource "aws_rds_cluster" "global_upgradable" { + count = local.create && var.global_upgradable ? 1 : 0 + + allocated_storage = var.allocated_storage + allow_major_version_upgrade = var.allow_major_version_upgrade + apply_immediately = var.apply_immediately + availability_zones = var.availability_zones + backup_retention_period = var.backup_retention_period + backtrack_window = local.backtrack_window + cluster_identifier = var.cluster_use_name_prefix ? null : var.name + cluster_identifier_prefix = var.cluster_use_name_prefix ? "${var.name}-" : null + cluster_members = var.cluster_members + copy_tags_to_snapshot = var.copy_tags_to_snapshot + database_name = var.is_primary_cluster ? var.database_name : null + db_cluster_instance_class = var.db_cluster_instance_class + db_cluster_parameter_group_name = var.create_db_cluster_parameter_group ? aws_rds_cluster_parameter_group.this[0].id : var.db_cluster_parameter_group_name + db_instance_parameter_group_name = var.allow_major_version_upgrade ? var.db_cluster_db_instance_parameter_group_name : null + db_subnet_group_name = local.db_subnet_group_name + delete_automated_backups = var.delete_automated_backups + deletion_protection = var.deletion_protection + enable_global_write_forwarding = var.enable_global_write_forwarding + enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports + enable_http_endpoint = var.enable_http_endpoint + engine = var.engine + engine_mode = var.engine_mode + engine_version = var.engine_version + final_snapshot_identifier = var.final_snapshot_identifier + global_cluster_identifier = var.global_cluster_identifier + iam_database_authentication_enabled = var.iam_database_authentication_enabled + # iam_roles has been removed from this resource and instead will be used with aws_rds_cluster_role_association below to avoid conflicts per docs + iops = var.iops + kms_key_id = var.kms_key_id + manage_master_user_password = var.global_cluster_identifier == null && var.manage_master_user_password ? var.manage_master_user_password : null + master_user_secret_kms_key_id = var.global_cluster_identifier == null && var.manage_master_user_password ? var.master_user_secret_kms_key_id : null + master_password = var.is_primary_cluster && !var.manage_master_user_password ? var.master_password : null + master_username = var.is_primary_cluster ? var.master_username : null + network_type = var.network_type + port = local.port + preferred_backup_window = local.is_serverless ? null : var.preferred_backup_window + preferred_maintenance_window = local.is_serverless ? null : var.preferred_maintenance_window + replication_source_identifier = var.replication_source_identifier + + dynamic "restore_to_point_in_time" { + for_each = length(var.restore_to_point_in_time) > 0 ? [var.restore_to_point_in_time] : [] + + content { + restore_to_time = try(restore_to_point_in_time.value.restore_to_time, null) + restore_type = try(restore_to_point_in_time.value.restore_type, null) + source_cluster_identifier = restore_to_point_in_time.value.source_cluster_identifier + use_latest_restorable_time = try(restore_to_point_in_time.value.use_latest_restorable_time, null) + } + } + + dynamic "s3_import" { + for_each = length(var.s3_import) > 0 && !local.is_serverless ? [var.s3_import] : [] + + content { + bucket_name = s3_import.value.bucket_name + bucket_prefix = try(s3_import.value.bucket_prefix, null) + ingestion_role = s3_import.value.ingestion_role + source_engine = "mysql" + source_engine_version = s3_import.value.source_engine_version + } + } + + dynamic "scaling_configuration" { + for_each = length(var.scaling_configuration) > 0 && local.is_serverless ? [var.scaling_configuration] : [] + + content { + auto_pause = try(scaling_configuration.value.auto_pause, null) + max_capacity = try(scaling_configuration.value.max_capacity, null) + min_capacity = try(scaling_configuration.value.min_capacity, null) + seconds_until_auto_pause = try(scaling_configuration.value.seconds_until_auto_pause, null) + timeout_action = try(scaling_configuration.value.timeout_action, null) + } + } + + dynamic "serverlessv2_scaling_configuration" { + for_each = length(var.serverlessv2_scaling_configuration) > 0 && var.engine_mode == "provisioned" ? [var.serverlessv2_scaling_configuration] : [] + + content { + max_capacity = serverlessv2_scaling_configuration.value.max_capacity + min_capacity = serverlessv2_scaling_configuration.value.min_capacity + } + } + + skip_final_snapshot = var.skip_final_snapshot + snapshot_identifier = var.snapshot_identifier + source_region = var.source_region + storage_encrypted = var.storage_encrypted + storage_type = var.storage_type + tags = merge(var.tags, var.cluster_tags) + vpc_security_group_ids = compact(concat([try(aws_security_group.this[0].id, "")], var.vpc_security_group_ids)) + + timeouts { + create = try(var.cluster_timeouts.create, null) + update = try(var.cluster_timeouts.update, null) + delete = try(var.cluster_timeouts.delete, null) + } + + lifecycle { + ignore_changes = [ + # See https://github.com/terraform-aws-modules/terraform-aws-rds-aurora/issues/425 + engine_version, + # See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster#replication_source_identifier + # Since this is used either in read-replica clusters or global clusters, this should be acceptable to specify + replication_source_identifier, + # See docs here https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_global_cluster#new-global-cluster-from-existing-db-cluster + global_cluster_identifier, + snapshot_identifier, + ] + } + + depends_on = [aws_cloudwatch_log_group.this] +} + ################################################################################ # Cluster Instance(s) ################################################################################ @@ -163,7 +279,7 @@ resource "aws_rds_cluster_instance" "this" { auto_minor_version_upgrade = try(each.value.auto_minor_version_upgrade, var.auto_minor_version_upgrade) availability_zone = try(each.value.availability_zone, null) ca_cert_identifier = var.ca_cert_identifier - cluster_identifier = aws_rds_cluster.this[0].id + cluster_identifier = var.global_upgradable ? aws_rds_cluster.global_upgradable[0].id : aws_rds_cluster.this[0].id copy_tags_to_snapshot = try(each.value.copy_tags_to_snapshot, var.copy_tags_to_snapshot) db_parameter_group_name = var.create_db_parameter_group ? aws_db_parameter_group.this[0].id : try(each.value.db_parameter_group_name, var.db_parameter_group_name) db_subnet_group_name = local.db_subnet_group_name @@ -198,7 +314,7 @@ resource "aws_rds_cluster_endpoint" "this" { for_each = { for k, v in var.endpoints : k => v if local.create && !local.is_serverless } cluster_endpoint_identifier = each.value.identifier - cluster_identifier = aws_rds_cluster.this[0].id + cluster_identifier = var.global_upgradable ? aws_rds_cluster.global_upgradable[0].id : aws_rds_cluster.this[0].id custom_endpoint_type = each.value.type excluded_members = try(each.value.excluded_members, null) static_members = try(each.value.static_members, null) @@ -216,7 +332,7 @@ resource "aws_rds_cluster_endpoint" "this" { resource "aws_rds_cluster_role_association" "this" { for_each = { for k, v in var.iam_roles : k => v if local.create } - db_cluster_identifier = aws_rds_cluster.this[0].id + db_cluster_identifier = var.global_upgradable ? aws_rds_cluster.global_upgradable[0].id : aws_rds_cluster.this[0].id feature_name = each.value.feature_name role_arn = each.value.role_arn } @@ -275,7 +391,7 @@ resource "aws_appautoscaling_target" "this" { max_capacity = var.autoscaling_max_capacity min_capacity = var.autoscaling_min_capacity - resource_id = "cluster:${aws_rds_cluster.this[0].cluster_identifier}" + resource_id = "cluster:${var.global_upgradable ? aws_rds_cluster.global_upgradable[0].cluster_identifier : aws_rds_cluster.this[0].cluster_identifier}" scalable_dimension = "rds:cluster:ReadReplicaCount" service_namespace = "rds" @@ -293,7 +409,7 @@ resource "aws_appautoscaling_policy" "this" { name = var.autoscaling_policy_name policy_type = "TargetTrackingScaling" - resource_id = "cluster:${aws_rds_cluster.this[0].cluster_identifier}" + resource_id = "cluster:${var.global_upgradable ? aws_rds_cluster.global_upgradable[0].cluster_identifier : aws_rds_cluster.this[0].cluster_identifier}" scalable_dimension = "rds:cluster:ReadReplicaCount" service_namespace = "rds" @@ -429,7 +545,7 @@ resource "aws_cloudwatch_log_group" "this" { resource "aws_rds_cluster_activity_stream" "this" { count = local.create && var.create_db_cluster_activity_stream ? 1 : 0 - resource_arn = aws_rds_cluster.this[0].arn + resource_arn = var.global_upgradable ? aws_rds_cluster.global_upgradable[0].arn : aws_rds_cluster.this[0].arn mode = var.db_cluster_activity_stream_mode kms_key_id = var.db_cluster_activity_stream_kms_key_id engine_native_audit_fields_included = var.engine_native_audit_fields_included diff --git a/outputs.tf b/outputs.tf index 5a46173..e0dff05 100644 --- a/outputs.tf +++ b/outputs.tf @@ -13,37 +13,37 @@ output "db_subnet_group_name" { output "cluster_arn" { description = "Amazon Resource Name (ARN) of cluster" - value = try(aws_rds_cluster.this[0].arn, null) + value = try(aws_rds_cluster.this[0].arn, try(aws_rds_cluster.global_upgradable[0].arn, null)) } output "cluster_id" { description = "The RDS Cluster Identifier" - value = try(aws_rds_cluster.this[0].id, null) + value = try(aws_rds_cluster.this[0].id, try(aws_rds_cluster.global_upgradable[0].id, null)) } output "cluster_resource_id" { description = "The RDS Cluster Resource ID" - value = try(aws_rds_cluster.this[0].cluster_resource_id, null) + value = try(aws_rds_cluster.this[0].cluster_resource_id, try(aws_rds_cluster.global_upgradable[0].cluster_resource_id, null)) } output "cluster_members" { description = "List of RDS Instances that are a part of this cluster" - value = try(aws_rds_cluster.this[0].cluster_members, null) + value = try(aws_rds_cluster.this[0].cluster_members, try(aws_rds_cluster.global_upgradable[0].cluster_members, null)) } output "cluster_endpoint" { description = "Writer endpoint for the cluster" - value = try(aws_rds_cluster.this[0].endpoint, null) + value = try(aws_rds_cluster.this[0].endpoint, try(aws_rds_cluster.global_upgradable[0].endpoint, null)) } output "cluster_reader_endpoint" { description = "A read-only endpoint for the cluster, automatically load-balanced across replicas" - value = try(aws_rds_cluster.this[0].reader_endpoint, null) + value = try(aws_rds_cluster.this[0].reader_endpoint, try(aws_rds_cluster.global_upgradable[0].reader_endpoint, null)) } output "cluster_engine_version_actual" { description = "The running version of the cluster database" - value = try(aws_rds_cluster.this[0].engine_version_actual, null) + value = try(aws_rds_cluster.this[0].engine_version_actual, try(aws_rds_cluster.global_upgradable[0].engine_version_actual, null)) } # database_name is not set on `aws_rds_cluster` resource if it was not specified, so can't be used in output @@ -54,29 +54,29 @@ output "cluster_database_name" { output "cluster_port" { description = "The database port" - value = try(aws_rds_cluster.this[0].port, null) + value = try(aws_rds_cluster.this[0].port, try(aws_rds_cluster.global_upgradable[0].port, null)) } output "cluster_master_password" { description = "The database master password" - value = try(aws_rds_cluster.this[0].master_password, null) + value = try(aws_rds_cluster.this[0].master_password, try(aws_rds_cluster.global_upgradable[0].master_password, null)) sensitive = true } output "cluster_master_username" { description = "The database master username" - value = try(aws_rds_cluster.this[0].master_username, null) + value = try(aws_rds_cluster.this[0].master_username, try(aws_rds_cluster.global_upgradable[0].master_username, null)) sensitive = true } output "cluster_master_user_secret" { description = "The generated database master user secret when `manage_master_user_password` is set to `true`" - value = try(aws_rds_cluster.this[0].master_user_secret, null) + value = try(aws_rds_cluster.this[0].master_user_secret, try(aws_rds_cluster.global_upgradable[0].master_user_secret, null)) } output "cluster_hosted_zone_id" { description = "The Route53 Hosted Zone ID of the endpoint" - value = try(aws_rds_cluster.this[0].hosted_zone_id, null) + value = try(aws_rds_cluster.this[0].hosted_zone_id, try(aws_rds_cluster.global_upgradable[0].hosted_zone_id, null)) } ################################################################################ diff --git a/variables.tf b/variables.tf index ad5746f..37824bb 100644 --- a/variables.tf +++ b/variables.tf @@ -180,6 +180,12 @@ variable "global_cluster_identifier" { default = null } +variable "global_upgradable" { + description = "True if `engine_version` should be ignored for the cluster. This is only relevant if you want to be able to upgrade a member cluster of a global cluster. If this is enabled after creation, you'll need to `terraform mv` to move the resource to this new resource address." + type = bool + default = false +} + variable "iam_database_authentication_enabled" { description = "Specifies whether or mappings of AWS Identity and Access Management (IAM) accounts to database accounts is enabled" type = bool