Edited at

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 アノテーションがそれです。

$ 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) していることがわかります。

$ 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 の動作 を参照してください。


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

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


  • ユーザが POST したのち、何かを変更して適用する

  • ユーザが適用したのち、kubectl edit を使い、その後再適用する

  • ユーザが GET してからローカルで編集したのち適用する

  • ユーザがアノテーションを調整したのち適用する

  • アリスが何かを適用したのち、ボブが何かを適用する

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


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

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

$ 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 が正しく設定できているか確認します。

$ 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 フラグを指定する必要があります。

$ 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 で作成します。

$ 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. オブジェクトを適用する

  2. その後、誰かがオブジェクトを直接編集する

  3. オブジェクトを再度適用する

# オブジェクトを適用する

$ 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 で実行します。

$ 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 オプションを使って強制的に適用します。

# 衝突が検知され適用に失敗する

$ 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 のために新たに作成されたフィールドです。

# $ 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 が行った操作です。ここでは、kubectlApply を行ったことが分かります。

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

  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: といったフィールドは、次のような意味になっていますが、あまりに細かいので説明しません。気になる人は調べてみてください。

// 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"`
}


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

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

    _, 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 の基本的な利用方法はリポジトリをみてもらうとして、ここでは RESTClient を使ってパッチする部分だけを紹介します。PatchType としては、新たに用意された types.ApplyPatchType を指定し、fieldManager パラメータに FieldManager 名を指定します。fieldManager パラメータの指定は必須です。

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

$ 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 にあります。


curl から SSA を利用する

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

# 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 時点でアルファステージの機能です。プロダクションでの利用は避けたほうがよいでしょう。


参照