CloudFormationでマルチリージョン構成を実装するテクニック

CloudFormationでマルチリージョン構成を実装するテクニック

Clock Icon2025.05.11

はじめに

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

今までTerraformで細々と命を繋いできた私ですが、
クラスメソッドに入社してからCloudFormationと触れ合うことが多くなりました。
そして直近でCloudFormationでマルチリージョン構成を実装する機会があったのですが、
CloudFormation初学者ということもあり色々苦戦することがありました…。

というわけで、これからCloudFormationでマルチリージョン構成を作る方に向けて、
考えるべきことと使えそうな技をまとめておきます。

なおAWS CloudFormationを初めて触る方は、
まずは以下のベストプラクティスに目を通しておくことをおすすめします。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html

コードのメンテナンスについて

まず最初に考えることは、コードをメンテナンスするか、ということです。
「こいつは一体何を言っているんだ?」、と思うかもしれませんが、
正直なところCloudFormationの設計上、リリース後もコードをメンテナンスして保守し続けるのはかなり大変です。

なので個人的には初期構築やDR環境構築など、作り切りの環境はCloudFormation、
インフラレベルでのアップデートが多く、なおかつコードをメンテナンスし続けるのであればTerraformがおすすめです。
※ 作り切りの環境であっても、中規模以上ならTerraformがおすすめ

この辺りの話は以下の記事が大変参考になるので、よければご参照ください。

https://dev.classmethod.jp/articles/akiba-aws-iac-failure-cases/

設計編

さて、CloudFormationを利用することになって一番最初に考えることは、おそらくスタックの設計でしょう。
ここを間違えると後戻りが大変なので、めんどくさいですが少しだけ真面目に考えましょう。

マルチリージョンの使い方

マルチリージョンの使い方によって設計が大きく変わります。
使い方は大きく分けると「複製環境」と「DR環境」の2つのパターンがあります。

複製環境

複製環境は複数のリージョンで同一の環境を構築し、同時に稼働させるパターンです。
主な用途は以下のとおりです。

  • グローバル展開: 世界各地のユーザーに低レイテンシーでサービスを提供
  • 負荷分散: リージョン間でトラフィックを分散させる
  • 高可用性: 一部のリージョンで障害が発生しても、他のリージョンでサービスを継続

複製環境では、基本的に各リージョンのリソースが同一構成となるため、
コード作成においてはDR環境と比較すると考えることは少ないでしょう。

DR環境

DR(Disaster Recovery)環境は、
主要リージョンで障害が発生した際のバックアップとして別リージョンを準備するパターンです。

https://docs.aws.amazon.com/ja_jp/whitepapers/latest/disaster-recovery-workloads-on-aws/disaster-recovery-options-in-the-cloud.html

AWSに限った話ではないですが、DR環境は以下の4種類に大別されます。

マルチサイトアクティブ/アクティブ (ホットスタンバイ)
  • 特徴
    • 常に完全に同期された環境を別リージョンで稼働させておく
    • CloudFormationでは同一テンプレートを両リージョンに展開し、データ同期の仕組みを実装
  • 復旧時間
    • フェイルオーバー時間が最小(数秒から数分程度)
  • コスト
    • コストは最も高い(約2倍)
ウォームスタンバイ
  • 特徴
    • 縮小版の環境を別リージョンで稼働させておき、障害時にスケールアップ
    • CloudFormationではパラメータを変えて同一テンプレートを展開
  • 復旧時間
    • フェイルオーバー時間は中程度(数分から数十分程度)
  • コスト
    • コストは中程度(メインの50-70%程度)
パイロットライト (コールドスタンバイ)
  • 特徴
    • 最小限のリソース(データベースなど)のみ別リージョンで稼働
    • CloudFormationでは必要最小限のリソースのみを別リージョンに展開
  • 復旧時間
    • フェイルオーバー時間は長い(数十分程度)
  • コスト
    • コストは低い(メインの10-30%程度)
バックアップ&リストア
  • 特徴
    • データのバックアップのみを別リージョンに保存
    • CloudFormationでは障害時に新規スタックを展開
  • 復旧時間
    • 復旧時間は最も長い(数十分から数時間程度)
  • コスト
    • コストは最も低い

スタック構成

次にスタックの構成です。
それぞれメリットデメリットがあり、
一概にどれがベストとは言えないので要件をもとにメリデメを考慮して決めましょう。

ただ一つだけ言えるのは、
大規模な環境ほどスタックを分割する必要に迫られるということです。

個人的には20リソース以上、
またはコードが1000行を超えるようであれば分割したほうがいいと考えています。
(誰だって1ファイル数千行のコードは見たくないですよね…?)

