Keepalivedのシンタックスチェッカ「gokc」を作った

Keepalivedのシンタックスチェッカ「gokc」をGo言語で書きました。

github.com

執筆時点でのKeepalived最新版であるバージョン1.2.19まで対応していることと、include文に対応していることがポイントです。

使い方

https://github.com/yuuki/gokc/releases からバイナリをダウンロードします。OSXでHomebrewを使っていれば、

$ brew tap yuuki/gokc
$ brew install gokc

でインストールできます。

gokcコマンドを提供しており、-f オプションで設定ファイルのパスを指定するだけです。

gokc -f /path/to/keepalived.conf
gokc: the configuration file /etc/keepalived/keepalived.conf syntax is ok

シンタックスエラーのある設定ファイルを読み込むと、以下のようなエラーで怒られて、exit code 1で終了します。

gokc -f /path/to/invalid_keepalived.conf
./keepalived/doc/samples/keepalived.conf.track_interface:7:14: syntax error: unexpected STRING

なぜ作ったか

KeepalivedはIPVSによるロードバランシングと、VRRPによる冗長化を実現するためのソフトウェアです。 KeepalivedはWeb業界で10年前から使われており、はてなでは定番のソフトウェアです。 社内の多くのシステムで導入されており、今なお現役で活躍しています。

KeepalivedはNginxやHAProxy同様に、独自の構文を用いた設定ファイルをもちます。

  • しかし、Keepalived本体は構文チェックをうまくやってくれず、誤った構文で設定をreloadさせると、正常に動作しなくなることがありました。

そのため、これまでHaskellで書かれた kc というツールを使って、シンタックスチェックしていました。 initスクリプトのreloadで、kcによるシンタックスチェックに失敗するとreloadは即中断されるようになっています。

ところが、Haskellを書けるメンバーがいないので、メンテナンスができず、Keepalivedの新機能に対応できていないという問題がありました。 (Haskell自体がこのようなものを書くのに向いているとは理解しているつもりです。) さらに、kcについてはビルドを成功させるのが難しいというのもありました。Re: keepalived.confのシンタックスチェックツール「keepalived-check」「haskell-keepalived 」が凄い! - maoeのブログ

さすがに、Keepalivedの新しい機能を使うためだけに、Haskellを学ぶモチベーションがわかなかったので、Go言語とyaccで新規にgokcを作りました。 Go言語はインフラエンジニアにとって馴染みやすい言語だと思っています。 yaccは構文解析の伝統的なツールなので、情報系の大学で習っていたりすることもあります(僕は習わなかったけど、概念は習った)。

ちなみに、C言語+flex+yacc版のシンタックスチェッカである ftp://ftp.artech.se/pub/keepalived/ というものがあります。 新しい構文には対応しているのですが、include未対応だったり、動いてない部分が結構あるので、参考にしつつも一から作りました。

実装

シンタックスのチェックだけであれば、コンパイラのフェーズのうち、字句解析と簡単な構文解析だけで済みました。 「簡単な」と言ったのは、構文解析フェーズで、抽象構文木を作らなくて済んだということです。

一般に字句解析器は、自分で書くか、Flexのような字句解析器の自動生成ツールを使います。 後者の実装として、自分の知る限り、Go言語にはgolexnex があります。

ただし、include文のような字句解析をそこそこ複雑にする構文があるため、柔軟に書けたほうがよかろうということで自分で書くことにしました。 といっても、スキャナ部分はGo言語自体のスキャナであるtext/scannerを流用しました。 Go言語用のスキャナですが、多少カスタマイズできる柔軟性があるので、ユーザ定義の言語の字句解析器として利用できます。 Rational Number Calculator in Go を参照。

構文解析にはパーサジェネレータであるyaccを使いました。 yaccのGo版は標準でgo tool yaccがあります。 goyaccについて詳しくは、goyaccで構文解析を行う - Qiitaを参照してください。

多少面倒だったのはinclude文の対応です。 include対応とはつまり、字句解析器において、別の設定ファイルを開いて、また元の設定ファイルに戻るというコンテキストの切り替えをしつつ、トークンを呼び出し元の構文解析器に返すことが求められます。

字句解析器から構文解析器へトークンを渡す構造をどうするかが問題でした。 逐次的にトークンを構文解析器へ返すのを諦めて、一旦末尾まで字句解析した結果をメモリにすべてのせて、構文解析器から順に読ませるみたいなこともできました。

それでもよかったんですが、Rob Pikeの Lexical Scanning in Go の資料に、goroutineとchannelを利用して、字句解析器を作る方法が書かれており、この手法を部分的に真似てみました。

具体的には、字句解析を行うgoroutineと、構文解析を行うgoroutine(メインのgo routine)が2つがあり、字句解析goroutineが構文解析goroutineにemitetr channelを通じて、トークンを受け渡すという構造にして解決しました。 channelをキューとして扱うようなイメージです。

include文のもつ複雑さに対して、そこそこシンプルに書けたような気はしています。

参考