はじめに
Dockerを使ったWEBアプリケーションのディレクトリ成長設計について考えていきます。
提起
PCを使う上で、我々の扱うデータはディレクトリ構造の中に格納されています。 アプリケーション開発は、コーディングして、バージョン管理し、デプロイまで 行ってはじめてユーザーに届きます。 時が経過するに連れて、使用する技術やサービスの規模感が変わってきますが、 ディレクトリ構造の初期設計次第で、迅速に対応できるかどうかが決まってくると考えられます。
課題
マイクロサービスアプリケーションにおける、 コンテナーを用いたアプリケーションの ディレクトリの成長設計の考察を具体的に行う。
方向性
「UNIXという考え方ーその設計思想と哲学」の本に乗っ取り、 以下のことを目標に考えていきます。
- 小さく作る
- 1つのプログラムには1つのことだけをやらせる
- 試作が早く作れる状態にする
- 効率より移植性を取る
- すべてのプログラムをフィルターにする
これから外れる考えのものは、除外する方針で行います。
解説用のアプリケーションの要件定義
説明のため、あらかじめ要件定義をしておきます。 (問題の抽象度とここの具体度がかけ離れすぎなので、気が向いたら調整)
- WEBアプリケーションを作成する
- 言語はPythonとする
- 開発環境はDockerを用いる
- デプロイ先はKubernetesとする
- KubernetesはHelmによって管理する
- CI/CDを用いて、自動テスト、自動デプロイを行う
- コードはGitHubで管理する
- テスト駆動を行う
- ドキュメントを必ず残す
- マイクロサービスアーキテクチャを採用する
具体的な手順に落とし込んで俯瞰する
要件を満たすように、WEBアプリケーションを育ててみます。 その時のファイル構造、ディレクトリ構造の変化を見ていきます。 モチベーションの維持のために、「まず動かす」を前提に構築します
1. 小さなWEBアプリケーションを作成する
+ server.py
ローカルでサクッと試せるアプリケーションのコードを配置します。 ファイル分割とかは後回しです。まず動かす、という部分を達成する。
2. テストコードを追加する
server.py
+ test_server.py
この段階ではまだtest/
ディレクトリを切らないでおく。
理由は、まずはコンテナーで動かすが目標。
3. ドキュメントを追加する
server.py
test_server.py
+ README.md
git pullしてから、起動、テストを行う方法を書いておいたほうが良いでしょう。 アプリの想像力の方にリソースを持っていかれるので、忘れないうちに書きます。
4. CIの設定を組み込む
+ .ci_settings
server.py
test_server.py
README.md
Circle CIでも、Travis CIでも良いので、まず突っ込みます。 アプリケーションが肥大化する前に設置することが大事です。
5. コンテナー化する
.ci_settings
+ Dockerfile
server.py
test_server.py
README.md
Dockerfileは、このアプリケーションがdocker pull
したあと、
docker run
で立ち上がるように記述します。
6. git cloneしたあとに立ち上げやすいようにする
.ci_settings
Dockerfile
+ docker-compose.yml
server.py
test_server.py
README.md
docker-compose up
もしくは、それと同等のコマンドで、
ミドルウェアも含めたアプリケーションがすべて立ち上がると開発への着手がはやい。
7. frontendとbackendで切り分ける
.ci_settings
+ frontend/
+ Dockerfile
+ backend/
Dockerfile
server.py
test_server.py
docker-compose.yml # build contextの変更
README.md
FrontendとBackendのコードを分離して保守しやすい状態にします。 マイクロサービスの下地ができました。
8. 成長させる
.ci_settings
frontend/
Dockerfile
...
backend/
Dockerfile
server.py
module/
...
test/
test_server.py
docker-compose.yml # build contextの変更
README.md
前回でディレクトリの切り分けが終わったので、あとはそれぞれのマイクロサービスを成長させるだけです。 それぞれのマイクロサービスでドメイン設計を行いつつ、 ディレクトリの成長設計をすると、開発者にとっても、サービスにとっても良いことしかないでしょう。
まとめ
成長痛を味わいますが、小さいうちに、手の届く範囲からTDDできる状態に ディレクトリの成長設計を行っておくことで、 肥大化するアプリケーションに対して先手を打つことができます。
大規模なOSSを見たり、実際に運用されているコードを見て、 自らの知識や理論、経験からくる構造設計と照らし合わせながら今後も考察していきたいですね。
【余談】DockerfileのENTRYPOINT
とCMD
に関して、個人的な考察
いろいろ試した結果、要件が確定していない場合や、より明確に実行コマンドを開発者に伝えるには、
CMD
とENTRYPOINT
の役割を明確化しておく必要があると考えます。
具体的には次の2つです。
Dockerfile
の最後はCMD
で終わるようにし、フォアグラウンドプロセスとして起動するようにしておく。ENTRYPOINT
はexec $@
を末尾に記述しておき、環境変数によって、コンテナー内部の状態が切り替わるようにしておく。
比喩を用いて例えると、CMD
は手で、ENTRYPOINT
は腕のような感じです。
直接アプリケーションを実行するのはCMD
にしておき、
たとえば、同一のアプリケーションでも異なる立ち上げ方を試したような場合(引数などをつける)ときに、
素のコマンドを起動時に伝えるとより検証が早くなります。
ENTRYPOINT
の使い方は、先の検証によって導き出された設定を環境変数で切り替えることだけを考えます。
ENTRYPOINT
はシェルスクリプトで記述しますが、
末尾にexec "$@"
をつけることで、CMD
を実行することが可能です。
ここに至る前までに、環境変数でコンテナー内部の状態を切り替えることでCMD
の機動力を失わずに、設定を切り替えられます。
1つのアプリケーションが複数の起動コマンドを持っている場合に、この2つに分離しておくと良いことがわかります。
具体的な例だと、タスクキューライブラリのcelery
を使っている時にこの事象が発生しました。
Celeryは同一のコードで、worker
、scheduler
、server
の3つの役割をもたせることができます。
起動コマンドが異なるだけで、それ以外はまったく同じです。
これを環境変数で起動コマンドを切り替えるようにすることも可能ですが、 結局どんなコマンドで実行していたのかを確認するためには、ロジックを読まないとたどり着けず、 それだけで消耗してしまいます。
幸いにして、CMD
の部分はdocker-compose
のcommand
であとから変更できますし、
Kubernetesも同様のことがdeploymentでできます。
若干Portabilityの部分を下げる印象があります。
そもそもアプリケーションが複数の起動パターンを持っている場合はあとから変更することが可能な状態にしておいたほうが良いと考えられます。
反例
反例は有ります。getredash/redashです。
bin/docker-entrypoint
に起動ロジックが集約されています。
docker-compose.yml
はcommand
を受け付けていますが、純粋なcommandではなく、引数になっています。
(この部分、本当ならargs
を利用したほうが良いように思えますが、
今後docker-compose
が主流でなくなった時にargs
があるかどうかわからないのでこれはこれで正解かなと思います。)
redashの場合、アーキテクチャがすでに決まっているようなOSSになっているので、
ユーザー側にむしろ負担を強いいないような設計になっているのかな、と勝手に推測します。