ページ

2016年3月15日火曜日

Dockerと付き合う為の虎の巻 その02

 「Dockerと付き合う為の虎の巻 その02」です。今回はDockerコンテナイメージ作成についての説明の序章編的なものとなります。

 さてはて、大変便利なDockerコンテナ君ですが、docker searchコマンドやDockerHubでの検索にて色々と有名なソフトウェアの名前を入れつつ検索してみると、様々なソフトウェアが公式コンテナを公開しています。このような大きな流れの広がりを見ていると、「よーし今日から家もDockerだ!」と、Dockerコンテナを作って公開したくなるのが 人情といいますか、職業的な病気的なアレというものです。きっとチャレンジして損は無いに違いないですね!(=ノ゚ω゚)ノ ポヂテブ!

 それでは早速Dockerコンテナイメージを作ってみようとなる訳ですが、実際に作ろうとすると色々と謎のルールに遭遇して困惑する事が多いのがDocker君の困った所であったります。基本的にはそのような謎のルールについては公式ドキュメントに書いてあるに違いないので、別に謎ではないはずなのですが、公式ドキュメントは英語ですし、なかなか量も多いですし、肝心な事は書かずにスルーしていたり、散々引っ張ったあげく一番最後の方に、なおそんな機能は無いので自分で頑張るのだと書いてあったりと、Dockerを始めたばかりの頃は色々と苦労するものです。

 というわけで今回はそんな感じのルール等を忘れないようにTips化しておくの巻となります。慣れている方には当たり前の事ばかりかもしれませんが、この辺りのルールを知らないまま使用する事により、Dockerが不審な挙動を起こす事もあります。 そして、そのままDockerが不安定なものであるとか、怪しいものであるとか、ネガティブなイメージを持ってしまい、離れて行ってしまう方も一定数以上いるのではないかという気もしていますので、そのような経験を持ってDockerが嫌いになった方にもお勧めの内容となっておりますので、ぜひご覧になってみてください。


 それではDockerTipsの始まりです。(=ノシ゚ω゚)ノシ シャカシャカ



〇 公に公開するようなDockerコンテナイメージはDockerfileを記述して定義して作成するものである


 とりあえず最初なので基本からです。基本的には公開するようなDockerコンテナイメージは全てDockerfileから作るようにしなければならないと思って問題ありません。やり方によっては「起動した後に内部に入って設定等を行い、その時点のイメージを保存してマスターとする」等の方法も可能ですが、公開するようなコンテナでは原則としてはやりません。ただし、そのような方法の方がはるかに楽なケースも多いですので、非公開の開発用とか個人の趣味的な用途のコンテナ等、比較的どうでも良いものに関してはDockerfileを一々書く必要はないかと考えられます。


〇 Dockerコンテナは1つのコンテナに1つのプログラム(プロセス)を起動する事を基本として作成し、使用するものである


 1コンテナ1プログラム(プロセス)のルールです。これはDockerfileのENTRYPOINT(最初に起動するプログラム)として1つのプログラムを指定し、それ以外は起動しないという意味となります。シェルスクリプトを置いてserviceコマンド等で起動する等ではありません。例えばredisが起動するコンテナであればENTRYPOINTとして/usr/bin/redis-server等と直接プログラムを指定する事によりプログラムを1つ起動するという意味になります。(※実際の所はシェルスクリプトをENTRYPOINTに指定し、内部でexecコマンドによるプロセスの置き換えを実行する事により、目的のプログラムを起動するのが一般的です。説明がややこしいですが。)

 また、このENTRYPOINTで指定して起動するプログラムはフォワグラウンドプロセスとして起動される必要があります。要は普通のプロセスとして起動しっぱなしにする必要があります。単純にENTRYPOINTで起動したプロセスが終了すると、Dockerコンテナもそのまま終了する仕組みに
なっているというのもありますが、後述のPID1の問題やログの問題他様々な要因によるルールとなっております。

 通常の今迄の非コンテナのアプリケーションの場合、そのアプリケーション単独で動作するのではなく、例えばcronに登録して時間で処理を実行させたり、ログはsyslogサーバに出力したりetc...と各種アプリケーション間で連携して動作するようになっていたりする事も多いものですが、Dockerコンテナでは基本的にはそのようなものは同時に動かしたりはしないような方針を採る事となっているようです。そのような場合は例えばcron処理のみを行う別のコンテナを作成し、2つのコンテナを起動する事により、1つのアプリケーションと成すような作り方が基本形となってくるようです。

 この1コンテナ1プログラム(プロセス)というルールは必ずしも必須ではないかと思いますが、なにせDockerの製作者サイドがそのように想定しているものですので、この道を外れると色々と不都合が出て来る場面も出て来るかもしれません。初見で聞くと「へ〜」ぐらいの印象であったりするのですが、実際に作り始めるようになると必ず落とし穴にハマるポイントではないかなと思います。


