ようへいの日々精進XP

よかろうもん

【細かすぎて伝わらないかもしれない tips】時代はイミュータブルインフラストラクチャだけど, 敢えて monit について書いてみる

tl;dr

お仕事にて, 指定したプロセスが停止したら (それだけではないですが), 自動的にそのプロセスを起動してくれる monit というツールを使いました.

mmonit.com

monit の詳細については, インターネット上の記事がたくさんありますので, そちらをご一読ください.

monit は 2000 年前半にリリースされたいにしへのツールで, 現在でもメンテナンスが続けれています. monit が出来ることは以下の通りで, その気になれば, monit だけでサーバーの運用が出来てしまうと思います.

  • プロセス, ファイル, サービスの監視
  • 監視対象に異常を検知したら, 対象のプロセスの再起動, 指定したメールアドレスに通知, 指定したコマンド実行

サーバーリソースは使い捨てが望ましいと言われている時代, サーバーはコンテナ化され, そのコンテナのオーケストレーションも自動化されていく中で, 1 台のサーバーの中で個々のサービスプロセス自体の死活を監視し, 再起動を促すようなツールの存在意義が薄れてしまいそうな状況にありながら, monit は現在もバージョンアップを重ねていることは素晴らしいことだと思います.

ということで, 今回, 格闘した環境は以下の通りです.

$ cat /etc/system-release
Amazon Linux AMI release 2018.03

$ monit -V
This is Monit version 5.2.5
Copyright (C) 2000-2011 Tildeslash Ltd. All Rights Reserved.

先述の通り, monit については, インターネット上に多くの記事がありますので, そちらの記事の方が monit の出来ることや良さ, 欠点等も網羅されているかと思います. あくまでも, 本記事は自分が monit でやりたかったことをどのように実現したか, 簡単なデモを交えて書いていきます.

俺はこうした

プロセスが停止したら, 再起動して欲しい

シンプルな例です.

Apache のプロセスが停止したら, Apache のプロセスを再起動して欲しい場合, monit の設定は以下のように書きます. 尚, 設定は /etc/monit.conf に書いても良いですし, /etc/monit.d/ 以下に任意のファイル名で作成しても構いません.

check process httpd with pidfile "/var/run/httpd/httpd.pid"
  start program = "/sbin/service httpd start"
  stop  program = "/sbin/service httpd stop"

/var/log/monit には以下のようなログが記録され, httpd プロセスは自動的に起動されています.

[UTC Mar 20 23:43:20] error    : 'httpd' process is not running
[UTC Mar 20 23:43:20] info     : 'httpd' trying to restart
[UTC Mar 20 23:43:20] info     : 'httpd' start: /sbin/service
[UTC Mar 20 23:44:21] info     : 'httpd' process is running with pid 2686

尚, monit はデフォルトは 60 秒間隔で監視対象をチェックしています.

プロセスが停止したら, 通知してから再起動して欲しい

人間というのは, 色々と欲が出てくるもので, 再起動してくれるんなら, 一報入れてから再起動してよって思ってしまいます. そんな時には以下のように設定を書きました.

check process httpd with pidfile "/var/run/httpd/httpd.pid"
  start program = "/sbin/service httpd start"
  stop  program = "/sbin/service httpd stop"
  if does not exist
    then exec "/bin/bash -c '/path/to/bin/slak -env /path/to/bin/.env && /sbin/service httpd restart'"
    else if succeeded then exec "/path/to/bin/slak -env /path/to/bin/.env"

上記の例は, exec コマンドを使って, Slack に通知する為の別のスクリプトを叩いている例となります. キモというか, 苦肉の策となるのが, 以下の部分です.

  if does not exist
    then exec "/bin/bash -c '/path/to/bin/slak -env /path/to/bin/.env && /sbin/service httpd restart'"

if does not exist は, 監視対象のプロセスが存在しない場合という条件で, その条件が真となる場合に then exec 以降が実行されることになりますが, 複数のコマンドを実行する場合には /bin/bash -c... で書きはじめて && でコマンドを連結するみたいな書き方をする必要がありました.

httpd プロセスが停止すると, Slack の指定したチャンネルに以下のように通知されます.

f:id:inokara:20190321092926p:plain

/var/log/monit には以下のようなログが記録されます.

[UTC Mar 21 00:02:27] error    : 'httpd' process is not running
[UTC Mar 21 00:02:27] info     : 'httpd' exec: /bin/bash
[UTC Mar 21 00:03:27] info     : 'httpd' process is running with pid 3153
[UTC Mar 21 00:03:27] info     : 'httpd' exec: /path/to/bin/slak

プロセスの再起動が完了したら通知して欲しい

せっかくなので, monit によって再起動が完了したら, 通知してもらいましょう. 既に, 設定は掲出していますが, 改めて, 以下に記載します.

check process httpd with pidfile "/var/run/httpd/httpd.pid"
  start program = "/sbin/service httpd start"
  stop  program = "/sbin/service httpd stop"
  if does not exist
    then exec "/bin/bash -c '/path/to/bin/slak -env /path/to/bin/.env && /sbin/service httpd restart'"
    else if succeeded then exec "/path/to/bin/slak -env /path/to/bin/.env"

ポイントは, else if succeeded then exec "/path/to/bin/slak -env /path/to/bin/.env" となります. これは, monit によるチェックが成功したら exec 以降の処理を実行することを意味しています.