単一スタック

cloudformation-stack-1.drawio

  • メリット
    • シンプルで管理が容易
    • デプロイが一度で完了する
    • リソース間の依存関係が明確
  • デメリット
    • スタックが大きくなりすぎると更新時間が長くなる
    • 一部のリソース更新で全体に影響が出る可能性がある

クロススタック

cloudformation-stack-2.drawio

  • メリット
    • リソースをカテゴリ別にスタック分割できる
    • 部分的な更新が容易
    • 作業分担がしやすい
  • デメリット
    • スタック間の依存関係の管理が必要
    • デプロイ順序を考慮する必要がある

ネストスタック

cloudformation-stack-3.drawio

  • メリット
    • 親スタックから一括デプロイが可能
    • モジュール化による再利用性の向上
    • 複雑なアーキテクチャの管理が可能
  • デメリット
    • テンプレートの管理が複雑になる

スタック間の値の受け渡しについて

次に決める必要があるのは、スタック間の値の受け渡し方法でしょう。
単一スタックでない限りは、必ず議論する必要が出てきます。

Export / Import

クロススタックの場合は、基本的にこの方法を使います。

ただしExportした値は同じリージョンのすべてのスタックから参照できてしまうため、命名規則に気をつける必要があったり、
スタック間の参照関係によって更新順を考える必要があったりします。

そのため複数システムのスタックが乱立するような大規模な環境では、
デメリットの方が目立ちがちな気がします。

  • メリット
    • CloudFormation標準の機能で簡単に実装できる
    • スタック間の依存関係が明示的になる
    • コンソールからも参照関係が確認しやすい
  • デメリット
    • エクスポート名はリージョン内でユニークである必要がある
    • エクスポート値を参照しているスタックがある場合、元のスタックを更新/削除できない
export.yaml
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24

Outputs:
  VpcId:
    Value: !Ref VPC
    Export:
      Name: !Sub VpcId

  SubnetId:
    Export:
      Name: !Ref Subnet
import.yaml
Resources:
  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !ImportValue VpcId
      CidrBlock: 10.0.0.0/24

GetAtt

ネストスタックの場合は、基本的にこの方法を使います。

親スタックを経由して値を受け渡すため記述量が少々増えてしまいますが、
Export / Importで問題となるExportした値のスコープや、スタック更新時のスタック間の依存関係などの問題点が多少解消されます。

あくまで個人的な意見ですが、
ネストスタック + GetAtt が一番汎用的に利用できる気がします。

  • メリット
    • 親スタックから一元管理できる
    • 出力された値がスタック内で完結する
    • デプロイ順序が自動的に解決される
  • デメリット
    • ネストスタックでのみ利用可能
    • 親スタックの記述量が増える
    • 深い階層になると管理が複雑になる
parent.yaml
Resources:
  NetworkStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.amazonaws.com/bucket/network.yaml

  ComputeStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.amazonaws.com/bucket/compute.yaml
      Parameters:
        SubnetId: !GetAtt NetworkStack.Outputs.SubnetId
network.yaml
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24

Outputs:
  VpcId:
    Value: !Ref VPC

  SubnetId:
    Value: !Ref Subnet
compute.yaml
Parameters:
  VpcId:
    Type: String
  SubnetId:
    Type: String

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      SubnetId: !Ref SubnetId

パラメータストア (動的参照)

クロススタック、ネストスタックのどちらでも利用できます。

パラメータストアに格納する分コードの記述量は増えてしまいますが、
スタック間の依存関係を分離できます。

そのため特定のリソースを複数システムで共通して利用するなど、
大規模や複雑な構成になる場合におすすめです。

  • メリット
    • リージョン間での値の共有が可能(各リージョンのパラメータストアに保存)
    • スタック以外のシステム(CI/CDパイプラインなど)からも参照可能
    • 暗号化された値の保存が可能(SecureString型)
  • デメリット
    • 記述量が増える
    • 依存関係が暗黙的になりがち
network.yaml
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24

  VpcParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: vpcid
      Type: String
      Value: !Ref VPC

  SubnetParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: subnetid
      Type: String
      Value: !Ref Subnet
compute.yaml
Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      SubnetId: '{{resolve:ssm:subnetid}}'

スタックセットの利用について

マルチリージョンのデプロイをする場合、スタックセットの利用が考えられます。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html

確かに便利な機能ではありますが、以下の性質を考慮して採用を考える必要があるでしょう。

  • 管理者アカウントでの一元的な管理が必要である
  • 複数のアカウント、複数のリージョンでのデプロイ可能
  • 同一のテンプレートを複数のリージョンやアカウントに展開する

