読者です 読者をやめる 読者になる 読者になる

オープンソースこねこね

Webプログラミングなどについてあれこれ。

Goでプロビジョニングツールを作った

golang

GitHub - kohkimakimoto/cofu: Minimum configuration management tool written in Go.github.com

CofuというサーバプロビジョニングツールをGoで実装しました。Itamaeを参考に作りました。実装言語の違い(ItamaeはRubyによる実装)はありますが、外部仕様、内部実装、共にかなり似せて作ってあるので、ItamaeまたはItamaeが参考にしているchefを使ったことがあると、理解は簡単かと思います。特徴をざっくり説明すると、

  • ローカルでのプロビジョニングのみ対応。SSHなどでのリモートサーバのプロビジョニングはサポートしない
  • Goなので実行ファイル一個で動く。導入が簡単
  • レシピはLuaDSLで記述する
  • 今のところ動作プラットフォームは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_ifonly_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を実行するようにしてあります。

よって私の環境では実際に使っているリソースはtemplateservice,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実装をセットで使うのが気に入っていて、これをベースにLuaDSL風に使った設定ファイルを設計してアプリケーションを組み上げるのが最近、個人的によく使う手法になっています。

LuaDSLはnginxの設定ファイルやHCLに見た目が似ていて、かつ変数や制御構文もつかえるので、なかなか便利だと思っています。

他にも幾つかこの技術スタックでツールを書いているので、後日それについても記事を書こうかと思っています。