TL;DR
-
サンプルリポジトリをclone
$ git clone https://github.com/nmatsui/kubernetes-restartpod-example.git $ cd kubernetes-restartpod-example
-
サンプルのDeploymentを起動
$ kubectl apply -f hello-world-api.yaml
-
再起動スケジュールを変更
- スケジュールは、crontabスタイルを用いてUTCで記述する
- 下記のように
45 3 * * *
であれば、毎日UTCの午前3時45分(日本時間で12時45分)に再起動する、ということになる
$ git diff restart-hello-world-api-cronjob.yaml
spec:
concurrencyPolicy: Replace
- schedule: "0 1 * * *"
- schedule: "45 3 * * *"
jobTemplate:
-
cronjobを登録
$ kubectl apply -f restart-hello-world-api-cronjob.yaml
-
指定した時間になったら、サンプルDeploymentに所属しているPodが再起動される
はじめに
昔懐かしいアプリケーションでは、リソースリークやネットワーク再接続手順の不備等により、定期的に再起動が必要になるようなものがありました。
そもそもそのような筋の悪いアプリケーションを動かすべきではない、というのはその通りなのですが、既存のアプリケーションを流用してKubernetes上に再構築する場合など、どうにも手の入れようがないため暫定的に日次リスタートでごまかす、という運用をせざるを得ないこともあります。
従来はアプリケーションが動作しているサーバー上のcronに再起動タスクを仕込むようなものですが、これをKubernetesのREST APIとCronJobを利用して実現してみました。
Kuberntes上のアプリケーションを安全にリスタートするには
DeploymentのPod Templateのannotationを更新することでRolling Updateを開始し、従来のPodを一つづつ新しいPodに入れ替えることで、Podに内包されるアプリケーションを再起動します。
(このあたり、 @tkusumi さんの Kubernetes: Deployment のローリングアップデートの条件と設定 を参考にしました。ありがとうございます!)
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 を参照してください。
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 を参照してください。
ServiceAccount
resterter
という名前をつけて、再起動ジョブに紐付けるServiceAccountを定義します。
apiVersion: v1
kind: ServiceAccount
metadata:
name: restarter
Role
default
namespaceのDeploymentを書き換える(PATCHする)権限を定義します。DeploymentのAPIは apps
に属するため、 apiGroup
に apps
を指定します(DeploymentのapiVersionは apps/v1 と指定しますよね)。
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する)ことが可能になります。
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を操作することができません。
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しかありません。
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の現在日時で書き換えることにしました。
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
- Azure AKSは、デフォルトでRBACが有効になります。
Kubernetes | バージョン |
---|---|
クラスタ(AKS) | 1.11.1 |
クライアント(kubectl) | 1.11.2 |
サンプルDeploymentの起動後
サンプルDeployemntをapplyします。 lastUpdate
が初期の"manual deploy"になっており、ReplicaSetも一つだけ作られています。
-
Deploymentの状態
$ 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の状態
$ kubectl get replicasets -l app=hell-world-api NAME DESIRED CURRENT READY AGE hello-world-api-5bd998dd8 3 3 3 7m
-
Podの状態
$ 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の状態
$ 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の状態
$ 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の状態
$ 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の状態
$ 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のログ
$ 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を自動でリスタートさせることができました。どことなくバッドノウハウな気がしなくもないですが、お役に立てば幸いです。