👁

UptimeRobotとTerraformでおこなう死活監視

2022/10/19に公開

LAPRAS 株式会社でSREをしていますyktakaha4と申します
プロダクトの死活監視について考えたこと・やったことの備忘メモになります ✍

死活監視 / 合成監視ってなに

合成監視(Synthetic Monitoring)とは、Webシステムの監視手法のひとつで、システム外の環境からHTTPクライアント等を通じて特定のエンドポイントに対して定期的にアクセスすることで、エンドユーザーが受けている体験に近い指標値を計測するものです
プロダクトの稼働状況やパフォーマンスの評価などに活用することが一般的です

https://newrelic.com/jp/blog/how-to-relic/synthetic-versus-real-user-monitoring

なお、日本語では外形監視という訳語が当てられる場合が多いものと思いますが、原義を考えると適切でないと指摘している方もいるようです

https://takehora.hatenadiary.jp/entry/2019/07/05/012036

死活監視(Alive monitoring)は、合成監視の中でもシステムの稼働状況に焦点をあてた監視になります
Webページにアクセスした際にHTTP200ステータスが返されるかレスポンスに特定のキーワードが含まれるか といった項目を数分おきにプロダクト外のネットワークから自動チェックし、条件を満たさなかった場合にメールやSlack等に通知を送ります

死活監視だけではユーザーがプロダクトに対してどのような価値(あるいは不満)を感じているかわかりませんが、
Webサービスを提供している会社であれば、プロダクトのURLにアクセスした際にサービスページが表示されるのは事業継続の観点で大前提になっているものと思います
性質上、既存のシステムに対してエージェントを追加するといったシステム改修も不要で導入できるため、監視が不十分なレガシーシステムに対する最初の一歩としてもオススメです

https://e-words.jp/w/死活監視.html

弊社では、UptimeRobotというWebアプリケーションの死活監視に特化したSaaSを利用して死活監視をおこなっています
シンプルながらツボを抑えた機能群と、50エンドポイントまでは無料で監視をおこなえるという導入コストの低さから、副業や個人的な開発でも気に入って使っています

https://uptimerobot.com/pricing/

もっとも、これだけだと本番稼働するプロダクトの監視としては不十分なので、弊社ではDatadogSentryといった複数の監視サービスを用いて様々なレイヤで監視をおこなうようにしていますが、今回は記事の趣旨からは逸れるため割愛します

課題

前述したUptimeRobotによる監視について、従来はいくつか課題がありました

第一に、どのエンドポイントをどのような内容 / 頻度で監視しているか分かりづらい問題がありました
以下はUptimeRobotの設定画面のイメージですが、各ページの詳細画面を開かないと設定状況がわからず、設定値がエンドポイント毎にずれてしまっていても気づきづらいものと思います


UptimeRobotの編集画面

もうひとつの問題として、設定を手動でおこなっており抜け、漏れが発生しやすいというのもありました

例えば、弊社でエンジニアとして働いているとLAPRASLAPRAS SCOUTといった主要プロダクトについては頻繁にリリースをおこないますし日々稼働状況に注意を払っていますが、
自社で運用しているオウンドメディアであるHR TECH LAB や、外部SaaSにドメイン設定して運用しているLAPRASのヘルプページといったサイト、コーポレートページについては、エンジニアによる改修が発生しづらいため意識から漏れがちです
一方で、事業運営という観点においてはいずれのサイトも重要な意味を持っており、SREとしては稼働状況をチェックしておきたいところです

入門 監視でも、監視の手動設定はアンチパターンとして紹介されています
プロダクトがリリースされたら完全自動で設定される…といったことにまではならないにしても、監視設定をコード管理できると適用の半自動化や変更管理もできてよさそうです

また、現在弊社には15〜20人ほどのエンジニアが在籍しており、全てのメンバーが監視やインフラ領域を得意としているわけでもありません
そうした人でも設定がしやすいように、新しくリリースしたプロダクトにどのような監視を設定すべきか予めパターンが提示されており、
そこに対象のエンドポイントを追加する…といった運用にできるとよりよさそうに思えます

ということで、今回はUptimeRobotの監視設定をコード化するということをスコープに、どのような監視を実現すべきか考えていきました

どのような監視をすべきか

私が運用を引き継いだ当初は、いくつかの主要サービスのみまちまちに監視設定をおこなっている状況だったのですが、
この機会に改めて自社で管理している各エンドポイントを整理して考えた結果、以下のようなグルーピングをおこなうこととしました

サービス種類 監視観点 監視頻度
a.主要プロダクト 正常応答を返すこと
b.オウンドメディア
コーポレートサイト
正常応答を返すこと
c.資格情報が必要なサービス (権限無しの時)異常応答を返すこと
d.社内向けサービス (権限無しの時)応答を返さないこと

こうした分類をしたのは、調査を通じて一般ユーザーにとって見れない/繋がらないことがプロダクトの提供する機能である場合もあるという実感を得たからでした

