Dinghyすごく便利です。何が便利って、MacでDockerをシームレスに扱えるのはもちろんのこと、DNSサーバとバーチャルホストを扱えるリバースプロキシもバンドルされていて、ひとつのDockerホストにapp1.docker
・app2.docker
・app3.docker
…とドメイン名を当てつつ、複数のWebサーバが共存できるところです。Dinghyを使うまでは、めんどくさくてVM(docker machine)を複数作って、プロジェクトごとに分けていましたが、VMがSSDの容量を食いつぶすこともありましたが、Dinghyは無駄な容量も減らせるので気に入っています。
モバイル向けウェブサービスを作るっていると、iPhoneやAndroidの実機からDinghy内のコンテナに繋ぎたくなります。が…、これが思いの外面倒でした。
実機からは*.dockerでは繋がらない
当然のことならが、DinghyのDNSサーバはMacに立っています。なので、iPhoneなどの実機でURLに*.dockerを入れてもつながりません…。
stoneでTCPをリピートするもバーチャルホストでNG
次に、stoneを使ってTCP通信をリピートしてみました。
stone app1.docker:80 9000
iPhoneのSafariで192.168.3.2:9000と打って…。結局のところDinghy HTTP Proxyの画面が虚しく表示されるだけでした。それもそのはず、HTTPヘッダのHost
が192.168.3.2:9000
になるので、dinghyのリバースプロキシがupstreamを特定できないためです。
DNSサーバを立てつつ、stoneでTCPリピートすれば行けそうかと思ったのですが、実機の数だけDNSサーバの設定をするのかと考えたら、気が重くなり途中でやめました。
「Macにnginxとかhipacheでリバプロ立てれば?」
これは、同僚から提案されたことです。「ですよねー」と思いつつも、一時的につなげればいいかなという状況で、毎度設定を書くのも面倒だなと思い見送りました…。
欲しいのはHTTPレベルの「stone」
http_stone app1.docker 9000
やりたいことはこういうことなんです。シンプルなコマンドで起動する、その場限りのリバースプロキシ。Macで9000番で受けたHTTPリクエストを、Hostヘッダを書き換えつつそのままapp1.dockerに投げつけるようなもの。
GitHubをあさってみましたが、ぴったりなものが見つからなかったので作ることにしました。それで出来上がったのが「ishi」というツールです(苦笑)。
久々にGo言語書きました。ほぼすべて忘れてました。for構文の書き方ググりました…。あの38日間は一体何だったのか…orz
ishiの使い方
ishiはとてもシンプルなリバースプロキシです。引数にアップストリームのホスト名を渡すだけで起動します。
$ ishi app.docker
Listening on 127.0.0.1:8000
Fowarding to app.docker
この状態で、LAN上の別マシンからhttp://(MacのIP):8000
にアクセスするとapp.dockerを見ることができます。
もっと詳しい説明は https://github.com/suin/ishi のREADMEにあるとおりなので、関心があれば御覧ください。
ishiを作る過程で学んだこと
docopt-goというCLIフレームワークが便利でした
Go言語でCLIフレームワークというとurfave/cliが有名で、ずっとこれを使っていろいろ作ってきたのですが、今回はdocopt-goを試してみました。
unfrave/cliはコマンドの設定を、同ライブラリが提供するAPIをゴリゴリ書いていってコマンドの解釈機と実行機が出来上がる正攻法的なものに対して、docopt-goはヘルプテキストをパーサに渡すとコマンドの解釈機ができあがるという変わり種です。下記はdocopt-goのREADMEにもあるサンプルですが、CLIの設定はヘルプテキストそのままです。
package main
import (
"fmt"
"github.com/docopt/docopt-go"
)
func main() {
usage := `Naval Fate.
Usage:
naval_fate ship new <name>...
naval_fate ship <name> move <x> <y> [--speed=<kn>]
naval_fate ship shoot <x> <y>
naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
naval_fate -h | --help
naval_fate --version
Options:
-h --help Show this screen.
--version Show version.
--speed=<kn> Speed in knots [default: 10].
--moored Moored (anchored) mine.
--drifting Drifting mine.`
arguments, _ := docopt.Parse(usage, nil, true, "Naval Fate 2.0", false)
fmt.Println(arguments)
}
docopt.Parse
がコマンドに渡された引数をパースしてarguments
を返してきますが、その後の処理は自分でゴリゴリ書く形になります。複雑なコマンドを実装するなら、APIに関数を渡していけば実行機まで出来上がるunfrave/cliが良さそうですが、今回作ったishiは引数が一つしかないシンプルなものなのでdocopt-goを採用しました。
使えるポートを探すロジック
ishiでは--listen
オプションでリバースプロキシのポートを指定しない場合、Mac側でlisten可能なポートを探す仕様になっています。
この仕様は無くてもいいかなと思いましたが、毎回リッスンポートを考えるのも面倒だなと思ったので実装してみました。findAvailablePort
関数がその実装です。
func findAvailablePort() (int, error) {
for port := 8000; port < 9000; port++ {
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err == nil {
defer ln.Close()
return port, nil
}
}
return 0, errors.New("There is no available port to listen")
}
おわり
めんどくさがりで本当にすみませんでした?