Help us understand the problem. What is going on with this article?

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

皆さん、シングルバイナリ サイコーですかー!(挨拶)

この記事は Go4 Advent Calendar 2019 の3日目の記事です。
今日は「ぼくのかんがえたさいきょうのシングルバイナリ生成ツール」を紹介したいと思います。

ことの発端

毎回言ってるのでもうご存知かもしれませんが、自分はシングルバイナリ大好きな人です。そして、コマンドをパイプでつなげるのが大好きな人でもあります。

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

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

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

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

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

さてここで、ログデータに位置情報を付加してほしいという要望がありました。
というわけで、GeoIp2を使ってサクッと ip2geo コマンドを実装しました。
これをpipelineに追加しまして:

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

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

$ 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

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

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

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

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

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

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

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

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

というわけでWhole-In-One、略してWIO(わいおー)というのを作りました。

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

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

Installation

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

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

なお、Windows用にPowerShell版もあります4

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

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

Init

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

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

$ mkdir mylog
$ cd mylog

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

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

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

Generate

次に、上の例にあった pipeline.bashjsonizepaw-uaip2geoinsert-db を、それぞれ次のように頭に mylog-がつくようにリネームして、PATHの通っているところに置いておきます:

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

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

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ファイルを開いて、末尾にファイル名を追記しておきます:

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

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

$ wio build

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

Deploy

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

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

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

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

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

しかしさらなる問題が…

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

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

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

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

$ 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 さんには 鬼車 が必要ですもんねー!

気を取り直して

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

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

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

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

今度は動きました!8

仕組み

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

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

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

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

それは、ソースコードのこのへんを見てもらえればわかると思うのですが、pack.go のひな形の中に %q というフォーマット文字列が見えるかと思います:

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

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

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

詳しい使い方は WIOのドキュメントを参照してくださいね。

それでは、シングルバイナリまじサイコー!(別れの挨拶)


  1. ギョーム内容につき、お察しください。 

  2. ちなみに自分は、とりあえずログをJSONに変換してしまって、LDJSONをパイプラインでひとつひとつ変換処理かけていくのが好みです。こうするとパイプラインの各コマンドが並列で走るので、自然とマルチコアの恩恵を受けられますしね。 

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

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

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

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

  7. ちなみに、作業環境は開発環境・本番環境ともにUbuntuです。(ただし、開発環境はUbuntu on WSL) 

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

  9. お気づきの方もいるかもしれませんが、これは古式ゆかしい(?)PAR::Packaer と同じ仕組みです。 

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

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away