Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

AWS ECS & TerraformによるSansanの統合監視運用とその仕組み

はじめに

Sansan株式会社プロダクト開発部インフラチームの岡本です。

事業欲求に応じ優先度と軽重が決められたタスクに向き合いつつ、チームへの依頼事項に対し日々柔軟に対応するよう努めています。
また、Sansanサービス全般のインフラ運用・保守を行いつつも、併せて運用業務の撲滅に取り組んでいます。

今回は、Sansanサービスにおける監視ツールの導入経緯からインフラ構成、監視の設計方針、リリース方法、本構成におけるツラミ等をお伝えできればと思います。

IcingaとMunin

Sansanではかつて、IcingaとMuninを利用していました。

Icingaはnagiosをフォークして作られた、死活監視・サービス監視ツールです。
Bash / Pythonスクリプトの自前プラグインを多数稼働させていました。
エージェントレスで監視スクリプトを柔軟に設定・実行出来る強みがありますが、メトリクスのビジュアライズ (グラフ描画等) には特化していません。

Muninはリソース監視ツールとして利用していました。
デフォルトで多くの監視プラグインが存在し*1、カスタマイズも可能です。
リソース関連のビジュアライズに特化していますが、柔軟な監視設定や通知は苦手です。

Munin masterのCrond (Perl) がmunin-nodeのデータ取得・閾値チェック・グラフ描画生成を逐一実行しますが、デフォルトだとホストのリソースをそれなりに消費する為、チューニングが必要です。

Zabbixへの移行

元々はこれらメトリクスのビジュアライズ、柔軟な監視設定・通知を統合し、一元管理したい思いがありました *2
当時、色々な監視ツールやSaaS *3 が候補として挙がりましたが、時系列データが時間の経過に連れて極度に長い間隔に丸められてしまったり、ログ監視が苦手だったり、データ保持期間が短い等の理由から、Zabbixが最有力候補となりました。

先人の知恵が詰まったZabbixの強みとしては

  • APIが充実している。
  • 監視のカスタマイズ性が高い。
  • 監査が行える。
  • データ取得間隔を柔軟に設定可能。
  • 長期間保持してもメトリクスデータが丸まらないように設定することが可能。
  • Grafanaのデータソースとしても連携可能。

等、多くの優れた点が挙げられると思います。

一方で、Zabbix自体の仕様が複雑且つ多機能なため、属人化しやすかったり、監視アイテムやホストの増加に併せてZabbixパラメータやDB、ホストのチューニングを行う必要があり、運用負荷が比較的高い傾向にあると思います。
そのような後々負債になり得る側面に対しては、コンテナを利用したり、Zabbix TemplateやExternal Scripts等を全てコード管理することにより、軽減出来ると見込んでいました。

そして上記選定、設計、構築を経て、Zabbixコンテナクラスタ on AWS ECS へ2018年に完全移行しました *4

環境構築

VPC/LB/ECS/ECR/Route53等、TerraformでAWSリソースは管理しています *5
監視対象である各種サーバー群へのZabbix Agentインストールは、Chef経由で行っています。

f:id:hrt0kmt:20190914135642p:plain

特徴としては、以下の点でしょうか。

  • ECSタスク定義のイメージタグを更新後、"$ terraform apply" のみで、タスクのローリングアップデートが可能。
  • Zabbixパッシブ用経路としてALBを動的ポートマッピングし、アクティブチェック用経路としてNLBを利用。
  • SlackからLambda経由で特定のホストグループをメンテナンス状態にすることでアラートを抑止し、アプリケーションのリリースを行っている。
  • Zabbix WebサーバーコンテナからGrafanaコンテナへリバースプロキシを行い、プラグイン経由で各種メトリクスをビジュアライズしている。
  • LDAP認証でZabbix / Grafanaへログインしている。
  • fluentdをログドライバーとして、Kibanaで各種コンテナログを閲覧出来るようにしている。

