SSHラッパーコマンドEsshのv1.0.0をリリースしました
Esshは、zsh補完やLuaスクリプトによる動的なコンフィグレーションなど、便利機能を実装した、sshのラッパーコマンドです。
ドキュメントをまとめたWebサイトも作成しました。
上記Webサイトのトップにasciinemaで撮ったターミナルオペレーションのデモを乗せましたので、そちらを見ていただければ、どういったものか大体のことはわかると思います。
開発の経緯
2015/11に以下の記事を書き、Zsshというコマンドを作成しました。
その後、CentOSのリポジトリにzssh
というパッケージが存在していたため、コマンド名をzssh
からessh
に変え、自分の利用にあわせて、適宜、機能の追加や変更を続けていました。そして、自分が使う上で必要十分な機能がそろって、大きな変更もなくなってきたので、この度これをv1.0.0
としてリリースすることにしました。なお2015/11時点のものとは仕様が大きく様変わりしており、以下に改めて、機能について説明します。
機能
EsshはGoで書かれたシングルバイナリのCLIツールで多機能sshコマンドとして利用できます。実装的には内部でssh
コマンドを実行するラッパーコマンドになっています。標準のsshコマンドでは使えない以下のような機能が利用できます。
- Luaによる設定ファイル。これによりSSH接続サーバの設定(ssh_config)をより動的に構築することができる。
- サーバ接続、切断時に発火するフックコマンドを設定できる。
- サーバにタグをつけられ、一覧をターミナルに出力できる。
- ZshとBash用の強力な補完を組み込みで提供。
- カレントディレクトリごとに設定ファイルの切り替え。
- Capistranoのような複数サーバに並行してコマンドを実行できるタスクランナー。
- 設定用のLuaコードはモジュールとしてGitリポジトリ上で共有できる。
一通りの機能に触れるためのチュートリアルを用意したので、利用の際はここから始めるとよいかと思います。
https://essh.sitespread.net/intro/ja/index.html
利用例
上記のチュートリアルには書いていないが、私個人が使っている機能などを中心にいくつか紹介します。
ログイン時にターミナルの色を変える
接続、切断時のフックコマンドhooks_before_connect
とhooks_after_disconnect
でターミナルの色を変えます。私は本番環境につなぐときは赤にしています。
-- ~/.essh/config.lua -- 事前にmacのターミナル設定でRedとBlackというプロファイルを作っておく local red_screen_command = "osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Red\"'" local black_screen_command = "osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Black\"'" host "webserver-01" { HostName = "192.168.56.32", Port = "22", User = "kohkimakimoto", description = "web server-01", hooks_before_connect = { red_screen_command }, hooks_after_disconnect = { black_screen_command }, tags = { "web", }, } host "webserver-02" { HostName = "192.168.56.33", Port = "22", User = "kohkimakimoto", description = "web server-02", hooks_before_connect = { red_screen_command }, hooks_after_disconnect = { black_screen_command }, tags = { "web", }, }
アプリケーションのデプロイ
リモートサーバにコマンドを実行します。以下はただのサンプルです。実環境ではもっと複雑なスクリプトを実行させています。
host "webserver-01" { HostName = "192.168.56.32", Port = "22", User = "kohkimakimoto", description = "web server-01", tags = { "web", }, } host "webserver-02" { HostName = "192.168.56.33", Port = "22", User = "kohkimakimoto", description = "web server-02", tags = { "web", }, } task "deploy" { backend = "remote", targets = {"web"}, prefix = true, parallel = true, script = [=[ echo "deploy app to $ESSH_HOSTNAME" # your deploy commands... echo "Done." ]=], }
以上です。日々のSSHライフのお供に、どうぞお使いください。m( )m
テストやビルドスクリプトをDockerコンテナで実行させるための便利ツールを作った
ある程度複雑なプログラムだと、ローカル環境やCIサーバ環境など、どこでも動くようにテストを書くのが難しくなったりします。また、昨今のWebアプリケーションだと、デプロイ前に何らかのビルドプロセスが必要になることがほとんどで、依存物をインストールしたり、Webpackを動かしたり、Goのビルドを走らせたりします。こういうタスクもnodeやらGoに依存していて、複数の環境で差異なく動かすことを考えると、いろいろツライものがあります。
そこで、こういったテストやビルド処理はシェルスクリプトを書いて、Dockerコンテナ内で動かすことによって環境に対しての依存を解消していました。
たとえばGo言語の場合以下のようなテスト実行スクリプトtest_run.sh
を書いておきます。
#!/usr/bin/env bash set -eu go test $GOTEST_FLAGS $(go list ./... | grep -v vendor)
そしてこれをDockerコンテナで実行するためのスクリプトとして以下のようなtest.sh
を書きます。
#!/usr/bin/env bash set -eu DOCKER_IMAGE=${DOCKER_IMAGE:-'kohkimakimoto/golang:centos7'} GOTEST_FLAGS=${GOTEST_FLAGS:--cover -timeout=360s} repo_dir=$(cd $(dirname $0); pwd) docker run \ --env GOTEST_FLAGS="${GOTEST_FLAGS}" \ -v $repo_dir:/build/src/github.com/username/repo \ -w /build/src/github.com/username/repo \ --rm \ ${DOCKER_IMAGE} \ bash ./test_run.sh
goコマンドなど、テストに必要な環境は全てDockerイメージとして用意しておきます。あとはリポジトリのルートにいる状態で./test.sh
を実行すれば、テストが実行されます。TravisCIのようなCIサーバはDockerに対応しているので、同様に./test.sh
を実行するだけで、ローカル環境と同じようにテストが実行できます。テストコード側で環境の違いを意識する必要はなくなります。
ところで、しばらくこの方式でやっていたところ、以下のような課題が浮き彫りになってきました。
- 新しいプロジェクトごとにシェルスクリプトをコピペ、修正して使いまわしている。
- これらのスクリプトはだいたい似たようなコードになりがちだが、利用するDockerイメージや
docker run
のオプションなどが微妙にちがう。 - テストやビルドなど個々のタスクごとに「実際のタスクを実行するスクリプト」と「それをDockerコンテナで実行するスクリプト」の2つのファイルを作る必要があり、数が増えるとファイルがごちゃごちゃしてきた。
ベタに書いたシェルスクリプトによる実装なので、プロジェクトが増えると、共通化などができずコードが冗長になりがちでした。そこで、大雑把に処理を整理してみると
などが共通な処理であったので、これらをまとめて、いい感じにDockerコンテナを起動してその中でスクリプトを実行するコマンドラインツールを作りました。
Goで実装したシングルバイナリなので、Githubのリリースページからバイナリをダウンロードして、パスの通ったディレクトリに配置すればすぐに使えます。
使い方
まずは単純に
$ buildsh
を実行してみます。これだけで、デフォルトでDockerイメージkohkimakimoto/buildsh:latest
(2Gくらいあります)をダウンロードして、カレントディレクトリをコンテナ内の/build
にマウントした状態でコンテナを起動します。そのままbashでログインした状態になるので、たとえばPHPのテストだったら
$ php phpunit
などを実行すればテストができます。PHPやPython,nodeなど主なLLのランタイムを入れてあるので、ホストマシンの環境に関係なくすぐにテストやビルドができます。作業が終わったら
$ exit
すれば、ホストマシンにもどってコンテナが破棄されます。特定の環境でちょっとしたことを動作確認したい場合などに便利に使えます。
そしてテストやビルドのシェルスクリプトを実行させたい場合は
$ buildsh test.sh
のようにスクリプトファイルを指定して実行すれば、そのスクリプトがDockerコンテナ内で実行されます。これで用意するスクリプトはテストを実行する部分のみでよくなり、「それをDockerコンテナで実行するスクリプト」を書く必要がなくなりました。
設定ファイル
使用するDockerイメージを変更したい場合などは、設定ファイルを利用することができます。.buildsh.yml
をカレントディレクトリに配置してください。以下のような設定ができます。
use_cache: true docker_image: kohkimakimoto/buildsh:latest additional_docker_options: --net=host -v=/var/run/docker.sock:/var/run/docker.sock environment: FOO: bar FOO2: bar2 home_in_container: /build/src/github.com/kohkimakimoto/buildsh
詳細はREADME.mdを参照していただくとして、いくつかピックアップして概要を説明します。
use_cache
use_cache
はtrueにするとカレントディレクトリ配下に.buildsh/cache
ディレクトリを作り、コンテナ内でパスを環境変数BUILDSH_CACHEDIR
に設定します。これはスクリプト実行ごとに破棄されてしまうコンテナ内のデータを保存するときに使うことができます。たとえばyarn install
を以下のようにすれば、キャッシュを保持できて、次回以降の処理の高速化が望めます。
$ yarn install --cache-folder=$BUILDSH_CACHEDIR/yarn
.gitignore
に.buildsh
を追加するのを忘れずに。。。
docker_image
docker_image
は利用するDockerイメージです。自分の用途に合わせて好きなイメージを使えます。
additional_docker_options
additional_docker_options
は内部で実行しているdocker run
に付け加えるオプションを指定できます。例のように-v=/var/run/docker.sock:/var/run/docker.sock
を利用すればDockerコンテナ内から新しくDockerコンテナを立ち上げることもできたりします。
home_in_container
home_in_container
はコンテナ内でホストのカレントディレクトリがマウントされるパスを変更できます。デフォルトは/build
ですが、例えばGoのテストやビルドをおこなうとき、GOPATHの関係上/build/src/github.com/kohkimakimoto/buildsh
のようにGOの流儀にそったパスに配置したいことがあります。この設定を使うことでそのようなケースに対応できます。
実装について
このツールはつまるところ、テストやビルド目的の使い捨てコンテナである場合docker run
にセットするオプションや引数がおおむね共通化できるので、それをまとめたラッパーコマンドというわけです。
もともとはbashのスクリプトで書いていたこともあって、実装は単純なdocker run
のラッパーコマンドになっているので、メインのコードはbuildsh.goのみです。なのでをこれを見れば何をやっているのか大体わかりますので、細かいことはソースを見たほうが早いかもしれません。
mattn/memoのzsh補完
Golang実装のメモコマンド
Big Sky :: golang でメモ専用コマンド「memo」作った。
GitHub - mattn/memo: 📓 Memo Life For You
が便利だったので、雑にzsh補完を書きました。
_memo_options() { local -a __memo_options __memo_options=( '--help:show help' '-h:show help' '--version:print the version' '-v:print the version' ) _describe -t option "option" __memo_options } _memo_sub_commands() { local -a __memo_sub_commands __memo_sub_commands=( 'new:create memo' 'n:create memo' 'list:list memo' 'l:list memo' 'edit:edit memo' 'e:edit memo' 'delete:delete memo' 'd:delete memo' 'grep:grep memo' 'g:grep memo' 'config:configure' 'c:configure' 'serve:start http server' 's:start http server' 'help:Shows a list of commands or help for one command' 'h:Shows a list of commands or help for one command' ) _describe -t command "command" __memo_sub_commands } _memo_list() { local -a __memo_list PRE_IFS=$IFS IFS=$'\n' __memo_list=($(memo list)) IFS=$PRE_IFS _describe -t memo "memo" __memo_list } _memo () { local state line _arguments \ '1: :->objects' \ '*: :->args' \ && ret=0 case $state in objects) case $line[1] in -*) _memo_options ;; *) _memo_sub_commands ;; esac ;; args) last_arg="${line[${#line[@]}-1]}" case $last_arg in edit|e|delete|d) _memo_list ;; *) ;; esac ;; *) _files ;; esac } compdef _memo memo
~/.zshrc
とかにコピペして使ってください。
※2017-02-10 追記: 本家リポジトリに取り込んでもらいました。https://github.com/mattn/memo/blob/master/misc/completion.zsh
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に見た目が似ていて、かつ変数や制御構文もつかえるので、なかなか便利だと思っています。
他にも幾つかこの技術スタックでツールを書いているので、後日それについても記事を書こうかと思っています。
sshのラッパーコマンドを作った
GoでSSHコマンドに便利機能を追加したコマンドを作りました。
2017/04/11 追記: この記事の内容は古くなっています!
2017/04/11時点の最新の仕様は、次の記事を参照してください
SSHラッパーコマンドEsshのv1.0.0をリリースしました - オープンソースこねこね
追記ここまで。以下は2015/11時点の古い情報となります。
機能として
- Luaで
~/.ssh/config
に相当する設定を書ける。 - zshの補完機能を使って、接続先一覧を出す。
- サーバ接続時にフックを仕込める。自分はスクリーンの色を変えるの使っている。
- 複数のリモートサーバにまとめてコマンドを実行する。
といったところです。詳細はリポジトリのREADMEを見ていだければと思います。 ビルド済みバイナリをリリースページにおいてあるので、インストールはダウンロードして解凍してパスの通ったディレクトリに配置すればOKです。
~/.ssh/config
を上書きするので~/.ssh/config
のバックアップを取っておくのをお忘れなく。
https://github.com/kohkimakimoto/zssh/releases/latest
使い方
zssh
はssh
のラッパーコマンドになっているのでssh
コマンドと同様に使えます。zssh
を実行するとssh
コマンドで実際の処理を実行する前に、設定ファイル~/.ssh/zssh.lua
を読み込んで~/.ssh/config
を生成するしくみになっています。
~/.ssh/zssh.lua
に以下のような設定を書いておくと
Host "web01.localhost" { ForwardAgent = "yes", HostName = "192.168.0.11", Port = "22", User = "kohkimakimoto", -- 小文字で始まる設定は、~/.ssh/configに出力されない。descriptionはzsh補完の説明文に使用される(後述) description = "my web01 server", } Host "web02.localhost" { ForwardAgent = "yes", HostName = "192.168.0.12", Port = "22", User = "kohkimakimoto", description = "my web02 server", }
zssh
実行時に次のような~/.ssh/config
を自動生成して上書きします。
Host web01.localhost ForwardAgent yes HostName 192.168.0.11 Port 22 User kohkimakimoto Host web02.localhost ForwardAgent yes HostName 192.168.0.12 Port 22 User kohkimakimoto
これで、sshコマンドと同様に以下のようにしてサーバにSSH接続できます。
zssh web01.localhost
ZSH補完
zsh補完をサポートしているので、以下のコードを~/.zshrc
に書いておくと
eval "$(zssh --zsh-completion)"
上に貼り付けたアニメgifのようにサーバが説明文付きで候補にでるようになります。
フック
hooks
の設定でサーバ接続時と切断時にローカルでLuaのコードを実行できます。
os.execute
でコマンドを実行できるので、以下のようにするとMacのターミナルの色を変更できます。
Host "web01.localhost" { HostName = "192.168.0.11", Port = "22", User = "kohkimakimoto", ForwardAgent = "yes", description = "my web01 server", -- フックの設定 hooks = { before = function() -- This is an example to change screen color to red. os.execute("osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Red Sands\"'") end, after = function() -- This is an example to change screen color to black. os.execute("osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Pro\"'") end, } }
ちなみに、ターミナルの色を変える方法は以前にも書きましたので、よければそちらも参照ください。
http://kohkimakimoto.hatenablog.com/entry/2015/04/02/211232
マクロ
コマンドを複数サーバにまとめて実行できます。
Host "web01.localhost" { HostName = "192.168.0.11", Port = "22", User = "kohkimakimoto", ForwardAgent = "yes", description = "my web01 server", tags = { role = "web" }, } Host "web02.localhost" { HostName = "192.168.0.12", Port = "22", User = "kohkimakimoto", ForwardAgent = "yes", description = "my web02 server", tags = { role = "web" }, } Macro "example" { -- 並列実行するか? parallel = true, -- 実行前に確認プロンプトを出す。 confirm = "Are you OK?", -- zsh補完時の説明 description = "example macro", -- 実行先サーバを指定。Host設定のtagsで設定したタグを指定する。指定しないとローカルでの実行になる。 on = {role = "web"}, -- ttyを使うか? tail -f でログを監視するときなどはtrueに tty = false, -- コマンドの内容 command = [[ ls -la ]], }
マクロ名を指定して実行できます。
$ zssh example
Goでgraceful restartに対応したデーモンプロセスをつくる
Goで書かれたWebアプリのプロセスをデーモンにしたり、ダウンタイムなしでデプロイできるようにするための情報をいろいろ調べていたのですが、どうもSupervisorなど外部のツールを使ったりするのが定番か、herokuなどのPaaSにまかせてしまうという手法が多いようです。 でも個人的にGoの一番気に入っているところは依存のない独立した単一バイナリを生成できる点なので、Webアプリもなるべく外部に依存せずにどうにかできないものかと調査していました。
さてGoのデーモン化とgracefulのライブラリはそれぞれいくつかあるのですが、両方に対応したライブラリは見つからず、組み合わせたサンプルなども見当たらなかったので、各種ライブラリのソースを読んだりしながら、最終的に以下のものを取り上げてみました
- daemonize
- graceful restart
この2つを組み合わせてgraceful restartできるデーモンを作ろうとしたわけですが、上記のライブラリではdaemonziseもgracefulも内部的にforkの代わりにos.StartProcess
で外部コマンドとして自分自身を再起動し、環境変数を使って状態の制御をおこなうということをやっています。一緒に使うと、その辺が干渉してうまく動かない。。。
で、いろいろワークアラウンドをいれて、ひと通り動くようになったのが以下のコードです。やっていることはコード内のコメントを参照してください。
package main import ( "flag" "fmt" "io/ioutil" "log" "net/http" "os" "path/filepath" "strconv" "time" "github.com/VividCortex/godaemon" "github.com/facebookgo/grace/gracehttp" "github.com/kardianos/osext" ) var ( logfile = flag.String("l", "goserver.log", "log file") pidfile = flag.String("p", "goserver.pid", "pid file") ) func main() { flag.Parse() // os.Args[0]を絶対パスに書き換える。 // デーモナイズ後、カレントディレクトリが変わるので、 // 相対パスのままだとgracehttpがos.StartProcessするとき自分自身を指し示すファイルパスを解決できない。 bin, err := osext.Executable() if err != nil { log.Fatal(err) } os.Args[0] = bin // pidとlogのファイルのパスも絶対パスにする。 logfilepath, err := filepath.Abs(*logfile) if err != nil { log.Fatal(err) } pidfilepath, err := filepath.Abs(*pidfile) if err != nil { log.Fatal(err) } // デーモナイズする // "LISTEN_FDS"があるときはgraceful restart時なので、スキップさせる。 if os.Getenv("LISTEN_FDS") == "" { godaemon.MakeDaemon(&godaemon.DaemonAttr{}) } // 以下のロジックはデーモナイズ後の状態で実行される。 // ログの出力先をファイルに。 f, err := os.OpenFile(logfilepath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatal(err) } defer f.Close() log.SetOutput(f) // pidファイルのプロセスIDを書き込む pid := os.Getpid() if err = ioutil.WriteFile(pidfilepath, []byte(strconv.Itoa(pid)), 0666); err != nil { log.Fatal(err) } log.Printf("Generated pidfile %s (%d)\n", pidfilepath, pid) // WebAppサーバ mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { time.Sleep(5 * time.Second) // 動作検証用に5秒リクエストをつかみっぱなしにする(restartしても切れないことを見る) fmt.Fprintf(w, "hello world!") }) server := &http.Server{Addr: ":1234", Handler: mux} // graceful restartをサポートして起動 gracehttp.Serve(server) }
github.com/kardianos/osextは実行ファイルの絶対パスを取得するための便利ライブラリです。
これをビルドし起動した後、graceful restartするには-USR2
シグナルを送る。
kill -USR2 5678
シャットダウンもリクエストが途中で切れないgraceful shutdownになる。
kill 5678
動作させてみたところうまく動いているようではあるが、os.Args[0]
を書き換えたりしてキモいコードになっている。
daemonizeとgracefulをまとめて、ルーティングとmiddlewareまわりの構造を提供してくれる軽量フレームワークがあったらいいなあ、と妄想しています。
Goについての雑感
最近Golangでコマンドラインツールを書いているので、ちょっと思うところを書いておく。
気に入っているところ
シングルバイナリをクロスプラットフォームで生成できる
とくにコマンドラインツールを作ってみると、この特性がすごく気持ちよい。シングルバイナリはインストールが楽という利点があり、特に今はgithubのリリースページにバイナリを置いたり、https://bintray.com/とかを使うことでファイルをHTTPでダウンロードすることができ、導入の敷居がすごく下がる。アンインストールもファイルを消せばいいだけなので気楽だ。
ところで自分の場合、RubyやPythonなど自分がメインで使っていない言語の処理系を必要とするツールだと、まずその処理系を「正しく」インストールする方法を調べるのに気を使ったりしてしまう。もちろん本当のところは目的のツールが単に動けばいいだけなので、Macにプリインストールされている処理系を使えばいいのだけど、どうせ使うんだったら、Linuxサーバ上のものと同じ環境にしたいとか、メジャーな方法を使いたいとかでrbenvを調べたりごにょごにょ環境周りをいじって時間を浪費することが多くて、しょんぼりする。
そのへんの事情から開放してくれて他に依存が無いのですぐに使えるし、すぐに捨てられるというシングルバイナリは思いの外、快適だったりする。
静的型付けのコンパイル言語
今はIntelliJでGoを書いているので静的型な言語だとIDEの補完や定義先へのジャンプがかなり強力に作用する。ちょっと作業環境が重いと感じることもあるが、許容できる。プライベートで書いているプログラムだと、ちょっと変数の名前の付け方やディレクトリ構成が気に食わないとか、気分でコード全体をもりっと変えることがよくあって、そういうときもコンパイラのチェックがあるので、ある程度のコードの正当性を担保してくれるのがよい。
また、型推論のためコードの見た目はLLっぽいのも気に入っている。
OSSですでに周辺ライブラリがいろいろある、導入も簡単
Githubで検索すれば必要なライブラリは大抵あるし、それを使ったサンプルコードも検索すれば大体出てくる。
困っているところ
GOPATHとimport
もういろんなところで言及されているので詳細は省くが、GoはGOPATHというパスを起点にプロジェクトのコードと依存する外部パッケージのコードを同列にフラットに管理するという独特の方式を採用している。これのせいで明確に困ることがあって、
GithubのGo言語プロジェクトにPull Requestを送るときのimport問題 | SOTA
のようにforkしたプロジェクトだと問題になる。また自分の作業環境だと、依存物をプロジェクトのディレクトリ配下に収められないのでctagsでタグ生成する範囲が広がりすぎる問題がある。で、この辺は以前書いたようにdirenv
とGom
を使ってGOPATHをプロジェクトごとに複数、切り替えて対応している。
幸いIntelliJもGOPATHを複数設定し、プロジェクトごとに切り替えることができるのでその機能を使っている。 とはいえちょっとややこしくて、非標準のツールに頼っているため悩ましいところではある。
プラグイン的なものが作れない
Goは動的に他のビルド済みのGoのコードをロードすることができないので、後からプロダクトに機能追加できるようにする、いわゆるプラグイン的なものはつくれない。
考えていること
普通のWebアプリだったら依然PHPを使うと思う。