Kubernetes で Deployment のローリングアップデートを実現する 『kubectl rollout restart』コマンドをソースコードから解説

このブログを書くことになったきっかけです。

kubernetes で deployment リソースに変更を加えずに rolling update させる - Qiita

TL;DR

  • PodTemplateSpec の annotation を付与/更新して実現してる

kubectl rollout restart とは

$ kubectl rollout restart -h
Restart a resource.

     A deployment with the "RolloutStrategy" will be rolling restarted.

Examples:
  # Restart a deployment
  kubectl rollout restart deployment/nginx

resource の restart を実行するコマンドです。

(筆者の環境はサーバは v1.13.5 だったが使えた)

ここの記述を見て、v1.12.6 である Amazon EKS クラスタに対して試してみたら使えたのでソースを追ってみました。

ソースコード

v1.15.0-rc.1 の tag を使ってみていきます。

kubectl rollout restart コマンドはここに定義されています。

Run: func(cmd *cobra.Command, args []string)

cmd := &cobra.Command{
    Use:                   "restart RESOURCE",
    DisableFlagsInUseLine: true,
    Short:                 i18n.T("Restart a resource"),
    Long:                  restartLong,
    Example:               restartExample,
    Run: func(cmd *cobra.Command, args []string) {
        cmdutil.CheckErr(o.Complete(f, cmd, args))
        cmdutil.CheckErr(o.Validate())
        cmdutil.CheckErr(o.RunRestart())
    },
    ValidArgs: validArgs,
}

Run: func(cmd *cobra.Command, args []string) の中がこのコマンドで実行される処理の中身です。

func (o *RestartOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error

cmdutil.CheckErr(o.Complete(f, cmd, args))

ここで呼ばれているのが

// Complete completes all the required options
func (o *RestartOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
    o.Resources = args

    o.Restarter = polymorphichelpers.ObjectRestarterFn

    var err error
    o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
    if err != nil {
        return err
    }

    o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
        o.PrintFlags.NamePrintFlags.Operation = operation
        return o.PrintFlags.ToPrinter()
    }

    o.Builder = f.NewBuilder

    return nil
}

なんか色々やってますが、注目すべきはここ。

o.Restarter = polymorphichelpers.ObjectRestarterFn

Restarter に function らしき物を突っ込んでいます。 名前からして restart のロジックはこの function っぽいのが推測できますね。

Restarter の定義はこうです。

type RestartOptions struct {
    Restarter        polymorphichelpers.ObjectRestarterFunc
}

polymorphichelpers.ObjectRestarterFunc は type として定義されています。

// ObjectRestarterFunc is a function type that updates an annotation in a deployment to restart it..
type ObjectRestarterFunc func(runtime.Object) ([]byte, error)

今度は Restarter に突っ込んでいる polymorphichelpers.ObjectRestarterFn をみていきます。

定義されているのはここ。

var ObjectRestarterFn ObjectRestarterFunc = defaultObjectRestarter

defaultObjectRestarter が実体らしいです。今度はこれをみていきます。

func defaultObjectRestarter(obj runtime.Object) ([]byte, error)

func defaultObjectRestarter(obj runtime.Object) ([]byte, error) {
    switch obj := obj.(type) {
    case *extensionsv1beta1.Deployment:
        if obj.Spec.Paused {
            return nil, errors.New("can't restart paused deployment (run rollout resume first)")
        }
        if obj.Spec.Template.ObjectMeta.Annotations == nil {
            obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
        }
        obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
        return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj)
    // 略
}

Object を渡して switch で分岐しています。そして kubectl.kubernetes.io/restartedAt という annotation を更新しているっぽいのがわかりますね。

ここまでくるとだいたい予想がついたのではないでしょうか。それでは、この function を実行している場所に移動します。

func (o RestartOptions) RunRestart() error

for _, patch := range set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Restarter)) {
    info := patch.Info

    if patch.Err != nil {
        resourceString := info.Mapping.Resource.Resource
        if len(info.Mapping.Resource.Group) > 0 {
            resourceString = resourceString + "." + info.Mapping.Resource.Group
        }
        allErrs = append(allErrs, fmt.Errorf("error: %s %q %v", resourceString, info.Name, patch.Err))
        continue
    }

    if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
        allErrs = append(allErrs, fmt.Errorf("failed to create patch for %v: empty patch", info.Name))
    }

    obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
    if err != nil {
        allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err))
        continue
    }

    info.Refresh(obj, true)
    printer, err := o.ToPrinter("restarted")
    if err != nil {
        allErrs = append(allErrs, err)
        continue
    }
    if err = printer.PrintObj(info.Object, o.Out); err != nil {
        allErrs = append(allErrs, err)
    }
}

