Skip to main content

Overview

An Aurora Global Database spans multiple AWS Regions, with a single primary cluster handling writes and one or more secondary clusters providing low-latency reads close to geographically distributed users. Replication from primary to secondary typically occurs in under a second. Use a global cluster when you need:
  • Disaster recovery — promote a secondary cluster to primary within minutes if a regional outage occurs.
  • Global read performance — serve read traffic locally from the region nearest to your users.
  • Write forwarding — allow secondary clusters to forward write operations back to the primary without application-level routing changes.
Promoting a secondary cluster to primary is a manual, multi-step process that involves modifying both the global cluster and the affected regional clusters. Plan and test your failover procedure before relying on it for production DR. See the AWS documentation on managed planned failover for details.

Setup Order

1

Create the global cluster resource

The aws_rds_global_cluster resource defines the shared engine, version, and encryption settings. Both the primary and secondary modules reference this resource.
resource "aws_rds_global_cluster" "this" {
  global_cluster_identifier = local.name
  engine                    = "aurora-postgresql"
  engine_version            = "17.5"
  database_name             = "example_db"
  storage_encrypted         = true
}
2

Create the primary cluster

The primary cluster is a normal Aurora cluster that references the global cluster via global_cluster_identifier. Set is_primary_cluster = true (the default) so the module creates a writer instance and accepts master_username.
module "aurora_primary" {
  source = "terraform-aws-modules/rds-aurora/aws"

  name                      = local.name
  database_name             = aws_rds_global_cluster.this.database_name
  engine                    = aws_rds_global_cluster.this.engine
  engine_version            = aws_rds_global_cluster.this.engine_version
  master_username           = "root"
  global_cluster_identifier = aws_rds_global_cluster.this.id
  cluster_instance_class    = "db.r8g.large"
  instances                 = { for i in range(2) : i => {} }
  kms_key_id                = aws_kms_key.primary.arn

  vpc_id               = module.primary_vpc.vpc_id
  db_subnet_group_name = module.primary_vpc.database_subnet_group_name
  security_group_ingress_rules = {
    private-az1 = {
      cidr_ipv4 = element(module.primary_vpc.private_subnets_cidr_blocks, 0)
    }
    private-az2 = {
      cidr_ipv4 = element(module.primary_vpc.private_subnets_cidr_blocks, 1)
    }
    private-az3 = {
      cidr_ipv4 = element(module.primary_vpc.private_subnets_cidr_blocks, 2)
    }
  }

  # Global clusters do not support managed master user password
  master_password_wo         = random_password.master.result
  master_password_wo_version = 1

  skip_final_snapshot = true

  tags = local.tags
}
Global clusters do not support manage_master_user_password. You must supply the master password directly using master_password_wo.
3

Create the secondary cluster

The secondary cluster sets is_primary_cluster = false, which prevents the module from creating a writer instance or setting a master username. It inherits its data via replication from the primary.Set source_region to the primary cluster’s region when using encrypted storage — this is required for Aurora to establish cross-region replication of encrypted clusters.
module "aurora_secondary" {
  source = "terraform-aws-modules/rds-aurora/aws"

  region = local.secondary_region

  is_primary_cluster = false

  name                      = local.name
  engine                    = aws_rds_global_cluster.this.engine
  engine_version            = aws_rds_global_cluster.this.engine_version
  global_cluster_identifier = aws_rds_global_cluster.this.id
  source_region             = local.primary_region
  cluster_instance_class    = "db.r8g.large"
  instances                 = { for i in range(2) : i => {} }
  kms_key_id                = aws_kms_key.secondary.arn

  vpc_id               = module.secondary_vpc.vpc_id
  db_subnet_group_name = module.secondary_vpc.database_subnet_group_name
  security_group_ingress_rules = {
    private-az1 = {
      cidr_ipv4 = element(module.secondary_vpc.private_subnets_cidr_blocks, 0)
    }
    private-az2 = {
      cidr_ipv4 = element(module.secondary_vpc.private_subnets_cidr_blocks, 1)
    }
    private-az3 = {
      cidr_ipv4 = element(module.secondary_vpc.private_subnets_cidr_blocks, 2)
    }
  }

  # Global clusters do not support managed master user password
  master_password_wo         = random_password.master.result
  master_password_wo_version = 1

  skip_final_snapshot = true

  depends_on = [
    module.aurora_primary
  ]

  tags = local.tags
}
The depends_on block ensures the primary cluster is fully provisioned before the secondary cluster attempts to join the global database.

Complete Working Example

The following is the full example from examples/global-cluster/, provisioning a primary in eu-west-1 and a secondary in us-east-1:
provider "aws" {
  region = local.primary_region
}

data "aws_caller_identity" "current" {}

data "aws_availability_zones" "primary" {
  region = local.primary_region

  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}

data "aws_availability_zones" "secondary" {
  region = local.secondary_region

  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}

locals {
  name = "ex-global-cluster"

  primary_region   = "eu-west-1"
  primary_vpc_cidr = "10.0.0.0/16"
  primary_azs      = slice(data.aws_availability_zones.primary.names, 0, 3)

  secondary_region   = "us-east-1"
  secondary_vpc_cidr = "10.1.0.0/16"
  secondary_azs      = slice(data.aws_availability_zones.secondary.names, 0, 3)

  tags = {
    Example    = local.name
    GithubRepo = "terraform-aws-rds-aurora"
    GithubOrg  = "terraform-aws-modules"
  }
}