f:id:hrt0kmt:20190909231557p:plain
ZabbixをデータソースにしてGrafanaで各種メトリクスをビジュアライズしています。

インフラの運用チェックではGrafanaのダッシュボードを活用し、各ホスト毎のリソース状況や、PostgreSQLの各種メトリクス *6 に異常が無いかをチェックしています。

Zabbixの監視内容

Zabbixにおける監視数は以下の通りです。

  • 監視ホスト数(有効): 約100
  • 監視アイテム数 (有効): 約10000
  • 監視トリガー数 (有効): 約4000
  • 1秒あたりの監視項目数: 約160

各トリガーの障害レベルに応じて、複数のメール宛先+Slackへ通知が行われるようになっています。

監視アイテムを設定する上では、次のような監視の仕様を定義することで、当該監視がどのカテゴリーに属し、どの程度のPriorityでどの関係者へ通知すべきなのかを定め、それに沿うようにしています。

f:id:hrt0kmt:20190909155844p:plain

監視のリリース方法

各種APIやクエリ監視のスクリプトは、ZabbixのExternal Scripts *7 で管理されています。External Scriptsは、Data volumeコンテナからZabbix Serverコンテナへマウントされています。

f:id:hrt0kmt:20190915140244p:plain

監視スクリプトの追加・更新は、大まかに以下のような流れで行われています。

  1. External Scriptsが配置されているリポジトリにて、ブランチを切って監視スクリプトを変更。
  2. 当該ブランチを引数で指定してData volumeイメージをビルド (git clone & checkoutして特定パスへ配置)。
  3. イメージをAmazon ECRへプッシュ。
  4. Terraformリポジトリにて、タスク定義リソースのイメージタグを更新し "$ terraform apply" を実行。
  5. ECSタスクがローリングアップデートされてリリース完了。

もしリリース後に何かしら異常が発生し、ロールバックを行う必要がある場合、Terraformのタスク定義リソースで更新前のイメージタグを指定し、再度applyします。

本構成のリリースで気を付けたいのは、リソース配分とバージョンの固定化です。

リソース配分

タスクデプロイ時は数分間コンテナの数が倍になる為、CPU unitやメモリ等のパラメータはホストリソースの半分以下に定義しておく必要があります。

バージョンの固定化

