Route 53 ヘルスチェックをTerraformで定義する

## tl;dr

この記事は、[Increments Advent Calendar 2017 - Qiita](https://qiita.com/advent-calendar/2017/increments) の6日目です

- Route 53 のヘルスチェックが便利
- たくさん定義するためにTerraform でヘルスチェックを作成している
- HTTPSプロキシのcamoの実例

## Route 53 のヘルスチェック

- [Amazon Route 53 ヘルスチェックの作成と DNS フェイルオーバーの設定 - Amazon Route 53](http://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/dns-failover.html)

外形監視用のサーバを自社で運用する場合、それ自身が稼働していないと監視が正常に行なえません。監視対象と同じデータセンターで監視していると、同じ障害の影響を受けていたり、データセンター内では疎通はできてるけど外部からのアクセスはできていないといったことも起こり得ます。とはいえ、そこの可用性に注力するのは微妙なので、SaaS的なものを使いたいわけです。

この手の外形監視サービスはいろいろなサービスが存在しており、たとえば [Mackerel](https://mackerel.io/ja/docs/entry/external-monitoring) の機能や、[uptime](https://uptime.com/)、[pingdom](https://www.pingdom.com/) などいろいろあります。

その中で、Route 53 ヘルスチェックを利用するメリットとしては、

- AWSを既に使っていればサービス契約とか考えなくてよい
- 東京を含む複数拠点からヘルスチェックしてくれる
- CloudWatchにメトリクスを出力したり、アラームを設定したり、慣れてるしくみが使える
- Route 53 の DNS フェイルオーバーに**もちろん**利用できる

といったところです。

### 価格について

- [料金 - Amazon Route 53 | AWS](https://aws.amazon.com/jp/route53/pricing/)

基本的なヘルスチェックを同一アカウントのELBやEC2インスタンス、S3といったところに行う場合、50件までは無料です。お得ですね。
が、オプション料金というのがあり、以下のオプションを利用すると、1つにつき月1ドルが発生します。

- HTTPS: HTTPSでヘルスチェックする
- 文字列マッチング: HTTP/HTTPSレスポンスに指定した文字列が含まれるかチェックする
- 短インターバル: 通常30秒間隔を10秒間隔に
- レイテンシー計測: レイテンシーを計測してCloudWatchメトリクスをputする

Incrementsのサービスは基本HTTPS化したので、HTTPSでヘルスチェックしたいですし、指標としてレイテンシーも計測したいので、1ヘルスチェックあたり2ドルオプション料金がかかっていて、積み上げるとそこそこの金額となっています。


## Terraform でのヘルスチェック作成

Increments では、AWSやHerokuのインフラ環境の構成管理にTerraformを利用しています。
Herokuであったり、外部サービスであったり、自社でAWS上で構築、運用しているサービス以外も含め監視を行っており、その定義は Terraform で行っています。
1つのケースとして、camoの監視を実例を含めて紹介します。

### Calculated Health Check の作成

Qiita では、HTTPS化の一貫として、記事に埋め込まれた画像を [camo](https://github.com/atmos/camo) を通して、HTTPSで提供しています。
CloudFrontを介してオリジンのCache-Controlに応じてキャッシュもしているのですが、この場合、全体のメトリクスを見ていても、以下の理由から正常稼働しているかを判断が難しく、False Alarmがたびたび発生していました。

- キャッシュヒット率が低い: オリジンのコンテンツのCache-Controlに応じて決まるため低くても正しい
- 40x、50x の割合が多い: そもそもオリジンが40x、50xを返していたり、デッドリンクが存在する
- レイテンシが高い: オリジンのレイテンシで左右される

そのため全体のメトリクスとは別に、キャッシュがされず常にオリジンフェッチをし、常に200 OKが返却されることが想定されるターゲットを複数用意し、これを監視対象とすることとしました。
ターゲットは、たとえば、複数リージョンのS3に画像を配置、`Cache-Control: no-cache, no-store, must-revalidate` をメタデータに指定してキャッシュ抑制したものなど、概ね安定したレスポンスで可用性が担保されていて、CDNがキャッシュされないものとします。

### tf定義

#### locals

ヘルスチェックのターゲットを定義します。ここでは、オリジンのURLと、Camoでそこにアクセスする際のパスを記載しています。

```
locals {
  camo_child_healthchecks = [
    {
      origin_url    = "http://example.com/test.png"
      resource_path = "/camo_path"
    },
    ...
  ]
}
```

#### aws_route53_health_check

複数のヘルスチェックをまとめて作成します。
また、このヘルスチェックは画像に対するヘルスチェックということもありデータサイズが大きいので、デフォルトの30秒毎に8リージョン毎に2つのヘルスチェッカーから呼び出す (=毎分32リクエスト) のヘルスチェックでは転送量も馬鹿にならないので、最小の3リージョンに減らしています。

タグをつけておくと、Datadog連携で、例えばモニターでドメイン名を表示させるということが便利です。(逆に CloudWatch メトリクスは `21bc5f55-7897-493e-a013-b8bc5807c099` という感じのヘルスチェックIDでPutされるため扱いづらいです)

```
resource "aws_route53_health_check" "camo_child_healthchecks" {
  count             = "${ length( local.camo_child_healthchecks ) }"
  fqdn              = "camo.qiitausercontent.com"
  port              = 443
  type              = "HTTPS"
  resource_path     = "${lookup(local.camo_child_healthchecks[count.index], "resource_path")}"
  failure_threshold = "2"
  request_interval  = "30"
  measure_latency   = true
  regions           = ["ap-northeast-1", "ap-southeast-1", "us-west-2"]

  tags = {
    Name    = "via Camo to ${lookup(local.camo_child_healthchecks[count.index], "origin_url")}"
    env     = "production"
    fqdn    = "camo.qiitausercontent.com"
  }
}
```

そして、calculated health check をつくります。
ここでは全体のうち2個が失敗したら失敗とみなすとしています。

```
resource "aws_route53_health_check" "camo_calculated_healthcheck" {
  type                   = "CALCULATED"
  child_health_threshold = 2
  child_healthchecks     = ["${aws_route53_health_check.camo_child_healthchecks.*.id}"]

  tags = {
    Name    = "Camo calculated health check"
    env     = "production"
    fqdn    = "camo.qiitausercontent.com"
  }
}
```

#### aws_cloudwatch_metric_alarm

なにげにわかりづらいところですが、Route 53はグローバルサービスなのでリージョンを気にしなくてよいのですが、CloudWatchメトリクスは us-east-1 に書き出されるので、アラームも同じリージョンを使う必要があります。
デフォルトのリージョンと異なるのでエイリアスを定義して指定しています。

```
resource "aws_cloudwatch_metric_alarm" "qiita_camo_healthcheck" {
  provider            = "aws.us_east_1"
  alarm_name          = "route53-healthcheck-camo-qiitausercontent-com"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "HealthCheckStatus"
  namespace           = "AWS/Route53"
  period              = "60"
  statistic           = "Minimum"
  threshold           = "1"
  alarm_actions       = ["${var.aws_sns_topic_route53_healthcheck_arn}"]

  dimensions {
    HealthCheckId = "${aws_route53_health_check.camo_calculated_healthcheck.id}"
  }

  depends_on = ["aws_route53_health_check.camo_calculated_healthcheck"]
}
```

### コンソール

こんな感じで見られます。

![Route_53_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/19607/8b07d56a-ddac-299c-176b-bfd35ed89d62.png)


## まとめ

Route 53 ヘルスチェックを利用することにより、false alarmを無くし、サービスに問題があったときに速やかに検知して対応することに注力できるようになったので気に入っています。

今回は特殊なケースとなるプロキシの外形監視を説明しましたが、普通の監視を Terraform モジュール化したはなしを次に書く予定です。