関連するところを見ていきます。

for _, patch := range set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Restarter)) {

ここでパッチの計算をしています。set.PatchFn(o.Restarter) の部分で先ほど見た func defaultObjectRestarter(obj runtime.Object) ([]byte, error) を渡しています。set.CalculatePatches() の中で呼び出しが行われます。

obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)

ここで実際に Patch API を呼び出して Object の更新を行なっています。

annotation が書き換えられることで Deployment のローリングアップデートが行われていたという仕組みでした。

動かして確認

雑に kubectl run で Deployment を作成してみます。

$ kubectl run nginx --image=nginx --replicas=1

YAML をみてみます。

$ kubectl get deployment.apps/nginx -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
  creationTimestamp: "2019-06-17T15:51:10Z"
  generation: 1
  labels:
    run: nginx
  name: nginx
  namespace: default
  resourceVersion: "2441509"
  selfLink: /apis/apps/v1/namespaces/default/deployments/nginx
  uid: ba468f9e-9117-11e9-aa8e-06fb2bea1d76
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      run: nginx
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        run: nginx
    spec:
      containers:
      - image: nginx
        imagePullPolicy: Always
        name: nginx
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status:
  availableReplicas: 1
  conditions:
  - lastTransitionTime: "2019-06-17T15:51:19Z"
    lastUpdateTime: "2019-06-17T15:51:19Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  - lastTransitionTime: "2019-06-17T15:51:10Z"
    lastUpdateTime: "2019-06-17T15:51:19Z"
    message: ReplicaSet "nginx-dbddb74b8" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
  observedGeneration: 1
  readyReplicas: 1
  replicas: 1
  updatedReplicas: 1

次に kubectl rollout restart を実行します。

$ kubectl rollout restart deployment.apps/nginx
deployment.apps/nginx restarted

その後、もう一度 kubectl get deployment.apps/nginx -o yaml をすると diff が下記のようになりました。

--- foo.yaml    2019-06-18 00:53:14.000000000 +0900
+++ bar.yaml    2019-06-18 00:54:32.000000000 +0900
@@ -2,14 +2,14 @@
 kind: Deployment
 metadata:
   annotations:
-    deployment.kubernetes.io/revision: "1"
+    deployment.kubernetes.io/revision: "2"
   creationTimestamp: "2019-06-17T15:51:10Z"
-  generation: 1
+  generation: 2
   labels:
     run: nginx
   name: nginx
   namespace: default
-  resourceVersion: "2441509"
+  resourceVersion: "2442002"
   selfLink: /apis/apps/v1/namespaces/default/deployments/nginx
   uid: ba468f9e-9117-11e9-aa8e-06fb2bea1d76
 spec:
@@ -26,6 +26,8 @@
     type: RollingUpdate
   template:
     metadata:
+      annotations:
+        kubectl.kubernetes.io/restartedAt: "2019-06-18T00:53:51+09:00"
       creationTimestamp: null
       labels:
         run: nginx
@@ -52,12 +54,12 @@
     status: "True"
     type: Available
   - lastTransitionTime: "2019-06-17T15:51:10Z"
-    lastUpdateTime: "2019-06-17T15:51:19Z"
-    message: ReplicaSet "nginx-dbddb74b8" has successfully progressed.
+    lastUpdateTime: "2019-06-17T15:53:56Z"
+    message: ReplicaSet "nginx-857965c444" has successfully progressed.
     reason: NewReplicaSetAvailable
     status: "True"
     type: Progressing
-  observedGeneration: 1
+  observedGeneration: 2
   readyReplicas: 1
   replicas: 1
   updatedReplicas: 1

期待通りに kubectl.kubernetes.io/restartedAt: "2019-06-18T00:53:51+09:00" という annotation が付与されました。

まとめ

kubectl rollout restart コマンドによるローリングアップデートは kubectl.kubernetes.io/restartedAt という annotation を PodTemplateSpec に付与/更新して実現している。