イメージ、ミドルウェア、プラグイン等、各々のバージョンは全て固定し、バージョンアップは適切なタイミングで行うことが望まれますが、例えばGrafanaのプラグインはDockerのリポジトリで管理せずTerraform側で管理しており、次のようにバージョンをスペース区切りで指定する必要があります。過去にECSタスクをデプロイする際に、当該プラグインのバージョンを固定しておらず、Grafanaイメージ側との互換性が無くなったことがありました。

  {
    "name": "grafana",
    "environment": [
      {
        "name": "GF_INSTALL_PLUGINS",
        "value": "grafana-simple-json-datasource 1.4.0,alexanderzobnin-zabbix-app 3.10.2,grafana-piechart-panel 1.3.9"
      },

本監視システムにおけるツラミ

コンテナになったからといって、ZabbixのQueueが忙しない時はZabbixやDBのパラメータ値をチューニングしたり、アラート発報のタイミングを調整する為にトリガーやアクションを1から整理し直したり、タスクが意図せず停止した場合はECSログやコンテナログ、アプリケーションログから原因調査、対応を行う必要があったりと、運用負荷と感じる部分は少なからず存在します。

Zabbixの独自仕様に消耗する

Zabbix独自の用語や独特な画面設計、仕様をチーム全員がインプットし、運用に乗せるのはそれなりに時間が掛かります。
インフラをTerraformで管理し、監視スクリプトやテンプレートは全てコードに落としていても、何かしら監視動作に意図しない挙動があった場合の調査や切り分け、チューニングは、Zabbixの一通りの仕様や仕組みが分かっていないと難しいと感じます。一方でAWS ECSに関しては、タスクやサービス等独自概念はあれど、比較的学習コストは低めかと思います。

また、Zabbixは監視設定の自由度が比較的高いため、運用としてベストな方法を選択する必要があります。例えば、Windows OSのサービスディスカバリ(LLD)で、自動生成されたトリガーの一部を無効化したい場合は

  • 手動で無効化する
  • ディスカバリルールで除外する
  • アクションで当該のトリガー名を除外する

等、様々なやり方が存在します。Zabbixの多数ある機能で、何をどのように利用すればやりたいことが実現出来るのかを調査し、その中から最適解を導き出す必要があります。

テンプレートやモジュールに関しても、事前に互換性やAgent側の負荷を調査する必要があります。PostgreSQL関連はlibzbxpgsql *8 というネイティブエージェントモジュールを利用して一部監視していますが、アイテムを全て有効化するとPostgreSQLのバックエンドコネクションを食いつぶす可能性がある為、必要なアイテムのみ有効化して後は削除しています。

Zabbixの仕様にインフラ構成を追従している

ALBは動的ポートマッピングを利用してZabbix Serverコンテナをマッピングしていますが、Zabbixのアクティブチェック疎通用にNLBも利用しています。ECSが1つのコンテナに対し1つのLBしかマッピング出来ない為、かなり泥臭いですが以下のようなbashスクリプトをEC2のuser_dataにてcronで定期実行するようにして、NLBのターゲットグループへアタッチしています。

#!/bin/bash
set -Ceu

AWS="/usr/bin/aws --region ap-northeast-1"
declare -r CONTAINER_NAME="zabbix-server"
declare -r INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)

function set_container_properties() {
    readonly CONTAINER_ID=$(docker ps -qf "name=zabbix-server")
    readonly CONTAINER_PORT=$(docker inspect \
        --format='{{(index (index .NetworkSettings.Ports "10051/tcp") 0).HostPort}}' ${CONTAINER_ID})
}

function set_tg_arn() {
    readonly TG_ARN=$($AWS elbv2 describe-target-groups \
        --names nlb-zabbix \
        --query "TargetGroups[].TargetGroupArn" \
        --output text)
}

# return port list of registered targets which status is healthy or unhealthy
# arg:
#   $1 healthy / unhealthy
function get_port_list() {
    check_state=$1
    local res=$(${AWS} elbv2 describe-target-health \
        --target-group-arn ${TG_ARN} \
        --query "TargetHealthDescriptions[?TargetHealth.State==\`${check_state}\`].Target.Port" \
        --output text)

    if [ -n "${res}" ]; then
        echo ${res}
    else
        return 1
    fi
}

# register or deregister targets to NLB
# arg:
#   $1 register / deregister
#   $2 healthy  / unhealthy
function elbv2_target_group_operation() {
    local operation=$1
    local check_state=$2

    local port_list=$(get_port_list $check_state)

    case ${operation} in
        register)
            if [ -z ${port_list} ]; then
                ${AWS} elbv2 register-targets \
                    --target-group-arn ${TG_ARN} \
                    --targets Id=${INSTANCE_ID},Port=${CONTAINER_PORT}
            fi
            ;;
        garbage_collect)
            if ! ${port_list} ; then
                for p in ${port_list}
                do
                    ${AWS} elbv2 deregister-targets \
                        --target-group-arn ${TG_ARN} \
                        --targets Id=${INSTANCE_ID},Port=${p}
                done
            fi
            ;;
        *)
            #error
            exit 1
    esac
}

リリース手順の複雑化

監視のリリース方法 でご紹介した図の通り、監視スクリプトを格納しているリポジトリ、Dockerイメージをビルドする為のリポジトリ、Terraformのリポジトリといったようにリポジトリが複数あります。そのため監視スクリプトの一部を変更するだけでも、各リポジトリで各環境における対応、動作確認とマージリクエストが必要となります。

複数人が監視スクリプトの一部を並行して変更し、各環境・各リポジトリのマージリクエストがそれぞれで進んでいた場合、取り込み漏れやコンフリクトが発生しやすくなります。当たり前ですが、ブランチの運用フローをしっかりと定義して周知・管理することや、CIを回すことでこれらを防ぐ必要があります。

