【DR対策】AWS 国内東西リージョンでウォームスタンバイ戦略の構成管理を容易にする

## はじめに
とあるシステムでAWSの東京リージョンをプライマリリージョン、大阪リージョンをセカンダリリージョンとした、ウォームスタンバイ戦略の環境を構築しました。
この中で環境の更新など、構成管理について工夫した点を紹介します。

## ウォームスタンバイ戦略とは
ウォームスタンバイ戦略は、ディザスタリカバリ（DR)戦略の中の１つです。
ディザスタリカバリ戦略は４種類あり、以下のAmazon Web Services ブログで詳しく紹介されています。

https://aws.amazon.com/jp/blogs/news/disaster-recovery-dr-architecture-on-aws-part-1-strategies-for-recovery-in-the-cloud/

どの戦略を選択するかはRPO、RTO、コストなどの観点で決定します。

今回のシステムでは以下の理由により、ウォームスタンバイ戦略を選択しました。
1. RPO/RTOが2時間であり、「バックアップ＆リストア」や「パイロットライト」では間に合わない      
1. マルチサイトほどの可用性は不要であり、コストをかける必要がない

ちなみに、前述のAmazon Web Services ブログでは「パイロットライト＝分単位」や、「ウォームスタンバイ戦略＝秒単位」と紹介されていますが、今回のシステムではシステムの構成要素が多く、紹介されている時間での切り替えは不可能だと判断していました。
（単純なDNSレコードの変更では切り替えできない）

この辺りは「自分たちのシステムでは、DR発動時にどのような作業を行う必要があるか」を踏まえた上で選択する必要があります。

    
## セカンダリリージョンのメンテナンスの必要性
ウォームスタンバイ戦略（に限らずですが）ではスムーズに切り替えを行うため、日頃からセカンダリリージョン（DR環境）の構成も最新化しておく必要があります。
リリースを溜めておき、DR発動時に最新化するような形を取ると、RTO目標に間に合わなくりますし、いざという時にデプロイ失敗なんてことも考えられます。

一方、日々、プライマリリージョンとセカンダリリージョンの環境を個別に管理するのは更新漏れが発生しやすく、手間もかかります。
なので、この構成管理を更新漏れの無いように、楽に行いたいと考えました。
具体的には以下の２点について工夫しました。
1. CFnテンプレートの管理
1. CI/CDパイプラインの構築


## CFnテンプレートの管理
今回の環境はCloudFormationテンプレートを使って構築しています。
（今回の記事では触れませんが、アプリはほぼSAMで構築しています）

このCFnテンプレートの管理を効率化したいと考えました。

通常、セカンダリリージョンはプライマリリージョンのミラーサイトになります。
なので原則、同じCFnテンプレートを使って両方の環境を構築できるはずです。

しかし、実際は以下のような理由でリージョン間の構成や設定に差分が発生してきます。
1. グローバルリソースの命名重複、リージョンごとのパラメータの違い（S3バケット名、IAM、VPC CIDRなど）
1. メインリージョンのみに作成が必要なリソース（Route 53レコード、ユーザーのスイッチ用IAMロールなど）
1. リージョン間で構成が異なるリソース（リージョン間のサービス提供差異などによる構成の違い）

命名やパラメータの違いなどの差であれば、１つのテンプレートで`Conditions`や`Mappings`、`Fn::If`などを用いて制御が可能です。
しかし、構成が異なる場合など、差分が大きい場合は条件分岐等で頑張ってしまうと可読性・保守性が低下します。
なので無理せず、リージョンごとにテンプレートを分けることも必要になります。
今回の場合、無理せず共通的に利用できるテンプレートは８～９割でした。

この東西リージョンのCFnテンプレートを以下のように同一リポジトリで階層化して管理するようにしました。   
東西リージョン共通テンプレート、東京リージョン専用テンプレート、大阪リージョン専用テンプレートの３つです。
 
![dr_pipeline-CodeCommit.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231658/4e8c7881-9854-ae05-8b28-adc1dd21fb13.png)

このリポジトリの各テンプレートを、リージョン毎のroot.yamlから参照する形としています。

<summary>primary/root.yamlのイメージ（東京リージョン用 親CFnテンプレート）</summary>

```yaml
Resources:
  Resource1:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateUrl: ../common/templates/template1.yaml
  Resource2:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateUrl: ./templates/template2.yaml
  Resource3:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateUrl: ./templates/template3.yaml

```

<summary>secondary/root.yamlのイメージ（大阪リージョン用 親CFnテンプレート）</summary>

```yaml
Resources:
  Resource1:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateUrl: ../common/templates/template1.yaml
  Resource2:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateUrl: ./templates/template3.yaml
```

これで、８～９割のテンプレートは共通利用しつつ、リージョン毎の個別定義も可能になりました。

また、この構成にすることで、例えばcommonのテンプレートを後からリージョン毎の個別テンプレートに分割したいといった場合でも、root.yaml側で参照先パスを変えるだけで分割が可能となります。（スタックの再作成ではなく、更新で対応が可能になる）


## CI/CDパイプラインの構築

