はじめに
以下の記事で、AWS でリージョン間フェイルオーバーが可能なディザスタリカバリ (DR) 環境を構築しました。
今回はもうひとつのパターンである、AWS Global Accelerator を使った構成を紹介します。要件は以下の通りで、NLB パターンの時と同じです。
- AZ 間フェイルオーバーは自動
- リージョン間フェイルオーバーは自動または手動
- 静的 IP を公開できる
- RTO を 30 分、 RPO を数分とする (かなりシビア)
検証リソースの構築は CDK で一撃化しています。
なお、本記事では NLB パターンと被る部分は割愛します。また、最後に 2 つのパターンを比較します。
概要
構成としては、NLB ではなく Global Accelerator を使うことで DNS フェイルオーバーが不要になっています。
Global Accelerator は、AWS グローバルネットワークを利用してユーザーに最も近いリージョンのエンドポイントにトラフィックを転送することで通信レイテンシーを改善できる点が特徴です。
また、リージョン単位のロードバランサーのように動作することや、ユーザーに静的 IP を提供できることから DR の観点でも利用されます。トラフィックのルーティングに関しても、エンドポイントグループごと、およびエンドポイントごとに細かく制御できます。
今回は以下のような単純な構成です。リスナーは 443 と 80 を作り、それぞれ東京/大阪に振り分けます。HTTP から HTTPS へのリダイレクトは ALB で行います。都合 4 つのエンドポイントを作ることになり、これらは 2 つの ALB をリージョンごとに登録します。
注意点
Global Accelerator を使った場合の注意点は次の通りです。
- エンドポイントに ALB, NLB を設定した場合はそのヘルスチェックがそのまま使われる
- Global Accelerator は自動フェイルオーバーが前提
エンドポイントに ALB, NLB を設定した場合はそのヘルスチェックがそのまま使われる
ドキュメントに記載があります。
Global Accelerator で選択するヘルスチェックオプションは、エンドポイントとして追加したネットワークロードバランサーやアプリケーションロードバランサーには影響しません。
つまり、Global Accelerator で指定するヘルスチェックオプションは、Amazon EC2 と Elastic IP アドレスのヘルスチェックには使用されますが、ロードバランサーのエンドポイントのヘルスチェックには使用されません。
今回 ALB を関連づけた際は以下の挙動になることが確認できました。Global Accelerator の設定画面でヘルスチェックのパラメータを入力しても使われずに無視されます。
- Auto Scaling Group のキャパシティをゼロにするとヘルスチェックに失敗する
- ALB セキュリティグループのインバウンドを全拒否してもヘルスチェックは正常のまま
Global Accelerator は自動フェイルオーバーが前提
Global Accelerator は、片側のトラフィックダイヤルやウェイトをすべてゼロにした状態でも、ヘルスチェックが失敗した場合は自動で正常なエンドポイントにトラフィックがルーティングされます。このため、システム全体を特定の手順で手動フェイルオーバーさせたいといったケースには不向きです。
ドキュメントにフェイルオーバーのアルゴリズムについて記載があります。
エンドポイント・グループ内にウェイトが 0 より大きい健全なエンドポイントがない場合、Global Accelerator は、別のエンドポイント・グループ内のウェイトが 0 より大きい健全なエンドポイントへのフェイルオーバーを試みます。
このフェイルオーバーでは、Global Accelerator はトラフィックダイヤルの設定を無視します。そのため、たとえばエンドポイントグループのトラフィックダイヤルがゼロに設定されている場合でも、Global Accelerator はそのエンドポイントグループをフェイルオーバーの試行に含めます。
Global Accelerator は、最も近い 3 つのエンドポイントグループ(つまり、AWS リージョン)を試した後、0 より大きいウェイトを持つ健全なエンドポイントが見つからない場合、クライアントに最も近いエンドポイントグループ内のランダムなエンドポイントにトラフィックをルーティングします。つまり、オープンに失敗します。
構築
Global Accelerator を使った構成も CDK を使って一撃化することが可能です。
. ├── bin │ └── main.ts ├── lib │ ├── constructs │ │ ├── accelerator.ts │ │ ├── database.ts │ │ ├── network.ts │ │ ├── peering.ts │ │ └── service.ts │ ├── global-stack.ts │ ├── peering-stack.ts │ └── regional-stack.ts ├── scripts │ └── helper │ └── request.sh ├── src │ └── ec2 │ ├── userdata-osaka.sh │ └── userdata-tokyo.sh ├── test │ └── aws-dr-sample.test.ts ├── LICENSE ├── README.md ├── cdk.context.json ├── cdk.example.json ├── cdk.json ├── jest.config.js ├── package-lock.json ├── package.json └── tsconfig.json
ほとんどが NLB パターンと同じなので、主に Global Accelerator の部分を紹介します。
Global Accelerator
Global Accelerator の構築では、東京/大阪それぞれの ALB の情報を props で渡します。
流れ
- Accelerator を作る
- 東京側の ALB が設定されたエンドポイントを作る
- 大阪側の ALB が設定されたエンドポイントを作る
- Accelerator に HTTPS リスナーを追加する
- HTTPS リスナーに東京側のエンドポイントが入ったエンドポイントグループを追加する
- HTTPS リスナーに大阪側のエンドポイントが入ったエンドポイントグループを追加する
- Accelerator に HTTP リスナーを追加する
- HTTP リスナーに東京側のエンドポイントが入ったエンドポイントグループを追加する
- HTTP リスナーに大阪側のエンドポイントが入ったエンドポイントグループを追加する
- Route 53 にエイリアスレコードを登録する
前述の図の通りに設定するとこんなコードになります。
// lib/constructs/accelerator.ts ... export interface AcceleratorProps { serviceName: string; globalDomainName: string; hostedZone: cdk.aws_route53.IHostedZone; albPrimary: cdk.aws_elasticloadbalancingv2.IApplicationLoadBalancer; albSecondary: cdk.aws_elasticloadbalancingv2.IApplicationLoadBalancer; } export class Accelerator extends Construct { constructor(scope: Construct, id: string, props: AcceleratorProps) { super(scope, id); // Global Accelerator const accelerator = new cdk.aws_globalaccelerator.Accelerator(this, "Accelerator", { acceleratorName: `${props.serviceName}-accelerator`, enabled: true, ipAddressType: cdk.aws_globalaccelerator.IpAddressType.IPV4, }); accelerator.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); // Endpoint for primary ALB const albPrimary = new cdk.aws_globalaccelerator_endpoints.ApplicationLoadBalancerEndpoint(props.albPrimary, { weight: 128, preserveClientIp: true, }); // Endpoint for secondary ALB const albSecondary = new cdk.aws_globalaccelerator_endpoints.ApplicationLoadBalancerEndpoint(props.albSecondary, { weight: 128, preserveClientIp: true, }); // Listener for HTTPS const listenerHTTPS = accelerator.addListener("ListenerHTTPS", { protocol: cdk.aws_globalaccelerator.ConnectionProtocol.TCP, portRanges: [{ fromPort: 443, toPort: 443 }], clientAffinity: cdk.aws_globalaccelerator.ClientAffinity.SOURCE_IP, }); listenerHTTPS.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); // Add primary ALB to endpoint group for HTTPS listener listenerHTTPS.addEndpointGroup("EndpointGroup1", { endpoints: [albPrimary], trafficDialPercentage: 100, }); // Add secondary ALB to endpoint group for HTTPS listener listenerHTTPS.addEndpointGroup("EndpointGroup2", { endpoints: [albSecondary], trafficDialPercentage: 0, }); // Listener for HTTP const listenerHTTP = accelerator.addListener("ListenerHTTP", { protocol: cdk.aws_globalaccelerator.ConnectionProtocol.TCP, portRanges: [{ fromPort: 80, toPort: 80 }], clientAffinity: cdk.aws_globalaccelerator.ClientAffinity.SOURCE_IP, }); listenerHTTP.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); // Add primary ALB to endpoint group for HTTP listener listenerHTTP.addEndpointGroup("EndpointGroup1", { endpoints: [albPrimary], trafficDialPercentage: 100, }); // Add secondary ALB to endpoint group for HTTP listener listenerHTTP.addEndpointGroup("EndpointGroup2", { endpoints: [albSecondary], trafficDialPercentage: 0, }); // Alias record for Global Accelerator const gaARecord = new cdk.aws_route53.ARecord(this, "ARecord", { recordName: props.globalDomainName, target: cdk.aws_route53.RecordTarget.fromAlias(new cdk.aws_route53_targets.GlobalAcceleratorTarget(accelerator)), zone: props.hostedZone, }); gaARecord.node.addDependency(accelerator); } }
スタックのデプロイ時に東京/大阪それぞれの ALB 情報を渡します。
// bin/main.ts ... // Deploy tokyo stack const tokyoStack = new DrSampleRegionalStack(app, "DrSampleRegionalStackTokyo", { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: "ap-northeast-1", }, terminationProtection: false, serviceName: serviceName, area: "tokyo", cidr: "10.0.0.0/16", azPrimary: "ap-northeast-1a", azSecondary: "ap-northeast-1c", globalDatabaseIdentifier: globalDatabaseIdentifier, isPrimaryDatabaseCluster: true, hostedZoneName: hostedZoneName, globalDomainName: globalDomainName, userDataFilePath: "./src/ec2/userdata-tokyo.sh", }); // Deploy osaka stack const osakaStack = new DrSampleRegionalStack(app, "DrSampleRegionalStackOsaka", { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: "ap-northeast-3", }, terminationProtection: false, serviceName: serviceName, area: "osaka", cidr: "10.1.0.0/16", azPrimary: "ap-northeast-3a", azSecondary: "ap-northeast-3c", globalDatabaseIdentifier: globalDatabaseIdentifier, isPrimaryDatabaseCluster: false, hostedZoneName: hostedZoneName, globalDomainName: globalDomainName, userDataFilePath: "./src/ec2/userdata-osaka.sh", }); ... // Global Accelerator const gaStack = new DrSampleGlobalStack(app, "DrSampleGlobalStack", { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }, terminationProtection: false, crossRegionReferences: true, serviceName: serviceName, globalDomainName: globalDomainName, hostedZone: tokyoStack.hostedZone, albPrimary: tokyoStack.alb, // <= これ albSecondary: osakaStack.alb, // <= これ }); ...
フェイルオーバー検証
NLB パターンの時と同じスクリプトを使ってフェイルオーバーにかかる時間を計測します。
- 東京側の ALB ターゲットグループから EC2 インスタンスをすべて解除してヘルスチェックを失敗させる (Global Accelerator では異常扱いになる)
- curl を定期的に実行して RTO を計測
$ sh scripts/helper/request.sh https://your-domain.com Request succeeded at Wed Jan 31 16:46:48 JST 2024 Request succeeded at Wed Jan 31 16:46:49 JST 2024 Request succeeded at Wed Jan 31 16:46:51 JST 2024 Request succeeded at Wed Jan 31 16:46:52 JST 2024 Request succeeded at Wed Jan 31 16:46:53 JST 2024 Request succeeded at Wed Jan 31 16:46:54 JST 2024 Request succeeded at Wed Jan 31 16:46:55 JST 2024 Request succeeded at Wed Jan 31 16:46:56 JST 2024 Request succeeded at Wed Jan 31 16:46:57 JST 2024 Request succeeded at Wed Jan 31 16:46:58 JST 2024 Request succeeded at Wed Jan 31 16:46:59 JST 2024 Request succeeded at Wed Jan 31 16:47:01 JST 2024 Request succeeded at Wed Jan 31 16:47:02 JST 2024 Request succeeded at Wed Jan 31 16:47:03 JST 2024 Request succeeded at Wed Jan 31 16:47:04 JST 2024 Request succeeded at Wed Jan 31 16:47:05 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:06 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:07 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:08 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:10 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:11 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:12 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:13 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:14 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:15 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:16 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:17 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:18 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:20 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:21 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:22 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:23 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:24 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:25 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:26 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:27 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:28 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:29 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:31 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:32 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:33 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:34 JST 2024 Request succeeded at Wed Jan 31 16:47:35 JST 2024 Request failed with HTTP code 503 at Wed Jan 31 16:47:36 JST 2024 Request succeeded at Wed Jan 31 16:47:37 JST 2024 Request succeeded at Wed Jan 31 16:47:38 JST 2024
レスポンスが戻ってくるまで 31 秒でした。かなり早いですね。
比較
NLB パターンと Global Accelerator パターンをいくつかの観点から比較します。
フェイルオーバーにかかる時間
- DB のフェイルオーバー操作の時間は含まない
- あくまで参考値
NLB パターン | Global Accelerator パターン |
---|---|
2分程度 | 1分以下 |
手動フェイルオーバー
NLB パターン | Global Accelerator パターン |
---|---|
可能 | 不可能 |
払い出される IP の数
NLB パターン | Global Accelerator パターン |
---|---|
多い (AZの数 x リージョンの数) | 少ない (2) |
設定の容易さ
NLB パターン | Global Accelerator パターン |
---|---|
面倒 (制約あり) | NLBと比較すると簡単 |
ALB で受けた場合のクライアント IP 保持 (X-Forwarded-For)
NLB パターン | Global Accelerator パターン |
---|---|
可能 | 可能 |
ちなみに料金に関しては、NLB パターンだと 2 台用意しなければならない点や、サービス間での課金体系に違いがある点を考えると、一概に NLB のほうが安いというわけではないように思います。ユースケースごとにシミュレーションが必要です。
おわりに
静的 IP を持つマルチリージョン DR 構成において、Global Accelerator を使ったパターンを注意点を踏まえながら紹介しました。今回の対応で AWS における DR 設計に対する知見が深まりました。