例えば弊社の運用するプロダクトではGitHub等の外部サービスをクローリングして技術力スコアを計算する…といった機能がありますが、
それらに管理画面があったとすれば、それは特定のIPや社内向けVPNからのみアクセス可能であるべきです
仮に、AWS WAFやセキュリティグループなどでそうしたアクセス制限をおこなっていたとして、本番作業時のイレギュラーな手作業や設定ミスなどで設定が外れてしまった場合、何かしらの監視をおこなっていなければ機能を毀損してしまっていることに気づくのはかなり難しくなります

監視という言葉からは動作することのチェックという連想をしやすいですが、(特定の条件において)動作しないということに対して何かしらアプリケーション外での作り込みや設定をしているのであれば、それに対してチェックをおこなうと安心してプロダクトが運用できるものと思います

ちなみに、今回一番大変だったのが、Route53等のDNSサービスに登録されているレコードとサービスの実態調査でした
歴史的経緯から弊社のRoute53は全てのレコードをTerraform管理できていないのですが、
その結果として既に廃止されていたり用途不明になっているサービスなど、実態を把握できていないものもちらほらあることがわかり、意義のある調査だったと思います
(ココらへんの話もまとまった内容ができたら記事にしたいなと思っています…🐤)


イメージ図。実際は更にx倍あって震えた

Terraformプロバイダについて

弊社では、インフラの構成管理にTerraformを使っています
Terraformにはプロバイダという仕組みがあり、REST APIが公開されているサービスであればサードパーティにてコードを書くことでTerraformにて構成管理ができます

UptimeRobotについても、個人にて作成されたlouy/terraform-provider-uptimerobotというプロバイダがあるのですが、こちらについては直近でPRマージが滞っており、
弊社の監視を実現するのにあたって使いたいいくつかの機能が実装されていないという状況にありました

このため、今回弊社のOrganizationでForkをおこない(経緯から実際にForkしたのは上記リポジトリとはまた別のものなのですが…)、必要な機能を足したものをTerraform registryに登録しました

https://registry.terraform.io/providers/lapras-inc/uptimerobot/0.8.3

リポジトリはこちらです
よければPRお待ちしています🐔

https://github.com/lapras-inc/terraform-provider-uptimerobot

なお、今回初めてTerraform Registryにプロバイダを公開したのですが、公式ドキュメントが充実しており、チュートリアルもあったのでわかりやすかったです

https://developer.hashicorp.com/terraform/registry/providers/publishing

設定サンプル

作成したTerraform定義のイメージについても簡単に紹介します
プロバイダ定義は以下のようにおこないます

terraform.tf
terraform {
  required_providers {
    uptimerobot = {
      source = "lapras-inc/uptimerobot"
    }
  }
}

provider "uptimerobot" {
  # 環境変数に UPTIMEROBOT_API_KEY をセットしてください
}

リソースとしては、モニターが uptimerobot_monitor に対応しています
オリジナルのものと比較して、 custom_http_statuses などのプロパティに対応しています

監視対象のエンドポイント毎にResourceを作るのが大変なので、for_each を使って配列の内容からいい感じに生成するようにしています

uptimerobot_monitor.tf
resource "uptimerobot_monitor" "should_get_http" {
  # 稼働状態を高頻度(1分ごと)にチェックしたいエンドポイント(a.)

  for_each = toset(var.uptimerobot_alert_should_get_http)

  friendly_name     = "${split("/", each.value)[2]} (should_get_http)"
  type              = "http"
  http_method       = "GET"
  url               = each.value
  interval          = 60
  ignore_ssl_errors = false

  alert_contact {
    id = var.uptimerobot_alert_contact_id_should
  }
}

resource "uptimerobot_monitor" "should_connect_port" {
  # 特定のポートを公開しているエンドポイント(a.)
  for_each = toset(var.uptimerobot_alert_should_connect_port)

  friendly_name     = "${split(":", each.value)[0]} (should_connect_port)"
  type              = "port"
  sub_type          = "custom"
  url               = split(":", each.value)[0]
  port              = split(":", each.value)[1]
  interval          = 60
  ignore_ssl_errors = false

  alert_contact {
    id = var.uptimerobot_alert_contact_id_should
  }
}

resource "uptimerobot_monitor" "should_get_http_low_interval" {
  # 稼働状態を低頻度(5分ごと)にチェックしたいエンドポイント(b.)

  for_each = toset(var.uptimerobot_alert_should_get_http_low_interval)

  friendly_name     = "${split("/", each.value)[2]} (should_get_http_low_interval)"
  type              = "http"
  http_method       = "GET"
  url               = each.value
  interval          = 300
  ignore_ssl_errors = false

  alert_contact {
    id = var.uptimerobot_alert_contact_id_should
  }
}