個人的にはアカウントの初期セットアップでマルチアカウント + マルチリージョンに共通のリソースを作成する場合などは有用だと思います。
なので単一システムのマルチリージョン構成などであれば、使う意味合いは薄いかと思います。

文法編

以降は役立つ文法と具体例を挙げていきます。
まさに小技といった内容ですが、具体的に実装する際に役立つと思うのでご参考になれば幸いです。

Mappings + 擬似パラメータ

リージョンごとにリソースの設定値を変更したい場合、大抵はこの記法で何とかなります。

例:VPC / Subnet の IP CIDR と AZ

これはIP CIDRとAZをリージョンごとに異なる値に設定する例です。
なおAZについては GetAZs であれば直接値を埋め込まなくて済むので、こちらの利用も検討してみてください。

Mappings:
  RegionToCIDRs:
    ap-northeast-1:
      VpcCidr: 192.168.0.0/24
      PublicSubnet1Cidr: 192.168.0.0/26
      PublicSubnet1Cidr: 192.168.0.64/26
	ap-northeast-3:
      VpcCidr: 192.168.1.0/24
      PublicSubnet1Cidr: 192.168.1.0/26
      PublicSubnet1Cidr: 192.168.1.64/26
  RegionToAvailabilityZones:
    ap-northeast-1:
      AZ1: "ap-northeast-1a"
      AZ2: "ap-northeast-1c"
    ap-northeast-3:
      AZ1: "ap-northeast-3a"
      AZ2: "ap-northeast-3b"

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [RegionToCIDRs, !Ref "AWS::Region", VpcCidr]

  Subnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
	  CidrBlock:
        !FindInMap [RegionToCIDRs, !Ref "AWS::Region", PublicSubnet1Cidr]
      AvailabilityZone:
        !FindInMap [RegionToAvailabilityZones, !Ref "AWS::Region", AZ1]

  Subnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
	  CidrBlock:
        !FindInMap [RegionToCIDRs, !Ref "AWS::Region", PublicSubnet2Cidr]
      AvailabilityZone:
        !FindInMap [RegionToAvailabilityZones, !Ref "AWS::Region", AZ2]

例:リージョンごとに異なるNameタグをつける

以下はリージョンごとに異なるNameタグをつける例です、
AZなどリージョンごとに異なる値をNameタグに含めたい場合に役立ちます。

Mappings:
  AvailabilityZoneToShortname:
    ap-northeast-1:
      AZ1: "1a"
      AZ2: "1c"
    ap-northeast-3:
      AZ1: "3a"
      AZ2: "3b"

  NatGateway1:
    Type: AWS::EC2::NatGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - "natgw"
              - !FindInMap [
                  AvailabilityZoneToShortname,
                  !Ref "AWS::Region",
                  AZ1,
                ]

  NatGateway2:
    Type: AWS::EC2::NatGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Join
            - "-"
            - - "natgw"
              - !FindInMap [
                  AvailabilityZoneToShortname,
                  !Ref "AWS::Region",
                  AZ2,
                ]

例:ElastiCache レプリカ用AZ

ElastiCache::ReplicationGroup ではそのリージョンに対応したAZを指定する必要があります。
なおAZについては GetAZs であれば直接値を埋め込まなくて済むので、こちらの利用も検討してみてください。

Mappings:
  RegionToAZs:
    ap-northeast-1:
      AZ1: "ap-northeast-1a"
      AZ2: "ap-northeast-1c"
    ap-northeast-3:
      AZ1: "ap-northeast-3a"
      AZ2: "ap-northeast-3b"

  ElastiCacheReplicationGroup:
    Type: AWS::ElastiCache::ReplicationGroup
    Properties:
      ReplicationGroupId: "elasticache"
      PreferredCacheClusterAZs:
        - !FindInMap [RegionToAZs, !Ref "AWS::Region", AZ1]
        - !FindInMap [RegionToAZs, !Ref "AWS::Region", AZ2]

例:TemplateURL

リージョン障害を想定すると、S3に格納するテンプレートファイルも複製しておく必要があります。
そんな時はS3のバケット名にリージョン識別子を埋め込んで、親スタックのTemplateURLで参照するといい感じになります。

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

Resources:
  CloudWatch:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub
        - "https://s3.amazonaws.com/${RegionShortname}-cfn-s3/cfn.yaml"
        - RegionShortname:
            !FindInMap [RegionToShortname, !Ref "AWS::Region", Shortname]

例:ELB アクセスログ用S3バケットポリシー

ELBのアクセスログ配信元のアカウントは、リージョンごとに異なります。

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/enable-access-logging.html