resource "aws_rds_global_cluster" "this" {
  global_cluster_identifier = local.name
  engine                    = "aurora-postgresql"
  engine_version            = "17.5"
  database_name             = "example_db"
  storage_encrypted         = true
}

module "aurora_primary" {
  source = "terraform-aws-modules/rds-aurora/aws"

  name                      = local.name
  database_name             = aws_rds_global_cluster.this.database_name
  engine                    = aws_rds_global_cluster.this.engine
  engine_version            = aws_rds_global_cluster.this.engine_version
  master_username           = "root"
  global_cluster_identifier = aws_rds_global_cluster.this.id
  cluster_instance_class    = "db.r8g.large"
  instances                 = { for i in range(2) : i => {} }
  kms_key_id                = aws_kms_key.primary.arn

  vpc_id               = module.primary_vpc.vpc_id
  db_subnet_group_name = module.primary_vpc.database_subnet_group_name
  security_group_ingress_rules = {
    private-az1 = {
      cidr_ipv4 = element(module.primary_vpc.private_subnets_cidr_blocks, 0)
    }
    private-az2 = {
      cidr_ipv4 = element(module.primary_vpc.private_subnets_cidr_blocks, 1)
    }
    private-az3 = {
      cidr_ipv4 = element(module.primary_vpc.private_subnets_cidr_blocks, 2)
    }
  }

  master_password_wo         = random_password.master.result
  master_password_wo_version = 1

  skip_final_snapshot = true

  tags = local.tags
}

module "aurora_secondary" {
  source = "terraform-aws-modules/rds-aurora/aws"

  region = local.secondary_region

  is_primary_cluster = false

  name                      = local.name
  engine                    = aws_rds_global_cluster.this.engine
  engine_version            = aws_rds_global_cluster.this.engine_version
  global_cluster_identifier = aws_rds_global_cluster.this.id
  source_region             = local.primary_region
  cluster_instance_class    = "db.r8g.large"
  instances                 = { for i in range(2) : i => {} }
  kms_key_id                = aws_kms_key.secondary.arn

  vpc_id               = module.secondary_vpc.vpc_id
  db_subnet_group_name = module.secondary_vpc.database_subnet_group_name
  security_group_ingress_rules = {
    private-az1 = {
      cidr_ipv4 = element(module.secondary_vpc.private_subnets_cidr_blocks, 0)
    }
    private-az2 = {
      cidr_ipv4 = element(module.secondary_vpc.private_subnets_cidr_blocks, 1)
    }
    private-az3 = {
      cidr_ipv4 = element(module.secondary_vpc.private_subnets_cidr_blocks, 2)
    }
  }

  master_password_wo         = random_password.master.result
  master_password_wo_version = 1

  skip_final_snapshot = true

  depends_on = [
    module.aurora_primary
  ]

  tags = local.tags
}

resource "random_password" "master" {
  length  = 20
  special = false
}

module "primary_vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 6.0"

  name = local.name
  cidr = local.primary_vpc_cidr

  azs              = local.primary_azs
  public_subnets   = [for k, v in local.primary_azs : cidrsubnet(local.primary_vpc_cidr, 8, k)]
  private_subnets  = [for k, v in local.primary_azs : cidrsubnet(local.primary_vpc_cidr, 8, k + 3)]
  database_subnets = [for k, v in local.primary_azs : cidrsubnet(local.primary_vpc_cidr, 8, k + 6)]

  tags = local.tags
}

module "secondary_vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 6.0"

  region = local.secondary_region

  name = local.name
  cidr = local.secondary_vpc_cidr

  azs              = local.secondary_azs
  public_subnets   = [for k, v in local.secondary_azs : cidrsubnet(local.secondary_vpc_cidr, 8, k)]
  private_subnets  = [for k, v in local.secondary_azs : cidrsubnet(local.secondary_vpc_cidr, 8, k + 3)]
  database_subnets = [for k, v in local.secondary_azs : cidrsubnet(local.secondary_vpc_cidr, 8, k + 6)]

  tags = local.tags
}

Lifecycle Considerations

The module’s aws_rds_cluster resource includes a lifecycle block that ignores changes to both global_cluster_identifier and replication_source_identifier after initial creation:
lifecycle {
  ignore_changes = [
    replication_source_identifier,
    global_cluster_identifier,
    snapshot_identifier,
  ]
}
This is intentional. After a cluster joins a global database, Aurora manages these values internally. Terraform tracking them would cause spurious plan diffs or unwanted resource recreation during normal operations (such as failover or cluster promotion).

Write Forwarding

To allow a secondary cluster to forward writes back to the primary without application changes, set enable_global_write_forwarding = true on the secondary module:
module "aurora_secondary" {
  source = "terraform-aws-modules/rds-aurora/aws"

  # ...
  is_primary_cluster            = false
  global_cluster_identifier     = aws_rds_global_cluster.this.id
  enable_global_write_forwarding = true
  # ...
}
Write forwarding adds latency to write operations on the secondary (since they traverse the network to the primary) but simplifies application connection strings.

Variable Reference

VariableTypeDefaultDescription
global_cluster_identifierstringnullThe identifier of the aws_rds_global_cluster to join
is_primary_clusterbooltrueSet to false for secondary clusters — disables writer instance creation and master_username
source_regionstringnullSource region for encrypted cross-region replication
replication_source_identifierstringnullARN of a source DB cluster or instance (for read replicas)
enable_global_write_forwardingboolnullAllow secondary cluster to forward writes to the primary

Build docs developers (and LLMs) love