全体的な処理の流れとしては, httpd プロセスが停止したら, Slack に通知してから httpd プロセスの再起動を行い, 60 秒後 (厳密には 60 秒よりも短い) の次のチェックでプロセスチェックに成功 (プロセスが存在) していたら Slack に通知という感じです.

PID ファイルではなく, プロセス名で監視したい

with pidfile ではなく, matching を利用します.

check process httpd matching "httpd"
  start program = "/sbin/service httpd start"
  stop  program = "/sbin/service httpd stop"

matching の後のプロセス名がちゃんとチェックされるかどうかは, 以下のコマンドを利用してチェックすることが出来ます.

$ sudo monit procmatch httpd

以下のように出力されます.

List of processes matching pattern "httpd":
------------------------------------------
        /usr/sbin/httpd
        /usr/sbin/httpd
        /usr/sbin/httpd
        /usr/sbin/httpd
        /usr/sbin/httpd
        /usr/sbin/httpd
        /usr/sbin/httpd
        /usr/sbin/httpd
        /usr/sbin/httpd
------------------------------------------
Total matches: 9
WARNING: multiple processes matched the pattern. The check is FIRST-MATCH based, please refine the pattern

どんな風にモニタリングされているのか知りたい

monit -vI を実行します.

$ sudo monit -vI

以下のように出力されます.

Process Name          = httpd
 Pid file             = /var/run/httpd/httpd.pid
 Monitoring mode      = active
 Start program        = '/sbin/service httpd start' timeout 30 second(s)
 Stop program         = '/sbin/service httpd stop' timeout 30 second(s)
 Existence            = if does not exist 1 times within 1 cycle(s) then exec '/bin/bash -c /path/to/bin/slak -env /path/to/bin/.env && /sbin/service httpd restart' timeout 0 cycle(s) else if succeeded 1 times within 1 cycle(s) then exec '/path/to/bin/slak -env /path/to/bin/.env' timeout 0 cycle(s)
 Pid                  = if changed 1 times within 1 cycle(s) then alert
 Ppid                 = if changed 1 times within 1 cycle(s) then alert

System Name           = system_ip-xx-x-x-xxx.ap-northeast-1.compute.internal
 Monitoring mode      = active

以上

今回, monit のほんの一部の機能を使ってみました. 通知の部分については, ちょっと格闘しちゃいましたが, プロセスが停止したら再起動させるというシンプルな使い方であれば, とても簡単に使うことが出来ました.

ということで, 素敵な monit ライフをお過ごしください.

おまけ

monit から Slack に通知するコマンドは初心者レベルの Go 言語で作ってみました. クソコードでお恥ずかしい限りですが, おまけとして掲載させて頂きます.

package main

import (
    "bytes"
    "flag"
    "fmt"
    "github.com/joho/godotenv"
    "net/http"
    "os"
    "strings"
)

const (
    AppVersion = "0.0.1"
)

var (
    argEnv     = flag.String("env", "", ".env ファイルのパスを指定.")
    argVersion = flag.Bool("version", false, "バージョンを出力.")
)

func loadEnv(envPath string) {
    var err error
    if envPath != "" {
        err = godotenv.Load(envPath)
    } else {
        err = godotenv.Load()
    }

    if err != nil {
        fmt.Println("Error loading .env file")
        os.Exit(1)
    }
}

func main() {
    flag.Parse()

    if *argVersion {
        fmt.Println(AppVersion)
        os.Exit(0)
    }

    loadEnv(*argEnv)
    username := os.Getenv("SLACK_USER_NAME")
    channel := os.Getenv("SLACK_CHANNEL")
    icon_emoji := os.Getenv("SLACK_ICON")

    descStr := `{"title":"description","value":"` + os.Getenv("MONIT_DESCRIPTION") + `"}`
    var text string
    var attachmentStr string
    if strings.Contains(os.Getenv("MONIT_DESCRIPTION"), "not running") {
        text = os.Getenv("MONIT_HOST") + " にて " + os.Getenv("MONIT_SERVICE") + " プロセスが停止しました."
        attachmentStr = `{"color":"#ff0000","fields":[` + descStr + `]}`
    } else {
        text = os.Getenv("MONIT_HOST") + " にて " + os.Getenv("MONIT_SERVICE") + " プロセスが再起動しました."
        attachmentStr = `{"color":"#008000","fields":[` + descStr + `]}`
    }
    jsonStr := `{"channel":"` + channel + `","username":"` + username + `","text":"` + text + `","icon_emoji":"` + icon_emoji + `","attachments":[` + attachmentStr + `]}`

    req, err := http.NewRequest(
        "POST",
        os.Getenv("SLACK_URL"),
        bytes.NewBuffer([]byte(jsonStr)),
    )
    if err != nil {
        fmt.Print(err)
    }
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Print(err)
    }

    // fmt.Print(resp)
    defer resp.Body.Close()
}

monit は監視対象に異常を検知した場合, 以下のような環境変数に値をセットします.

  • MONIT_HOST
  • MONIT_SERVICE
  • MONIT_DESCRIPTION

これらを利用して, プロセスが停止した場合, 再起動した場合の処理分岐を行っています. また, Slack の Incomming Webhook の URL 等の情報は .env ファイルに書いて, 別で管理することを想定しています. コマンドは以下のように実行します.

/path/to/bin/command -env /path/to/env