Kubernetes 1.14: Server-side Apply (alpha)

## はじめに

ここでは、Kubernetes 1.14 で実装された Server-side Apply (SSA) をみていきます。SSA は、1.14 時点で alpha ステージの機能であり、利用するには kube-apiserver feature-gates で明示的に有効にする必要があります。

## 適用 (Apply) とはなにか

まず SSA の説明に入るまえに、適用 (Apply) とは何かについて説明します。

Kubernetes での適用とは、kubectl apply コマンドによるオブジェクトの作成/更新方法のひとつです。Kubernetes でのオブジェクト管理には、大きく命令的な手法 (kubectl create/replace/delete コマンドなど）と宣言的手法 (kubectl apply コマンド) の2つがあります。普段のオペレーションでは用途に合わせてどちらの手法も利用するのですが、デプロイに関しては「システムのあるべき姿を設定ファイル（マニフェスト）として管理し、kubectl apply コマンドでクラスタに適用する」宣言的手法を用いることが一般的です。kubectl apply コマンドによるマニフェストファイルの適用は、マニフェストファイルに記述されたオブジェクトが存在しなければ作成し、存在すれば差分を反映するという動作です。なぜ置き換え（replace）してはいけないかというと、作成されたオブジェクトは、一部のフィールドが Kubernetes 自体によって管理されます。そのため、完全にオブジェクトを置き換えてしまうと、Kubernetes 自体が管理するフィールドの情報が失われてしまうため、差分を反映する形にしなければなりません。

では差分はどのように求めるのでしょうか。現在のオブジェクトの状態と適用したいオブジェクトをそのまま比較してしまうと、現在のオブジェクトには Kubernetes 自体が管理するフィールドに変更が加えられているため、正しく差分を求めることができません。そのため、差分を正確に求めるためには、Kubernetes が変更を加える前の適用した時点でのオブジェクトと今回適用するオブジェクトを比較する必要があります。kubectl apply コマンドは、この前回適用したオブジェクトの保持に、アノテーションを利用しています。一度は見たことがあるかと思いますが、オブジェクトの `kubectl.kubernetes.io/last-applied-configuration` アノテーションがそれです。

```console
$ kubectl apply -f nginx.yaml
deployment.apps/nginx created
$ kubectl get deploy nginx -o 'jsonpath={.metadata.annotations.kubectl\.kubernetes\.io/last-applied-configuration}'
{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"creationTimestamp":null,"labels":{"app":"nginx"},"name":"nginx","namespace":"default"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"nginx"}},"strategy":{},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"nginx"}},"spec":{"containers":[{"image":"nginx","name":"nginx","resources":{}}]}}},"status":{}}
# kubectl apply view-last-applied コマンドで上のアノテーションを出力することもできます
```

kubectl apply コマンドは、すでにオブジェクトが存在する場合は、先にそのオブジェクトの `kubectl.kubernetes.io/last-applied-configuration` アノテーションを取得したのち、差分を計算してパッチします。下記は、差分がある場合に kubectl コマンドが kube-apiserver にどのようなリクエストをしているかのログです。一度オブジェクトを取得 (GET) したのちに、差分をパッチ (PATCH) していることがわかります。

```console
$ kubectl apply -f nginx.yaml -v=7
I0411 09:53:24.382668   75865 loader.go:359] Config loaded from file /Users/ksuda/.kube/config
I0411 09:53:24.384400   75865 round_trippers.go:416] GET https://192.168.99.100:8443/openapi/v2?timeout=32s
I0411 09:53:24.384413   75865 round_trippers.go:423] Request Headers:
I0411 09:53:24.384421   75865 round_trippers.go:426]     Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf
I0411 09:53:24.384429   75865 round_trippers.go:426]     User-Agent: kubectl/v1.14.0 (darwin/amd64) kubernetes/641856d
I0411 09:53:24.395074   75865 round_trippers.go:441] Response Status: 200 OK in 10 milliseconds
I0411 09:53:24.486323   75865 round_trippers.go:416] GET https://192.168.99.100:8443/apis/apps/v1/namespaces/default/deployments/nginx
I0411 09:53:24.486342   75865 round_trippers.go:423] Request Headers:
I0411 09:53:24.486346   75865 round_trippers.go:426]     Accept: application/json
I0411 09:53:24.486350   75865 round_trippers.go:426]     User-Agent: kubectl/v1.14.0 (darwin/amd64) kubernetes/641856d
I0411 09:53:24.488646   75865 round_trippers.go:441] Response Status: 200 OK in 2 milliseconds
I0411 09:53:24.489279   75865 round_trippers.go:416] PATCH https://192.168.99.100:8443/apis/apps/v1/namespaces/default/deployments/nginx
I0411 09:53:24.489289   75865 round_trippers.go:423] Request Headers:
I0411 09:53:24.489294   75865 round_trippers.go:426]     Accept: application/json
I0411 09:53:24.489298   75865 round_trippers.go:426]     Content-Type: application/strategic-merge-patch+json
I0411 09:53:24.489302   75865 round_trippers.go:426]     User-Agent: kubectl/v1.14.0 (darwin/amd64) kubernetes/641856d
I0411 09:53:24.496582   75865 round_trippers.go:441] Response Status: 200 OK in 7 milliseconds
deployment.apps/nginx configured
```

このパッチ方法には、Kubernetes 独自の Strategic Merge Patch というアルゴリズムが利用されていて、これだけでもかなり奥深いのでより詳細を知りたい方は [Kubernetes: kubectl apply の動作](https://qiita.com/tkusumi/items/0bf5417c865ef716b221) を参照してください。

## Server-side Apply (SSA) とはなにか

ここまでで説明してきた適用は、クライアントサイド (kubectl) で差分を計算していました。この差分の計算をサーバサイドで行うのが Server-side Apply (SSA) です。なぜ現在クライアントサイドでうまくいっているものをサーバサイドで行うように変更しているかというと、実は次のようなケースで問題があることが分かっています。 [^enhancements-555]

- ユーザが POST したのち、何かを変更して適用する
- ユーザが適用したのち、kubectl edit を使い、その後再適用する
- ユーザが GET してからローカルで編集したのち適用する
- ユーザがアノテーションを調整したのち適用する
- アリスが何かを適用したのち、ボブが何かを適用する

このような問題を解決するには既存のクライアントで差分を計算する方法では修正が必要なコンポーネントが多すぎるといった理由から SSA が実装されました。

[^enhancements-555]: https://github.com/kubernetes/enhancements/issues/555

## SSA が利用できるクラスタの準備

実際に試していく前に minikube を利用して SSA を有効にしたクラスタを作成します。SSA を利用するには、FeatureGates で明示的に機能を有効にする必要があります。

```console
$ minikube start --kubernetes-version=v1.14.0 --extra-config=apiserver.feature-gates=ServerSideApply=true
😄  minikube v1.0.0 on darwin (amd64)
🤹  Downloading Kubernetes v1.14.0 images in the background ...
🔥  Creating virtualbox VM (CPUs=2, Memory=2048MB, Disk=20000MB) ...
📶  "minikube" IP address is 192.168.99.100
🐳  Configuring Docker as the container runtime ...
🐳  Version of container runtime is 18.06.2-ce
⌛  Waiting for image downloads to complete ...
✨  Preparing Kubernetes environment ...
    ▪ apiserver.feature-gates=ServerSideApply=true
🚜  Pulling images required by Kubernetes v1.14.0 ...
🚀  Launching Kubernetes v1.14.0 using kubeadm ...
⌛  Waiting for pods: apiserver proxy etcd scheduler controller dns
🔑  Configuring cluster permissions ...
🤔  Verifying component health .....
💗  kubectl is now configured to use "minikube"
🏄  Done! Thank you for using minikube!
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.0", GitCommit:"641856db18352033a0d96dbc99153fa3b27298e5", GitTreeState:"clean", BuildDate:"2019-03-25T15:53:57Z", GoVersion:"go1.12.1", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.0", GitCommit:"641856db18352033a0d96dbc99153fa3b27298e5", GitTreeState:"clean", BuildDate:"2019-03-25T15:45:25Z", GoVersion:"go1.12.1", Compiler:"gc", Platform:"linux/amd64"}
```

下記のコマンドで kube-apiserver の feature-gates が正しく設定できているか確認します。

```console
$ minikube ssh -- sudo cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep feature-gates
    - --feature-gates=ServerSideApply=true
```

## kubectl apply コマンドから SSA を利用する

では、kubectl apply コマンドから SSA を使ってみます。Kubernetes v1.14 時点で SSA はまだアルファ機能のため、kubectl apply コマンドで SSA を利用するには、`--experimental-server-side` フラグを指定する必要があります。

```console
$ kubectl apply -h | grep experimental-server-side
      --experimental-server-side=false: If true, apply runs in the server instead of the client. This is an alpha feature and flag.
```

ここでは、例として `nginx` という名前の Deployment オブジェクトを SSA で作成します。

```console
$ cat nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.15
        name: nginx
$ kubectl apply -f nginx.yaml --experimental-server-side
deployment.apps/nginx serverside-applied
```

問題なくオブジェクトが作成されました。

## Server-side Apply と Client-side Apply の比較

それでは、Client-side Apply での問題が、Server-side Apply で解決されているのかをみていきます。

ここでは、次のシナリオを Client-side Apply で実行します。

1. オブジェクトを適用する
1. その後、誰かがオブジェクトを直接編集する
1. オブジェクトを再度適用する

```console
# オブジェクトを適用する
$ kubectl apply -f nginx.yaml
deployment.apps/nginx created
# 誰かが命令的にコンテナイメージのタグを更新した
$ kubectl set image deploy nginx nginx=nginx:1.15
deployment.extensions/nginx image updated
# 正しくコンテナイメージのタグが更新されている
$ kubectl get deploy nginx -o 'jsonpath={.spec.template.spec.containers[?(@.name=="nginx")].image}'
nginx:1.15
# それを知らずに再度オブジェクトが適用された
$ kubectl apply -f nginx.yaml
deployment.apps/nginx configured
# コンテナイメージタグの変更が失われている
$ kubectl get deploy nginx -o 'jsonpath={.spec.template.spec.containers[?(@.name=="nginx")].image}'
nginx
```

ここでの問題は、誰かによって命令的に変更されていたことに気づかないまま適用に成功してしまうことです。

では次に同じシナリオを Server-side Apply で実行します。

```console
$ kubectl apply -f nginx.yaml --experimental-server-side
deployment.apps/nginx serverside-applied
$ kubectl set image deploy nginx nginx=nginx:1.15
deployment.extensions/nginx image updated
$ kubectl get deploy nginx -o 'jsonpath={.spec.template.spec.containers[?(@.name=="nginx")].image}'
nginx:1.15
$ kubectl apply -f nginx.yaml --experimental-server-side
Error from server (Conflict): Apply failed with 1 conflict: conflict with "kubectl" using extensions/v1beta1 at 2019-04-09T06:39:30Z: .spec.template.spec.containers[name="nginx"].image
```

SSA では、再適用時にエラーとなりました。エラー文から1つの衝突が検知され、 `.spec.template.spec.containers[name="nginx"].image` が事前に操作されていることが分かります。

では、衝突してしまった場合にどのように解決するかというと、`kubectl diff` コマンドでオブジェクトの差分を確認し、マニフェストファイルを修正してから適用することです。もしマニフェストファイルを正として強制的にオブジェクトを上書きしたい場合（Client-side Apply 時の再適用と同じ動作）は、`--experimental-force-conflicts` オプションを使って強制的に適用します。

```console
# 衝突が検知され適用に失敗する
$ kubectl apply -f nginx.yaml --experimental-server-side
Error from server (Conflict): Apply failed with 1 conflict: conflict with "kubectl" using extensions/v1beta1 at 2019-04-09T06:53:48Z: .spec.template.spec.containers[name="nginx"].image
# --experimental-force-conflicts オプションを使って、強制的に適用する
$ kubectl apply -f nginx.yaml --experimental-server-side --experimental-force-conflicts
deployment.apps/nginx serverside-applied
# マニフェストファイルが正としてオブジェクトが更新される
$ kubectl get deploy nginx -o 'jsonpath={.spec.template.spec.containers[?(@.name=="nginx")].image}'
nginx
```

## SSA は、適用したオブジェクトの状態をどのように管理しているのか

Client-side Apply では、アノテーションを利用して前回適用したオブジェクトの状態を管理していましたが、SSA では `kubectl.kubernetes.io/last-applied-configuration` アノテーションが存在しません。その代わりに見慣れない `metadata.managedFields` フィールド(1)があります。このフィールドは、SSA のために新たに作成されたフィールドです。

```yaml
# $ kubectl get deploy nginx -o yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "2"
  creationTimestamp: "2019-04-11T01:26:06Z"
  generation: 2
  labels:
    app: nginx
  managedFields:  # (1) SSA 管理用のフィールド
  - apiVersion: apps/v1
    fields:
      f:metadata:
        f:labels:
          f:app: null
      f:spec:
        f:replicas: null
        f:selector:
          f:matchLabels:
            f:app: null
        f:template:
          f:metadata:
            f:creationTimestamp: null
            f:labels:
              f:app: null
          f:spec:
            f:containers:
              k:{"name":"nginx"}:
                .: null
                f:name: null
    manager: kubectl # (2) フィールドマネージャ
    operation: Apply # (3) 操作 (Apply/Update)
  - apiVersion: apps/v1
    fields:
      f:metadata:
        f:annotations: null
      f:status:
        f:conditions:
          .: null
          k:{"type":"Available"}:
            .: null
            f:type: null
          k:{"type":"Progressing"}:
            .: null
            f:lastTransitionTime: null
            f:status: null
            f:type: null
    manager: kube-controller-manager
    operation: Update
    time: "2019-04-11T01:26:06Z"
  - apiVersion: apps/v1
    fields:
      f:metadata:
        f:annotations:
          f:deployment.kubernetes.io/revision: null
      f:status:
        f:conditions:
          k:{"type":"Available"}:
            f:lastTransitionTime: null
            f:lastUpdateTime: null
            f:message: null
            f:reason: null
            f:status: null
        f:observedGeneration: null
        f:updatedReplicas: null
    manager: kube-controller-manager
    operation: Update
    time: "2019-04-11T01:26:11Z"
  - apiVersion: extensions/v1beta1
    fields:
      f:spec:
        f:template:
          f:spec:
            f:containers:
              k:{"name":"nginx"}:
                f:image: null
    manager: kubectl
    operation: Update
    time: "2019-04-11T01:26:11Z"
  - apiVersion: apps/v1
    fields:
      f:status:
        f:availableReplicas: null
        f:conditions:
          k:{"type":"Progressing"}:
            f:lastUpdateTime: null
            f:message: null
            f:reason: null
        f:readyReplicas: null
        f:replicas: null
    manager: kube-controller-manager
    operation: Update
    time: "2019-04-11T01:26:15Z"
  name: nginx
  namespace: default
  resourceVersion: "38910"
  selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/nginx
  uid: c786fd8e-5bf8-11e9-8adf-08002714f1ee
spec:
  (略)
```

`metadata.managedFields.manager` フィールド(2)は、FieldManager (フィールドマネージャ）というオブジェクトを操作したコンポーネント名が入ります。kubectl apply コマンドで操作した場合のデフォルトの FieldManager 名は `kubectl` です。FieldManager 名は、kubectl apply コマンドの `--experimental-field-manager` オプションで変更できます。Kubernetes コントローラの一部として kubectl apply コマンドを利用する場合などで、FieldManager 名を変更するとよいでしょう。

また、`metadata.managedFields.operation`(3) は、FieldManager が行った操作です。ここでは、`kubectl` が `Apply` を行ったことが分かります。

`metadata.managedFields` フィールド(1)は、配列となっており、よくみると kubectl による Apply 操作以降に kube-controller-manager や kubectl がオブジェクトを操作していることがわかります。

```yaml
  managedFields:
  - (略)
    manager: kubectl
    operation: Apply
  - (略)
    manager: kube-controller-manager
    operation: Update
  - (略)
    manager: kube-controller-manager
    operation: Update
  - (略) # kubectl set image による操作です
    manager: kubectl
    operation: Update
  - (略)
    manager: kube-controller-manager
    operation: Update
```

これは Client-side Apply では記録されていなかった情報です。このようにオブジェクトのどのフィールドを誰か管理 (manage) しているのかが記録されることで Client-side Apply での問題が解決されるようになっています。

`f:` や `k:` といったフィールドは、次のような意味になっていますが、あまりに細かいので説明しません。気になる人は調べてみてください。

```go
// Fields stores a set of fields in a data structure like a Trie.
// To understand how this is used, see: https://github.com/kubernetes-sigs/structured-merge-diff
type Fields struct {
	// Map stores a set of fields in a data structure like a Trie.
	//
	// Each key is either a '.' representing the field itself, and will always map to an empty set,
	// or a string representing a sub-field or item. The string will follow one of these four formats:
	// 'f:<name>', where <name> is the name of a field in a struct, or key in a map
	// 'v:<value>', where <value> is the exact json formatted value of a list item
	// 'i:<index>', where <index> is position of a item in a list
	// 'k:<keys>', where <keys> is a map of  a list item's key fields to their unique values
	// If a key maps to an empty Fields value, the field that key represents is part of the set.
	//
	// The exact format is defined in k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal
	Map map[string]Fields `json:",inline" protobuf:"bytes,1,rep,name=map"`
}
```

- https://github.com/kubernetes/apimachinery/blob/kubernetes-1.14.1/pkg/apis/meta/v1/types.go#L1115-L1130

## k8s.io/client-go から SSA を利用する

次は、Kubernetes の Go ライブラリを使って SSA を利用する方法をみていきます。これまでの差分の計算はクライアント (kubectl) に直接実装されていたため、サードパーティのツールなどから利用するには kubectl コマンドをバンドルすることが必要でした。SSA は差分計算がサーバサイドに実装されたことで、ライブラリからでも簡単に利用できるようになっています。

```go
	_, err = clientset.AppsV1().RESTClient().Patch(types.ApplyPatchType).
		Namespace("default").
		Resource("deployments").
		Name("nginx").
		Param("fieldManager", "try-server-side-apply").
		Body(data).
		Do().
		Get()
	if err != nil {
		log.Fatal(err)
	}
```

client-go の基本的な利用方法は[リポジトリ](https://github.com/kubernetes/client-go)をみてもらうとして、ここでは RESTClient を使ってパッチする部分だけを紹介します。PatchType としては、新たに用意された `types.ApplyPatchType` を指定し、`fieldManager` パラメータに FieldManager 名を指定します。`fieldManager` パラメータの指定は必須です。

注意点としては、通常のパッチでは差分のみをボディで送信するところ、SSA の場合はオブジェクト全体を送信します。また、パッチは通常オブジェクトが存在しないと操作に失敗しますが、SSA ではオブジェクトが存在しなくてもオブジェクトの作成に成功します。

```console
$ kubectl apply -f nginx.yaml --experimental-server-side -v=7
I0411 10:39:49.284996   79127 loader.go:359] Config loaded from file /Users/ksuda/.kube/config
I0411 10:39:49.286156   79127 round_trippers.go:416] GET https://192.168.99.100:8443/openapi/v2?timeout=32s
I0411 10:39:49.286171   79127 round_trippers.go:423] Request Headers:
I0411 10:39:49.286180   79127 round_trippers.go:426]     Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf
I0411 10:39:49.286185   79127 round_trippers.go:426]     User-Agent: kubectl/v1.14.0 (darwin/amd64) kubernetes/641856d
I0411 10:39:49.300244   79127 round_trippers.go:441] Response Status: 200 OK in 14 milliseconds
I0411 10:39:49.377397   79127 round_trippers.go:416] PATCH https://192.168.99.100:8443/apis/apps/v1/namespaces/default/deployments/nginx?fieldManager=kubectl&force=false
I0411 10:39:49.377412   79127 round_trippers.go:423] Request Headers:
I0411 10:39:49.377417   79127 round_trippers.go:426]     Accept: application/json
I0411 10:39:49.377421   79127 round_trippers.go:426]     Content-Type: application/apply-patch+yaml
I0411 10:39:49.377425   79127 round_trippers.go:426]     User-Agent: kubectl/v1.14.0 (darwin/amd64) kubernetes/641856d
I0411 10:39:49.382945   79127 round_trippers.go:441] Response Status: 201 Created in 5 milliseconds
deployment.apps/nginx serverside-applied
```

上記は、kubectl apply コマンドで SSA を利用した際のログです。Client-side Apply 時にはあったオブジェクト情報の取得 (GET) の操作がないことが分かります。そのため、これまでのようなオブジェクトが存在しない場合は作成 (Create) して、存在する場合はパッチする (Update) といった CreateOrUpdate のような操作が不要になっているところも大きな変化です。

コードの全体は [serverside\-apply\.go](https://gist.github.com/superbrothers/1b689412c38c3203109f335bea89924c) にあります。

## curl から SSA を利用する

わざわざライブラリを利用せずに curl からでも簡単に利用できます。

```console
# ServiceAccount から操作したいので、ServiceAccount に admin 権限を付与する
$ kubectl create rolebinding default --clusterrole admin --serviceaccount default:default
rolebinding.rbac.authorization.k8s.io/default created
# ServiceAccount のトークンを TOKEN 環境変数に設定する
$ export TOKEN="$(kubectl get secret default-token-gwqqz --template='{{.data.token | base64decode }}')"
# レッツ Server-side Apply
$ curl -X PATCH --insecure -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/apply-patch+yaml" -d "$(cat ./nginx.yaml)" "https://192.168.99.100:8443/apis/apps/v1/namespaces/default/deployments/nginx?fieldManager=kubectl"
```

## 所感

SSA により、これまで以上に安全に宣言的なオブジェクト管理ができるようになりそうです。また、どのコンポーネントによってオブジェクトが操作されたのかがわかるようになるのも、問題があった際に役立ちそうです。そのほか、client-go や curl からであったも簡単にオブジェクトを適用できるようになるため、これまでオブジェクトを適用したいがためだけに kubectl コマンドをバンドルしなければならなかったことが今後は不要になります。

:warning: SSA は、Kubernetes 1.14 時点でアルファステージの機能です。プロダクションでの利用は避けたほうがよいでしょう。

## 参照

- [Kubernetes API Concepts \- Kubernetes](https://kubernetes.io/docs/reference/using-api/api-concepts/#server-side-apply)
- [Server\-side Apply · Issue \#555 · kubernetes/enhancements](https://github.com/kubernetes/enhancements/issues/555)