何かしらの設定ファイルが必要なアプリケーションをDockerイメージとして作成する場合、可搬性を考慮するならコンテナ内に予め特定の状況の設定ファイルを格納するのではなく、コンテナ起動時に環境変数から設定ファイルを生成するのが望ましかったりする。 (設定ファイルをマウントするという手もあるけど、あんまエレガントじゃない気がするんだよね。それこそK8Sだと手間だし。設定は全部環境変数で賄う形に統一してしまうのがDockerの運用としては美しいと思う。)
そんな場合によく用いられる手段の一つがenvsubstを用いて設定ファイルを生成した後にアプリケーションの起動コマンドを実行するという方法であり、具体的には
- CMDとしてアプリケーションの起動コマンドを設定
- ENTRYPOINTなんかで生成コマンドを終えた後に、CMD設定値なり実行コマンドなりが引数で渡ってくるのでそれをexecで実行
なんて作りにしたりする。
こんな形にしておくと嬉しいのが、commandを差し替えても設定ファイルそのものは生成されることであり、同じイメージで起動オプションだけを変えたコンテナを立てたりする場合に便利だったりする。
というわけで僕はよくENTRYPOINT+CMDな構成のイメージを作るんだけど、最近K8Sの案件で意図と違う挙動をしたりすることがあったので、ちょっと検証してみた。
検証用のDockerイメージ
以下のようなものを想定。ENTRYPOINTとして文字列を出力した後に引数として渡ってくるコマンドを実行するような記述をしておく。
FROM busybox:latest ENTRYPOINT ["sh", "-c", "echo 'EXEC ENTRYPOINT'; exec $0 $*"] CMD ["echo", "EXEC CMD"]
これをboot-test
として以降使用していく。
Dockerコマンドで普通に起動する
まずは普通に動かしてみる。
$ docker run boot-test EXEC ENTRYPOINT EXEC CMD
ENTRYPOINTに設定したコマンドによってCMDのコマンドが実行される。
別のコマンドを与える
$ docker run boot-test echo "EXEC MANUAL COMMAND" EXEC ENTRYPOINT EXEC MANUAL COMMAND
当然CMDの内容が置き換わって実行される。
Kubernetesでの検証
minikube上でイメージ作成
まずは検証のための準備。ローカルのminikubeを用いる。
特にリポジトリにプッシュしないので以下の手順でminikube上にイメージを作成。
$ minikube ssh # 以下minikube 上 $ echo 'FROM busybox:latest > ENTRYPOINT ["sh", "-c", "echo EXEC ENTRYPOINT; exec $0 $*"] > CMD ["echo", "EXEC CMD"] > '> Dockerfile $ docker build -t flag needs an argument: -t See 'docker build --help'. $ docker build -t local/boot-test:v1 . Sending build context to Docker daemon 6.656 kB Step 1 : FROM busybox:latest ---> efe10ee6727f Step 2 : ENTRYPOINT sh -c echo EXEC ENTRYPOINT; exec $0 $* ---> Running in f857dd8df653 ---> 25fedd764fbd Removing intermediate container f857dd8df653 Step 3 : CMD echo EXEC CMD ---> Running in 4dc26dd26953 ---> fe395a6fd8d6 Removing intermediate container 4dc26dd26953 Successfully built fe395a6fd8d6 $ docker run local/boot-test:v1 EXEC ENTRYPOINT EXEC CMD
- 当たり前だけどminikube sshしてminikube内でビルド
- 単なる
boot-test
みたいな形式だとレジストリに拾いに行っちゃうみたいなんで、名称としてlocal/boot-test:v1
みたいな命名にしておく必要がある
あたりがポイント。
k8sで起動
以下のような普通に起動するだけのdeployment.yamlを作成してk8s上で動かしてみる。
kind: Deployment apiVersion: apps/v1beta1 metadata: name: boot-test spec: template: metadata: labels: app: boot-test spec: containers: - name: boot-test image: local/boot-test:v1
永続化しないコマンドなんでCrashLoopBackOff
になってしまうが今回は気にしない。
$ kubectl create -f deployment.yaml deployment "boot-test" created $ kubectl get po NAME READY STATUS RESTARTS AGE boot-test-2271867219-4xpc8 0/1 CrashLoopBackOff 1 3s
ログを確認すると普通にdocker runした時と同様の動きをしてくれていることが確認できる。
$ kubectl logs boot-test-2271867219-4xpc8 EXEC ENTRYPOINT EXEC CMD
commandを与えてみる
それではということでcommand項を記述してみると・・・
kind: Deployment apiVersion: apps/v1beta1 metadata: name: boot-test spec: template: metadata: labels: app: boot-test spec: containers: - name: boot-test image: local/boot-test:v1 command: - echo - K8S COMMAND
$ kubectl replace -f deployment.yaml deployment "boot-test" replaced $ kubectl get po NAME READY STATUS RESTARTS AGE boot-test-2092889346-zb11g 0/1 CrashLoopBackOff 1 10s boot-test-2271867219-4xpc8 0/1 CrashLoopBackOff 5 3m $ kubectl logs boot-test-2092889346-zb11g K8S COMMAND
あれ?commandという名称なのがややこしいけど、どうやら元のENTRYPOINTを置き換えてしまっている模様。
argsを与えてみる
それではということでargsの方に記述すると・・・
kind: Deployment apiVersion: apps/v1beta1 metadata: name: boot-test spec: template: metadata: labels: app: boot-test spec: containers: - name: boot-test image: local/boot-test:v1 args: - echo - K8S ARGS
$ kubectl replace -f deployment.yaml deployment "boot-test" replaced $ kubectl get po NAME READY STATUS RESTARTS AGE boot-test-2092889346-zb11g 0/1 Completed 5 2m boot-test-321451056-vg45k 0/1 CrashLoopBackOff 1 3s $ kubectl logs boot-test-321451056-vg45k EXEC ENTRYPOINT K8S ARGS
argsの方にdocker run
とかdocker composeでいうところのcommandを与えれば同等の動きをしてくれそう。
おまけ
こんな感じでyaml記述すればENTRYPOINT+CMD的なことができそう。
$ cat deployment.yaml kind: Deployment apiVersion: apps/v1beta1 metadata: name: boot-test spec: template: metadata: labels: app: boot-test spec: containers: - name: boot-test image: local/boot-test:v1 command: - sh - -c - > echo K8S COMMAND; exec $0 $* args: - echo - K8S ARGS
$ kubectl replace -f deployment.yaml deployment "boot-test" replaced $ kubectl get po NAME READY STATUS RESTARTS AGE boot-test-1254161467-bmhbv 0/1 Completed 2 28s boot-test-2475555958-c18n6 0/1 Completed 0 1s $ kubectl logs boot-test-2475555958-c18n6 K8S COMMAND K8S ARGS
おまけその2 postStart
ここまで書いて、そういえばK8Sには「起動時のコマンド単発実行」のニュアンスとしてLifecycleEventのpostStartがあったじゃん!と思い出して追加検証。標準出力ではなくファイル出力をするように以下のようなイメージを作成。
FROM busybox:latest ENTRYPOINT ["sh", "-c", "echo EXEC ENTRYPOINT >> /test; exec $0 $*"]
各ステップで/test
に文字列を吐くように以下のようなdeployment.yaml。
kind: Deployment apiVersion: apps/v1beta1 metadata: name: boot-test spec: template: metadata: labels: app: boot-test spec: containers: - name: boot-test image: local/boot-test:v2 lifecycle: postStart: exec: command: - sh - -c - echo EXEC K8S POSTSTART >> /test command: - sh - -c - echo EXEC K8S COMMAND >> /test; tail -f /dev/null
起動して/test
を見てみると・・・
$ kubectl exec boot-test-2354855813-vxqj1 -it cat /test EXEC K8S COMMAND EXEC K8S POSTSTART
うーん・・・
- postStartはcommandの後に実行されてそう
- こちらもENTRYPOINTは通過しない(ちなみにpostStart.exec.argsは無い)
ということで、(実用上は起動のタイムラグがあるので問題無い場合が多いとは思うが)厳密にはこれを設定ファイル生成に用いるべきではなさそう。
ドキュメントを良く読むと
PostStart This hook executes immediately after a container is created. However, there is no guarantee that the hook will execute before the container ENTRYPOINT. No parameters are passed to the handler.
と、「ENTRYPOINTより先に実行されるとは保証しないよ♪」書いてある。
まとめ
- K8Sでいうところの
command
はDocker Composeでいうところのentrypoint
であり、args
があちらでいうところのcommand
! postStart
は順序が保証されないので、実行順序がシビアな設定ファイル生成などの用途には適さない!
- 作者: ?橋健一,谷口禎英,井本大登,山崎勝平,大和田純,内村元樹,坂東昌哉,平田敏之,牧大輔,板敷康洋,大?浩崇,穴井宏幸,原口宗悟,久田真寛,ふしはらかん,のざきひろふみ,うらがみ,ひげぽん,池田拓司,はまちや2,竹原,片田雄樹,渋江一晃,WEB+DB PRESS編集部編
- 出版社/メーカー: 技術評論社
- 発売日: 2017/06/24
- メディア: 大型本
- この商品を含むブログを見る