KubernetesにおけるPodの初期化処理

## これはなに？

本記事ではKubernetesにおけるPodという形でアプリケーションを起動する際、どのように初期化処理を実行できるのかについて取り上げる。
なお終了処理は触れない。

## lifecycle.postStartを使う

`spec.containers.lifecycle.postStart`を利用すればライフサイクルにおける"起動"直後に何かしら処理を差し込むことが可能。
これはPodの起動、ENTRYPOINTやCMDと同時(非同期)に実行される。
ドキュメント: [Attach Handlers to Container Lifecycle Events - Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/)

postStartで初期化処理をするdeploymentのyaml例を書いてみる。
サンプルのファイルは[Github](https://github.com/petitviolet/kubernetes_prac/blob/27fa2bae6f09fac7763ddac62e79bf4338090ea0/initialize/post_start.yaml)においてある。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: python
  labels:
    name: python
spec:
  replicas: 1
  selector:
    matchLabels:
      app: python
  template:
    metadata:
      labels:
        app: python
    spec:
      containers:
        - name: python
          image: python:3.7.1
          tty: true
          lifecycle:
            postStart:
              exec:
                command:
                  - sh
                  - -c
                  - "mkdir -p /var/log/backup/${HOSTNAME} \
                    && mv /var/log/python.log /var/log/backup/${HOSTNAME} \
                    && echo START! >> /var/log/python.log"
          volumeMounts:
            - name: log
              mountPath: /var/log
          command:
            - sh
            - -c
            - "python -m http.server 8080 --directory /var/log/ 1>/var/log/python.log 2>/var/log/python.log"
          volumeMounts:
            - name: log
              mountPath: /var/log
      volumes:
        - name: log
          hostPath:
            path: /var/log/python
            type: DirectoryOrCreate
```

PodそのものはPythonでWebサーバーを立ち上げるだけだが、postStartにおいてログファイルをバックアップとっている。
kubernetesのサンプルとしてこれが正しいかどうかはさておき...。

applyして様子を見てみる。

```console
$ kubectl apply -f post_start.yaml
...
$ kubectl get pod
NAME                      READY     STATUS    RESTARTS   AGE
python-7d57c47cb9-d7zxr   1/1       Running   0          1m
$ kubectl exec -it python-7d57c47cb9-d7zxr -- ls -lh /var/log/backup
total 4.0K
drwxr-xr-x 2 root root 4.0K Dec  1 13:08 python-7d57c47cb9-d7zxr
```

`kubectl exec`して`ls`した結果、バックアップ処理が正常に動いているのがわかる。

### postStartの問題点

[ドキュメント](https://kubernetes.io/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/)によると以下のような事が書いてある。

> Kubernetes sends the postStart event immediately after the Container is created. There is no guarantee, however, that the postStart handler is called before the Container’s entrypoint is called.

つまり、コンテナ(Pod)のentrypointより先に実行される保証がないということ。
今回の例で考えて見ると、アプリケーションが起動する前に以前のログをバックアップとっておきたいが、postStartの実行タイミングによってはアプリケーション起動後に実行される可能性があるということ。
そこで欲しくなってくるのはENTRYPOINTより前に実行されることが保証された初期化処理の設定。
また、postStartで使用したいソフトウェアはPodの実行に本来不要なソフトウェアが必要になる可能性もある。

## 初期化処理を別コンテナで

postStartの欠点を補うことが出来るのが、Init Containersというもの。
Podの初期化処理を実行するコンテナを実行することが出来る。
ドキュメント:[Init Containers - Kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/)

Pod自体の機能としていくつかのコンテナをまとめて動かすことが出来るが、コンテナ群の初期化順は決まっておらず、さらに非同期に初期化されるため、初期化処理をするコンテナをPodそのものに組み込んでも期待するような初期化を保証することが出来ない。
さらに何かしらの初期化処理だけで必要なソフトウェアがあるなどする場合に、そのためだけに本来不要なコマンドをDockerイメージに追加しておくなどが必要になるケースもある。
たとえば設定ファイルを外部から読み込んだり、Gitリポジトリをcloneしてきたりしたいようなケース。
具体的に後者だとWebアプリを動かすのにGitコマンドは必要ないが、初期化のために必要になってしまう。 

そういった時にInit Containersが使える。

Init Containersを使った初期化を行ってみる。
こちらのサンプル用ファイルも[Github](https://github.com/petitviolet/kubernetes_prac/blob/27fa2bae6f09fac7763ddac62e79bf4338090ea0/initialize/init_container.yaml)にあげてある。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: python
  labels:
    name: python
spec:
  replicas: 1
  selector:
    matchLabels:
      app: python
  template:
    metadata:
      labels:
        app: python
    spec:
      initContainers:
        - name: git
          image: alpine/git
          command:
            - git
            - clone
            - https://github.com/petitviolet/kubernetes_prac.git
            - /tmp/git/
          volumeMounts:
            - name: storage
              mountPath: /tmp/git/
      containers:
        - name: python
          image: python:3.7.1
          tty: true
          command:
            - sh
            - -c
            - "python -m http.server 8080 --directory /www/public"
          volumeMounts:
            - name: storage
              mountPath: /www/public
      volumes:
        - name: storage
          emptyDir: {}
```

先ほどとは少し違って、`git clone`をしている。
必要なファイルをgitリポジトリから取得してくる、というような初期化処理。

起動してみると、正しく動作していることが確認できる。

```console
$ kubectl apply -f ./init_container.yaml
kudeployment "python" created
service "python-service" created
$
$ kubectl get po -w
NAME                      READY     STATUS    RESTARTS   AGE
python-86cc996576-glntt   0/1       Pending   0          0s
python-86cc996576-glntt   0/1       Init:0/1   0         0s
python-86cc996576-glntt   0/1       PodInitializing   0         17s
python-86cc996576-glntt   1/1       Running   0         18s
^C%
$
$ curl $(minikube service python-service --url)/message.txt
Hello, initContainer!
```

最後にcurlでとってきているのは[Githubに置いたmessage.txt](https://github.com/petitviolet/kubernetes_prac/blob/27fa2bae6f09fac7763ddac62e79bf4338090ea0/message.txt)。
ちゃんとGithubから`git clone`出来ていることがわかる。

また、`kubectl get po -w`の様子から、PodのSTATUSがPending→Init→PodInitializing→Runningと遷移しているのがわかる。
ここで注目したいのがInitというSTATUSがあること。
`lifecycle.postStart`と異なり、初期化用のステータスがあり、それが終わらないと本体のPodの起動に遷移しないことが保証できている。
ちなみに`Init:0/1`となっているのはInit Containersが1個あってそのうち0個が完了していることを示す。
なのでInit Containersを複数個用意すれば分母は変わる。

また、Initに使われたコンテナのログや詳細なステータス`kubectl`コマンド経由で取得することは出来る。

```console
$ kubectl logs python-86cc996576-glntt -c git
Cloning into '/tmp/git'...
$ kubectl get pod python-86cc996576-glntt -ojson | jq '.status.initContainerStatuses'
[
  {
    "containerID": "docker://3685acb1a79188c840d6723f2b29b4aaaa876f561ab832732c455512cd2d9ff1",
    "image": "alpine/git:latest",
    "imageID": "docker-pullable://alpine/git@sha256:d76d5ab84de3a35514f8621df4550c59680c3bb623e8c1332ed7af39e33afb0b",
    "lastState": {},
    "name": "git",
    "ready": true,
    "restartCount": 0,
    "state": {
      "terminated": {
        "containerID": "docker://3685acb1a79188c840d6723f2b29b4aaaa876f561ab832732c455512cd2d9ff1",
        "exitCode": 0,
        "finishedAt": "2018-12-02T02:13:50Z",
        "reason": "Completed",
        "startedAt": "2018-12-02T02:13:49Z"
      }
    }
  }
]
```

使いどころとしては

- 確実に初期化処理を本体のPod起動前に完了することを保証したい場合
- 本体のPodに持たせたくないソフトウェアが初期化に必要な場合

というあたり。

## まとめ

Podの初期化処理に使えるのは2つ。

- `spec.containers.lifecycle.postStart`
  - Podの起動と同時に実行される
  - 実行はPodの各コンテナ内
- Init Containers
  - Podの本体のコンテナ起動前のPendingフェーズで実行される
  - 実行はPodとは別のコンテナ

それぞれキックされるタイミング等が異なるので用途に応じて使い分けることが大事。