Creating an Amazon Aurora Global Database + headless cluster together in a single CloudFormation template.

Creating an Amazon Aurora Global Database + headless cluster together in a single CloudFormation template.

2025.09.07

Introduction

Hello everyone, I'm Akaike.

"Creating an Aurora Global Database through the management console is a bit of a hassle..."
"If only I could create it all at once with CloudFormation..."
Have you ever thought something like this? I have.

Especially when creating a headless cluster in a secondary region,
manually, you need to create instances and then deliberately delete them afterward, which is honestly troublesome.

To solve these issues,
I've created a single CloudFormation template that supports multiple regions to create an Aurora Global Database + headless cluster.

Motivation

1. Manual Creation Process

When trying to manually create an Amazon Aurora Global Database + headless cluster, the following steps are required:

  • Main region
    • Create Aurora cluster
    • Create global database
  • Secondary region
    • Create Aurora cluster & instance (simultaneously with global database creation)
    • Delete Aurora instance

As mentioned earlier, the annoying part is having to manually create the cluster and then deliberately delete the secondary instance.
This series of operations would probably take more than 30 minutes...

2. Creating with CloudFormation

Next, when creating with CloudFormation.
Compared to manual creation, it reduces the effort of deleting the secondary Aurora instance, saving time.

  • Main region
    • Create Aurora cluster
    • Create global database
  • Secondary region
    • Create Aurora cluster

And when creating resources across multiple regions,
I think it's common to separate CloudFormation templates by region as follows:

			
			├── README.md
├── region1/
│   └── aurora.yaml
└── region2/
    └── aurora.yaml

		

However, dividing by region doubles the number of files and results in many duplicate entries, which is honestly tiring...
So this time, I'll create it so that the same template file can be used across regions.

Current Architecture

The architecture is as follows:

aurora-global-db.drawio

A simple configuration with one primary cluster and one secondary cluster.
The primary cluster has two instances, one writer and one reader,
while the secondary cluster is a headless cluster, so there are no instances.

Stack / Code Structure

I decided to use a nested stack structure for the following reasons:

  • Improved reusability through modularization (child stacks)
  • Ability to reference values between stacks without using cross-stack references

nest-stack-image

The directory structure is as follows.
Parent stacks/child stacks are designed to be usable across regions.

			
			.
└── cloudformation/
    ├── 01-parent/ # Parent stack
    │   └── parent-cfn.yaml
    └── 02-child/  # Child stack
        ├── vpc-child-cfn.yaml
        ├── sg-child-cfn.yaml
        └── aurora-child-cfn.yaml
```### Parent Stack

```yaml:parent-cfn.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Production Parent Stack"

Parameters:
  # Common Parameters
  ProjectName:
    Type: String
    Default: globaldb-test-cfn
    Description: Project name prefix for resources
  Environment:
    Type: String
    Default: dev
    Description: Environment name (prod, dev, etc.)

  # Aurora Parameters
  AuroraInstanceClass:
    Type: String
    Default: db.r7g.large
    Description: Aurora PostgreSQL Instance Class
  AuroraDeletionProtection:
    Type: String
    Default: false
    Description: Deletion Protection Status for Aurora
  MasterUsername:
    Type: String
    Description: Aurora master username
  MasterUserPassword:
    Type: String
    NoEcho: true
    Description: Aurora master user password

Mappings:
  RegionToShortname:
    ap-northeast-1:
      Shortname: "apne1"
    ap-northeast-3:
      Shortname: "apne3"