# Dockerfileで言う所のココです
ENTRYPOINT /usr/sbin/redis-server
CMD []

 なお、どうしても複数のプロセスを起動したい場合等はsupervisord等のソフトウェアを介して1つのプロセスを主として起動し、そのプロセスに下位のプロセスをまとめて管理させる手法を採る事が推奨されています。


〇 Dockerコンテナで起動したプロセスはPID1(プロセスIDが1)のプロセスとして起動される


 前項の1コンテナ1プログラム(プロセス)のルールにおいて、最もやっかいな部分でもあるのですが、基本的に何も起動していないDockerコンテナでは文字通り、何もプロセスが起動していないような状態にあります。

 よって最初に起動する1コンテナ1プログラム(プロセス)のプロセスはPID1のプロセスとして起動する事になります。

 Linuxでは通常、起動時にはまずinitプログラムがPID1のプログラムとして起動され、その他のプログラムはこのPID1のプログラムの子プロセスとして起動するような状態となります。つまり、PID1のプロセスはその他のプロセスと一線を介した特殊なプロセスであり、またLinux的にもそのような動作を求めているのが実際の所であったりします。

 実はDockerにて1コンテナ1プログラム(プロセス)のプロセスとして起動する場合にもこの条件は当てはまる為、Dockerで起動するプログラムはPID1としての動作をする事がLinuxから求められている事になります。

 この問題は見た目以上にやっかいな問題であったりします。PID1に求められる独特の挙動により、不審な動きをする事が実際に確認されていますが、Linux的には当たり前の挙動であったりする為、Docker固有のクセと混同しがちであったりします。

 どうしてもという時は前項にありました、supervisord等をあえて1枚かましてみるのも手かもしれません。supervisordはPID1として動作させた場合、PID1として求められる振る舞いをしてくれるようです。


〇 Dockerコンテナの問題の無い通常の停止(docker stop 等)はPID1に対するSIGTERMシグナルによって行われる。


 またまたPID1ネタであったりしますが、今回は割と普通です。docker stopコマンドでdockerコンテナを停止した場合、PID1(ENTRYPOINTで指定したプロセス)にSIGTERMが到着しますので、受け取ったプログラムはそれを元に適切に終了処理を行う必要があります。

 SIGTERMはkillコマンドを実行した時に使用されるシグナルですので、適切に作られたプログラムであれば基本的には問題無く停止処理に入ります。自作のプログラム等を動作させる場合は、SIGTERMによる停止処理の対応を忘れないようにする必要があります。

 ちなみにsupervisordを用いた場合には、supervisordにSIGTERMが到着すると、その配下にある子プロセスにSIGTERMを連鎖的に送出するような仕組みがあるようですので、適切に作られているプログラムであればsupervisord経由でも問題なく停止出来るかと考えられます。

 なお、docker stop 等で停止命令としてSIGTERMが送出された際には、10秒以内に適切に終了しないと強引にkillされてしまいますので、適切に処理がなされない場合、データ破損等の危険がありますので注意が必要です。


〇 Dockerコンテナは基本的には環境変数によりそのコンテナの挙動を変える設定を行い起動するものである。


 なんか小難しい書き方になっておりますが、極端に言い換えれば「設定は全部環境変数で行う事」と言い換えても良いかもしれません。通常の今迄の非コンテナのアプリケーションですと、インストールした後に設定ファイルを書き換えて、それから起動スクリプトを有効にしてetc...といった流れで使用する事になるのですが、Dockerコンテナでは基本的にインストール、起動、設定は同時に行うようなイメージとなります。簡単に言うと「環境変数を指定してコンテナを起動した→やった動いた!」となるのが理想です。

# Dockerfileで言う所のこんな感じの所です
ENV DATABASE_URL="example.jp"


〇 Dockerコンテナ内部のストレージは遅いのでそのままデータディレクトリとしては使わない/使えないものである。


 Dockerコンテナのイメージをpullしたり作成したりすると分かるのですが、1つのDockerコンテナは階層化された複数のイメージがセットとなり動作するようになっています。Dockerでは、このような階層化されたイメージの仕組みを利用して、ある特定の時点での状態を新しいイメージとして保存する事が出来たり、特定のイメージから継承や派生等を行いつつ新しいイメージを作成したり等が出来るようになっています。そのような理由もあり、基本的にはDockerコンテナ内部のストレージ領域はとにかく遅いと言いますか、頻繁に書き換えが走るようなデータディレクトリにはそもそも向いていないものと言えるかと思われます。

 そのような場合、一般的にはホストのボリュームをマウントして使用するか、Dockerfile中でVOLUME指定を行う等により、速度面の問題を回避する事となります。ただし、同時にDockerコンテナ内部のストレージである事のメリットも失われてしまいますので、使用方法には注意が必要です。