また、例えば暫定対応で特定のアイテムのみ手動で無効化すると、後日、無効化したのはどのホストのアイテムなのか、トリガーなのか、Web監視なのかLLDなのか、そもそも無効化したことを失念しがちです。リリース手順が複雑だからと言って手動での対応は行わず、Zabbixは基本的にインポート以外、参照専用にするのが望ましいです。

サービスの成長に合わせたサイジングやチューニング

弊社でZabbixを運用している限りだと、アイテムの削除処理を担うHousekeeperや、Passive型Zabbix Agentデータを取得するPoller Processの負荷が高まりがちです。通常時だけでなく、多量のホスト・アイテムが一度に障害状態となった場合の各種プロセスの稼働負荷を概算し、コンテナ、ホストの各種リソース配分を行う必要があります。

また、上でも述べましたがECSタスクのデプロイ時にローリングアップデートが行われる為、一時的に数秒〜数分間コンテナ数が倍になります。タスク定義にてCPUユニット / メモリのソフト / ハードなリミット値は、コンテナインスタンスのリソースを考慮して決定しなければなりません *9

おわりに

Zabbixで出来ることを把握し、適切にサイジング・チューニングを行った上で運用フローが確立していれば、本構成は比較的シンプルであり得られるメリットは大きいと思っています。ZabbixコンテナとECS、Auroraを運用して1年以上経過しますが、この構成で長期間安定稼働しています。

コンテナオーケストレーションシステムとしてはKubernetesが主流となっている流れを感じる一方で、今年参加した de:code (decode) 2019 | 開発者をはじめとする IT に携わるすべてのエンジニアのためのイベント の複数セッションにて次のような点を拝聴し、まだ発展途上と感じる部分は少なくありません (AKSの事例ですが)。

  • AKSのようなマネージドサービスにより運用が楽になる反面、通常運用まで持っていくのに相当な知識が必要。
  • Kubernetesのバージョンアップサイクルが早く、常に追いついていく必要がある。
  • サービスプリンシパルが期限切れでPodがapply出来なくなったり、VMのバグによるノード停止があったりと想定以上に障害が発生した。

ECSはコンテナデプロイ・クラスタリングサービスという認識でありKubernetesとはスコープが異なりますが、Kubernetesを選択する理由が特になければECSから始めることで、高くない学習コストと共に比較的短期間でDocker化を実現出来るのではないでしょうか。

今後はDatadog等のSaaSも活用していくことにより、運用負荷を更に減らしつつも、必要となった監視リソースをAPI経由で必要な分だけ独自にカスタマイズ出来る、より柔軟な監視システムの仕組みを作っていければと考えます。併せて、TerraformだけでなくPulumiを利用したり、OpsGenieやPagerDuryによるアラートの集約、一元管理も行っていきたいところです。

以上となります。
最後までお読みいただきありがとうございました。

*1:https://github.com/munin-monitoring/munin/tree/master/plugins/node.d.linux

*2:CloudWatchやNew Relic等、他監視ツールも併用しています。

*3:Mackerel, Datadog, Prometheus, Sensu等

*4:TokyoリージョンでAWS Fargateが利用出来るようになった 2018/07 以前に移行し、まだEC2コンテナインスタンスタイプです。

*5:一部、Zabbix ActionやGrafanaのPlugin設定、Grafana用DB、ユーザーの作成等、コード管理出来ていない部分に関しては手で設定しています。

*6:http://cavaliercoder.com/libzbxpgsql/documentation/reference/server/

*7:https://www.zabbix.com/documentation/4.2/manual/config/items/itemtypes/external

*8:https://github.com/cavaliercoder/libzbxpgsql

*9:Fargateの場合は、コンテナのスケールに併せたコンテナインスタンスのスケール設計は不要ですね。

© Sansan, Inc.