Resources:
  # VPC Infrastructure
  ChildStackVPC:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub
        - "https://s3.amazonaws.com/${ProjectName}-${Environment}-${RegionShortname}-cfn-s3/vpc-child-cfn.yaml"
        - RegionShortname:
            !FindInMap [RegionToShortname, !Ref "AWS::Region", Shortname]
      Parameters:
        ProjectName: !Ref ProjectName
        Environment: !Ref Environment
        RegionToShortname:
          !FindInMap [RegionToShortname, !Ref "AWS::Region", Shortname]

  # Security Groups
  ChildStackSG:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub
        - "https://s3.amazonaws.com/${ProjectName}-${Environment}-${RegionShortname}-cfn-s3/sg-child-cfn.yaml"
        - RegionShortname:
            !FindInMap [RegionToShortname, !Ref "AWS::Region", Shortname]
      Parameters:
        ProjectName: !Ref ProjectName
        Environment: !Ref Environment
        RegionToShortname:
          !FindInMap [RegionToShortname, !Ref "AWS::Region", Shortname]
        VpcId: !GetAtt ChildStackVPC.Outputs.VpcId

  # Aurora PostgreSQL (only for Tokyo initially)
  ChildStackAurora:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub
        - "https://s3.amazonaws.com/${ProjectName}-${Environment}-${RegionShortname}-cfn-s3/aurora-child-cfn.yaml"
        - RegionShortname:
            !FindInMap [RegionToShortname, !Ref "AWS::Region", Shortname]
      Parameters:
        ProjectName: !Ref ProjectName
        Environment: !Ref Environment
        RegionToShortname:
          !FindInMap [RegionToShortname, !Ref "AWS::Region", Shortname]
        PrivateSubnet1Id: !GetAtt ChildStackVPC.Outputs.PrivateSubnet1Id
        PrivateSubnet2Id: !GetAtt ChildStackVPC.Outputs.PrivateSubnet2Id
        AuroraSecurityGroupId: !GetAtt ChildStackSG.Outputs.AuroraSecurityGroupId
        InstanceClass: !Ref AuroraInstanceClass
        DeletionProtection: !Ref AuroraDeletionProtection
        MasterUsername: !Ref MasterUsername
        MasterUserPassword: !Ref MasterUserPassword
```### Child Stack

```yaml:vpc-child-cfn.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "VPC Infrastructure Child Stack"

Parameters:
  ProjectName:
    Type: String
  Environment:
    Type: String
  RegionToShortname:
    Type: String

Mappings:
  AvailabilityZoneToShortname:
    ap-northeast-1:
      AZ1: "1a"
      AZ2: "1c"
    ap-northeast-3:
      AZ1: "3a"
      AZ2: "3b"
  RegionToAvailabilityZones:
    ap-northeast-1:
      AZ1: "ap-northeast-1a"
      AZ2: "ap-northeast-1c"
    ap-northeast-3:
      AZ1: "ap-northeast-3a"
      AZ2: "ap-northeast-3b"
  RegionToCIDRs:
    ap-northeast-1:
      VpcCidr: 10.0.0.0/16
      PrivateSubnet1Cidr: 10.0.0.0/24
      PrivateSubnet2Cidr: 10.0.1.0/24
    ap-northeast-3:
      VpcCidr: 10.1.0.0/16
      PrivateSubnet1Cidr: 10.1.0.0/24
      PrivateSubnet2Cidr: 10.1.1.0/24

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [RegionToCIDRs, !Ref "AWS::Region", VpcCidr]
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-vpc"

  # Internet Gateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-igw"

  # Attach Internet Gateway to VPC
  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  # Private Subnets
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !FindInMap [RegionToAvailabilityZones, !Ref "AWS::Region", AZ1]
      CidrBlock: !FindInMap [RegionToCIDRs, !Ref "AWS::Region", PrivateSubnet1Cidr]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref ProjectName
              - !Ref Environment
              - !Ref RegionToShortname
              - "private-subnet"
              - !FindInMap [
                  AvailabilityZoneToShortname,
                  !Ref "AWS::Region",
                  AZ1,
                ]

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !FindInMap [RegionToAvailabilityZones, !Ref "AWS::Region", AZ2]
      CidrBlock: !FindInMap [RegionToCIDRs, !Ref "AWS::Region", PrivateSubnet2Cidr]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref ProjectName
              - !Ref Environment
              - !Ref RegionToShortname
              - "private-subnet"
              - !FindInMap [
                  AvailabilityZoneToShortname,
                  !Ref "AWS::Region",
                  AZ2,
                ]

  # Route Tables
  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - !Ref ProjectName
              - !Ref Environment
              - !Ref RegionToShortname
              - "private-rtb"
              - !FindInMap [
                  AvailabilityZoneToShortname,
                  !Ref "AWS::Region",
                  AZ1,
                ]

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet1

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet2

  # Network ACLs
  PrivateNetworkAcl:
    Type: AWS::EC2::NetworkAcl
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-private-nacl"

  PrivateInboundRule:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      NetworkAclId: !Ref PrivateNetworkAcl
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      CidrBlock: 0.0.0.0/0

  PrivateOutboundRule:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      NetworkAclId: !Ref PrivateNetworkAcl
      RuleNumber: 100
      Protocol: -1
      Egress: true
      RuleAction: allow
      CidrBlock: 0.0.0.0/0

  PrivateSubnetNetworkAclAssociation1:
    Type: AWS::EC2::SubnetNetworkAclAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      NetworkAclId: !Ref PrivateNetworkAcl

  PrivateSubnetNetworkAclAssociation2:
    Type: AWS::EC2::SubnetNetworkAclAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      NetworkAclId: !Ref PrivateNetworkAcl

