はじめまして。技術開発室の與島( @shiimaxx )です。
先日、mackerelio/go-check-pluginsにSMTP接続の正常性を監視するプラグインcheck-smtpを追加するPull Requestを送り、Mergeしていただきました。 そこで、本記事ではcheck-smtpの概要、新規プラグイン開発をするにあたってどのような点を意識したか、について紹介します。
なお、チェックプラグインの作成方法自体については説明しません。作成方法については、以下のヘルプが参考になりますのでご参照ください。
また、プラグインの開発時のGoのバージョンは1.10です。
背景
ハートビーツでは、NagiosとNagios Pluginsを使って監視をしています(その他、監視の要件によっては自作Nagiosプラグインも使っています)。 Nagios PluginsはNagios公式のプラグイン集であり、標準的な監視項目に必要なひととおりのプラグインが含まれています。
弊社でも大変重宝していますが、YumやAPTなどのパッケージマネージャー経由でインストールするときにパッケージ依存によって意図しないアップデートやインストールが発生することがあり、ときどき問題になっていました。 そんなこともあり、最近はOSやシステムの依存関係から切り離してインストールできるGo製のgo-check-pluginsを利用する方向にシフトしてます。現在は、ハートビーツにおけるユースケースを満たすために機能追加をして、都度Pull Requestで送らさせていただている段階です。check-smtpの追加もその取り組みの一環として実施しました。
check-smtp
監視対象のSMTPサーバを指定して、動作の正常性とレスポンスタイムを監視できます。 接続方法は、SMTP(デフォルト)、SMTP over TLS、STARTTLSが選択できます。また、現状はPLAIN認証のみですが、SMTP認証にも対応しています。
# SMTP check-smtp -H smtp.example.com -p 25 -w 3 -c 5 -t 10 # SMTP over TLS check-smtp -H smtp.example.com -p 465 -s -w 3 -c 5 -t 10 # STARTTLS check-smtp -H smtp.example.com -p 587 -S -w 3 -c 5 -t 10 # SMTP AUTH check-smtp -H smtp.example.com -p 587 -A PLAIN -S -U username -P password -w 3 -c 5 -t 10
GitHubのソースコードは mackerelio/go-check-plugins - check-smtp です。
check-smtpのチェック方法
ここからは、実装方法やコードの中身について少しお話したいと思います。
まず、SMTP接続の正常性のチェック方法は、Nagios Pluginsに含まれているcheck_smtpの実装を参考にしました。check_smtpでは、次の流れでSMTP接続のチェックをしています。
- TCPコネクションを確立
- HELOコマンド(オプションでEHLO, LHLOも選択可能)
- STARTTLSコマンド(オプション)
- AUTHコマンド(オプション)
- MAILコマンド
- QUITコマンド
今回作成したcheck-smtpでも、大体同じようにチェックをするようにしました。 QUITコマンドの前にRSETコマンドを実行しているのは、社内レビューの際にもらった「MAIL FROMから始まったトランザクションを中断してからセッションを終了するほうが好ましい」というアドバイスを取り入れたためです。
- TCPコネクションを確立
- HELOコマンド
- STARTTLSコマンド(オプション)
- AUTHコマンド(オプション)
- MAILコマンド
- RSETコマンド
- QUITコマンド
テストコード
閾値とタイムアウトのオプションの挙動をテストしています。
warning
,critical
,timeout
オプションで指定した値が意図どおりに反映されているかwarning
,critical
オプションがどちらも指定されていない場合に意図したステータスが返るか
1のテストケースの結果はチェック対象のSMTPサーバに依存するため、SMTPプロトコルに沿ってレスポンスを返すだけのモックサーバを作りました。この方法は、標準パッケージである"net/smtp"のテストコードを一部参考にしています。
type mockSMTPServer struct { listener net.Listener delay int responses []string } func (m *mockSMTPServer) runServe() error { doneCh := make(chan struct{}) errCh := make(chan error) go func() { time.Sleep(time.Duration(m.delay) * time.Second) conn, err := m.listener.Accept() if err != nil { errCh <- err return } tc := textproto.NewConn(conn) for _, res := range m.responses { if err := tc.PrintfLine(res); err != nil { errCh <- err return } } doneCh <- struct{}{} }() select { case <-doneCh: return nil case err := <-errCh: return err } }
mockSMTPServer
はresponses
フィールドに設定された[]string
型の要素を順番にレスポンスとして返します。
response
フィールドはテストケースごとに設定できますが、今回のケースでは以下のresponseDefault
のみで十分でした。
STARTTLSやSMTP AUTHのテストケースを追加する場合は、それに対応するSMTPサーバ側のレスポンスを要素にした、新たな[]string
型の変数を定義する必要があります。
var responseDefault = []string{ "220 mail.example.com ESMTP unknown", "250 mail.example.com", "250 OK", "250 OK", "221 Bye", }
オプション用のstructを定義するときの注意点
go-check-pluginsでは、コマンドライン引数のパースにjessevdk/go-flagsを使います。このライブラリでは、READMEのExampleにあるとおり、オプションをstruct
として定義します。今回のケースでは定義の仕方に問題があり、一部のテストケースが意図した結果にならないということがありました。
最初は、具体的な処理を書いているrun()
の外で次のように変数として定義していました。
var opts struct { Host string `short:"H" long:"host" default:"localhost" description:"Hostname"` Port string `short:"p" long:"port" default:"25" description:"Port"` FQDN string `short:"F" long:"fqdn" description:"FQDN used for HELO"` SMTPS bool `short:"s" long:"smtps" description:"Use SMTP over TLS"` StartTLS bool `short:"S" long:"starttls" description:"Use STARTTLS"` Auth string `short:"A" long:"authmech" description:"SMTP AUTH Authentication Mechanisms (only PLAIN supported)"` User string `short:"U" long:"authuser" description:"SMTP AUTH username"` Password string `short:"P" long:"authpassword" description:"SMTP AUTH password"` Warning float64 `short:"w" long:"warning" description:"Warning threshold (sec)"` Critical float64 `short:"c" long:"critical" description:"Critical threshold (sec)"` Timeout int `short:"t" long:"timeout" default:"10" description:"Timeout (sec)"` }
オプションのパースはrun()
の中で行います。具体的には、run()
の外で定義したoptsのポインタをflags.ParseArgs()
の第1引数に渡します。このとき、実際に与えられたコマンドライン引数をもとにopts
のフィールドが直接書き換えられます。
そのため、複数のテストケースを実行するときに、前のテストケースで書き換えられたopts
が次のテストケースでも使いまわされるため、これにより
テスト結果が意図しないものになる可能性があります。
func run(args []string) *checkers.Checker { _, err := flags.ParseArgs(&opts, args)
次のようにtype options
で型としてstruct
を定義しておき、run()
の中でoption
型の変数を定義する方法に変更しました。これで、テストケースが複数ある場合でもrun()
を呼び出すごとにopts
が初期化されるため、意図どおりのオプションでテストが実行できます。
type options struct { Host string `short:"H" long:"host" default:"localhost" description:"Hostname"` Port string `short:"p" long:"port" default:"25" description:"Port"` FQDN string `short:"F" long:"fqdn" description:"FQDN used for HELO"` SMTPS bool `short:"s" long:"smtps" description:"Use SMTP over TLS"` StartTLS bool `short:"S" long:"starttls" description:"Use STARTTLS"` Auth string `short:"A" long:"authmech" description:"SMTP AUTH Authentication Mechanisms (only PLAIN supported)"` User string `short:"U" long:"authuser" description:"SMTP AUTH username"` Password string `short:"P" long:"authpassword" description:"SMTP AUTH password"` Warning float64 `short:"w" long:"warning" description:"Warning threshold (sec)"` Critical float64 `short:"c" long:"critical" description:"Critical threshold (sec)"` Timeout int `short:"t" long:"timeout" default:"10" description:"Timeout (sec)"` } func run(args []string) *checkers.Checker { var opts options _, err := flags.ParseArgs(&opts, args) :
まとめ
同じくgo-check-pluginsのcheck-tcpでもSMTP接続のチェックが行えますが、L7の挙動まで踏み込んだ監視をしたいなど、check-smtpがマッチする場合もあるかと思いますので、ぜひご活用ください。
また、今後も引き続き、機能追加、改修など、コントリビュートしていければと思っています。