CI/CDパイプラインはCodeCommitの更新をトリガーに東西リージョンのパイプラインが実行されるように、リージョンを跨いだパイプラインを構築しました。

![dr_pipeline-Pipeline.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231658/15b7d337-ba46-cb01-2245-4e73494d695a.png)

以下に簡単に説明します。

### 1.東京リージョンのCI/CDパイプライン

図中の青枠「１」は東京リージョン用のCI/CDパイプラインです。

具体的には以下の流れで処理を行っています。
1. CodeCommitでプルリクマージのイベントが発生した際、EventBridgeがLambdaを起動する
1. Lambdaでイベントの内容からCodePipelineの起動要否を判定し、必要であればCodePipelineを起動する
1. CodePipelineでCodeBuildによるドリフト検出およびCloudFormation変更セットの作成を行う
1. 上位者が変更内容を確認した後、CodePipelineの承認アクションで承認を行う
1. CloudFormationが実行され、スタックが更新される

##### 補足
前述したとおり、このリポジトリは東西リージョンのテンプレートを管理しているモノリポジトリのため、EventBridgeから直接CodePipelineを起動するのではなく、間にLambdaを挟んで起動要否を判定しています。

例えば"secondary"ディレクトリ内のファイルを変更された際、東京リージョンのパイプラインが起動するのは無駄な処理です。
そのため、Lambdaでイベントの内容からコミット差分を取得し、"primary"または"common"ディレクトリのファイルが１つでも更新されているかを確認します。

参考までにCodeCommitでのモノリポジトリ構成について書かれている記事を見つけましたので、以下にリンクを貼らせていただきました。

https://qiita.com/k-sasaki-hisys-biz/items/a44c3f957d8f3fc45b26


ちなみに、<strong>先日CodePipelineにファイルパスでのトリガーフィルター機能が追加されました。</strong>
これにより、<strong>GitHubやBitbucketを利用している場合はLambda不要でパイプラインのトリガーを制御できます。</strong>
ただし、<strong>現時点ではCodeCommitには対応していない</strong>ため、CodeCommitをリポジトリとして利用する場合は今まで通りLambda等でのフィルタ処理が必要になります。

https://aws.amazon.com/jp/about-aws/whats-new/2024/02/codepipeline-trigger-filters-execution-modes/


### 2.大阪リージョンのCodeCommitへの同期処理

図中の青枠「２」は東京リージョンのCodeCommitの変更を大阪リージョンのCodeCommitへ同期する処理です。
この処理でCodeCommitの遠隔地バックアップを取りつつ、大阪リージョンのCI/CDパイプラインのトリガーにしています。

具体的には以下の流れで処理を行っています。
1. CodeCommitでプルリクマージのイベントが発生した際、EventBridgeがLambdaを起動する
1. Lambdaで対象リポジトリやブランチ名、コミットIDなどを取得し、CodeBuildの環境変数をオーバーライドしつつ起動する
1. CodeBuildで東京リージョンの対象リポジトリからgit cloneし、大阪リージョンのCodeCommitに`git push`する


#### 補足
CodeCommitリポジトリのリージョン間レプリケーションについては、AWSブログでLambdaとFargateを利用した方法が紹介されています。

https://aws.amazon.com/jp/blogs/news/replicate-aws-codecommit-repository-between-regions-using-aws-fargate/

この構成でも良かったのですが、以下の理由でLambda＋CodeBuildを採用しました。
1. Fargateを利用する場合、コンテナイメージの管理やECSタスクの管理など、管理対象が増えてしまう
1. CodeCommitの同期処理はgitコマンドになるため、コマンドベースで定義するbuildspecの方が相性が良い


### 3.大阪リージョンのCI/CDパイプライン

図中の青枠「３」は大阪リージョン用のCI/CDパイプラインです。
処理の流れは東京リージョン用のCI/CDパイプラインと同じです。

違いとしては、大阪リージョンのLambdaでは"secondary"または"common"ディレクトリのファイルが１つでも更新されている場合にCodePipelineを起動します。

#### 補足
初期構築時は大阪リージョンにはCodePipelineは提供されておらず、CodeBuildを直接起動する構成でした。
しかし、<strong>2023/11に大阪リージョンにもCodePipelineが提供されたため</strong>、東西リージョンで同じ構成にすることができました。

https://aws.amazon.com/jp/about-aws/whats-new/2023/11/aws-codepipeline-seven-regions/


### DR発動後の運用
  DR発動後は大阪リージョンのCodeCommitがマスターになります。
  開発者は大阪リージョンのCodeCommitにpushすることで、大阪リージョンのCI/CDパイプラインを利用して環境を更新します。
  
  ![dr_pipeline-Pipeline_DR.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231658/800192b4-4443-647f-3ba0-166ba4032a14.png)


## まとめ
ウォームスタンバイ戦略では、日頃から最新構成を維持しておく必要があります。
※ 最新構成を維持することでDR発動時の作業時間短縮およびトラブル回避を行います。
 
日々の運用負荷を抑えつつ構成を維持するためには、IaCテンプレートの管理や更新の自動化において、プライマリリージョンだけではなくセカンダリリージョン（DRリージョン）も含めた管理方法を考える必要があると思いました。