Outputs:
  VpcId:
    Description: VPC ID
    Value: !Ref VPC
    Export:
      Name: !Sub "${AWS::StackName}-VpcId"

  PrivateSubnet1Id:
    Description: Private Subnet 1 ID
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub "${AWS::StackName}-PrivateSubnet1Id"

  PrivateSubnet2Id:
    Description: Private Subnet 2 ID
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub "${AWS::StackName}-PrivateSubnet2Id"
``````yaml:sg-child-cfn.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Security Groups Child Stack"

Parameters:
  ProjectName:
    Type: String
  Environment:
    Type: String
  RegionToShortname:
    Type: String
  VpcId:
    Type: String
    Description: VPC ID where security groups will be created

Resources:
  # Aurora Security Group
  AuroraSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-sg"
      GroupDescription: Security group for Aurora PostgreSQL
      VpcId: !Ref VpcId
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-sg"

Outputs:
  AuroraSecurityGroupId:
    Description: Aurora Security Group ID
    Value: !Ref AuroraSecurityGroup
    Export:
      Name: !Sub "${AWS::StackName}-AuroraSecurityGroupId"

		
aurora-child-cfn.yaml
			
			AWSTemplateFormatVersion: "2010-09-09"
Description: "Aurora PostgreSQL Child Stack"

Parameters:
  ProjectName:
    Type: String
  Environment:
    Type: String
  RegionToShortname:
    Type: String
  PrivateSubnet1Id:
    Type: String
    Description: Private Subnet 1 ID
  PrivateSubnet2Id:
    Type: String
    Description: Private Subnet 2 ID
  AuroraSecurityGroupId:
    Type: String
    Description: Aurora Security Group ID
  InstanceClass:
    Type: String
  DeletionProtection:
    Type: String
  MasterUsername:
    Type: String
  MasterUserPassword:
    Type: String
    NoEcho: true

Conditions:
  IsTokyoRegion: !Equals [!Ref "AWS::Region", "ap-northeast-1"]
  IsOsakaRegion: !Equals [!Ref "AWS::Region", "ap-northeast-3"]

