Skip to content

Commit

Permalink
Merge pull request #61 from schubergphilis/fvb/global-database
Browse files Browse the repository at this point in the history
feat: Add Global Database support
  • Loading branch information
fatbasstard authored Jan 6, 2025
2 parents 256d799 + de2a7a2 commit 58eec9d
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Local .terraform directories
**/.terraform/*
*.lock.hcl

# .tfstate files
*.tfstate
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This can be changed by updating `var.instance_count`. By default all instances u

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3 |
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.8 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 5.81.0 |

## Providers
Expand All @@ -30,7 +30,7 @@ This can be changed by updating `var.instance_count`. By default all instances u

| Name | Source | Version |
|------|--------|---------|
| <a name="module_rds_enhanced_monitoring_role"></a> [rds\_enhanced\_monitoring\_role](#module\_rds\_enhanced\_monitoring\_role) | github.com/schubergphilis/terraform-aws-mcaf-role | v0.3.3 |
| <a name="module_rds_enhanced_monitoring_role"></a> [rds\_enhanced\_monitoring\_role](#module\_rds\_enhanced\_monitoring\_role) | schubergphilis/mcaf-role/aws | ~> 0.4.0 |

## Resources

Expand All @@ -43,8 +43,10 @@ This can be changed by updating `var.instance_count`. By default all instances u
| [aws_rds_cluster_instance.first](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_instance) | resource |
| [aws_rds_cluster_instance.rest](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_instance) | resource |
| [aws_rds_cluster_parameter_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_parameter_group) | resource |
| [aws_rds_global_cluster.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_global_cluster) | resource |
| [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
| [aws_vpc_security_group_ingress_rule.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource |
| [aws_kms_alias.rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source |
| [aws_subnet.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source |

## Inputs
Expand Down Expand Up @@ -75,13 +77,15 @@ This can be changed by updating `var.instance_count`. By default all instances u
| <a name="input_engine_mode"></a> [engine\_mode](#input\_engine\_mode) | The engine mode of the Aurora cluster | `string` | `"provisioned"` | no |
| <a name="input_engine_version"></a> [engine\_version](#input\_engine\_version) | The engine version of the Aurora cluster | `string` | `null` | no |
| <a name="input_final_snapshot_identifier"></a> [final\_snapshot\_identifier](#input\_final\_snapshot\_identifier) | Identifier of the final snapshot to create before deleting the cluster | `string` | `null` | no |
| <a name="input_global_database_primary"></a> [global\_database\_primary](#input\_global\_database\_primary) | Whether the cluster is part of a global database as the primary cluster | `bool` | `false` | no |
| <a name="input_global_database_secondary"></a> [global\_database\_secondary](#input\_global\_database\_secondary) | Whether the cluster is part of a global database as the seconday cluster | <pre>object({<br> global_cluster_identifier = string<br> enable_global_write_forwarding = optional(bool, true)<br> })</pre> | `null` | no |
| <a name="input_iam_database_authentication_enabled"></a> [iam\_database\_authentication\_enabled](#input\_iam\_database\_authentication\_enabled) | Specify if mapping AWS IAM accounts to database accounts is enabled. | `bool` | `true` | no |
| <a name="input_iam_roles"></a> [iam\_roles](#input\_iam\_roles) | A list of IAM Role ARNs to associate with the cluster | `list(string)` | `null` | no |
| <a name="input_instance_class"></a> [instance\_class](#input\_instance\_class) | The class of RDS instances to attach to the cluster instances (not used when `engine_mode` set to `serverless`) | `string` | `null` | no |
| <a name="input_instance_config"></a> [instance\_config](#input\_instance\_config) | Map of instance specific settings that override values set elsewhere in the module, map keys should match instance number | <pre>map(object({<br> instance_class = optional(string, null)<br> promotion_tier = optional(number, null)<br> }))</pre> | `null` | no |
| <a name="input_instance_count"></a> [instance\_count](#input\_instance\_count) | The number of RDS instances to attach (not used when `engine_mode` set to `serverless`) | `number` | `2` | no |
| <a name="input_iops"></a> [iops](#input\_iops) | The amount of Provisioned IOPS to be initially allocated for each DB instance. (Required for Multi-AZ DB cluster) | `number` | `null` | no |
| <a name="input_kms_key_id"></a> [kms\_key\_id](#input\_kms\_key\_id) | ID of KMS key to encrypt storage and performance insights data | `string` | `null` | no |
| <a name="input_kms_key_id"></a> [kms\_key\_id](#input\_kms\_key\_id) | ARN of KMS key to encrypt storage and performance insights data | `string` | `null` | no |
| <a name="input_manage_master_user"></a> [manage\_master\_user](#input\_manage\_master\_user) | Set to false to provide a custom password using `master_password` | `bool` | `true` | no |
| <a name="input_master_password"></a> [master\_password](#input\_master\_password) | Password for the master DB user, must set `manage_master_user` to false if specifying a custom password | `string` | `null` | no |
| <a name="input_master_user_secret_kms_key_id"></a> [master\_user\_secret\_kms\_key\_id](#input\_master\_user\_secret\_kms\_key\_id) | ID of KMS key to encrypt the master user Secrets Manager secret | `string` | `null` | no |
Expand Down Expand Up @@ -114,6 +118,7 @@ This can be changed by updating `var.instance_count`. By default all instances u
| <a name="output_custom_endpoints"></a> [custom\_endpoints](#output\_custom\_endpoints) | The DNS addresses of the custom endpoints. |
| <a name="output_database"></a> [database](#output\_database) | Name of the first database created when the cluster was created |
| <a name="output_endpoint"></a> [endpoint](#output\_endpoint) | DNS address of the RDS instance |
| <a name="output_global_cluster_identifier"></a> [global\_cluster\_identifier](#output\_global\_cluster\_identifier) | If the cluster is the primary of a global cluster, the global cluster ID |
| <a name="output_id"></a> [id](#output\_id) | ID of the Aurora cluster |
| <a name="output_instance_ids"></a> [instance\_ids](#output\_instance\_ids) | Aurora instances IDs |
| <a name="output_master_user_secret"></a> [master\_user\_secret](#output\_master\_user\_secret) | The generated database master user secret when `var.manage_master_user` is set to `true` |
Expand Down
105 changes: 105 additions & 0 deletions examples/global-database/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
locals {
vpc_cidr = "10.0.0.0/16"

azs_primary = slice(data.aws_availability_zones.available_primary.names, 0, 3)
azs_secondary = slice(data.aws_availability_zones.available_secondary.names, 0, 3)
}

# Primary region
data "aws_availability_zones" "available_primary" {}

provider "aws" {
alias = "primary"
region = "eu-west-1"
}

module "vpc_primary" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 4.0"

name = "example"
azs = local.azs_primary
cidr = local.vpc_cidr
database_subnets = [for k, v in local.azs_primary : cidrsubnet(local.vpc_cidr, 8, k + 6)]
private_subnets = [for k, v in local.azs_primary : cidrsubnet(local.vpc_cidr, 8, k + 3)]
public_subnets = [for k, v in local.azs_primary : cidrsubnet(local.vpc_cidr, 8, k)]
}

resource "random_string" "master_password" {
length = 16
special = false
}

module "aurora_primary" {
providers = {
aws = aws.primary
}

source = "../.."

name = "example"
engine = "postgresql"
engine_mode = "serverlessv2"
engine_version = "16.4"
global_database_primary = true
instance_count = 2 # 1 Writer, 1 Reader
master_password = random_string.master_password.result
subnet_ids = module.vpc_primary.private_subnets

security_group_ingress_rules = [
{
cidr_ipv4 = local.vpc_cidr
description = "Allow access from the VPC CIDR range"
}
]
}

# Secondary region
provider "aws" {
alias = "secondary"
region = "eu-central-1"
}

data "aws_availability_zones" "available_secondary" {}

module "vpc_secondary" {
providers = {
aws = aws.secondary
}

source = "terraform-aws-modules/vpc/aws"
version = "~> 4.0"

name = "example"
azs = local.azs_secondary
cidr = local.vpc_cidr
database_subnets = [for k, v in local.azs_secondary : cidrsubnet(local.vpc_cidr, 8, k + 6)]
private_subnets = [for k, v in local.azs_secondary : cidrsubnet(local.vpc_cidr, 8, k + 3)]
public_subnets = [for k, v in local.azs_secondary : cidrsubnet(local.vpc_cidr, 8, k)]
}

module "aurora_secondary" {
providers = {
aws = aws.secondary
}

source = "../.."

name = "example"
engine = "postgresql"
engine_mode = "serverlessv2"
engine_version = "16.4"
instance_count = 1 # 1 Reader
subnet_ids = module.vpc_secondary.private_subnets

global_database_secondary = {
global_cluster_identifier = module.aurora_primary.global_cluster_identifier
}

security_group_ingress_rules = [
{
cidr_ipv4 = local.vpc_cidr
description = "Allow access from the VPC CIDR range"
}
]
}
13 changes: 13 additions & 0 deletions examples/global-database/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.81.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.4.0"
}
}
required_version = ">= 1.8"
}
42 changes: 37 additions & 5 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ locals {
instance_class = var.engine_mode == "serverlessv2" ? "db.serverless" : var.instance_class
skip_final_snapshot = var.final_snapshot_identifier == null

global_cluster_identifier = var.global_database_primary ? aws_rds_global_cluster.default[0].id : var.global_database_secondary != null ? var.global_database_secondary.global_cluster_identifier : null

// For a secondary cluster, the KMS key must be specified explicitly even if defaulted to the AWS Managed alias ("For encrypted cross-region replica, kmsKeyId should be explicitly specified").
kms_key_arn = var.global_database_secondary != null && var.storage_encrypted && var.kms_key_id == null ? data.aws_kms_alias.rds.target_key_arn : var.kms_key_id

// Backtrack is only supported for MySQL clusters
backtrack_window = {
"mysql" = var.backtrack_window
Expand All @@ -21,7 +26,7 @@ locals {
}[var.engine]

// Default master username to use unless otherwise specified
master_username = var.master_username != null ? var.master_username : {
master_username = var.master_username != null || var.global_database_secondary != null ? var.master_username : {
"mysql" = "root"
"postgresql" = "postgres"
}[var.engine]
Expand All @@ -31,6 +36,25 @@ data "aws_subnet" "selected" {
id = var.subnet_ids[0]
}

################################################################################
# Global Database
################################################################################

data "aws_kms_alias" "rds" {
name = "alias/aws/rds"
}

resource "aws_rds_global_cluster" "default" {
count = var.global_database_primary ? 1 : 0

global_cluster_identifier = "${var.name}-global"
engine = "aurora-${var.engine}"
engine_version = var.engine_version
database_name = var.database
storage_encrypted = var.storage_encrypted
tags = var.tags
}

################################################################################
# Cluster
################################################################################
Expand All @@ -51,14 +75,16 @@ resource "aws_rds_cluster" "default" {
deletion_protection = var.deletion_protection
enable_http_endpoint = var.enable_http_endpoint
enabled_cloudwatch_logs_exports = var.enable_cloudwatch_logs_exports ? local.enabled_cloudwatch_logs_exports : null
enable_global_write_forwarding = var.global_database_secondary != null ? var.global_database_secondary.enable_global_write_forwarding : null
engine = "aurora-${var.engine}"
engine_mode = var.engine_mode == "serverlessv2" ? "provisioned" : var.engine_mode
engine_version = var.engine_version
final_snapshot_identifier = var.final_snapshot_identifier
global_cluster_identifier = local.global_cluster_identifier
iam_database_authentication_enabled = var.iam_database_authentication_enabled
iam_roles = var.iam_roles
iops = var.iops
kms_key_id = var.kms_key_id
kms_key_id = local.kms_key_arn
manage_master_user_password = var.manage_master_user ? var.manage_master_user : null
master_password = var.master_password
master_user_secret_kms_key_id = var.master_user_secret_kms_key_id
Expand Down Expand Up @@ -93,6 +119,10 @@ resource "aws_rds_cluster" "default" {
seconds_until_auto_pause = var.min_capacity == 0 ? var.seconds_until_auto_pause : null
}
}

lifecycle {
ignore_changes = [replication_source_identifier]
}
}

################################################################################
Expand Down Expand Up @@ -140,7 +170,7 @@ resource "aws_rds_cluster_instance" "first" {
monitoring_interval = var.monitoring_interval
monitoring_role_arn = try(module.rds_enhanced_monitoring_role[0].arn, null)
performance_insights_enabled = var.performance_insights
performance_insights_kms_key_id = var.performance_insights ? var.kms_key_id : null
performance_insights_kms_key_id = var.performance_insights ? local.kms_key_arn : null
performance_insights_retention_period = var.performance_insights ? var.performance_insights_retention_period : null
promotion_tier = try(var.instance_config[count.index + 1]["promotion_tier"], null)
publicly_accessible = var.publicly_accessible
Expand All @@ -164,7 +194,7 @@ resource "aws_rds_cluster_instance" "rest" {
monitoring_interval = var.monitoring_interval
monitoring_role_arn = try(module.rds_enhanced_monitoring_role[0].arn, null)
performance_insights_enabled = var.performance_insights
performance_insights_kms_key_id = var.performance_insights ? var.kms_key_id : null
performance_insights_kms_key_id = var.performance_insights ? local.kms_key_arn : null
performance_insights_retention_period = var.performance_insights ? var.performance_insights_retention_period : null
promotion_tier = try(var.instance_config[count.index + 2]["promotion_tier"], null)
publicly_accessible = var.publicly_accessible
Expand Down Expand Up @@ -192,13 +222,15 @@ resource "aws_db_subnet_group" "default" {
module "rds_enhanced_monitoring_role" {
count = var.monitoring_interval != null ? 1 : 0

source = "schubergphilis/mcaf-role/aws"
version = "~> 0.4.0"

name = "RDSEnhancedMonitoringRole-${var.name}"
permissions_boundary = var.permissions_boundary
policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"]
postfix = false
principal_identifiers = ["monitoring.rds.amazonaws.com"]
principal_type = "Service"
source = "github.com/schubergphilis/terraform-aws-mcaf-role?ref=v0.3.3"
tags = var.tags
}

Expand Down
5 changes: 5 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ output "database" {
description = "Name of the first database created when the cluster was created"
}

output "global_cluster_identifier" {
value = var.global_database_primary ? aws_rds_global_cluster.default[0].id : null
description = "If the cluster is the primary of a global cluster, the global cluster ID"
}

output "endpoint" {
value = aws_rds_cluster.default.endpoint
description = "DNS address of the RDS instance"
Expand Down
42 changes: 41 additions & 1 deletion variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ variable "database" {
type = string
default = null
description = "The name of the first database to be created when the cluster is created"

validation {
condition = (var.global_database_secondary != null && var.database == null) || var.global_database_secondary == null
error_message = "Cannot specify database name for global secondary cluster"
}
}

variable "database_parameters" {
Expand Down Expand Up @@ -174,6 +179,26 @@ variable "final_snapshot_identifier" {
description = "Identifier of the final snapshot to create before deleting the cluster"
}

variable "global_database_primary" {
type = bool
default = false
description = "Whether the cluster is part of a global database as the primary cluster"
}

variable "global_database_secondary" {
type = object({
global_cluster_identifier = string
enable_global_write_forwarding = optional(bool, true)
})
default = null
description = "Whether the cluster is part of a global database as the seconday cluster"

validation {
condition = !(var.global_database_primary && var.global_database_secondary != null)
error_message = "Cannot configure a cluster as both primary and secondary in a global database"
}
}

variable "iam_database_authentication_enabled" {
type = bool
default = true
Expand Down Expand Up @@ -216,19 +241,29 @@ variable "iops" {
variable "kms_key_id" {
type = string
default = null
description = "ID of KMS key to encrypt storage and performance insights data"
description = "ARN of KMS key to encrypt storage and performance insights data"
}

variable "manage_master_user" {
type = bool
default = true
description = "Set to false to provide a custom password using `master_password`"

validation {
condition = var.global_database_primary == false || (var.global_database_primary && var.manage_master_user == false)
error_message = "Cannot enable manage_master_user for a global database"
}
}

variable "master_password" {
type = string
default = null
description = "Password for the master DB user, must set `manage_master_user` to false if specifying a custom password"

validation {
condition = (var.global_database_secondary != null && var.master_password == null) || var.global_database_secondary == null
error_message = "Cannot specify master_password for global secondary cluster"
}
}

variable "master_user_secret_kms_key_id" {
Expand All @@ -241,6 +276,11 @@ variable "master_username" {
type = string
default = null
description = "Username for the master DB user"

validation {
condition = (var.global_database_secondary != null && var.master_username == null) || var.global_database_secondary == null
error_message = "Cannot specify master_username for global secondary cluster"
}
}

variable "max_capacity" {
Expand Down
2 changes: 1 addition & 1 deletion versions.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ terraform {
version = ">= 5.81.0"
}
}
required_version = ">= 1.3"
required_version = ">= 1.8"
}

0 comments on commit 58eec9d

Please sign in to comment.