Amazon Aurora Global Database + ヘッドレスクラスター を一つのCloudFormationテンプレートでまとめて作る。

Amazon Aurora Global Database + ヘッドレスクラスター を一つのCloudFormationテンプレートでまとめて作る。

2025.09.07

はじめに

皆様こんにちは、あかいけです。

「Aurora Global Databaseってマネジメントコンソールからポチポチ作るのめんどくさいなぁ...。」
「CloudFormationで一発で作れたらいいのに…。」
そんなことを思ったことはありませんか?私はあります。

特にセカンダリリージョンでヘッドレスクラスターを作る場合、
手動だとインスタンスを作った後わざわざ削除する必要があったりして、正直面倒ですよね。

そんな悩みを解決すべく、
今回は 複数リージョンに対応した一つのCloudFormationテンプレートで、Aurora Global Database + ヘッドレスクラスター を作ってみました。

モチベーション

1. 手動で作成する場合

まず手動でAmazon Aurora Global Database + ヘッドレスクラスターを作ろうとすると、以下の手順が必要です。

  • メインリージョン
    • Aurora クラスター作成
    • グローバルデータベース作成
  • セカンダリリージョン
    • Aurora クラスター&インスタンス 作成 (グローバルデータベース作成と同時)
    • Aurora インスタンス削除

冒頭で触れた通り手動でクラスターを作成した上で、セカンダリのインスタンスをわざわざ削除しないといけないのがめんどくさいポイントです。
この一連の作業だけでおそらく30分以上かかるでしょう…。

2. CloudFormationで作成する場合

次に CloudFormation で作成する場合です。
手動で作るときに比べて セカンダリのAurora インスタンス削除の手間が減っていて、その分時間が削減できます。

  • メインリージョン
    • Aurora クラスター作成
    • グローバルデータベース作成
  • セカンダリリージョン
    • Aurora クラスター作成

そしてマルチリージョンでリソースを作成する場合、
以下のようにリージョン別にCloudFormationテンプレートを分けることが一般的かな?と思います。

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

ただリージョンごとに分けると、ファイル数が2倍になりまた重複した記載が多くなるので、正直色々疲れます…。
なので今回はリージョン間で同いテンプレートファイルを使えるように作ってみます。

今回の構成

構成は以下の通り。

aurora-global-db.drawio

プライマリクラスター×1、セカンダリクラスター×1のシンプルな構成です。
またプライマリクラスターにはライターとリーダーでインスタンスが2つ、
セカンダリクラスターはヘッドレスクラスターのためインスタンスは存在しない状況です。

スタック / コード構成

以下の理由からネストスタック構成にしてみました。

  • モジュール化(子スタック)による再利用性の向上
  • クロススタック参照を使わずにスタック間の値参照が可能

nest-stack-image

またディレクトリ構成は以下の通りです。
親スタック/子スタックはリージョン共通で使えるようにしています。

.
└── cloudformation/
    ├── 01-parent/ # 親スタック
    │   └── parent-cfn.yaml
    └── 02-child/  # 子スタック
        ├── vpc-child-cfn.yaml
        ├── sg-child-cfn.yaml
        └── aurora-child-cfn.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

子スタック

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"
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"

デプロイ

子スタック用のテンプレートファイル格納用のS3を作成します。
バケット名はテンプレートファイル側でParametersとして定義している、
「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;

子スタック用のテンプレートファイルを格納します。

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;

以下コマンドでスタックを作成します。
DBで利用するユーザー名、パスワードを環境変数で定義しておきます。

環境変数 設定
MasterUsername='postgres'
MasterUserPassword=$(openssl rand -base64 12)

まずはプライマリ側(東京リージョン)のスタックを作成します。

プライマリ スタック作成
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

プライマリ側の作成が完了してから、
セカンダリ側(大阪リージョン)のスタックを作成します。

セカンダリ スタック作成
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

セカンダリ側のスタック作成が完了すれば、冒頭の構成が完成します。

削除方法

リソースを削除する際は、
作成時とは逆にセカンダリスタックから削除すればOKです。

セカンダリ スタック削除
aws cloudformation delete-stack \
  --stack-name parent-stack-apne3 \
  --region ap-northeast-3
プライマリ スタック削除
aws cloudformation delete-stack \
  --stack-name parent-stack-apne1 \
  --region ap-northeast-1

コードの解説

このままだと内容が薄すぎるので、重要な箇所を解説します。

1. リージョンでの分岐

まず一番のポイントとなるのが、
同じテンプレートファイルでリージョンによって異なるリソースを作り分ける部分です。

まずCloudFormationのConditionsを使って、現在のリージョンを判定しています。

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

リソース側ではConditionでConditionsを参照することで、
東京リージョンではプライマリクラスター、大阪リージョンではセカンダリクラスターを作成するように制御できます。

# 東京リージョンでのみ作成されるプライマリクラスター
AuroraCluster:
  Type: AWS::RDS::DBCluster
  Condition: IsTokyoRegion
  Properties: # プライマリクラスターの設定
# 大阪リージョンでのみ作成されるセカンダリクラスター
AuroraSecondaryCluster:
  Type: AWS::RDS::DBCluster
  Condition: IsOsakaRegion
  Properties: # セカンダリクラスター(ヘッドレス)の設定

2. グローバルデータベースの記述

グローバルデータベースの作成では、既存のクラスターを指定する必要があります。

# まず通常のクラスターを作成
AuroraCluster:
  Type: AWS::RDS::DBCluster
  Condition: IsTokyoRegion
  Properties:
    DBClusterIdentifier: !Sub "${ProjectName}-${Environment}-${RegionToShortname}-aurora-cluster"
    Engine: aurora-postgresql
    # その他の設定...

# 既存クラスターをグローバル化
AuroraGlobalCluster:
  Type: AWS::RDS::GlobalCluster
  Condition: IsTokyoRegion
  Properties:
    GlobalClusterIdentifier: !Sub "${ProjectName}-${Environment}-aurora-global-cluster"
    SourceDBClusterIdentifier: !Ref AuroraCluster  # 上記で作成したクラスターを参照
  DependsOn: AuroraCluster  # 依存関係を明示

ポイントはSourceDBClusterIdentifierで既存クラスターを指定し、
またDependsOnでしっかりと依存関係を制御することです。

3. セカンダリクラスターでのヘッドレス構成

大阪リージョンのセカンダリクラスターでは、インスタンスを作成せずクラスターのみを作成します。

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
    # インスタンスは作成しない = ヘッドレスクラスター

今後のアップデートでマネジメントコンソールでも、
インスタンスを作成するかどうか選べるようになったら嬉しいですね…。

4. リージョン間でのリソース名の統一

各リージョンで適切にリソース名を分けるために、Mappingsを活用しています。

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

これにより!FindInMap [RegionToShortname, !Ref "AWS::Region", Shortname]でリージョンの短縮名を取得し、リソース名に含めています。

さいごに

以上、Aurora グローバルデータベース + ヘッドレスクラスター を一つのCloudFormationでまとめて作ってみました。

同じテンプレートファイルでリージョンによって作成するリソースを分岐させるテクニックは、Aurora Global Database以外にも様々な場面で応用できます。
例えば、プライマリ・セカンダリ構成のアプリケーションや、リージョン別に異なるリソース構成が必要な場合など、マルチリージョン展開でお困りの際にはぜひ活用してみてください。

正直Aurora Global Databaseを触る機会はそんなに多くないかもしれませんが、
本記事が迷えるCloudFormationユーザーのお役に立てれば嬉しいです。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.