Resources:
  # Aurora Subnet Group
  AuroraSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupName: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-private-sbntgroup"
      DBSubnetGroupDescription: Subnet group for Aurora cluster
      SubnetIds:
        - !Ref PrivateSubnet1Id
        - !Ref PrivateSubnet2Id
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-private-sbntgroup"

  # Aurora Cluster Parameter Group
  AuroraClusterParameterGroup:
    Type: AWS::RDS::DBClusterParameterGroup
    Properties:
      DBClusterParameterGroupName: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-cpgp"
      Description: Aurora PostgreSQL cluster parameter group
      Family: aurora-postgresql16
      Parameters:
        timezone: Asia/Tokyo
        client_encoding: UTF8
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-cpgp"

  # Aurora DB Parameter Group
  AuroraParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      DBParameterGroupName: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-pgp"
      Description: Aurora PostgreSQL parameter group
      Family: aurora-postgresql16
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-pgp"

  # Aurora Cluster
  AuroraCluster:
    Type: AWS::RDS::DBCluster
    Condition: IsTokyoRegion
    Properties:
      DBClusterIdentifier: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-cluster"
      Engine: aurora-postgresql
      EngineVersion: "16.6"
      Port: 5432
      MasterUsername: !Ref MasterUsername
      MasterUserPassword: !Ref MasterUserPassword
      DBSubnetGroupName: !Ref AuroraSubnetGroup
      VpcSecurityGroupIds:
        - !Ref AuroraSecurityGroupId
      DBClusterParameterGroupName: !Ref AuroraClusterParameterGroup
      BackupRetentionPeriod: 7
      PreferredBackupWindow: "19:00-19:30"
      PreferredMaintenanceWindow: "sat:20:00-sat:20:30"
      DeletionProtection: !Ref DeletionProtection
      StorageEncrypted: true
      AutoMinorVersionUpgrade: false
      EnableCloudwatchLogsExports:
        - postgresql
        - instance
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-cluster"
        - Key: aws-backup
          Value: enable

  # Aurora Global Cluster (only for Tokyo, created after DB Cluster)
  AuroraGlobalCluster:
    Type: AWS::RDS::GlobalCluster
    Condition: IsTokyoRegion
    Properties:
      GlobalClusterIdentifier: !Sub "${ProjectName}-${Environment}-aurora-global-cluster"
      SourceDBClusterIdentifier: !Ref AuroraCluster
    DependsOn: AuroraCluster

  # Aurora DB Instance - Writer (Primary)
  AuroraDBInstanceWriter:
    Type: AWS::RDS::DBInstance
    Condition: IsTokyoRegion
    Properties:
      DBInstanceIdentifier: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-instance-1"
      DBInstanceClass: !Ref InstanceClass
      Engine: aurora-postgresql
      EngineVersion: "16.6"
      DBClusterIdentifier: !Ref AuroraCluster
      DBParameterGroupName: !Ref AuroraParameterGroup
      PreferredMaintenanceWindow: "sat:20:30-sat:21:00"
      AvailabilityZone: ap-northeast-1a
      MonitoringInterval: 0
      EnablePerformanceInsights: false
      AutoMinorVersionUpgrade: false
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-instance-1"
        - Key: Role
          Value: "Writer"

  # Aurora DB Instance - Reader (Replica in different AZ)
  AuroraDBInstanceReader:
    Type: AWS::RDS::DBInstance
    Condition: IsTokyoRegion
    Properties:
      DBInstanceIdentifier: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-instance-2"
      DBInstanceClass: !Ref InstanceClass
      Engine: aurora-postgresql
      EngineVersion: "16.6"
      DBClusterIdentifier: !Ref AuroraCluster
      DBParameterGroupName: !Ref AuroraParameterGroup
      PreferredMaintenanceWindow: "sat:20:30-sat:21:00"
      AvailabilityZone: ap-northeast-1c
      MonitoringInterval: 0
      EnablePerformanceInsights: false
      AutoMinorVersionUpgrade: false
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-instance-2"
        - Key: Role
          Value: "Reader"

  # Aurora Secondary Cluster (headless - only for Osaka region)
  AuroraSecondaryCluster:
    Type: AWS::RDS::DBCluster
    Condition: IsOsakaRegion
    Properties:
      DBClusterIdentifier: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-cluster"
      GlobalClusterIdentifier: !Sub "${ProjectName}-${Environment}-aurora-global-cluster"
      Engine: aurora-postgresql
      EngineVersion: "16.6"
      Port: 5432
      DBSubnetGroupName: !Ref AuroraSubnetGroup
      VpcSecurityGroupIds:
        - !Ref AuroraSecurityGroupId
      DBClusterParameterGroupName: !Ref AuroraClusterParameterGroup
      BackupRetentionPeriod: 7
      PreferredBackupWindow: "19:00-19:30"
      PreferredMaintenanceWindow: "sat:20:00-sat:20:30"
      DeletionProtection: !Ref DeletionProtection
      StorageEncrypted: true
      KmsKeyId: alias/aws/rds
      AutoMinorVersionUpgrade: false
      EnableCloudwatchLogsExports:
        - postgresql
        - instance
      Tags:
        - Key: Name
          Value: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-cluster"
        - Key: Type
          Value: "Secondary-Headless"