そのためMappingsで定義した値を埋め込んであげると、いい感じにバケットポリシーを作れます。

Mappings:
  ElbRegionToAccountId:
    ap-northeast-1:
      AccountId: "582318560864"
    ap-northeast-3:
      AccountId: "383597477331"

  AlbLogS3Bucket:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: "https://s3.amazonaws.com/bucket/s3.yaml"
      Parameters:
        ElbRegionToAccountId:
          !FindInMap [ElbRegionToAccountId, !Ref "AWS::Region", AccountId]
Parameters:
  ElbRegionToAccountId:
    Type: String

Resources:
  AlbLogS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: "alb-s3"

  AlbLogS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AlbLogS3Bucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub "arn:aws:iam::${ElbRegionToAccountId}:root"
            Action: "s3:PutObject"
            Resource: !Sub "${AlbLogS3Bucket.Arn}/*"

例:DNS Firewall マネージドドメインリスト

DNS Firewall の マネージドドメインリスト はリージョンごとにIDが決まっています。

https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/resolver-dns-firewall-managed-domain-lists.html

そのためMappingsで定義した値を埋め込んであげると、いい感じに指定できます。

Mappings:
  AWSManagedDomainsAggregateThreatListId:
    ap-northeast-1:
      Id: "rslvr-fdl-103b4302c274455e"
    ap-northeast-3:
      Id: "rslvr-fdl-2e57899062984ed1"

Resources:
  DnsFirewallRuleGroup:
    Type: AWS::Route53Resolver::FirewallRuleGroup
    Properties:
      FirewallRules:
        - Action: BLOCK
          BlockResponse: NODATA
          Priority: 100
          FirewallDomainListId: !FindInMap [AWSManagedDomainsAggregateThreatListId, !Ref "AWS::Region", Id]

Condition + 擬似パラメータ

作成するリソースを制御したい場合は、大抵この方法で何とかなります。

例:リージョンごとに作成するリソースを制御する

DR環境など普段は一部リソースのみ作成して、EC2などは作成しない環境の場合、
親スタック側でConditionを利用することで、作成するリソースを制御できます。

以下であれば東京リージョンの場合だけ、子スタックを作成します。

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

Resources:
  SecretsManager:
    Type: AWS::CloudFormation::Stack
    Condition: IsTokyoRegion
    Properties:
      TemplateURL: "https://s3.amazonaws.com/${RegionShortname}-cfn-s3/cfn.yaml"
Resources:
  Secret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: "secret"

If 関数 + AWS::NoValue

条件に応じてリソースのパラメータを制御したい場合、
この方法が使えます。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html#cfn-pseudo-param-novalue

例:RDS スナップショットの制御

以下はAuroraクラスター作成時にスナップショットを利用するか、
またDBインスタンスを作成するかを制御しています。

例えば東京リージョンで初回構築の場合、
元となるスナップショットは存在しないですし、DBインスタンスを作成する必要があります。

逆にDR環境の場合は東京リージョンで作成されたスナップショットからクラスターを作成し、
またクラスターレベルのスナップショットであればDBインスタンスを個別に作成する必要はありません。

Parameters:
  PostgressqlAuroraSnapShot:
    Type: String
    Default: ""

Conditions:
  HasPostgressqlAuroraSnapShot:
    !Not [!Equals [!Ref PostgressqlAuroraSnapShot, ""]]
  CreatePostgresqlInstances:
    !Equals [!Ref PostgressqlAuroraSnapShot, ""]

Resources:
  PostgresqlCluster:
    Type: AWS::RDS::DBCluster
    Properties:
      DBClusterIdentifier: "aurora-postgresql"
      Engine: aurora-postgresql
      SnapshotIdentifier:
        !If [
          HasPostgressqlAuroraSnapShot,
          !Ref PostgressqlAuroraSnapShot,
          !Ref "AWS::NoValue",
        ]

  PostgresqlInstance1:
    Type: AWS::RDS::DBInstance
    Condition: CreatePostgresqlInstances
    Properties:
      DBInstanceIdentifier: "aurora-postgresql-01"

さいごに

以上、CloudFormationでマルチリージョン構成を実装するテクニックでした。

CloudFormationはPoCや単一リージョンでの構築、また小規模な環境であればパパッとできてとても便利なのですが、
マルチリージョンなど複雑な構成になってくると途端に考えることが増えてきます。

マルチリージョン構成を検討する際は、環境の目的(複製かDRか)スタック構成値の受け渡し方法などをしっかり設計することで、後々の運用がスムーズになります。
ぜひ本記事のテクニックを参考に、最適なマルチリージョン環境を構築してみてください。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.