Podを自動リスタートするCronJob with RBAC

# TL;DR
1. サンプルリポジトリをclone

    ```bash
    $ git clone https://github.com/nmatsui/kubernetes-restartpod-example.git
    $ cd kubernetes-restartpod-example
    ```
1. サンプルのDeploymentを起動

    ```bash
    $ kubectl apply -f hello-world-api.yaml
    ```
1. 再起動スケジュールを変更
    * スケジュールは、crontabスタイルを用いてUTCで記述する
    * 下記のように `45 3 * * *`であれば、毎日UTCの午前3時45分（日本時間で12時45分）に再起動する、ということになる

    ```diff
    $ git diff restart-hello-world-api-cronjob.yaml
 spec:
   concurrencyPolicy: Replace
-  schedule: "0 1 * * *"
+  schedule: "45 3 * * *"
   jobTemplate:
    ```
1. cronjobを登録

    ```bash
    $ kubectl apply -f restart-hello-world-api-cronjob.yaml
    ```
1. 指定した時間になったら、サンプルDeploymentに所属しているPodが再起動される :yum:

# はじめに
昔懐かしいアプリケーションでは、リソースリークやネットワーク再接続手順の不備等により、定期的に再起動が必要になるようなものがありました。

そもそもそのような筋の悪いアプリケーションを動かすべきではない、というのはその通りなのですが、既存のアプリケーションを流用してKubernetes上に再構築する場合など、どうにも手の入れようがないため暫定的に日次リスタートでごまかす、という運用をせざるを得ないこともあります。

従来はアプリケーションが動作しているサーバー上のcronに再起動タスクを仕込むようなものですが、これをKubernetesのREST APIとCronJobを利用して実現してみました。