Outputs:
  AuroraClusterIdentifier:
    Description: Aurora Cluster Identifier
    Value: !If
      - IsTokyoRegion
      - !Ref AuroraCluster
      - !Ref AuroraSecondaryCluster
    Export:
      Name: !Sub "${AWS::StackName}-AuroraClusterIdentifier"

  AuroraClusterEndpoint:
    Condition: IsTokyoRegion
    Description: Aurora Cluster Writer Endpoint (Tokyo only)
    Value: !GetAtt AuroraCluster.Endpoint.Address
    Export:
      Name: !Sub "${AWS::StackName}-AuroraClusterEndpoint"

  AuroraClusterReaderEndpoint:
    Condition: IsTokyoRegion
    Description: Aurora Cluster Reader Endpoint (Tokyo only)
    Value: !GetAtt AuroraCluster.ReadEndpoint.Address
    Export:
      Name: !Sub "${AWS::StackName}-AuroraClusterReaderEndpoint"

  AuroraSecondaryClusterIdentifier:
    Condition: IsOsakaRegion
    Description: Aurora Secondary Cluster Identifier (Osaka headless)
    Value: !Ref AuroraSecondaryCluster
    Export:
      Name: !Sub "${AWS::StackName}-AuroraSecondaryClusterIdentifier"

  AuroraWriterInstanceIdentifier:
    Condition: IsTokyoRegion
    Description: Aurora Writer Instance Identifier
    Value: !Ref AuroraDBInstanceWriter
    Export:
      Name: !Sub "${AWS::StackName}-AuroraWriterInstanceIdentifier"

  AuroraReaderInstanceIdentifier:
    Condition: IsTokyoRegion
    Description: Aurora Reader Instance Identifier
    Value: !Ref AuroraDBInstanceReader
    Export:
      Name: !Sub "${AWS::StackName}-AuroraReaderInstanceIdentifier"

  AuroraGlobalClusterIdentifier:
    Condition: IsTokyoRegion
    Description: Aurora Global Cluster Identifier
    Value: !Ref AuroraGlobalCluster
    Export:
      Name: !Sub "${AWS::StackName}-AuroraGlobalClusterIdentifier"
```### Deployment

Create an S3 bucket to store template files for child stacks.
The bucket name should follow the format defined as Parameters in the template file:
"ProjectName-Environment-RegionShortname-cfn-s3".


		

aws s3api create-bucket
--bucket globaldb-test-cfn-dev-apne1-cfn-s3
--region ap-northeast-1
--create-bucket-configuration LocationConstraint=ap-northeast-1;
aws s3api create-bucket
--bucket globaldb-test-cfn-dev-apne3-cfn-s3
--region ap-northeast-3
--create-bucket-configuration LocationConstraint=ap-northeast-3;

			
			
Store the template files for child stacks.


		

aws s3 cp --recursive cloudformation/02-child/ s3://globaldb-test-cfn-dev-apne1-cfn-s3;
aws s3 cp --recursive cloudformation/02-child/ s3://globaldb-test-cfn-dev-apne3-cfn-s3;

			
			
Create stacks with the following commands.
Define the username and password for DB use as environment variables.

```txt:Environment variable setup
MasterUsername='postgres'
MasterUserPassword=$(openssl rand -base64 12)

		

First, create the primary stack (Tokyo region).

Primary stack creation
			
			aws cloudformation create-stack \
  --stack-name parent-stack-apne1 \
  --template-body file://cloudformation/01-parent/parent-cfn.yaml \
  --region ap-northeast-1 \
  --parameters \
    ParameterKey=MasterUsername,ParameterValue=$MasterUsername \
    ParameterKey=MasterUserPassword,ParameterValue=$MasterUserPassword

		

After the primary stack creation is complete,
create the secondary stack (Osaka region).

