From ac21bc248500f316bfadf255192d97900b6c9587 Mon Sep 17 00:00:00 2001 From: Marcin Cuber Date: Mon, 9 Nov 2020 12:41:14 +0000 Subject: [PATCH] Add support for global clusters (#7) * Add support for global clusters * Add full support for global cluster --- .pre-commit-config.yaml | 4 +- README.md | 18 ++- examples/aurora-mysql/kms.tf | 109 ++++++++++++++++++ examples/aurora-mysql/main.tf | 97 +--------------- examples/global-aurora-mysql/kms.tf | 76 +++++++++++++ examples/global-aurora-mysql/main.tf | 164 +++++++++++++++++++++++++++ main.tf | 92 +++++++++++++-- outputs.tf | 18 +-- variables.tf | 7 ++ versions.tf | 4 +- 10 files changed, 471 insertions(+), 118 deletions(-) create mode 100644 examples/aurora-mysql/kms.tf create mode 100644 examples/global-aurora-mysql/kms.tf create mode 100644 examples/global-aurora-mysql/main.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5094823..5e4b5ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v3.3.0 hooks: - id: check-added-large-files args: ['--maxkb=500'] @@ -18,7 +18,7 @@ repos: args: ['--allow-missing-credentials'] - id: trailing-whitespace - repo: git://github.com/antonbabenko/pre-commit-terraform - rev: v1.43.0 + rev: v1.44.0 hooks: - id: terraform_fmt - id: terraform_docs diff --git a/README.md b/README.md index 44b4514..9fe241f 100644 --- a/README.md +++ b/README.md @@ -67,25 +67,36 @@ Module is to be used with Terraform > 0.12. ## Examples * [Aurora MySQL](https://github.com/umotif-public/terraform-aws-rds-aurora/tree/master/examples/aurora-mysql) +* [Global Aurora MySQL](https://github.com/umotif-public/terraform-aws-rds-aurora/tree/master/examples/global-aurora-mysql) ## Authors Module managed by [Marcin Cuber](https://github.com/marcincuber) [LinkedIn](https://www.linkedin.com/in/marcincuber/). +## Global Aurora Cluster + +Module supports configuration for Global Cluster, see an appropriate example for full configuration. + +Please note that there are various limitations from AWS that you need to consider. See the [AWS doc](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database.html#aurora-global-database.limitations). + +On the Terraform side, if you decide to upgrade engine version. You will need to run `terraform apply` twice. This is required since Terraform will only upgrade 2nd cluster during first run. During second run Terraform will upgrade the 1st cluster and automatically update global cluster version to match all clusters. + +In order to activate global cluster, set `enable_global_cluster = true` when using this module. + ## Requirements | Name | Version | |------|---------| -| terraform | >= 0.12.6, < 0.14 | -| aws | >= 3.8, < 4.0 | +| terraform | >= 0.12.6 | +| aws | >= 3.8 | | random | >= 2.3 | ## Providers | Name | Version | |------|---------| -| aws | >= 3.8, < 4.0 | +| aws | >= 3.8 | | random | >= 2.3 | ## Inputs @@ -112,6 +123,7 @@ Module managed by [Marcin Cuber](https://github.com/marcincuber) [LinkedIn](http | db\_parameter\_group\_name | The name of a DB parameter group to use | `string` | `null` | no | | db\_subnet\_group\_name | The existing subnet group name to use | `string` | `""` | no | | deletion\_protection | If the DB instance should have deletion protection enabled | `bool` | `false` | no | +| enable\_global\_cluster | Set this variable to `true` if DB Cluster is going to be part of a Global Cluster. | `bool` | `false` | no | | enable\_http\_endpoint | Whether or not to enable the Data API for a serverless Aurora database engine. | `bool` | `false` | no | | enabled\_cloudwatch\_logs\_exports | List of object which define log types to export to cloudwatch. See in examples. | `list` | `[]` | no | | engine | Aurora database engine type, currently aurora, aurora-mysql or aurora-postgresql | `string` | `"aurora"` | no | diff --git a/examples/aurora-mysql/kms.tf b/examples/aurora-mysql/kms.tf new file mode 100644 index 0000000..bfccb45 --- /dev/null +++ b/examples/aurora-mysql/kms.tf @@ -0,0 +1,109 @@ +data "aws_iam_policy_document" "rds" { + statement { + sid = "Enable IAM User Permissions" + + actions = ["kms:*"] + + resources = ["*"] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + data.aws_caller_identity.current.arn + ] + } + } + + statement { + sid = "Allow use of the key" + + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + + resources = ["*"] + + principals { + type = "Service" + identifiers = [ + "rds.amazonaws.com", + "monitoring.rds.amazonaws.com" + ] + } + } +} + +data "aws_iam_policy_document" "cloudwatch" { + statement { + sid = "Enable IAM User Permissions" + + actions = ["kms:*"] + + resources = ["*"] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + data.aws_caller_identity.current.arn + ] + } + } + + statement { + sid = "Allow use of the key" + + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + + resources = ["*"] + + principals { + type = "Service" + identifiers = [ + "logs.${data.aws_region.current.name}.amazonaws.com" + ] + } + } +} + +############# +# KMS key +############# +module "kms" { + source = "umotif-public/kms/aws" + version = "~> 1.0" + + alias_name = "rds-kms-test-key" + deletion_window_in_days = 7 + enable_key_rotation = true + policy = data.aws_iam_policy_document.rds.json + + tags = { + Environment = "test" + } +} + +module "kms-cloudwatch" { + source = "umotif-public/kms/aws" + version = "~> 1.0" + + alias_name = "cloudwatch-kms-test-key" + deletion_window_in_days = 7 + enable_key_rotation = true + policy = data.aws_iam_policy_document.cloudwatch.json + + tags = { + Environment = "test" + } +} diff --git a/examples/aurora-mysql/main.tf b/examples/aurora-mysql/main.tf index 783d515..82fd09f 100644 --- a/examples/aurora-mysql/main.tf +++ b/examples/aurora-mysql/main.tf @@ -10,9 +10,9 @@ data "aws_region" "current" {} ##### module "vpc" { source = "terraform-aws-modules/vpc/aws" - version = "~> 2.48" + version = "~> 2.63" - name = "simple-vpc" + name = "simple-rds-aurora-vpc" cidr = "10.0.0.0/16" @@ -23,99 +23,6 @@ module "vpc" { enable_nat_gateway = false } -############# -# KMS key -############# -module "kms" { - source = "umotif-public/kms/aws" - version = "~> 1.0" - - alias_name = "rds-kms-test-key" - deletion_window_in_days = 7 - enable_key_rotation = true - policy = jsonencode( - { - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "Enable IAM User Permissions", - "Effect" : "Allow", - "Principal" : { - "AWS" : [ - "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", - data.aws_caller_identity.current.arn - ] - }, - "Action" : "kms:*", - "Resource" : "*" - }, - { - "Sid" : "Allow use of the key", - "Effect" : "Allow", - "Principal" : { - "Service" : ["rds.amazonaws.com", "monitoring.rds.amazonaws.com"] - }, - "Action" : [ - "kms:Encrypt", - "kms:Decrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*", - "kms:DescribeKey" - ], - "Resource" : "*" - } - ] - } - ) - - tags = { - Environment = "test" - } -} - -module "kms-cloudwatch" { - source = "umotif-public/kms/aws" - version = "~> 1.0" - - alias_name = "cloudwatch-kms-test-key" - deletion_window_in_days = 7 - enable_key_rotation = true - policy = jsonencode( - { - "Version" : "2012-10-17", - "Statement" : [ - { - "Sid" : "Enable IAM User Permissions", - "Effect" : "Allow", - "Principal" : { - "AWS" : [ - "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", - data.aws_caller_identity.current.arn - ] - }, - "Action" : "kms:*", - "Resource" : "*" - }, - { - "Effect" : "Allow", - "Principal" : { "Service" : "logs.${data.aws_region.current.name}.amazonaws.com" }, - "Action" : [ - "kms:Encrypt*", - "kms:Decrypt*", - "kms:ReEncrypt*", - "kms:GenerateDataKey*", - "kms:Describe*" - ], - "Resource" : "*" - } - ] - } - ) - - tags = { - Environment = "test" - } -} ############# # RDS Aurora ############# diff --git a/examples/global-aurora-mysql/kms.tf b/examples/global-aurora-mysql/kms.tf new file mode 100644 index 0000000..d4eda8a --- /dev/null +++ b/examples/global-aurora-mysql/kms.tf @@ -0,0 +1,76 @@ +data "aws_iam_policy_document" "rds" { + statement { + sid = "Enable IAM User Permissions" + + actions = ["kms:*"] + + resources = ["*"] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + data.aws_caller_identity.current.arn + ] + } + } + + statement { + sid = "Allow use of the key" + + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ] + + resources = ["*"] + + principals { + type = "Service" + identifiers = [ + "rds.amazonaws.com", + "monitoring.rds.amazonaws.com" + ] + } + } +} + +module "kms-ireland" { + source = "umotif-public/kms/aws" + version = "~> 1.0" + + providers = { + aws = aws.primary + } + + alias_name = "global-rds-kms-test-key" + deletion_window_in_days = 7 + enable_key_rotation = true + policy = data.aws_iam_policy_document.rds.json + + + tags = { + Environment = "test" + } +} + +module "kms-london" { + source = "umotif-public/kms/aws" + version = "~> 1.0" + + providers = { + aws = aws.secondary + } + + alias_name = "global-rds-kms-test-key" + deletion_window_in_days = 7 + enable_key_rotation = true + policy = data.aws_iam_policy_document.rds.json + + tags = { + Environment = "test" + } +} diff --git a/examples/global-aurora-mysql/main.tf b/examples/global-aurora-mysql/main.tf new file mode 100644 index 0000000..e164bec --- /dev/null +++ b/examples/global-aurora-mysql/main.tf @@ -0,0 +1,164 @@ +provider "aws" { + region = "eu-west-1" + version = ">= 3.14" +} + +provider "aws" { + alias = "primary" + region = "eu-west-1" + version = ">= 3.14" +} + +provider "aws" { + alias = "secondary" + region = "eu-west-2" + version = ">= 3.14" +} + +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +variable "engine_version" { + type = string + default = "5.7.mysql_aurora.2.09.0" +} + +##### +# VPC and subnets +##### +module "vpc_ireland" { + providers = { + aws = aws.primary + } + + source = "terraform-aws-modules/vpc/aws" + version = "~> 2.63" + + name = "simple-vpc" + + cidr = "10.0.0.0/16" + + azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"] + private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] + public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] + + enable_nat_gateway = false +} + +module "vpc_london" { + providers = { + aws = aws.secondary + } + + source = "terraform-aws-modules/vpc/aws" + version = "~> 2.63" + + name = "simple-vpc" + + cidr = "10.0.0.0/16" + + azs = ["eu-west-2a", "eu-west-2b", "eu-west-2c"] + private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] + public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] + + enable_nat_gateway = false +} + +resource "aws_rds_global_cluster" "main" { + provider = aws.primary + + engine = "aurora-mysql" + engine_version = var.engine_version + global_cluster_identifier = "main-global-mysql-cluster" + deletion_protection = false + + storage_encrypted = true + + lifecycle { + ignore_changes = [engine_version] + } +} + +############# +# RDS Aurora +############# +module "aurora_primary" { + source = "../../" + + providers = { + aws = aws.primary + } + + global_cluster_identifier = aws_rds_global_cluster.main.id + + enable_global_cluster = true + + name_prefix = "example-aurora-mysql-ireland" + engine_mode = "provisioned" + engine = "aurora-mysql" + engine_version = var.engine_version + deletion_protection = false + + storage_encrypted = true + kms_key_id = module.kms-ireland.key_arn + + vpc_id = module.vpc_ireland.vpc_id + subnets = module.vpc_ireland.public_subnets + + replica_count = 2 + instance_type = "db.r5.large" + apply_immediately = true + allow_major_version_upgrade = true + skip_final_snapshot = true + + create_security_group = true + + tags = { + Environment = "test" + } + + depends_on = [aws_rds_global_cluster.main] +} + +module "aurora_secondary" { + source = "../../" + + providers = { + aws = aws.secondary + } + + enable_global_cluster = true + + source_region = "eu-west-1" + global_cluster_identifier = aws_rds_global_cluster.main.id + + name_prefix = "example-aurora-mysql-london" + engine_mode = "provisioned" + engine = "aurora-mysql" + engine_version = var.engine_version + deletion_protection = false + + storage_encrypted = true + kms_key_id = module.kms-london.key_arn + + username = null + password = null + + vpc_id = module.vpc_london.vpc_id + subnets = module.vpc_london.public_subnets + + replica_count = 2 + instance_type = "db.r5.large" + apply_immediately = true + allow_major_version_upgrade = true + skip_final_snapshot = true + + create_security_group = true + + tags = { + Environment = "test" + } + + depends_on = [module.aurora_primary, aws_rds_global_cluster.main] +} + diff --git a/main.tf b/main.tf index 0f32a61..9be573e 100644 --- a/main.tf +++ b/main.tf @@ -39,8 +39,8 @@ resource "aws_security_group_rule" "main_default_ingress" { description = "Ingress allowed from SGs" type = "ingress" - from_port = aws_rds_cluster.main.port - to_port = aws_rds_cluster.main.port + from_port = var.enable_global_cluster ? aws_rds_cluster.global[0].port : aws_rds_cluster.main[0].port + to_port = var.enable_global_cluster ? aws_rds_cluster.global[0].port : aws_rds_cluster.main[0].port protocol = "tcp" source_security_group_id = element(var.allowed_security_groups, count.index) security_group_id = join("", aws_security_group.main.*.id) @@ -52,8 +52,8 @@ resource "aws_security_group_rule" "main_cidr_ingress" { description = "Ingress allowed from CIDRs" type = "ingress" - from_port = aws_rds_cluster.main.port - to_port = aws_rds_cluster.main.port + from_port = var.enable_global_cluster ? aws_rds_cluster.global[0].port : aws_rds_cluster.main[0].port + to_port = var.enable_global_cluster ? aws_rds_cluster.global[0].port : aws_rds_cluster.main[0].port protocol = "tcp" cidr_blocks = var.allowed_cidr_blocks security_group_id = join("", aws_security_group.main.*.id) @@ -91,7 +91,13 @@ resource "aws_db_subnet_group" "main" { ) } +##### +# Standard RDS cluster +##### + resource "aws_rds_cluster" "main" { + count = var.enable_global_cluster ? 0 : 1 + global_cluster_identifier = var.global_cluster_identifier cluster_identifier = var.name_prefix replication_source_identifier = var.replication_source_identifier @@ -158,11 +164,83 @@ resource "aws_rds_cluster" "main" { depends_on = [aws_cloudwatch_log_group.audit_log_group] } +##### +# RDS cluster which is part of Global cluster +##### +resource "aws_rds_cluster" "global" { + count = var.enable_global_cluster ? 1 : 0 + + global_cluster_identifier = var.global_cluster_identifier + cluster_identifier = var.name_prefix + replication_source_identifier = var.replication_source_identifier + + source_region = var.source_region + engine = var.engine + engine_mode = var.engine_mode + engine_version = var.engine_version + enable_http_endpoint = var.enable_http_endpoint + + kms_key_id = var.kms_key_id + + database_name = var.database_name + master_username = var.username + master_password = var.password == "" ? random_password.master_password.result : var.password + + final_snapshot_identifier = "${var.final_snapshot_identifier_prefix}-${var.name_prefix}-${random_id.snapshot_identifier.hex}" + skip_final_snapshot = var.skip_final_snapshot + snapshot_identifier = var.snapshot_identifier + copy_tags_to_snapshot = var.copy_tags_to_snapshot + + deletion_protection = var.deletion_protection + backup_retention_period = var.backup_retention_period + preferred_backup_window = var.preferred_backup_window + preferred_maintenance_window = var.preferred_cluster_maintenance_window + + allow_major_version_upgrade = var.allow_major_version_upgrade + apply_immediately = var.apply_immediately + + port = var.port == "" ? var.engine == "aurora-postgresql" ? "5432" : "3306" : var.port + db_subnet_group_name = var.db_subnet_group_name == "" ? join("", aws_db_subnet_group.main.*.name) : var.db_subnet_group_name + vpc_security_group_ids = compact(concat(aws_security_group.main.*.id, var.vpc_security_group_ids)) + storage_encrypted = var.storage_encrypted + + db_cluster_parameter_group_name = var.create_parameter_group ? aws_rds_cluster_parameter_group.main[0].id : var.db_cluster_parameter_group_name + iam_database_authentication_enabled = var.iam_database_authentication_enabled + + backtrack_window = (var.engine == "aurora-mysql" || var.engine == "aurora") && var.engine_mode != "serverless" ? var.backtrack_window : 0 + iam_roles = var.iam_roles + + enabled_cloudwatch_logs_exports = [for log in var.enabled_cloudwatch_logs_exports : log.name] + + dynamic "scaling_configuration" { + for_each = length(keys(var.scaling_configuration)) == 0 ? [] : [var.scaling_configuration] + + content { + auto_pause = lookup(scaling_configuration.value, "auto_pause", null) + max_capacity = lookup(scaling_configuration.value, "max_capacity", null) + min_capacity = lookup(scaling_configuration.value, "min_capacity", null) + seconds_until_auto_pause = lookup(scaling_configuration.value, "seconds_until_auto_pause", null) + timeout_action = lookup(scaling_configuration.value, "timeout_action", null) + } + } + + tags = merge( + var.tags, + var.cluster_tags + ) + + lifecycle { + ignore_changes = [master_username, master_password, replication_source_identifier] + } + + depends_on = [aws_cloudwatch_log_group.audit_log_group] +} + resource "aws_rds_cluster_instance" "main" { count = var.replica_scale_enabled ? var.replica_scale_min : var.replica_count identifier = try(var.instances_parameters[count.index].instance_name, "${var.name_prefix}-${count.index + 1}") - cluster_identifier = aws_rds_cluster.main.id + cluster_identifier = var.enable_global_cluster ? aws_rds_cluster.global[0].id : aws_rds_cluster.main[0].id engine = var.engine engine_version = var.engine_version @@ -296,7 +374,7 @@ resource "aws_appautoscaling_target" "read_replica" { max_capacity = var.replica_scale_max min_capacity = var.replica_scale_min - resource_id = "cluster:${aws_rds_cluster.main.cluster_identifier}" + resource_id = var.enable_global_cluster ? "cluster:${aws_rds_cluster.global[0].cluster_identifier}" : "cluster:${aws_rds_cluster.main[0].cluster_identifier}" scalable_dimension = "rds:cluster:ReadReplicaCount" service_namespace = "rds" } @@ -306,7 +384,7 @@ resource "aws_appautoscaling_policy" "read_replica" { name = "target-metric" policy_type = "TargetTrackingScaling" - resource_id = "cluster:${aws_rds_cluster.main.cluster_identifier}" + resource_id = var.enable_global_cluster ? "cluster:${aws_rds_cluster.global[0].cluster_identifier}" : "cluster:${aws_rds_cluster.main[0].cluster_identifier}" scalable_dimension = "rds:cluster:ReadReplicaCount" service_namespace = "rds" diff --git a/outputs.tf b/outputs.tf index 0509207..c49c686 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,48 +1,48 @@ // aws_rds_cluster output "rds_cluster_arn" { description = "The ID of the aurora cluster" - value = aws_rds_cluster.main.arn + value = var.enable_global_cluster ? join("", aws_rds_cluster.global.*.arn) : join("", aws_rds_cluster.main.*.arn) } output "rds_cluster_id" { description = "The ID of the cluster" - value = aws_rds_cluster.main.id + value = var.enable_global_cluster ? join("", aws_rds_cluster.global.*.id) : join("", aws_rds_cluster.main.*.id) } output "rds_cluster_resource_id" { description = "The Resource ID of the cluster" - value = aws_rds_cluster.main.cluster_resource_id + value = var.enable_global_cluster ? join("", aws_rds_cluster.global.*.cluster_resource_id) : join("", aws_rds_cluster.main.*.cluster_resource_id) } output "rds_cluster_endpoint" { description = "The cluster endpoint" - value = aws_rds_cluster.main.endpoint + value = var.enable_global_cluster ? join("", aws_rds_cluster.global.*.endpoint) : join("", aws_rds_cluster.main.*.endpoint) } output "rds_cluster_reader_endpoint" { description = "The cluster reader endpoint" - value = aws_rds_cluster.main.reader_endpoint + value = var.enable_global_cluster ? join("", aws_rds_cluster.global.*.reader_endpoint) : join("", aws_rds_cluster.main.*.reader_endpoint) } output "rds_cluster_master_password" { description = "The master password" - value = aws_rds_cluster.main.master_password + value = var.enable_global_cluster ? aws_rds_cluster.global.*.master_password : aws_rds_cluster.main.*.master_password sensitive = true } output "rds_cluster_port" { description = "The port" - value = aws_rds_cluster.main.port + value = var.enable_global_cluster ? join("", aws_rds_cluster.global.*.port) : join("", aws_rds_cluster.main.*.port) } output "rds_cluster_master_username" { description = "The master username" - value = aws_rds_cluster.main.master_username + value = var.enable_global_cluster ? join("", aws_rds_cluster.global.*.master_username) : join("", aws_rds_cluster.main.*.master_username) } output "rds_cluster_instance_endpoints" { description = "A list of all cluster instance endpoints" - value = aws_rds_cluster_instance.main.*.endpoint + value = join("", aws_rds_cluster_instance.main.*.endpoint) } output "security_group_id" { diff --git a/variables.tf b/variables.tf index b2473ca..bed1440 100644 --- a/variables.tf +++ b/variables.tf @@ -397,3 +397,10 @@ variable "cluster_parameters" { description = "A list of cluster parameter objects" default = [] } + +variable "enable_global_cluster" { + type = bool + description = "Set this variable to `true` if DB Cluster is going to be part of a Global Cluster." + default = false +} + diff --git a/versions.tf b/versions.tf index 910dc6f..4bf35b3 100644 --- a/versions.tf +++ b/versions.tf @@ -1,8 +1,8 @@ terraform { - required_version = ">= 0.12.6, < 0.14" + required_version = ">= 0.12.6" required_providers { - aws = ">= 3.8, < 4.0" + aws = ">= 3.8" random = ">= 2.3" } }