Goでプロビジョニングツールを作った
GitHub - kohkimakimoto/cofu: Minimum configuration management tool written in Go.github.com
CofuというサーバプロビジョニングツールをGoで実装しました。Itamaeを参考に作りました。実装言語の違い(ItamaeはRubyによる実装)はありますが、外部仕様、内部実装、共にかなり似せて作ってあるので、ItamaeまたはItamaeが参考にしているchefを使ったことがあると、理解は簡単かと思います。特徴をざっくり説明すると、
- ローカルでのプロビジョニングのみ対応。SSHなどでのリモートサーバのプロビジョニングはサポートしない
- Goなので実行ファイル一個で動く。導入が簡単
- レシピはLuaのDSLで記述する
- 今のところ動作プラットフォームはRedHat(CentOS)のみをサポート
あたりでしょうか。以下に軽く使い方を記載します。
インストール
ビルド済みバイナリがありますので
https://github.com/kohkimakimoto/cofu/releases
からファイルをダウンロードして解凍し、パスの通ったディレクトリに配置するだけでOKです。RPMパッケージも用意してありますが、これは/usr/bin/cofu
をインストールしているだけなので、利用はお好みでどうぞ。
使い方
レシピファイルをこんなふうに記述して、
-- recipe.lua software_package "httpd" { action = "install", } service "httpd" { action = {"enable", "start"}, }
cofu
コマンドの引数に指定して実行します
$ sudo cofu recipe.lua ==> Starting cofu... ==> Loaded 2 resources. ==> Evaluating software_package[httpd] software_package[httpd]: 'installed' will change from 'false' to 'true' ==> Evaluating service[httpd] service[httpd]: 'enabled' will change from 'false' to 'true' service[httpd]: 'running' will change from 'false' to 'true' ==> Complete!
レシピを指定して、実行する。これだけ。
リソース
Itamaeと同様にレシピファイルにはサーバの状態をリソースという単位で記述します。以下が実装済みのリソースです。だいたいItamaeで用意されているのと同じです。
- directory
- execute
- file
- group
- link
- lua_function
- remote_file
- service
- software_package
- template
- user
実体はLuaの関数なのですが、リソース記述のsyntaxは以下のようになります
resource_type "name" { attribute = "value", action = "type_of_action", }
Tips
では、もうちょっと実用的な使い方やサンプルなどを幾つか紹介
dry-run
-n
オプションをつければdry-runで実行します。実際には変更が行われないモードですね。変更内容の確認のために使います。
$ sudo cofu -n recipe.lua
executeリソースでコマンド実行
コマンドを実行します。not_if
とonly_if
は全てのリソースで使用できる共通のアトリビュートです。
execute "echo hello > /tmp/example" { not_if = "grep hello /tmp/example", -- このコマンドが失敗した時のみ実行される -- only_if = "grep -v hello /tmp/example", -- このコマンドが成功した時のみ実行される }
templateで設定ファイルを更新
template
リソースで設定ファイルを更新します。テンプレートはGoのtext/templateで記述します。
template "/etc/httpd/conf.d/foo.conf" { owner = "root", group = "root", mode = "0644", source = "foo.conf.tmpl", }
source
を指定しない場合、配置先のファイルパスからデフォルトでテンプレートファイルの位置を決定します。上記の例ですとレシピファイルからの相対パスで
templates/etc/httpd/conf.d/foo.conf
または
templates/etc/httpd/conf.d/foo.conf.tmpl
を使用します。
remote_fileでファイルを配置
テンプレート処理が必要なく、単純にファイルを配置したいだけのときはremote_file
リソースを使います。
remote_file "/path/to/hoge.zip" { owner = "root", group = "root", mode = "0644", source = "hoge.zip", }
この場合もsource
を指定しない場合はデフォルトのパスを使用します。上記の例ですと
files/path/to/hoge.zip
となります。
変数
chefやItamaeでいうところのノードアトリビュートにあたるものとして、実行時にコマンドラインから任意の変数を割り当てられます。-var
オプションをつかってJSONを直接指定します。
$ cofu recipe.lua -var='{"name": "kohkimakimoto"}'
これでテンプレートなどでは
{{var.name}}
で値を参照できます。レシピ内ではLuaのグローバル変数var
に割り当てられるので
var.name
で参照できます。
JSONを直接文字列からではなくファイルから読みたいときは-var-file
オプションを使用してください。
$ cofu recipe.lua -var-file=variables.json
パッケージのインストールとサービスの起動
software_package
でパッケージのインストール、アンインストール。service
でサービスの起動、停止、再起動ができます。software_package
はItamaeやchefでいうところのpackage
リソースと同等なのですが、Luaだとpackage
がビルトインのモジュールが利用している予約語なので、名前を変えています。
software_package "httpd" { action = "install", } service "httpd" { action = {"enable", "start"}, }
notifiesでリソース更新に合わせて別のリソースのアクションを実行する
設定ファイルが更新されたらサービスをリロードさせたりするのに使います。
template "/etc/httpd/conf/httpd.conf" { owner = "root", group = "root", mode = "0644", notifies = {"reload", "service[httpd]"}, } service "httpd" { action = "nothing", }
Itamaeだとこれの逆バージョンのsubscribes
(別のリソースの更新を検知してアクションを実行する)があるのですが、個人的にnotifies
しか使っていなかったのでCofuではnotifies
のみを実装しています。
別のレシピを読み込む
include_recipe
関数で別のレシピを読み込みます。
include_recipe "nginx.lua" include_recipe "php.lua"
ドキュメント
まだまだ欠けていますが、ここにあります。
https://github.com/kohkimakimoto/cofu/blob/master/docs/README.md
動作プラットフォームについて
前述のようにCentOSでしか動作確認、サポートしていません。
単純に私が他のOSは普段使っていないので今のところ対応しないというだけで、プラットフォームごとに処理を切り分ける部分はItamaeが利用しているSpecinfraを参考にして実装してあるので、マルチプラットフォーム対応ができる構造にはなっています。Specinfraを参考にプラットフォームによって異なる部分のコードを追加すれば、対応できるはずです。
ただテストを自動化してないので、本格的にマルチプラットフォーム対応させる際にはそのへんも考えていかなくてはいけないのかも。
開発の経緯と実際のユースケース
Itamaeに比べて機能は少ないし、SSHバックエンドをサポートしてないし、対応プラットフォームもCentOSのみというプロダクトなので、機能面でいうとこれは劣化版Itamaeという趣になっています。実際のところ私自身今はプロビジョニングツールにItamaeを使っていて、既存のItamaeのレシピをCofuで置き換えるつもりはなく、今後もItamaeを使い続ける予定であります。
実はこのCofuを作成した経緯は、フルスタックなプロビジョニングツールを作ろうと思っていたわけではなく、 そもそもの発端は「アプリケーションのデプロイ時に、サーバーのミドルウェアの設定ファイルを更新したい」という要件でした。
具体的なもののひとつがcronの設定ファイルで、これにアプリケーション固有のバッチ処理などが定期実行されるように記述されているわけです。アプリでバッチ処理を追加する場合、アプリケーションのリポジトリとサーバーコンフィグレーションを管理しているItamaeレシピのリポジトリの二つにコミットする必要がありました。
またアプリ固有の処理なので、プログラムロジック自体も特定の定期実行のスケジューリングを前提にした作りになっていることも多々あり、スケジューリングの設定と本質的に不可分で、この場合cronの設定自体アプリケーションのレイヤで管理すべきものです。それができないため、バッチプログラムのコメントに「日次バッチ。cronで実行される(/etc/cron.d/app参照)」とかメモを書いていたりしました。
こういう背景から、アプリケーション側に寄せるべき設定を管理するために、デプロイ時に特定の設定ファイルを書き換える仕組みがほしいと思っていて、最初はシェルスクリプトを書いていたのですが、いろいろ考えていくと冪等性が欲しかったり、設定ファイルが更新された場合にはコマンドを実行したりする必要が出てきたりして、Itamaeのようなプロビジョニングツールと機能要件が似てきたという次第です。
まあそれならデプロイ時にitamae
コマンドを叩けばいい、というのも当然あるのですが、設定ファイル一個をリポジトリルートにおいておいてデプロイ時にこれを実行すればOKというシンプルな仕組みが欲しかったので、結局最後まで作りました。以下がそのサンプルです
-- config_cron.lua template "/etc/cron.d/app" { owner = "root", group = "root", mode = "0644", content = [=[# This is generated by cofu. don't edit it manually! MAILTO="" */1 * * * * kohkimakimoto /path/to/app/batch1 */1 * * * * kohkimakimoto /path/to/app/batch2 */30 * * * * kohkimakimoto /path/to/app/batch3 0 * * * * kohkimakimoto /path/to/app/batch3 ]=], }
これをアプリのリポジトリルートにおいておき、デプロイ時のスクリプトにsudo -E cofu config_cron.lua
を実行するようにしてあります。
よって私の環境では実際に使っているリソースはtemplate
とservice
,execute
あたりのみになります。ただ、今後の個人の趣味プロジェクトなどではサーバプロビジョニングの用途でも使っていこうかな、とも思っています。せっかくその他のパッケージやユーザを管理するリソースも実装したので。。。
ちなみにレシピはシバンで使うこともできます。
https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%90%E3%83%B3_(Unix)
レシピを以下のように記述して、実行権限をつければ、レシピファイル自体を実行可能な設定スクリプトとして扱うことができます。
#!/usr/bin/env cofu -- config.lua template "/path/to/foo" { -- ... } template "/path/to/bar" { -- ... } -- 実行権限をつけて直接実行。 -- $ ./config.lua
実装のはなし
今回のCofuがそうなのですが、最近CLIツールを作るのにGoとGoPherLuaというGoのLua実装をセットで使うのが気に入っていて、これをベースにLuaをDSL風に使った設定ファイルを設計してアプリケーションを組み上げるのが最近、個人的によく使う手法になっています。
LuaのDSLはnginxの設定ファイルやHCLに見た目が似ていて、かつ変数や制御構文もつかえるので、なかなか便利だと思っています。
他にも幾つかこの技術スタックでツールを書いているので、後日それについても記事を書こうかと思っています。