つい先日、 NGINX 公式ブログでアナウンスされた UDP ロードバランシング機能が気になっていたので、まだ正式リリース前ではありますが試してみました。
構成
UDP で代表的なプロトコルといえば DNS なので、今回は
graph LR
Client --- NGINX
NGINX --- dns1[Dnsmasq1
example.com A 127.0.0.10]
NGINX --- dns2[Dnsmasq2
example.com A 127.0.0.20]
という構成を Docker Compose で作り、 NGINX に対して dig
コマンドで example.com
の名前解決クエリを送った時に、 127.0.0.10
と 127.0.0.20
が交互に返ってくるかどうかを見ることにしました。この構成を作るに当たって必要なファイルは Github リポジトリとして公開しています。
準備
NGINX
まず、 UDP ロードバランシング機能が含まれている NGINX を用意する必要がありますが、現時点では開発リポジトリ上にのみ存在する状態のため( 1.9.13 で正式リリース予定)、 Mercurial 経由で
hg clone http://hg.nginx.org/nginx
としてソースコードを取得し、
cd nginx
./auto/configure --with-stream
make
make install
のように --with-stream
オプションをつけてコンパイルする必要があります。今回はこれらの一連の NGINX のビルド処理を、 NGINX の Docker 公式リポジトリ の alpine タグの Dockerfile を元に手を加えた Dockerfile を作り、自動化しています。
NGINX が用意できたところで設定ですが、 UDP のみのロードバランシングの場合非常にシンプルなものになります
worker_processes 1;
events {
worker_connections 1024;
}
stream {
upstream dns_udp_upstreams {
server ${DNS1_PORT_53_UDP_ADDR}:53;
server ${DNS2_PORT_53_UDP_ADDR}:53;
}
server {
listen 53 udp;
proxy_pass dns_udp_upstreams;
proxy_timeout 1s;
proxy_responses 1;
}
}
upstream
の設定は通常と同じで、ポイントは server
部ですが、新たに listen
ディレクティブに udp
が指定できるようになっています。
また、 proxy_responses
ディレクティブが追加され、それぞれのクライアント要求において、いくつアップストリームサーバから UDP パケットを受けとったかを数え、 proxy_responses
と同じだけパケットを受けとった場合には接続を閉じる、という処理を定義できます。デフォルトは無制限で、その場合、 proxy_timeout
の時間が経過するまで UDP パケットの到着を待ち続ける形となるようです。
Dnsmasq
実際の DNS リクエストに応答する Dnsmasq は andyshinn/dnsmasq の Docker イメージを使い、二つのサーバの起動オプションにシンプルに --host-record
を追加し、
dnsmasq -k --host-record=example.com,127.0.0.10
dnsmasq -k --host-record=example.com,127.0.0.20
という形でそれぞれ立ち上がるよう設定することとしました。
Docker Compose
上記の NGINX, Dnsmasq の一連の処理を docker-compose.yml にまとめ、 docker-compose up
で環境全体を構築できるようにしています。
動作確認
docker-compose up
を実行して環境を構築後、 Docker Machine を使っている場合はリクエスト先の IP アドレスを docker-machine ip your-machine
で調べておき、下記のようにコマンドを実行すると、交互に結果が返ってくることが確認できるかと思います。
$ dig @192.168.99.100 example.com +short
127.0.0.10
$ dig @192.168.99.100 example.com +short
127.0.0.20
$ dig @192.168.99.100 example.com +short
127.0.0.10
まとめ
非常にシンプルな設定で UDP ロードバランサが構築できることを確認できました。
これまで UDP ロードバランサを構築しようという場合には、ユーザーモードで動作するロードバランサ、リバースプロキシソフトウェアの選択肢がほぼ存在せず、たとえば Linux においては LVS を用いてのロードバランシングが一般的でした。しかしながら LVS はカーネルモードでの動作を必要とし、昨今隆盛を極めているコンテナ内では使用できないなど、 UDP をベースにしたプロトコルを持つサービスを構築する場合には、物理サーバまたは仮想マシンに頼らざるを得ない状況でした。
今回 NGINX による UDP ロードバランシング機能の提供により、他の TCP, HTTP などのプロトコルと同様に、 UDP もより自由度の高い形でロードバランサを構築することが可能となったことから、今後のサーバ構築時の選択肢を大きく変えていくのではないか、と期待しています
これは NSEG Advent Calendar 2015 -Adventar 15 日目の記事です。
前回 に引き続き Docker の話ですが、 Tiny Core Linux の Docker イメージとその仲間(たとえば tinycore-ruby)を作った話をまとめておこうと思います。
Tiny Core Linux とは?
The Core Project で作成されている Linux ディストリビューションの一つで、有名どころでは Docker Machine の OS としても使われていたりします。コア機能に絞り込むと 10MB 以下、 GUI 関連パッケージまで含めても 100MB 以下と、ディスク消費が非常に小さいながらデスクトップ環境まで構築でき、独自形式ながらパッケージ機構も持っているため、他のメジャーなディストリビューションと比べて使用が難しすぎるということもなく、なかなか便利なディストリビューションです。
イメージ作成動機
Docker をある程度使っていると感じることかと思いますが、公式で提供されている各種ディストリビューションの Docker イメージは容量が少なくとも 200MB 越えしているものがほとんどで、かつ、それを元にした各言語の公式実行環境はそれ以上の容量となることが一般的です。
一方で、特に Node.js などは比較的頻繁にリリースされ、その都度環境を更新することも多くなりますが、その度に大容量のイメージをダウンロード・展開することになります。
そこで、もっとディスク容量を抑えた環境はできないかなと思っていたところ、 Tiny Core Linux の存在を知り、特定の一言語の実行環境であればもっとコンパクトなものが作れるのではないかということで、イメージ作成を試してみることにしました。
結果、基本の OS イメージでは 7MB 弱、先に上げた tinycore-ruby イメージでも 60MB そこそこと、一通りの機能を組み込んだ上でもかなり容量を削ることに成功しています。
Tiny Core Linux 基本イメージ
各言語の環境を作る前に、まずは基本となる OS イメージが必要なので、それを x86, x86_64 それぞれで配布されている rootfs.gz ないし rootfs64.gz から構築しています。
この rootfs.gz は cpio 形式でアーカイブされていますが、 Docker に読み込ませるには tar 形式である必要があるため、一度これを展開してアーカイブし直す必要があります。
また、 Tiny Core Linux のパッケージは squashfs を利用して作成されており、パッケージインストール時にはこの squashfs をマウントしてファイルを取りだす仕組みになっているため、そのまま rootfs.gz を展開してイメージを作ってしまうと、パッケージ機構を利用する時にはコンテナに privileged 権限をつけて起動する必要が出てきます。これでは日常利用で不便ですので、それを回避するために、 unsquashfs を入れると同時にパッケージプログラムに少々手を加え、 privileged なしでもパッケージを扱えるようにしています
イメージ生成に Docker の Automated Build を使えるようにするため、イメージ作成に必要な処理は Github にて公開していますので、詳しくはそちらを参照してください。
tinycore-ruby イメージ
OS イメージができたので、それを元にして使いたい言語の環境を作るのですが、ここでは Ruby のものを例として見ていきます。
基本的には公式の Ruby イメージと同じような使い勝手になるよう、公開されている Dockerfile の手順と同様に記述していくことになりますが、元々の目的が容量削減でしたので、一度インストールしたパッケージも動作に必須でなければ極力アンインストールするなどしています。
また、公式でも同様ですが、 RUN
の処理を、できるだけ
RUN gem install bundler --version "$BUNDLER_VERSION" \
&& bundle config --global path "$GEM_HOME" \
&& bundle config --global bin "$GEM_HOME/bin"
のように \ &&
でつなげて書いています。これは、同一のシェルコンテキストで実行とするという意味合いもありますが、それぞれを分けて、たとえば
RUN gem install bundler --version "$BUNDLER_VERSION"
RUN bundle config --global path "$GEM_HOME"
RUN bundle config --global bin "$GEM_HOME/bin"
のように書くと、それぞれの RUN
の処理中で生成されたデータがそのままイメージ容量に加算されていき、後の処理で不要なデータを削除したとしてもイメージ全体のサイズが減らなくなってしまうため、それを回避する目的が主です。同一コンテキスト内で最終的に不要なデータを削除しておけば、その分はイメージ容量には加算されないので、容量肥大化を防ぐことができます。
ただし、このように書いてしまうと、一度イメージ作成処理に失敗すると、次に実行する時もキャッシュが効かず、同一コンテキストの最初から(つまり RUN
の最初から)やり直しになってしまい、特に大きなパッケージファイルをダウンロードしている時など実行時間がかかって待たされることになるため、試行錯誤している時などは個別の RUN
に分割しておくと、キャッシュの恩恵を受けて効率良く作業ができます。処理が固まったら改めて \ &&
でひとまとめにするといいでしょう。
こちらも Github にてイメージ生成過程は公開してますので、具体的な内容などはそちらを参照してください。
まとめ
以上のような流れで、 Ruby 以外の言語のイメージや、特定のプログラムだけを配置したイメージも比較的簡単に作ることができます。容量だけで見れば公式で配布されている busybox イメージを使う、という方法もありますが、それだけでは機能不足であるとか、容量は抑えつつ欲しい機能を手軽に追加したい、という向きには、このイメージは便利に使っていただけるかと思います。
余談ですが、現在公開中の Ruby, Python, Node.js 以外にも、 PHP, Go などの作成も試してみたのですが、 PHP は TinyCore のパッケージに含まれないライブラリへの依存関係が多く挫折、 Go は作成できたものの公式と容量が 100MB も違いがなく(主に go get
などの内部で使われる git, mercurial, subversion を組み込んだ影響)、あえて作る意味もないということで公開はしないこととしました。これらの言語では素直に公式イメージを使うのが吉、ということのようです。
これは NSEG Advent Calendar 2015 -Adventar 11 日目の記事です。
NSEG とは?
一言で言うと「長野の IT 勉強会」ということになりますが、勉強会と言ってもお堅いものではなく、長野県やコンピュータにかかわることを幅広く取り上げて、楽しんだり技術の向上を目指す、といった感じで活動しております。詳しくは nseg.jp もご覧ください。
これは何?
先日の「理論から学ぶデータベース実践入門」読書会スペシャルの懇親会で、「 Docker 周辺が盛り上がっているけど、実際業務に使ってたりする?」ということを聞かれたので、自分は「独自で RPM なんかのパッケージを作る時に便利に活用してますよ」ということを話したのですが、じゃあ具体的にどんなことをしているのか、というのを公開している h2o-rpm を元に書いてみようかな、という趣旨です。
Docker 自体はウェブアプリケーションなどの展開・運用にそのまま便利に使え、そちらが本筋とも思いますが、こんな使い方もできる、という一例にしてもらえれば幸いです。なお RPM の SPEC の書き方については、今回は省略します。
パッケージング作業の概要
そもそも RPM などのパッケージを作る時は、パッケージ対象のソースコードと関連ファイル、 SPEC ファイルのようなパッケージング手順が書かれたファイルなどが必要なわけですが、もう一つ、パッケージを作成する対象のディストリビューションのクリーンな環境が必要です。しかしながらこれを毎回物理サーバやいわゆる仮想マシン上でやっていると、メンテナンスが大変、起動に時間がかかる、など作業上のストレスがあっという間にたまることになるので、気軽に作成・削除のできる Docker でその部分をやろう、ということになります。かつての boot2docker や、最近では Docker Machine のおかげで、自分のマシン上でもお手軽に作業ができるのも利点です。
その場合のおおまかなパッケージ作成の流れは、
- パッケージ作成対象のディストリビューションの環境を整える
- パッケージ作成に必要なソースファイルなどを Docker コンテナに展開
- ビルド、パッケージングを行う
- できたパッケージを Docker コンテナから取り出す
となりますが、 h2o-rpm ではこれを Dockerfile
と Makefile
で記述して、
Dockerfile
を使ったイメージビルドを利用して、環境準備、ソース展開、ビルド、パッケージング
- できあがったイメージからデータ取り出しのためだけのダミーコンテナを作ってパッケージを取り出し
という形に落とし込んで、 make
一つで自動でパッケージングできるようにしています。パッケージングまでの部分は docker run
でやるのも一つなのですが、イメージビルドを使うことにしたのは、
docker run
だと、ローカルファイルシステム・コンテナ間でデータの受け渡し方法を用意するのが面倒
- 結局自動処理のためにシェルスクリプトを書くことになるのなら、
Dockerfile
の方がシンプルで見通しがよい
Dockerfile
の各ステップの中間イメージが残っていくため、失敗した時の作業再開や調査がやりやすい
という理由によるものです。
以下、リポジトリの Dockerfile
のうち Dockerfile.centos7
と Makefile
を元に、この流れを見ていきます。
ビルド環境の準備
まず、ビルド環境の準備は Dockerfile
に記述してある
FROM centos:7
ENV HOME /
RUN yum update -y
RUN yum install -y rpm-build redhat-rpm-config rpmdevtools cmake gcc-c++ tar make openssl-devel ruby bison
RUN rpmdev-setuptree
RUN echo '%dist .el7' >> /.rpmmacros
で行っています。 centos7
の公式イメージを元に、環境内のパッケージの全アップデート、ビルド作業に必要なコンパイラなどのパッケージやビルド対象と依存関係にあるパッケージのインストール、ビルド用ディレクトリツリーの作成などを順に実施しています。
ENV HOME /
については、 RPM のパッケージング処理がホームディレクトリ配下の rpmbuild
ディレクトリを使う、という方式なのと、一般的なコンテナ内ではトップディレクトリが起点になっているというのを合わせる意味で指定しています。イメージ環境自体は使い捨てなので、通常やらないだろう設定をやってもいっこうに構いません。
ソース展開
ソースの展開は Makefile
の
cp Dockerfile.$* Dockerfile
tar -czf - Dockerfile rpmbuild | docker build -t $(IMAGE_NAME) -
と Dockerfile
の
ADD ./rpmbuild/ /rpmbuild/
の共同作業になります。
まず Makefile
の方ですが、各ディストリビューション用に Dockerfile.{dist}
のような形でを用意しているものを Dockerfile
にコピーした後、パッケージ用ファイルを配置してある rpmbuild
ディレクトリとともに tar.gz
形式にまとめ、それを docker build
に STDIN
経由で渡しています。このように渡されたファイルは、 tar.gz
内のディレクトリツリー構造そのままに Dockerfile
の ADD
や COPY
で参照できるので、これを使ってコンテナ内の /rpmbuild
以下にファイルを展開しています。
この辺りについては Docker の build コマンドのリファレンス、 Dockerfile のリファレンス に詳しいです。
また、最初にこの方式で書いた時期とは異なり、現在では .dockerignore
や docker build
の -f
オプションも使えますので、上記のような形ではなく、これらの機能を使うことでも同様のものを実現可能と思います。
ビルドとパッケージング
ここは Dockerfile
内の記述になりますが、
RUN rpmbuild -ba /rpmbuild/SPECS/h2o.spec
RUN tar -czf /tmp/h2o.tar.gz -C /rpmbuild RPMS SRPMS
CMD ["/bin/true"]
という形で、 RPM のビルド処理を行い、できあがったものを /tmp
以下に tar.gz
ファイルとしてまとめています。このアーカイブファイルは次の取り出しで参照します。
最後の CMD
行は、イメージから作成したコンテナを即時終了させるために便宜的に設定したものとなります。
パッケージの取り出し
ここは Makefile
側の処理になりますが、前のステップまででできあがったイメージを参照し、
docker run --name $(IMAGE_NAME)-tmp $(IMAGE_NAME)
mkdir -p tmp
docker wait $(IMAGE_NAME)-tmp
docker cp $(IMAGE_NAME)-tmp:/tmp/$(TARGZ_FILE) tmp
docker rm $(IMAGE_NAME)-tmp
のコマンドを順に処理して、 Dockerfile
で生成された tar.gz
ファイルを取り出しています。
まず docker run
で、 Dockerfile
で作成したイメージを元にダミーコンテナを起動しています。 Dockerfile
内で指定してある CMD ["/bin/true"]
により、このコンテナの実行は即座に終わります。欲しいのはダミーコンテナ内のファイルなので、コンテナが起動している必要はありません。念のため docker wait
を入れて、コピー処理の前にコンテナが停止しているのを確実にしています。
ダミーコンテナが停止した後は docker cp
を使い、コンテナ内のファイルをローカルのファイルシステムにコピーし、用が済んだダミーコンテナを docker rm
で削除しています。
欲しいファイルを取り出せたので、後は好きにこれを展開すればいいのですが、本リポジトリではこれを {dist}.build
のようなディレクトリ配下に展開しています。
まとめ
以上のような形で自動パッケージングを行い、ローカル側にコピーしてきているわけですが、もちろんできたものを直接特定のサーバにアップロードするなども自由自在です。対象もパッケージングに限らず、 Docker 環境中でなにかしらの成果物を作り、取り出す、という方法として一般的に使える、と思います。
また、ここでは単に好みから Makefile
を使っていますが、シェルスクリプトやバッチファイルでも構いません。かつては tar.gz
などのアーカイブファイルを渡さなければいけない以上、 Windows で実行するのは若干難しかったように思いますが、今では前述の .dockerignore
や docker build
の -f
オプションもあるので、敷居は下がったのではないかと思います。
さらに Windows Server 2016 では Docker がサポートされることも発表されていますので、 Windows アプリケーションも同じような手法でパッケージングできたりするのかもしれず、今後が楽しみです
docker-machine を使っている環境上で MacVim を起動すると、なぜか編集ウィンドウが開かない問題に遭遇したので、対応内容をメモ。
結論から言うと、 docker-machine の仮想マシン起動後に設定される各種環境変数(たとえば DOCKER_HOST
)を自動設定するために .bash_profile
の中に書いていた
if [ "$(docker-machine status myvm)" = "Running" ]; then
eval "$(docker-machine env myvm)"
fi
の部分、特に eval "$(docker-machine env myvm)"
が問題を引き起こしており、これを MacVim からの起動時だけ実行しないようにすることで解消した。具体的には、 MacVim から起動した時は TERM
環境変数が dumb
となり、通常起動時の値とは異なっていることを利用して
if [ "$TERM" != "dumb" -a "$(docker-machine status myvm)" = "Running" ]; then
eval "$(docker-machine env myvm)"
fi
とした。
具体的にこの行がダメな理由についてはよくわからない。 MacVim 自体のソースコードを追った結果、 MacVim は mvim コマンドを開くために内部的にログインシェルを開き、そこからコマンドを起動しているらしく、その流れで .bash_profile
が読み込まれて実行されているようだったが、 eval
自体は同じ .bash_profile
中の他の部分でも使用しているので、直接それが問題のようには見えなかった。この辺りは機会があれば調べてみることに。
前回 は Docker 上で Libreboard を動かしてみたが、今のところ Libreboard の開発は非常に活発で、更新を Docker コンテナに反映させるたびに長々としたコマンドを叩くのが不便に感じてきたため、 Fig を使って必要なコマンドなどをあらかじめ定義しておき、 fig build
や fig up
で楽に更新適用ができるようにしてみた。以下はその手順
- Fig をインストール。今回は手元のサーバが Gentoo だったので emerge で入れてしまったが、 Installing Fig を見る限り、他の環境でも簡単に導入できると思う
Fig の作業ディレクトリを適当に作成して移動する
mkdir fig_libreboard
cd fig_libreboard
fig.yml
を作成する。今回は下記のようにしてみた
libreboard:
build: ../libreboard
links:
- mongo
ports:
- "5555:8080"
environment:
ROOT_URL: "http://libreboard.example.com"
command: "sh -c 'export MONGO_URL=mongodb://$MONGO_PORT_27017_TCP_ADDR:$MONGO_PORT_27017_TCP_PORT/libreboard; /meteor-run.sh'"
data:
image: busybox
volumes:
- /data/db
backup:
image: busybox
volumes_from:
- data
mongo:
image: mongo
volumes_from:
- data
entrypoint: mongod
libreboard
, data
, backup
, mongo
の 4 台構成になっている。それぞれの役割は、
libreboard
はアプリケーションを稼働させるコンテナ。先ほど作った fig_libreboard
と同一階層の libreboard
ディレクトリにある Dockerfile を読み込んで Docker イメージを構築し、それを実行する。データストアとなる MongoDB コンテナとリンクして、起動時に環境変数経由で接続を行っている。
data
は Managing data in containers - Docker Documentation で言うところのデータボリュームコンテナで、 MongoDB のデータを保持するためだけに使われている。
mongo
は実際に MongoDB が稼働するコンテナ。 data
のボリュームに接続してデータをそこに保存している。
backup
は、 data
からは /data/db
の中身が見えなかったため、確認作業などのために便宜上作成しておいたコンテナ。これがあると気軽に fig run --rm backup ls -al /data/db
などと打って中身を確認したり、 tar でのバックアップ作業がやりやすくなる。が、特別稼働そのものには関係ないので、なくてもかまわない
といったところ。起動コマンドやポートの設定は 前回 のものを引き継ぐ形で指定している
上記設定ができたら、 fig up
を実行してコンテナ群を起動する。初回は libreboard
コンテナ用のイメージのビルドが実行されるので、少しばかり時間がかかる。無事に起動してウェブブラウザで 5555 番ポートにアクセスしてログイン画面が出れば成功。バックグラウンドでの起動に切り替えたい場合は、いったん Ctrl-c
を入力してコンテナ群を停止し、改めて fig up -d
で起動すればよい。
Libreboard の更新があった場合は fig build
を実行して Docker イメージを再作成する。イメージ作成に成功したら fig up -d
を実行して環境を再起動すればよい
Fig を使うと、このような感じで一度 fig.yml
を書くだけで、機能ごとに分割した複数のコンテナを保持しつつ、日常の操作は簡単な fig
コマンドを実行するだけで済むようになる。今回の Libreboard に限らず、 Fig を使うことでホスト側の環境にあれこれ手を入れずとも気軽にウェブアプリケーションの実行環境を用意できるので、単一ホスト内で実行させたいウェブアプリケーションの管理には非常に優れていると思う