オープンソースこねこね

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

SSHラッパーコマンドEsshのv1.0.0をリリースしました

Esshは、zsh補完やLuaスクリプトによる動的なコンフィグレーションなど、便利機能を実装した、sshのラッパーコマンドです。

github.com

ドキュメントをまとめたWebサイトも作成しました。

https://essh.sitespread.net/

上記Webサイトのトップにasciinemaで撮ったターミナルオペレーションのデモを乗せましたので、そちらを見ていただければ、どういったものか大体のことはわかると思います。

開発の経緯

2015/11に以下の記事を書き、Zsshというコマンドを作成しました。

kohkimakimoto.hatenablog.com

その後、CentOSリポジトリzsshというパッケージが存在していたため、コマンド名をzsshからesshに変え、自分の利用にあわせて、適宜、機能の追加や変更を続けていました。そして、自分が使う上で必要十分な機能がそろって、大きな変更もなくなってきたので、この度これをv1.0.0としてリリースすることにしました。なお2015/11時点のものとは仕様が大きく様変わりしており、以下に改めて、機能について説明します。

機能

EsshはGoで書かれたシングルバイナリのCLIツールで多機能sshコマンドとして利用できます。実装的には内部でsshコマンドを実行するラッパーコマンドになっています。標準のsshコマンドでは使えない以下のような機能が利用できます。

  • Luaによる設定ファイル。これによりSSH接続サーバの設定(ssh_config)をより動的に構築することができる。
  • サーバ接続、切断時に発火するフックコマンドを設定できる。
  • サーバにタグをつけられ、一覧をターミナルに出力できる。
  • ZshBash用の強力な補完を組み込みで提供。
  • カレントディレクトリごとに設定ファイルの切り替え。
  • Capistranoのような複数サーバに並行してコマンドを実行できるタスクランナー。
  • 設定用のLuaコードはモジュールとしてGitリポジトリ上で共有できる。

一通りの機能に触れるためのチュートリアルを用意したので、利用の際はここから始めるとよいかと思います。

https://essh.sitespread.net/intro/ja/index.html

利用例

上記のチュートリアルには書いていないが、私個人が使っている機能などを中心にいくつか紹介します。

ログイン時にターミナルの色を変える

接続、切断時のフックコマンドhooks_before_connecthooks_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",
    },
}

f:id:kohkimakimoto:20170410083157g:plain

アプリケーションのデプロイ

リモートサーバにコマンドを実行します。以下はただのサンプルです。実環境ではもっと複雑なスクリプトを実行させています。

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."
    ]=],
}

f:id:kohkimakimoto:20170410104414g:plain

以上です。日々の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コンテナの特定の場所にマウントする
  • --rmオプションをつけて、コンテナ停止後自動でコンテナを削除するようにする
  • スクリプトを実行する。

などが共通な処理であったので、これらをまとめて、いい感じにDockerコンテナを起動してその中でスクリプトを実行するコマンドラインツールを作りました。

github.com

Goで実装したシングルバイナリなので、Githubのリリースページからバイナリをダウンロードして、パスの通ったディレクトリに配置すればすぐに使えます。

使い方

まずは単純に

$ buildsh

を実行してみます。これだけで、デフォルトでDockerイメージkohkimakimoto/buildsh:latest(2Gくらいあります)をダウンロードして、カレントディレクトリをコンテナ内の/buildにマウントした状態でコンテナを起動します。そのままbashでログインした状態になるので、たとえばPHPのテストだったら

$ php phpunit

などを実行すればテストができます。PHPPython,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なので実行ファイル一個で動く。導入が簡単
  • レシピは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に見た目が似ていて、かつ変数や制御構文もつかえるので、なかなか便利だと思っています。

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

sshのラッパーコマンドを作った

GoでSSHコマンドに便利機能を追加したコマンドを作りました。

2017/04/11 追記: この記事の内容は古くなっています!

2017/04/11時点の最新の仕様は、次の記事を参照してください

SSHラッパーコマンドEsshのv1.0.0をリリースしました - オープンソースこねこね

