suin.io

ishi:LAN上の別機からコンテナのウェブサーバにささっと接続できるツールを作った

suin2017年1月3日

Dinghyすごく便利です。何が便利って、MacでDockerをシームレスに扱えるのはもちろんのこと、DNSサーバとバーチャルホストを扱えるリバースプロキシもバンドルされていて、ひとつのDockerホストにapp1.dockerapp2.dockerapp3.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ヘッダのHost192.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」というツールです(苦笑)。

https://github.com/suin/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")
}

おわり

めんどくさがりで本当にすみませんでした?

RELATED POSTS