resource "uptimerobot_monitor" "should_missing_key_cloudfront" {
  # CloudFront+S3の署名付きURLが認証情報なし時にエラーとなる(c.)
  for_each = toset(var.uptimerobot_alert_should_missing_key_cloudfront)

  friendly_name     = "${split("/", each.value)[2]} (should_missing_key_cloudfront)"
  type              = "keyword"
  keyword_type      = "not exists"
  keyword_value     = "<Error><Code>MissingKey</Code>"
  http_method       = "GET"
  url               = each.value
  interval          = 60
  ignore_ssl_errors = false

  alert_contact {
    id = var.uptimerobot_alert_contact_id_should
  }
}

resource "uptimerobot_monitor" "should_forbidden_http" {
  # 適切なアクセス権がない場合に403を返すエンドポイント(c.)
  for_each = toset(var.uptimerobot_alert_should_forbidden_http)

  friendly_name = "${split("/", each.value)[2]} (should_forbidden_http)"
  type          = "http"
  http_method   = "GET"
  url           = each.value
  interval          = 300
  ignore_ssl_errors = false

  custom_http_statuses {
    up = ["403"]
    down = [
      "200", "201", "202", "203", "204", "205", "206", "207", "208", "226",
      "300", "301", "302", "303", "304", "305", "306", "307", "308",
    ]
  }

  alert_contact {
    id = var.uptimerobot_alert_contact_id_should
  }
}

resource "uptimerobot_monitor" "should_not_connect" {
  # 社内システムや開発環境など、特定ネットワーク以外からは接続自体を拒否するエンドポイント(d.)
  for_each = toset(var.uptimerobot_alert_should_not_connect)

  friendly_name     = "[SHOULD DOWN] ${split(":", each.value)[0]} (should_not_connect)"
  type              = "port"
  sub_type          = "custom"
  url               = split(":", each.value)[0]
  port              = split(":", each.value)[1]
  interval          = 60
  ignore_ssl_errors = false

  alert_contact {
    id = var.uptimerobot_alert_contact_id_should_not
  }
}

Resourceの生成元になる変数は以下になります

variable.tf
variable "uptimerobot_alert_contact_id_should" {
  # 接続できることを監視しているアラート
  # API仕様によりTerraformからは全てのプロパティが設定できないため、Alert Contactは手動で作成したものを紐付ける
  # IDはAPIの getAlertContacts から確認できる
  # https://uptimerobot.com/api/
  default = 1234567
}

variable "uptimerobot_alert_contact_id_should_not" {
  # 接続できないことを監視しているアラート
  default = 7654321
}

variable "uptimerobot_alert_should_get_http" {
  # あなたの会社の主要プロダクト
  default = [
    "https://your-main-product.com",
  ]
}

variable "uptimerobot_alert_should_get_http_low_interval" {
  # あなたの会社で管理しているドメイン名に紐づくブログなど
  default = [
    "https://your-owned-media.com",
  ]
}

variable "uptimerobot_alert_should_missing_key_cloudfront" {
  default = [
    # 署名付きURLによるアクセス保護がされているドメイン
    # CloudFrontのルートに `health.html` などの漏洩しても問題ないファイルを適当においておく
    "https://assets.your-main-product.com/health.html",
  ]
}

variable "uptimerobot_alert_should_forbidden_http" {
  default = [
    # アクセス制御がされているURL
    "https://your-owned-media.com/administrator/",
  ]
}

variable "uptimerobot_alert_should_connect_port" {
  default = [
    # ポートに対する接続性を監視するドメイン
    "your-product-service.com:22222",
  ]
}

variable "uptimerobot_alert_should_not_connect" {
  default = [
    # 社内向けに提供しておりIP制限をかけているエンドポイント
    "your-internal-site.com:443",
  ]
}

ポイントとしては、dのタイプの監視については本来Connection timeoutになることを定期チェックしたいのですが、Uptimerobotには現状そのような設定方法がないため、Downしていることを通常の状態と判断するようメッセージでわかるようにしています


常にエラーになるエンドポイント

また、コメント中でも記載していますが、アラート設定についてもuptimerobot_alert_contact を使うとTerraformにて定義できますが、 Custom Value という値がAPIから指定できないため、手動作成したものに紐付けています

https://uptimerobot.com/api/


アラート設定

本定義を設定の上、監視対象のサービスの稼働状態が変わると以下のようにエラーメッセージがSlack通知されます


Slack通知の様子

おわりに

今回やって気づいたこととして、実際の監視と同じくらい、 社内にどのようなエンドポイントが存在し、それをどのような重要度で監視をおこなうべきと考えているか という観点がコードに残るということが大きなメリットであると気づきました

チームの解散や異動、退職などでプロダクトに関わる人が変わった時に、保守すべき対象が引き継ぎ不十分で行方不明になってしまう…的なことはよくある話かと思いますが、
エンドポイントがあるサービスはユーザーに対する価値提供に直結している場合がとても多いと思いますので、まず外形監視の定義を見て、そこからプロダクトの詳細を探っていく…というようなやり方が定着すると、属人性の低い運用ができるように思いました


実際のソースコード

引き続き、なるべく人に仕事がつかないプロダクト運用について考えていきたいと思います🦃

Discussion