Kubernetes: kubectl apply の動作

`kubectl apply` はオブジェクトが存在しなければ作成し、存在すれば差分を反映してくれる便利なコマンドです。しかし差分の反映は単純な上書きではないので注意が必要です。この記事では [Kubernetes Meetup Tokyo #5 で発表した内容を](https://speakerdeck.com/tksm/how-kubectl-apply-works)ベースに kubectl apply の動作を整理してみました。Kubernetes v1.9.0 で確認をしています。

## kubectl apply とは

`kubectl` にはオブジェクトを操作する様々なサブコマンドが用意されています。apply 以外の create, replace などのサブコマンドは、現在のオブジェクトの状態を意識して操作をする必要があります。例えば `kubectl create` では存在する名前のオブジェクトを再度作ろうとするとエラーになりますし、`kubectl replace` では存在しないオブジェクトを上書きしようと思うとエラーになります。

一方 `kubectl apply` は自動的にオブジェクトの状態を見て、存在しなければ作成、存在すれば**差分を計算して反映**と自動で判断をしてくれます。このため `kubectl apply` を上手く使えば create / replace / patchなどのサブコマンドを使い分けることなく、マニフェストだけで宣言的 (declarative) に管理することができます。この管理の仕方は Kubernetes の公式ドキュメント内では [Declarative object configuration](https://kubernetes.io/docs/tutorials/object-management-kubectl/object-management/#declarative-object-configuration) と呼ばれています。

サブコマンド | オブジェクトが存在しない | オブジェクトが存在する
--- | --- | ---
`apply`   | :white_check_mark: 新規作成 (`POST`) | :white_check_mark: 差分反映 (`PATCH`, `DELETE`)
`create`  | :white_check_mark: 新規作成 (`POST`) | :warning:  エラー
`replace` | :warning: エラー   | :white_check_mark: 上書き (`PUT`)
`patch`   | :warning: エラー   | :white_check_mark: 差分反映 (`PATCH`)
`delete`  | :warning: エラー   | :white_check_mark: 削除 (`DELETE`)

以下は kubectl apply の動作例です。

```bash
# apply はオブジェクトが存在しない場合は新規作成
$ kubectl apply -f nginx-dep.yml
deployment "nginx" created

# マニフェストを編集
$ vim nginx-dep.yaml

# create はオブジェクトが存在する場合エラーになる
$ kubectl create -f nginx-dep.yaml
Error from server (AlreadyExists): error when creating "nginx-dep.yaml": deployments.extensions "nginx" already exists

# apply はオブジェクトが存在する場合もエラーにならない。差分があれば差分を反映
$ kubectl apply -f nginx-dep.yaml
deployment "nginx" configured
```

また `--prune` というオプションを利用することでマニフェストから削除されたオブジェクトを削除することもできます。以下は `--prune` の動作例です。削除を行うには同じラベル (例では app=myapp)が設定されている必要があります。

```bash
# 2 つのマニフェストがある
$ ls
myapp-api-dep.yaml myapp-web-dep.yaml

# ディレクトリ内の 2 つのマニフェストを適用
$ kubectl apply -f . --prune -l app=myapp
deployment "myapp-api" created
deployment "myapp-web" created

# myapp-api のマニフェストを削除
$ rm myapp-api-dep.yaml

# --prune が指定されているため削除したマニフェストに含まれる myapp-api が削除される
$ kubectl apply -f . --prune -l app=myapp
deployment "myapp-web" configured
# myapp-api が自動的に削除された
deployment "myapp-api" pruned
```

## kubectl apply のパッチの計算

公式ドキュメントの [How apply calculates differences and merges changes](https://kubernetes.io/docs/tutorials/object-management-kubectl/declarative-object-management-configuration/#how-apply-calculates-differences-and-merges-changes) という章の情報を参考にしています。

前述の通り `kubectl apply` はオブジェクトが存在した場合は差分の計算をして変更を反映さます。差分の計算には以下の **3 つのオブジェクト**が比較対象になります。

オブジェクト | 説明
--- | ---
適用するオブジェクト | 適用しようとしているマニフェストを読み込んだオブジェクト
現在のオブジェクト | クラスタ側のオブジェクトの現在の状態
前回適用したオブジェクト | クラスタ側のオブジェクトに前回 `kubectl apply`で適用した情報

「前回適用したオブジェクト」は以下のように対象オブジェクトの `kubectl.kubernetes.io/last-applied-configuration` というアノテーションに保存されています。

```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    # 前回適用したオブジェクトはアノテーションに JSON 形式で保存されている
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"extensions/v1beta1","kind":"Deployment" ....
```

この 3 つのオブジェクトを比較して以下のようにパッチを作成します。

1. **削除分の計算**。「前回適用したオブジェクト」に存在し、「適用するオブジェクト」から削除されたフィールドが対象になります。
2. **追加と変更の計算**。「適用するオブジェクト」と「現在のオブジェクト」で追加や変更があったフィールドが対象になります。

![qiita-kubectl-apply.001.png](https://qiita-image-store.s3.amazonaws.com/0/100377/5fcd6328-a056-7bf4-7d44-1f5ef56ead53.png)


`kubectl apply` の内部でこのパッチを作成し下記のようなパッチを送信しています。パッチの適用には [Strategic Merge Patch](https://github.com/kubernetes/community/blob/master/contributors/devel/strategic-merge-patch.md) という Kubernetese 独自のマージアルゴリズムが使われています。

## kubectl apply の利点

この差分のパッチを当てる動作は、[Horizontal Pod Autoscaling](https://kubernetes.io/docs/user-guide/horizontal-pod-autoscaling/) のように現在のオブジェクトの情報(この場合レプリカ数)が動的に変更される場合に利点があります。マニフェストで指定されていないフィールドはそのまま引き継がれるので、例えばレプリカ数を記載しなければ、`kubectl apply`でDeploymentの情報を更新した場合に、現在のオブジェクトの情報のレプリカ数がそのまま引き継がれます。

### レプリカ数の管理をマニフェストで行わない例

レプリカ数を Deployment に記載しないことで、Horizontal Pod Autoscaler や `kubectl scale` にレプリカ数の管理を移譲することができる例です。

1. レプリカ数を指定せず deployent を apply する
  - レプリカ数はデフォルトの 1 になる
2. レプリカ数を直接 4 に変更
  - `kubectl scale --replicas 4 deployment nginx` で更新
3. コンテナのタグのバージョンを更新してマニフェストを再度 apply する
  - 差分はタグのバージョンだけになる。レプリカ数は記載されていないので差分として出ない
  - レプリカ数は 4 のままタグのバージョンだけが更新される :tada:

![qiita-kubectl-apply.002.png](https://qiita-image-store.s3.amazonaws.com/0/100377/6bf868c8-bae0-2002-030f-0386e3a3dcd6.png)

## apply で注意が必要なパターン

kubectl では前回適用したオブジェクトを元に削除するフィールドを計算するため、**一度設定したフィールドを削除した場合はデフォルト値になる**ので注意が必要です。

前述のレプリカ数の例では最初にレプリカ数を指定していた場合、次にレプリカ数を削除して apply するとデフォルト値 (1) に戻ります。

1. **レプリカ数を 2 に指定して** deployent を作成する
2. レプリカ数を直接 4 に変更
  - `kubectl scale --replicas 4 deployment nginx` で更新
3. **レプリカ数の指定を削除し**コンテナのタグのバージョンを更新してマニフェストを再度反映
  - 差分はレプリカ数の削除とタグのバージョンの更新になる
  - **レプリカ数はデフォルト値の 1 になる** :warning: 

![qiita-kubectl-apply.003.png](https://qiita-image-store.s3.amazonaws.com/0/100377/ebf5209f-b6f5-e39b-bc76-c0069c4fecbf.png)


## Strategic Merge Patch
kubectl applyのパッチのマージには [Strategic Merge Patch](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/strategic-merge-patch.md) と呼ばれる Kubernetes独自のマージの方法が使われます。

[Declarative Management of Kubernetes Objects Using Configuration Files](https://kubernetes.io/docs/tutorials/object-management-kubectl/declarative-object-management-configuration/#merging-changes-to-primitive-fields) という公式ドキュメントにprimitive, map, listのそれぞれの場合のマージの仕方が書かれているので詳細はこちらをご覧ください。

また Strategic Merge Patch の細かい挙動については Kubecon 2017 で CoreOS の Aaron Levy 氏が発表した [kubectl apply, and The Dark Art of Declarative Object Management](https://kccncna17.sched.com/event/CU7K) ([セッション動画](https://www.youtube.com/watch?v=CW3ZuQy_YZw))というセッションが非常によくまとまっています。list のマージ方法はオブジェクトの Go の構造体のタグ `patchMergeKey` の指定によって動作が変わるといった詳細な動きが紹介されています。

## まとめ

kubectl は上書きではなく差分反映を行います。差分は「適用するオブジェクト」「現在のオブジェクト」「前回適用したオブジェクト」の 3 オブジェクトから計算されます。この仕組みによってレプリカ数など一部をマニフェストの管理から外して Horizontal Pod Autoscaler に任せるような柔軟な運用が可能です。