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 に付与/更新して実現している。