CDN の冗長化 (CloudFront + KeyCDN + Route53)

[Happy Elements 株式会社 カカリアスタジオ Advent Calendar 2016](http://qiita.com/advent-calendar/2016/hekk_cacalia_studio) の 5 日目です．本日の担当はインフラグループの @nagizero です．よろしくお願いします．[昨日投稿した CDN の冗長化に関する検討](http://qiita.com/nagizero/items/1738c0e4923bbcd0f2c2)の内容を実際に試してみます．



# TL;DR

- CDN をプライマリ (CloudFront) とセカンダリ (KeyCDN) の冗長構成にします
- CDN のフェイルオーバー・フェイルバックを DNS (Route53) のヘルスチェック機能で実現します
- トラブル発生時にフェイルオーバー・フェイルバックされることを確認します



# システム構成図

![architecture](https://qiita-image-store.s3.amazonaws.com/0/71014/2deaa055-784d-2991-0f41-669f94f9919f.png)

- 平時は Route53 が CloudFront のアドレスを返します
- 異常時は Route53 が KeyCDN のアドレスを返します
- 正常に復帰すると Route53 が CloudFront のアドレスを返します



# ToDo

1. オリジン (S3 バケット) を作成
2. CloudFront のオリジンに S3 を登録
3. KeyCDN のオリジンに S3 を登録
4. Route53 で CloudFront のヘルスチェックを作成
5. Route53 で CDN 用ドメインを作成して CNAME レコードに CloudFront と KeyCDN のアドレスを登録
  - ヘルスチェックを利用したフェイルオーバー・フェイルバックを設定
6. 動作確認



# S3

CDN 経由で配布するデータ (オリジン) を作成します．バケットにヘルスチェック用のファイル (**health-check**) とコンテンツファイル (**index.html**) をアップロードして公開状態にします．

![test-backet](https://qiita-image-store.s3.amazonaws.com/0/71014/9d5213d7-a301-92bb-037c-517c97f9f063.png)

公開アドレスにアクセスして `index.html` を取得できるようになりました．

![test-access](https://qiita-image-store.s3.amazonaws.com/0/71014/5fa31a55-eb8a-3fbc-0b70-465f9364c956.png)



# CloudFront

S3 のデータを CloudFront で配信できるようにします．

Web Distribution を作成してオリジンに S3 バケットを選択すれば OK です． CloudFront の設定例は色々なところで紹介されていますので，本稿では詳しい手順は割愛します．

![cloudfront-distribution](https://qiita-image-store.s3.amazonaws.com/0/71014/28b4f705-2e96-d63a-d4e1-9a3c23b16442.png)

![cloudfront-access](https://qiita-image-store.s3.amazonaws.com/0/71014/957ab2aa-1c95-abe1-069d-b6905ee3818c.png)



# KeyCDN

S3 のデータを KeyCDN で配信できるようにします．

**Zone** でオリジンを設定します． *KeyCDN の Zone ≒ CloudFront の Distribution* と考えてよさそうです．オリジンに S3 を利用するので **Zone Type** は **Pull** を選択します (オリジンを KeyCDN にする場合は **Zone Type** に **Push** を選択します)．

![keycdn](https://qiita-image-store.s3.amazonaws.com/0/71014/243c5f74-41a5-9d9c-4681-a722616f3860.png)

上記 **Pull Zone Settings > Origin URL** は S3 バケットのルートを指定します (下図の赤色下線部参照)．

![pull-zone](https://qiita-image-store.s3.amazonaws.com/0/71014/a916982e-f4e7-c4e6-8e54-19bd2e442c91.png)

しばらく待つと Zone が有効になります．

![keycdn-zone](https://qiita-image-store.s3.amazonaws.com/0/71014/90535c50-c4d0-ed58-3bc4-51d025023f4f.png)

![keycdn-access](https://qiita-image-store.s3.amazonaws.com/0/71014/300bc808-1b4e-72a5-72b3-8939fe150835.png)



# Route53

CDN 用のドメインを用意します．本稿では **redundancy.foo.bar** (仮) を用意して CNAME レコードに CDN のドメインを登録します．通常時は CloudFront のドメインを返し， CloudFront 異常時には KeyCDN のドメインを返すように設定します．

## CDN の CNAME 設定

Route53 に登録したドメイン経由で CDN にアクセスするためには CDN 側で CNAME/alias を設定する必要がありますのでドメインを登録する前に対応しておきます． CloudFront は， **Alternative Domain Names (CNAMEs)** に **redundancy.foo.bar** を登録します． KeyCDN は， **Zonealiases** を開き **Alias** に **redundancy.foo.bar** を登録します．これで **redundancy.foo.bar** というドメインで CDN にアクセスできるようになります．

## ヘルスチェック作成

Route53 のヘルスチェック機能を用いて CloudFront を監視し，異常があれば返却するドメインを KeyCDN に切り替えるようにします．

[Amazon Route 53 がエンドポイントの正常性を判断する方法 - Amazon Route 53](http://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/dns-failover-determining-health-of-endpoints.html) によると，

- ヘルスチェックは世界各地のヘルスチェッカーから実行される
- ヘルスチェッカーはエンドポイント (今回は CloudFront) に対して指定の間隔 (Request interval) でヘルスチェックリクエストを送信する
- エンドポイントがヘルスチェックに連続して不合格 (または合格) になった回数が閾値 (Failure threshold) に達すると，エンドポイントの正常性ステータスが更新される
  - その後，同じ回数続けてチェックに不合格 (または合格) になると，エンドポイントの正常性ステータスが変化する
      - 連続で異常を検知すると正常性ステータスが正常から異常に変化する
      - 連続で正常を検知すると正常性ステータスを異常から正常に変化する
- 全体の 18% を超えるヘルスチェッカーからエンドポイントが正常と報告された場合に， Route53 は関連付けられているリソースレコードセットを使用してクエリに応答する

とのことです．つまり，閾値が 3 のエンドポイントで 6 回連続で不合格になるとそのエンドポイントは異常とみなされます．閾値が 2 であれば 4 回連続で不合格になると異常とみなされます．そして，全体の 82% 以上のヘルスチェッカーから異常とみなされたエンドポイントはクエリ応答から除外されることになります．

---

**Create health check** を押して設定を開始します．

![create-health-check](https://qiita-image-store.s3.amazonaws.com/0/71014/1128f1ec-d0c0-b3b6-aedf-233b194d4dcb.png)

下図のように入力して **Next** 作成します． **Failure threshold** はデフォルトの **3** から **1** に変更しています． **Request interval** が **30 seconds** なので 30 秒に 1 回のペースでヘルスチェックが行われます． **Request interval** を **10 seconds** にする場合は[月額 1.00 USD の追加課金](http://aws.amazon.com/jp/route53/pricing/)となります．

![health-check-cloudfront](https://qiita-image-store.s3.amazonaws.com/0/71014/347a9bd8-916f-c2e7-da33-402ccbfcf894.png)

作成するとすぐにヘルスチェックが動作し始めます．

![health-check-list](https://qiita-image-store.s3.amazonaws.com/0/71014/458c79ff-b71d-0d27-c285-40d241a0d035.png)

## ドメイン登録 (DNS フェイルオーバー・フェイルバック設定)

**redundancy.foo.bar** の CNAME に CloudFront と KeyCDN のドメインを登録していきます．先ほど作成したヘルスチェックも組み込みます．

まずはプライマリ CDN の CloudFront を登録します．

|項目|値|
|:--|:--|
|TTL|30|
|Routing Policy|Failover|
|Failover Record Type|Primary|
|Associate with Health Check|Yes|
|Health Check to Associate|先ほど作成したヘルスチェック|

![cname-cloudfront](https://qiita-image-store.s3.amazonaws.com/0/71014/8e3a098a-05ad-9237-4dcd-58577cbadaa4.png)

次にセカンダリ CDN の KeyCDN を登録します．

|項目|値|
|:--|:--|
|TTL|30|
|Routing Policy|Failover|
|Failover Record Type|Secondary|
|Associate with Health Check|No|

![cname-keycdn](https://qiita-image-store.s3.amazonaws.com/0/71014/42f61f00-40e8-b8c8-58f8-ddaa09519aba.png)

以上で設定できました． CloudFront にエラーが発生した場合は KeyCDN にフェイルオーバーされ， CloudFront が正常に戻るとフェイルバックされるはずです．

# 動作確認

## ヘッダ確認

はじめに各 CDN から index.html を取得してレスポンスヘッダを確認します．

まずは CloudFront から確認します． **Server: AmazonS3** となっています．

```
% curl -s --head http://foo.cloudfront.net/index.html | grep -e '^HTTP' -e '^Server'
HTTP/1.1 200 OK
Server: AmazonS3
```

次に KeyCDN を確認します． **Server: keycdn-engine** となっています．

```
% curl -s --head http://redundancytest-bar.kxcdn.com/index.html | grep -e '^HTTP' -e '^Server'
HTTP/1.1 200 OK
Server: keycdn-engine
```

そして Route53 (redundancy.foo.bar) を確認します． **Server: AmazonS3** となっており，プライマリ CDN である CloudFront からデータを取得していることがわかります．

```
% curl -s --head http://redundancy.foo.bar/index.html | grep -e '^HTTP' -e '^Server'
HTTP/1.1 200 OK
Server: AmazonS3
```

以上から， Route53 (redundancy.foo.bar) へのアクセスで **Server: keycdn-engine** と表示されればフェイルオーバーされたことになります．

以下，約 1 秒ごとに Route53 (redundancy.foo.bar) 経由で index.html を取得し続け， CloudFront にエラーが発生した (あるいはエラーから復帰した) 際の動作を確認します．理論値としてはフェイルオーバー (フェイルバック) は 120 秒以内に完了する見込みです．

```txt:フェイルオーバーに要する見込み時間
  (Route 53 のヘルスチェック間隔 30 秒) * (閾値 1) * 2
+ (世界各地のヘルスチェッカーがエンドポイントの正常性を判断するまでの時間差．とりあえず 30 秒)
+ (redundancy.foo.bar の TTL 30 秒)
```

## フェイルオーバー

CloudFront の設定を変更してから実際に変更内容が反映されるまでにはタイムラグがあるため，最初に CloudFront のコンソールから **Distribution State** を **Disabled** にします (2015-11-24 11:50:20 JST に実行)．

そして約 1 秒ごとに CDN にアクセスします．しばらくは正常に HTTP ステータス 200 が返ってきました．

```
% while true; do echo '--'; date; curl -s --head http://redundancy.foo.bar/index.html | grep -e '^HTTP' -e '^Server'; sleep 1; done
--
2015年 11月24日 火曜日 11時50分25秒 JST
HTTP/1.1 200 OK
Server: AmazonS3
.
.
.
```

CloudFront を Disabled に設定してから 3 分 27 秒後に初めて 403 エラーが返ってきました．ただし， 200 を返す場合もあり， CloudFront が少しずつ Disabled に変化していることがわかります．

```
.
.
.
--
2015年 11月24日 火曜日 11時53分46秒 JST
HTTP/1.1 200 OK
Server: AmazonS3
--
2015年 11月24日 火曜日 11時53分47秒 JST
HTTP/1.1 403 Forbidden
Server: CloudFront
--
2015年 11月24日 火曜日 11時53分48秒 JST
HTTP/1.1 403 Forbidden
Server: CloudFront
--
2015年 11月24日 火曜日 11時53分49秒 JST
HTTP/1.1 403 Forbidden
Server: CloudFront
--
2015年 11月24日 火曜日 11時53分50秒 JST
HTTP/1.1 200 OK
Server: AmazonS3
.
.
.
```

CloudFront のエラーを検知し始めてから 51 秒後にステータスコードは完全に 403 となりました．

```
.
.
.
--
2015年 11月24日 火曜日 11時54分37秒 JST
HTTP/1.1 200 OK
Server: AmazonS3
--
2015年 11月24日 火曜日 11時54分38秒 JST
HTTP/1.1 403 Forbidden
Server: CloudFront
.
.
.
```

ステータスコードが完全に 403 に切り替わってから 57 秒後に CDN が CloudFront から KeyCDN へと切り替わりました．以後は KeyCDN からのみレスポンスが返ってきました．

```
.
.
.
--
2015年 11月24日 火曜日 11時55分34秒 JST
HTTP/1.1 403 Forbidden
Server: CloudFront
--
2015年 11月24日 火曜日 11時55分35秒 JST
HTTP/1.1 200 OK
Server: keycdn-engine
.
.
.
```



## ダウンタイム

下表に試験で検知したイベント発生時刻をまとめました． CloudFront のエラーを検知してからフェイルオーバーが行われるまでのダウンタイムは 108 秒であり，理論値 (120 秒以内) に収まっています (ただし試行回数は 1 なので結果の信頼性は低いです)．

|イベント|時刻|
|:--|:--|
|CloudFront にエラー発生|2015-11-24 11:53:47 JST|
|CloudFront が完全にエラーとなる|2015-11-24 11:54:38 JST|
|CloudFront から KeyCDN に切り替わる|2015-11-24 11:55:35 JST|

下図は Route53 によるヘルスチェック結果です． HealthCheckStatus がエンドポイントのヘルスチェックステータス (1: 正常， 0: 異常) であり， HealthCheckPercentageHealthy はエンドポイントのステータスを正常とみなしているヘルスチェッカーの割合です． HealthCheckPercentageHealthy が 18% 以下になると HealthCheckStatus が 0 となります (下図では 23% の時点で 0 となっています)．

![health-status](https://qiita-image-store.s3.amazonaws.com/0/71014/4414f3b4-f22c-ede5-2036-4b300d4358b9.png)

Route53 も 11:53:00 以降に異常を検知し始めており， 11:55:00 に正常性ステータスを異常に変化させています．また， 11:55:00 過ぎに HealthCheckPercentageHealthy が 18% 以下となり CDN の切り替えが発生しています． CDN の切り替えが DNS に反映されるのは TTL (30 秒) 後の 11:55:30 過ぎとなり，この時刻は KeyCDN からデータを取得し始めた時刻 11:55:35 におおよそ合致しています．

## フェイルバック

CloudFront の **Distribution State** を **Enabled** にした後，フェイルオーバーテスト時と同様に約 1 秒ごとに CDN にアクセスします．

```
% while true; do echo '--'; date; curl -s --head http://redundancy.foo.bar/index.html | grep -e '^HTTP' -e '^Server'; sleep 1; done
--
2015年 11月24日 火曜日 17時21分29秒 JST
HTTP/1.1 200 OK
Server: keycdn-engine
.
.
.
```

約 8 分後に KeyCDN から CloudFront に切り替わりました．フェイルバック後に CloudFront が 403 エラーを返すことはなく，ステータスコードはすべて 200 でした．

```
--
2015年 11月24日 火曜日 17時29分13秒 JST
HTTP/1.1 200 OK
Server: keycdn-engine
--
2015年 11月24日 火曜日 17時29分14秒 JST
HTTP/1.1 200 OK
Server: AmazonS3
.
.
.
```

下図は Route53 によるヘルスチェック結果です． HealthCheckPercentageHealthy が 17:28:00 に 5%, 17:29:00 に 31% を示している (18% を超えている) ことから， 17:28:ss にフェイルバックが実行され，その 30 秒後に Route 53 が返却するドメインが変更されたと考えられます．これは 17:29:14 現在にレスポンスが KeyCDN から CloudFront に戻っていることとも合致します．

![health-status](https://qiita-image-store.s3.amazonaws.com/0/71014/a6a9175a-ce50-aa9a-ab4a-6ed2a6018095.png)



# おわりに

CDN の冗長構成を実際に試してみました，動作試験の結果，異常時にフェイルオーバーされることと，ステータスが異常から正常に戻った際にフェイルバックされることが確認できました．予想できるトラブルに対しては転ばぬ先の杖を用意しておきたいですね．

余談ですが， CDN 冗長化対応を実施してからの約 1 年間で CloudFront がダウンした様子はなく， KeyCDN のクレジットは全然減っていませんでした．