# Dockerfileで言う所のこんな感じの所です
VOLUME /var/lib/example


〇 Dockerfileには基本的には同じ項目は同じ1行にまとめて記述するようにする


 Dockerfileを実際に記述する場合、例えば環境変数3つを定義したい場合にそのまま書くと
ENV ARG01="101"
ENV ARG02="102"
ENV ARG03="103"

 のように3行のENV定義を書く事になります。

 しかし、実際にこのように記述してイメージをビルドしてみると、このENV1行につき1つの内部イメージが作成される事が分かります。DockerではDockerfileの1行に付き1つの階層のイメージを作成するような動作をするようになっているようです。 この動きはイメージの共有やキャッシュという面では優れているのですが、実際にイメージの配布等を行う際に、1つのコンテナに多数の階層イメージが含まれていると、色々と煩わしい場面が出てきてしまう事も多いようです。

 そのような場合は1行にまとめて記述する事により、階層イメージも1つとする事が出来ます。
 
 ENV \
  ARG01=101 \
  ARG02=102 \
  ARG03=103

  机上論的なDockerfileとしての定義としては1行づつの記述の方がスマートな記述方法にも見えるのですが、どうやらこのイメージがやたら多くなる問題は世界中のみんなが大嫌いであるようで、基本的に世界中のみんなが1行にまとめる記述方式を採用しているようですので、この際気にせずに1行にまとめて記述するようにしましょう。(=゚ω゚) デファクトスタンダードデス


〇 Dockerfileにはやや冗長と思える場合でも基本的な値については意味を理解した上で正しく定義しておく方が良い


 具体的にはENV、VOLUME、EXPOSE、WORKDIR辺りの定義でしょうか、この辺りの定義については、適切に定義を行っておくと後でお得であったりします。Dockerではこれらの値はコンテナイメージを作成する際の命令文であると共に、そのコンテナイメージがどのような構成で作られているかを公示するような意味合いを持つようになって来ています。

 例えばENVの値は別にDockerfileで定義しなくても、環境変数を元に挙動を変える事は可能ですので、一見必要ないかもしれませんが、とあるDockerコンテナのデプロイツールでは、使用したいDockerコンテナを指定すると、そのコンテナイメージからENV定義されている値を引っ張り出し、「このような設定が行えるコンテナである」と、一覧表示してくれたりします。

 例えばEXPOSEの値ですが別にDockerfileで定義しなくても問題なく使えるケースも多いかと思いますが、とあるDockerコンテナアプリケーションでは同じホストで起動や停止するコンテナを監視し、その起動したコンテナのEXPOSEの値を引っ張り出し、DNSサーバのSRVレコードに自動登録するような動きをする物があります。

 このように単なる命令文ではなく、構成の公示となる事を意識して定義するのは結構大事であったりします。



〇 Dockerコンテナでは基本的にログ出力は使用せずに標準出力にそのまま内容を出力する


 割と基本にして議論の分かれる部分かもしれませんが、基本的にDockerでは起動したコンテナで標準出力したものは、Dockerホストにてログとして保存されるようになっています。つまり、Dockerの想定している基本的なコンテナのログの機構が、標準出力に出力する事という事になります。

 このような方法で標準出力を経由してホストに渡されたログはログドライバ等の機能により、別のサーバに飛ばす等、様々なログのコントロールが行えるようになっています。

 ただし、実際の所アプリケーションがそもそもログファイルに出力をする事を前提や必須として作られていたりする場合等もありますので、ハイソーデスカと標準出力しましょう!等とは行かなかったりします。

 ちなみにnginxのコンテナイメージではDockerfile中にて /dev/stdout のシンボリックリンクを普段のログ出力先ファイル名に割り当てる等の工夫をする事によってこの問題を回避していたりと、なかなか面白い仕組みを使用していますので、手段として覚えて置くとお得です。(;=゚ω゚)b ナイショデス


〇 とりあえず今回はここまでです


 書き始めて気が付いたら結構な量になってしまいましたのでとりあえず今回はここまでとしておきます。なんかまだまだ色々ありそうな感じですね。

 こうしてみると罠だらけのような気もして来ますが、問題解決の為のベターな手法等もある程度固まって来ていたりする気もしますので、見た目よりは大丈夫に違いないです。多分!


それでわまた(=゚ω゚)ノシ



0 件のコメント:

コメントを投稿