追記ここまで。以下は2015/11時点の古い情報となります。

f:id:kohkimakimoto:20151115163938g:plain

github.com

機能として

  • Lua~/.ssh/configに相当する設定を書ける。
  • zshの補完機能を使って、接続先一覧を出す。
  • サーバ接続時にフックを仕込める。自分はスクリーンの色を変えるの使っている。
  • 複数のリモートサーバにまとめてコマンドを実行する。

といったところです。詳細はリポジトリのREADMEを見ていだければと思います。 ビルド済みバイナリをリリースページにおいてあるので、インストールはダウンロードして解凍してパスの通ったディレクトリに配置すればOKです。

~/.ssh/configを上書きするので~/.ssh/configのバックアップを取っておくのをお忘れなく。

https://github.com/kohkimakimoto/zssh/releases/latest

使い方

zsshsshのラッパーコマンドになっているので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のライブラリはそれぞれいくつかあるのですが、両方に対応したライブラリは見つからず、組み合わせたサンプルなども見当たらなかったので、各種ライブラリのソースを読んだりしながら、最終的に以下のものを取り上げてみました

この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でダウンロードすることができ、導入の敷居がすごく下がる。アンインストールもファイルを消せばいいだけなので気楽だ。

ところで自分の場合、RubyPythonなど自分がメインで使っていない言語の処理系を必要とするツールだと、まずその処理系を「正しく」インストールする方法を調べるのに気を使ったりしてしまう。もちろん本当のところは目的のツールが単に動けばいいだけなので、Macにプリインストールされている処理系を使えばいいのだけど、どうせ使うんだったら、Linuxサーバ上のものと同じ環境にしたいとか、メジャーな方法を使いたいとかでrbenvを調べたりごにょごにょ環境周りをいじって時間を浪費することが多くて、しょんぼりする。

そのへんの事情から開放してくれて他に依存が無いのですぐに使えるし、すぐに捨てられるというシングルバイナリは思いの外、快適だったりする。

静的型付けのコンパイル言語

今はIntelliJでGoを書いているので静的型な言語だとIDEの補完や定義先へのジャンプがかなり強力に作用する。ちょっと作業環境が重いと感じることもあるが、許容できる。プライベートで書いているプログラムだと、ちょっと変数の名前の付け方やディレクトリ構成が気に食わないとか、気分でコード全体をもりっと変えることがよくあって、そういうときもコンパイラのチェックがあるので、ある程度のコードの正当性を担保してくれるのがよい。

また、型推論のためコードの見た目はLLっぽいのも気に入っている。

OSSですでに周辺ライブラリがいろいろある、導入も簡単

Githubで検索すれば必要なライブラリは大抵あるし、それを使ったサンプルコードも検索すれば大体出てくる。

困っているところ

GOPATHとimport

もういろんなところで言及されているので詳細は省くが、GoはGOPATHというパスを起点にプロジェクトのコードと依存する外部パッケージのコードを同列にフラットに管理するという独特の方式を採用している。これのせいで明確に困ることがあって、

GithubのGo言語プロジェクトにPull Requestを送るときのimport問題 | SOTA

のようにforkしたプロジェクトだと問題になる。また自分の作業環境だと、依存物をプロジェクトのディレクトリ配下に収められないのでctagsでタグ生成する範囲が広がりすぎる問題がある。で、この辺は以前書いたようにdirenvGomを使ってGOPATHをプロジェクトごとに複数、切り替えて対応している。

Goの開発環境 - オープンソースこねこね

幸いIntelliJもGOPATHを複数設定し、プロジェクトごとに切り替えることができるのでその機能を使っている。 とはいえちょっとややこしくて、非標準のツールに頼っているため悩ましいところではある。

プラグイン的なものが作れない

Goは動的に他のビルド済みのGoのコードをロードすることができないので、後からプロダクトに機能追加できるようにする、いわゆるプラグイン的なものはつくれない。

考えていること

単一で完結するコマンドラインツールは今後Goで書く。

普通のWebアプリだったら依然PHPを使うと思う。