ぼくのかんがえたさいきょうのシングルバイナリ生成ツール

皆さん、シングルバイナリ サイコーですかー！（挨拶）

この記事は [Go4 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/go4) の3日目の記事です。
今日は「ぼくのかんがえたさいきょうのシングルバイナリ生成ツール」を紹介したいと思います。

# ことの発端

[毎回言ってる](https://qiita.com/Maki-Daisuke/items/e0381ae50d95d1a18295)のでもうご存知かもしれませんが、自分はシングルバイナリ大好きな人です。そして、[コマンドをパイプでつなげるのが大好き](https://qiita.com/Maki-Daisuke/items/79146b1c447a36ccf306)な人でもあります。

そんな私が、あいも変わらずアクセスログの分析をしていました。
全体の処理はこの↓シェルスクリプトのようなかんじです：

```bash:pipeline.bash
#!/usr/bin/env bash

xz -cd "$@"                             \
  | jsonize                             \
  | jq -c 'select(.method == "GET")
         | select(.user_agent != "-")'  \
  | paw-ua                              \
  | insert-db
```

圧縮されたログを受け取って、解凍して、JSON形式にして、不要な行を`jq`でフィルタして、UserAgentをごにょごにょ[^1]して、DBにインサートするというだけです[^2]。

[^1]: ギョーム内容につき、お察しください。

[^2]: ちなみに自分は、とりあえずログをJSONに変換してしまって、[LDJSON](https://en.wikipedia.org/wiki/JSON_streaming#Line-delimited_JSON)をパイプラインでひとつひとつ変換処理かけていくのが好みです。こうするとパイプラインの各コマンドが並列で走るので、自然とマルチコアの恩恵を受けられますしね。

上の `jsonize`、`paw-ua`、`insert-db` は、もちろんGoで実装しています。
で、実装したコマンドたちを、本番環境にコピーして使うわけです。こんなふうに：

```shell
$ scp pipeline.bash jsonize paw-ua insert-db prod-server:~/bin
$ ssh prod-server
prod-server> pipeline.bash /var/log/archive/*.xz
```

コピーするだけで動く**シングルバイナリまじサイコー**ですね！

さてここで、ログデータに位置情報を付加してほしいという要望がありました。
というわけで、[GeoIp2](https://github.com/oschwald/geoip2-golang)を使ってサクッと `ip2geo` コマンドを実装しました。
これをpipelineに追加しまして：

```bash:pipeline.bash
#!/usr/bin/env bash

xz -cd "$@"                             \
  | jsonize                             \
  | jq -c 'select(.method == "GET")
         | select(.user_agent != "-")'  \
  | paw-ua                              \
  | ip2geo -db ./GeoLite2-City.mmdb     \
  | insert-db
```

また本番環境にコピーして実行しましたらば・・・

```shell
$ scp pipeline.bash jsonize paw-ua insert-db prod-server:~
$ ssh prod-server
prod-server> pipeline.bash /var/log/archive/*.xz
pipeline.bash: ip2geo: command not found
```

あっ、ハイ、すみません・・・新しいコマンドをコピーするの忘れてました。。。

やっぱり、コマンド履歴にばかり頼ってちゃいけませんね・・・気を取り直しまして、

```shell
$ scp pipeline.bash jsonize paw-ua ip2geo insert-db prod-server:~/bin
```

今度はちゃんとコピーしましてから、実行してみますに～

```shell
$ ssh prod-server
prod-server> pipeline.bash /var/log/archive/*.xz
ip2geo: open GeoLite2-City.mmdb: no such file or directory
```

あゔぁゔぁゔぁゔぁゔぁゔぁ・・・
デスヨネー！GetLite2データベース[^3]をコピるの忘れてましたよねー！！すいませんでしたよねー！！！

[^3]: MaxMaind社の提供するIPアドレスと地点情報の紐付けデータベースです（https://dev.maxmind.com/geoip/geoip2/geolite2/ ）。 [GeoIp2](https://github.com/oschwald/geoip2-golang) で使います。

事ここに至るに当たり、気がついてしまったのです。**おかしい、こんなはずじゃない**、と。

**僕らが夢見ていたシングルバイナリの世界はこんなはずじゃなかった！僕らが夢見ていたのは、バイナリ１つをコピーするだけで動くお手軽便利な世界ではなかったのか！？そうだ、今こそ取り戻そう！僕らの理想の世界を！！**

というわけでWhole-In-One、略して[WIO](https://github.com/Maki-Daisuke/go-whole-in-one/)（わいおー）というのを作りました。


# Whole-In-One なバイナリを作ってみよう

では、WIOの使い方の紹介です。まずはインストールから↓

## Installation

ちょっと面倒なのですが、 `go get` ではなく、次のようにビルドスクリプトを使ってビルドしてください：

```shell
$ git clone github.com/Maki-Daisuke/go-whole-in-one
$ cd go-whole-in-one/cmd/wio
$ ./make.sh install
```

なお、Windows用にPowerShell版もあります[^4]：

[^4]: ちなみに今のところ、Linux（Ubuntu 16.04）、macOS (Mojave)、Windows 10 (1909) で動作確認してあります。たぶん、BSDとかの他のUNIX系OSでも動くとは思いますが、確認環境がないためビルドタグはつけていません。どなたか興味がある人は、誰か試してみてほしいです。

```powershell
> git clone github.com/Maki-Daisuke/go-whole-in-one
> cd go-whole-in-one\cmd\wio
> .\make.ps1 install
```

これで `wio` コマンドが使えるようになりました。

## Init

では、さっそく上の例をシングルバイナリにしていきましょう。
今回作成するシングルバイナリなコマンド名は `mylog` とします。安直ですねー

なにはともあれ、プロジェクト用のディレクトリを作りましょう：

```shell
$ mkdir mylog
$ cd mylog
```

そして、このディレクトリで `wio init` を実行します：

```shell
$ wio init
$ ls
main.go  pack.go  packing-list
```

すると、このように３つのファイルが出来上がりました。とりあえず、ファイルはこのままにしておきます。

## Generate

次に、上の例にあった `pipeline.bash`、`jsonize`、`paw-ua`、`ip2geo`、`insert-db` を、それぞれ次のように頭に `mylog-`がつくようにリネームして、PATHの通っているところに置いておきます：

```shell
mv pipeline.bash mylog-pipeline
mv jsonize       mylog-jsonize
mv paw-ua        mylog-paw-ua
mv ip2geo        mylog-ip2geo
mv insert-db     mylog-insert-db
```

ファイル名の先頭に `mylog-` とつけておくことで、これから作るバイナリに中に含まれるようになります[^5]。

[^5]: あとで紹介しますが、ファイル名を変えなくてもバイナリに含む方法もあります。

リネームしたのに合わせて、pipelineの中身もファイル名を書き換えました：

```bash:mylog-pipeline
#!/usr/bin/env bash

xz -cd "$@"                                       \
  | mylog-jsonize                                 \
  | jq -c 'select(.method == "GET")
         | select(.user_agent != "-")'            \
  | mylog-paw-ua                                  \
  | mylog-ip2geo -db $WIOPATH/GeoLite2-City.mmdb  \
  | mylog-insert-db
```

おっと、GeoLite2データベースもバイナリに含めるのをわすれないようにしましょう。`packing-list`ファイルを開いて、末尾にファイル名を追記しておきます：

```plaintext:packing-list
# This file was generated by wio-init.
mylog-*
./GeoLite2-City.mmdb  # ←この行を追加
```

ここまでできたら、ビルドしましょう：

```
$ wio build
```

これで、`mylog` コマンドができました。

## Deploy

できたコマンドはこんなふうに使えます：

```shell
$ scp mylog prod-server:~/bin
$ ssh prod-server
prod-server> mylog pipeline /var/log/archive/*.xz
```

`mylog` と `pipeline` の間に`-`（ハイフン）がない点にご注意ください。`mylog-pipeline` が `mylog` のサブコマンドのようにして呼び出されていますね。これは、`mylog-jsonize` など他のコマンドについても同様です。

また、GeoLite2データベースも含まれているので、なんらかの外部ファイルに依存しているようなコマンドもシングルバイナリの中に押し込むことができます。なので、バイナリ１個コピーするだけでお手軽便利にコマンドが実行できる世界がやってきました！

やっぱり、**シングルバイナリ サイコーですね！**

## しかしさらなる問題が…

もしかしたら、上のスクリプトを見た人の中には気づいてしまった人もいるかもしれませんね。そう、`mylog-pipeline`スクリプトは `jq` がインストールされている環境じゃないと動かないということを！！[^6]
これでは真のシングルバイナリとは言えないのではないか！！！😨！！！

[^6]: まぁ、それを言ったら `xz` もなんですが、イマドキは `xz` は標準で入っていることが多いので、横においておきます。

しかし案ずるでない、迷える子羊よ。そなたは如何なるファイルでもバイナリに埋め込むことができる！ほれ、このように `packing-list` に追加するのじゃ：

```plaintext:packing-list
# This file was generated by wio-init.
mylog-*
./GeoLite2-City.mmdb
jq  # ←この行を追加
```

そしておもむろにビルドしてコピーすれば、あら不思議・・・

```shell
$ wio build
$ scp mylog prod-server:~/bin
$ ssh prod-server
prod-server> mylog pipeline /var/log/archive/*.xz
jq: error while loading shared libraries: libonig.so.2: cannot open shared object file: No such file or directory
```

エラーで怒られるのである・・・orz

デスヨネー。`jq` さんには [鬼車](https://github.com/kkos/oniguruma) が必要ですもんねー！

### 気を取り直して

はいはい、鬼車がいるんなら、libonig.soを埋め込んであげればいいんでしょ[^7]：

[^7]: ちなみに、作業環境は開発環境・本番環境ともにUbuntuです。（ただし、開発環境はUbuntu on WSL）

```plaintext:packing-list
# This file was generated by wio-init.
mylog-*
./GeoLite2-City.mmdb
jq
/usr/lib/x86_64-linux-gnu/libonig.so.2  # ←さらに追加
```

というわけで、再チャレンジ：

```shell
$ wio build
$ scp mylog prod-server:~/bin
$ ssh prod-server
prod-server> mylog pipeline /var/log/archive/*.xz
```

今度は動きました！[^8]

[^8]: 今回はたまたま`jq`の依存ライブラリが少なくてうまくいきましたが、現実にはこんなふうにうまくいくケースは少ないと思います。依存ライブラリが大量にある場合もありますし、そのライブラリがさらに他のライブラリに依存していたりするので、ただしく必要なライブラリのリストを抽出するのはかなり大変です。従って、シングルバイナリで動くGo製のコマンドか、シェルスクリプトを埋め込むのが現実的な範囲です。複雑な依存関係をデプロイしたいなら、素直にDockerみたいなコンテナで配布するのがいいと思います。だだし、Dockerを使うためにはroot権限が必要なんですよね。root権限なしに、ホームディレクトリにコピるだけで動くのが、シングルバイナリの利点ですよね！


# 仕組み

ちょっとだけWIOの仕組みについて書いておきましょう。

と言っても、とても単純で、`packing-list` で指定されたファイルを tar形式にかためて、バイナリに埋め込んでいるだけです。
`mylog`コマンドが実行されたときに、tarファイルをtempディレクトリに展開して、そこにPATHを通すなどの下準備をしてからサブコマンドに execしているというわけです。ちなみに、tarの展開はコマンドの初回起動時のみで、２回目以降はtempディレクトリのキャッシュが使われます[^9]。

[^9]: お気づきの方もいるかもしれませんが、これは古式ゆかしい(？)[PAR::Packaer](https://metacpan.org/pod/PAR::Packer) と同じ仕組みです。

`wio init` は、デフォルトで `コマンド名-サブコマンド名` という名前のコマンドをPATHから探索して実行する main関数を出力しますので、上記のように `mylog-サブコマンド` という命名規則にしておくと、埋め込んだコマンドがサブコマンドのように呼び出せるようになるのです[^10]

[^10]: ご想像のとおり、サブコマンドをPATHから探索して実行する様式はGitサブコマンドをマネたものです。このあたりのmain関数の挙動は `main.go` を書き換えれば好きなように変更できます。詳しくは [WIOのドキュメントを参照](https://github.com/Maki-Daisuke/go-whole-in-one/)してください。

### で、実際どうやってバイナリに埋め込んでんの？

それは、ソースコードの[このへん](https://github.com/Maki-Daisuke/go-whole-in-one/blob/bfa6ac8f875e0a7182d96d8ca8431b137e10892e/boilerplate.go#L67)を見てもらえればわかると思うのですが、`pack.go` のひな形の中に `%q` というフォーマット文字列が見えるかと思います：

```go
func WritePackGo(data []byte, codec string) error {
	...(中略)...
	_, err = fmt.Fprintf(out, `// DO NOT EDIT! This file was generated by wio-init.
package main
import(
	...(中略)...
)
var(
	data = %q
	hash = "%X"
)
	...(中略)...
`, data, md5.Sum(data), codec)
	return err
}
```

で、ここにtarで固めたデータをドカドカっと流し込んで、Goコンパイラに文字列としてコンパイルしてもらっているだけです。
そうなんですねー。こんな乱暴な方法でも、ちゃんとGoコンパイラはコンパイルしてくれるんですねー。すごいですねー！（小並感）[^11]

[^11]: ちなみにこの手法は、今一番主流のバイナリ埋め込みツールと目される[statik](https://github.com/rakyll/statik)を参考にしています。なので、こんな乱暴な方法でも問題はないと思います。


# Let's enjoy シングルバイナリ

というわけで、皆さんもお手軽便利なシングルバイナリ・ライフを楽しんでください。

詳しい使い方は [WIOのドキュメント](https://github.com/Maki-Daisuke/go-whole-in-one/)を参照してくださいね。

それでは、シングルバイナリまじサイコー！（別れの挨拶）