# Kuberntes上のアプリケーションを安全にリスタートするには
DeploymentのPod Templateのannotationを更新することでRolling Updateを開始し、従来のPodを一つづつ新しいPodに入れ替えることで、Podに内包されるアプリケーションを再起動します。
（このあたり、 @tkusumi さんの [Kubernetes: Deployment のローリングアップデートの条件と設定](https://qiita.com/tkusumi/items/7404e422cd6b4a1c6892) を参考にしました。ありがとうございます！）

# kube-apiserverのREST APIとRBAC
通常Kubernetesは `kubectl` コマンドで操作しますが、Podの内部から操作する場合はkube-apiserverのREST APIへ直接アクセスしたほうが手間がかかりません。ただしRBACが有効な場合、kube-apiserverのREST APIを操作するためには、再起動ジョブを実行するPodのServiceAccountに適切な権限を与える必要があります。

# 設定ファイルのポイント
## 再起動されるアプリケーションのDeployment
再起動されるアプリケーションを内包したPodのDeploymentには、 spec.template.metadata.annotationsに `lastUpdate` というエントリを付け加えています。値は何でもかまいませんし、Podの動作には特に意味を持ちません。

詳細な内容は [hello-world-api.yaml](https://github.com/nmatsui/kubernetes-restartpod-example/blob/master/hello-world-api.yaml) を参照してください。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world-api
#...（省略）...
spec:
  replicas: 3
#...（省略）...
  template:
    metadata:
#...（省略）...
      annotations:
        lastUpdate: "manual deploy" # <- コレ
    spec:
      containers:
#...（省略）...
```

## 再起動するジョブの権限
アプリケーションを再起動するジョブを内包したPodには、Deploymentのannotationsを書き換える（PATCHする）権限が必要です。

詳細な内容は [restart-hello-world-api-cronjob.yaml](https://github.com/nmatsui/kubernetes-restartpod-example/blob/master/restart-hello-world-api-cronjob.yaml) を参照してください。

### ServiceAccount
`resterter`という名前をつけて、再起動ジョブに紐付けるServiceAccountを定義します。

```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: restarter
```

### Role
`default` namespaceのDeploymentを書き換える（PATCHする）権限を定義します。DeploymentのAPIは `apps` に属するため、 `apiGroup` に `apps` を指定します（DeploymentのapiVersionは **apps**/v1 と指定しますよね）。

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: restarter
  namespace: default
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["patch"]
```

### RoleBinding
定義したServiceAccountに、定義したRoleを結びつけます。これにより、 ServiceAccount `restarter` に紐付けられたPodは、`default` namespaceのDeploymentを書き換える（PATCHする）ことが可能になります。

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: restarter
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: restarter
subjects:
- kind: ServiceAccount
  name: restarter
  namespace: default
```

## 再起動するCronJob
アプリケーションを再起動するジョブを内包したPodを、CronJobとして登録します。Jobとして実行されるPodのTemplateの `serviceAccountName` として、先程定義した `resterter` を指定してください。
明示的にServiceAccountを指定しなかった場合 `default` というServiceAccountに紐付けられますが、このServiceAccountではDeploymentを操作することができません。

```yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: restart-hello-world-api
spec:
  concurrencyPolicy: Replace
  schedule: "0 1 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: restarter # <- コレ
          containers:
          - name: hello-world-api-restarter
            image: nmatsui/hello-world-api-restarter:latest
            imagePullPolicy: Always
            env:
            - name: NAMESPACE
              value: default
            - name: DEPLOYMENT_NAME
              value: hello-world-api
          restartPolicy: OnFailure
```

# 再起動ジョブのコンテナ
## Dockerfile
kube-apiserverのREST APIにcurlでアクセスするだけですので、alpine linuxにcurlをインストールするだけでOKです。コンテナサイズはなんと4MBしかありません。

```Dockerfile
FROM alpine:3.8
MAINTAINER Nobuyuki Matsui <nobuyuki.matsui@gmail.com>

RUN apk update && apk add --no-cache curl
COPY ./entrypoint.sh /opt/entrypoint.sh
ENTRYPOINT ["/opt/entrypoint.sh"]
```

## entrypoint.sh
Kuberntesの内部で動作するPodは、自Podに紐付けられたServiceAccountに対応するTOKENを `/var/run/secrets/kubernetes.io/serviceaccount/token` から取得することができます。またkube-apiserverのIPアドレスやポートも、Podの環境変数から取得することができます。便利ですね！

これらのTOKENとkube-apiserverのエンドポイントを用い、 `/apis/apps/v1/namespaces/${NAMESPACE}/deployments/${DEPLOYMENT_NAME}/` にアクセスすれば、kube-apiserverが持つDeploymentを操作することができます。

今回は `spec.template.metadata.annotations.lastUpdate` を書き換えたいので、必ず異なる文字列になるUTCの現在日時で書き換えることにしました。

```bash
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -isSk -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/strategic-merge-patch+json" https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_PORT_443_TCP_PORT}/apis/apps/v1/namespaces/${NAMESPACE}/deployments/${DEPLOYMENT_NAME}/ -X PATCH -d @- <<__EOS__
{
  "spec": {
    "template": {
      "metadata": {
        "annotations": {
          "lastUpdate": "$(date '+%Y-%m-%dT%H:%M:%S.%sZ')"
        }
      }
    }
  }
}
__EOS__
```

# 動作確認
## 検証した環境
* [Microsoft Azure AKS](https://azure.microsoft.com/ja-jp/services/kubernetes-service/)
    * Azure AKSは、デフォルトでRBACが有効になります。

|Kubernetes|バージョン|
|:--|:--|
|クラスタ（AKS）|1.11.1|
|クライアント（kubectl)|1.11.2|

## サンプルDeploymentの起動後
サンプルDeployemntをapplyします。 `lastUpdate` が初期の"manual deploy"になっており、ReplicaSetも一つだけ作られています。

* Deploymentの状態

    ```bash
    $ kubectl describe deployment hello-world-api
    Name:                   hello-world-api
    Namespace:              default
    CreationTimestamp:      Sat, 18 Aug 2018 12:33:59 +0900
    # ...（省略）...
    RollingUpdateStrategy:  25% max unavailable, 25% max surge
    Pod Template:
      Annotations:  lastUpdate=manual deploy # <- コレ
      Containers:
       hello-world-api:
        Image:        nmatsui/hello-world-api:latest
    # ...（省略）...
    Events:
      Type    Reason             Age   From                   Message
      ----    ------             ----  ----                   -------
      Normal  ScalingReplicaSet  6m    deployment-controller  Scaled up replica set hello-world-api-5bd998dd8 to 3
    ```
* ReplicaSetの状態

    ```bash
    $ kubectl get replicasets -l app=hell-world-api
    NAME                        DESIRED   CURRENT   READY     AGE
    hello-world-api-5bd998dd8   3         3         3         7m
    ```
* Podの状態

    ```bash
    $ kubectl get pods -l app=hello-world-api
    NAME                              READY     STATUS    RESTARTS   AGE
    hello-world-api-5bd998dd8-8cfzh   1/1       Running   0          10m
    hello-world-api-5bd998dd8-mffkw   1/1       Running   0          10m
    hello-world-api-5bd998dd8-ptcfn   1/1       Running   0          10m
    ```

## CronJobの登録
UTC 03:45 （日本時間 12:45） に再起動するように、CronJobを登録します。

* cronjobの状態

    ```bash
    $ kubectl get cronjobs
    NAME                      SCHEDULE     SUSPEND   ACTIVE    LAST SCHEDULE   AGE
    restart-hello-world-api   45 3 * * *   False     0         <none>          5s
    ```

## 指定時間が到来
指定時間が到来した後に、サンプルDeploymentの状態を確認します。 `lastUpdate` の値がジョブPod起動時間に変わり、ReplicaSetが追加されて、Podが全てRoling Updateで入れ替わっていることが確認できます。

* Deploymentの状態

    ```bash
    $ kubectl describe deployment hello-world-api
    Name:                   hello-world-api
    Namespace:              default
    CreationTimestamp:      Sat, 18 Aug 2018 12:33:59 +0900
    # ...（省略）...
    Pod Template:
      Annotations:  lastUpdate=2018-08-18T03:45:12.1534563912Z # <- コレ
      Containers:
       hello-world-api:
        Image:        nmatsui/hello-world-api:latest
    # ...（省略）...
    Events:
      Type    Reason             Age   From                   Message
      ----    ------             ----  ----                   -------
      Normal  ScalingReplicaSet  12m   deployment-controller  Scaled up replica set hello-world-api-5bd998dd8 to 3
      Normal  ScalingReplicaSet  1m    deployment-controller  Scaled up replica set hello-world-api-79987786fc to 1
      Normal  ScalingReplicaSet  1m    deployment-controller  Scaled down replica set hello-world-api-5bd998dd8 to 2
      Normal  ScalingReplicaSet  1m    deployment-controller  Scaled up replica set hello-world-api-79987786fc to 2
      Normal  ScalingReplicaSet  1m    deployment-controller  Scaled down replica set hello-world-api-5bd998dd8 to 1
      Normal  ScalingReplicaSet  1m    deployment-controller  Scaled up replica set hello-world-api-79987786fc to 3
      Normal  ScalingReplicaSet  1m    deployment-controller  Scaled down replica set hello-world-api-5bd998dd8 to 0
    ```
* ReplicaSetの状態

    ```bash
    $ kubectl get replicasets -l app=hello-world-api
    NAME                         DESIRED   CURRENT   READY     AGE
    hello-world-api-5bd998dd8    0         0         0         12m
    hello-world-api-79987786fc   3         3         3         1m
    ```
* Podの状態

    ```bash
    $ kubectl get pods -l app=hello-world-api
    NAME                               READY     STATUS    RESTARTS   AGE
    hello-world-api-79987786fc-l2q8n   1/1       Running   0          41s
    hello-world-api-79987786fc-n6l6d   1/1       Running   0          36s
    hello-world-api-79987786fc-r28z8   1/1       Running   0          32s
    ```

またJobの状態を確認すると、CronJobがJobを一つ起動したことがわかります。

* Jobの状態

    ```
    $ kubectl get jobs
    NAME                                 DESIRED   SUCCESSFUL   AGE
    restart-hello-world-api-1534563900   1         1            2m
    ```

そのJobのPodのログを確認すると、kube-apiserverのAPIを使ってDeploymentを変更したことがわかります。

* Jobのログ

    ```bash
    $ kubectl logs restart-hello-world-api-1534563900-txwc7
    #### start job 2018-08-18T03:45:12.1534563912Z ####
    HTTP/1.1 200 OK
    Server: nginx/1.13.6
    Date: Sat, 18 Aug 2018 03:45:12 GMT
    Content-Type: application/json
    Transfer-Encoding: chunked
    Connection: close
    Strict-Transport-Security: max-age=15724800; includeSubDomains;
    
    {
      "kind": "Deployment",
      "apiVersion": "apps/v1",
      "metadata": {
        "name": "hello-world-api",
        "namespace": "default",
        "selfLink": "/apis/apps/v1/namespaces/default/deployments/hello-world-api",
        "uid": "8b59788e-a297-11e8-b098-2aa0be8615f1",
        "resourceVersion": "31971",
        "generation": 2,
        "creationTimestamp": "2018-08-18T03:33:59Z",
    #...省略...
      },
      "spec": {
        "replicas": 3,
        "selector": {
          "matchLabels": {
            "app": "hello-world-api"
          }
        },
        "template": {
          "metadata": {
            "creationTimestamp": null,
            "labels": {
              "app": "hello-world-api"
            },
            "annotations": {
              "lastUpdate": "2018-08-18T03:45:12.1534563912Z"
            }
          },
          "spec": {
    #...省略...
    }
    #### end job 2018-08-18T03:45:12.1534563912Z ####
    ```

# さいごに
ということで、CronJobを用いてPodを自動でリスタートさせることができました。どことなくバッドノウハウな気がしなくもないですが、お役に立てば幸いです。