Secondary stack creation
			
			aws cloudformation create-stack \
  --stack-name parent-stack-apne3 \
  --template-body file://cloudformation/01-parent/parent-cfn.yaml \
  --region ap-northeast-3 \
  --parameters \
    ParameterKey=MasterUsername,ParameterValue=$MasterUsername \
    ParameterKey=MasterUserPassword,ParameterValue=$MasterUserPassword

		

Once the secondary stack creation is complete, the structure described at the beginning will be complete.

Deletion Method

When deleting resources,
delete the secondary stack first, opposite to the creation order.

Secondary stack deletion
			
			aws cloudformation delete-stack \
  --stack-name parent-stack-apne3 \
  --region ap-northeast-3

		
Primary stack deletion
			
			aws cloudformation delete-stack \
  --stack-name parent-stack-apne1 \
  --region ap-northeast-1
```## Code Explanation

Since the content would be too thin as is, I will explain the important parts.

### 1. Region-based Branching

First, the most important point is 
how to create different resources in the same template file depending on the region.

We use CloudFormation's `Conditions` to determine the current region.

```yaml
Conditions:
  IsTokyoRegion: !Equals [!Ref "AWS::Region", "ap-northeast-1"]
  IsOsakaRegion: !Equals [!Ref "AWS::Region", "ap-northeast-3"]

		

On the resource side, by referencing these Conditions with the Condition property,
we can control the creation of a primary cluster in the Tokyo region and a secondary cluster in the Osaka region.

			
			# Primary cluster created only in the Tokyo region
AuroraCluster:
  Type: AWS::RDS::DBCluster
  Condition: IsTokyoRegion
  Properties: # Primary cluster configuration

		
			
			# Secondary cluster created only in the Osaka region
AuroraSecondaryCluster:
  Type: AWS::RDS::DBCluster
  Condition: IsOsakaRegion
  Properties: # Secondary cluster (headless) configuration

		

2. Global Database Description

When creating a global database, you need to specify an existing cluster.

			
			# First create a normal cluster
AuroraCluster:
  Type: AWS::RDS::DBCluster
  Condition: IsTokyoRegion
  Properties:
    DBClusterIdentifier: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-cluster"
    Engine: aurora-postgresql
    # Other settings...

# Make the existing cluster global
AuroraGlobalCluster:
  Type: AWS::RDS::GlobalCluster
  Condition: IsTokyoRegion
  Properties:
    GlobalClusterIdentifier: !Sub "${ProjectName}-${Environment}-aurora-global-cluster"
    SourceDBClusterIdentifier: !Ref AuroraCluster  # Reference the cluster created above
  DependsOn: AuroraCluster  # Explicitly specify dependency

		

The key points are to specify an existing cluster with SourceDBClusterIdentifier and
to properly control dependencies with DependsOn.

3. Headless Configuration for Secondary Cluster

In the Osaka region's secondary cluster, we create only a cluster without instances.

			
			AuroraSecondaryCluster:
  Type: AWS::RDS::DBCluster
  Condition: IsOsakaRegion
  Properties:
    DBClusterIdentifier: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-cluster"
    GlobalClusterIdentifier: !Sub "${ProjectName}-${Environment}-aurora-global-cluster"  # Join existing global cluster
    Engine: aurora-postgresql
    # No instances created = headless cluster

		

It would be nice if future updates to the management console would allow us
to choose whether to create instances or not...### 4. Unifying resource names across regions

To properly separate resource names in each region, we utilize Mappings.

			
			Mappings:
  RegionToShortname:
    ap-northeast-1:
      Shortname: "apne1"
    ap-northeast-3:
      Shortname: "apne3"

		

This allows us to obtain the abbreviated region name with !FindInMap [RegionToShortname, !Ref "AWS::Region", Shortname] and include it in resource names.

Conclusion

I've shown how to create Aurora Global Database + Headless Cluster together in one CloudFormation template.

The technique of branching resources created by region within the same template file can be applied to various scenarios beyond Aurora Global Database.
For example, when configuring primary-secondary applications or when different resource configurations are needed by region, please consider using this approach when dealing with multi-region deployments.

Honestly, you may not have many opportunities to work with Aurora Global Database, but
I hope this article will be helpful to CloudFormation users who are struggling with similar challenges.

Share this article

FacebookHatena blogX

Related articles