そんな今日この頃の技術ネタ

本家側に書くほどでもない小ネタ用

kubernetes学習番外編 コンテナ起動時のコマンド実行

 何かしらの設定ファイルが必要なアプリケーションをDockerイメージとして作成する場合、可搬性を考慮するならコンテナ内に予め特定の状況の設定ファイルを格納するのではなく、コンテナ起動時に環境変数から設定ファイルを生成するのが望ましかったりする。 (設定ファイルをマウントするという手もあるけど、あんまエレガントじゃない気がするんだよね。それこそK8Sだと手間だし。設定は全部環境変数で賄う形に統一してしまうのがDockerの運用としては美しいと思う。)

 そんな場合によく用いられる手段の一つがenvsubstを用いて設定ファイルを生成した後にアプリケーションの起動コマンドを実行するという方法であり、具体的には

  • CMDとしてアプリケーションの起動コマンドを設定
  • ENTRYPOINTなんかで生成コマンドを終えた後に、CMD設定値なり実行コマンドなりが引数で渡ってくるのでそれをexecで実行

なんて作りにしたりする。

blue1st-tech.hateblo.jp

 こんな形にしておくと嬉しいのが、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を用いる。

blue1st-tech.hateblo.jp

 特にリポジトリにプッシュしないので以下の手順で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
  1. 当たり前だけどminikube sshしてminikube内でビルド
  2. 単なる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は無い)

ということで、(実用上は起動のタイムラグがあるので問題無い場合が多いとは思うが)厳密にはこれを設定ファイル生成に用いるべきではなさそう。

 ドキュメントを良く読むと

kubernetes.io

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は順序が保証されないので、実行順序がシビアな設定ファイル生成などの用途には適さない!

WEB+DB PRESS Vol.99

WEB+DB PRESS Vol.99

  • 作者: ?橋健一,谷口禎英,井本大登,山崎勝平,大和田純,内村元樹,坂東昌哉,平田敏之,牧大輔,板敷康洋,大?浩崇,穴井宏幸,原口宗悟,久田真寛,ふしはらかん,のざきひろふみ,うらがみ,ひげぽん,池田拓司,はまちや2,竹原,片田雄樹,渋江一晃,WEB+DB PRESS編集部編
  • 出版社/メーカー: 技術評論社
  • 発売日: 2017/06/24
  • メディア: 大型本
  • この商品を含むブログを見る