Kubernetes: Deployment の仕組み

この記事は Kubernetes 1.5.2 で確認した情報を元に記載しています。

# Deployment

`Deployment`はローリングアップデートやロールバックといったデプロイ管理の仕組みを提供するものです。

## Deployment の仕組み

下記の図のように`Deployment`は`ReplicaSet`を生成・管理し、`ReplicaSet`は`Pod`を生成・管理します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/100377/03f4ff76-9408-256e-c019-645ab77f8b09.png" width="572">

`ReplicaSet`(`ReplicationController`の後継)は[`PodTemplate`](https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podtemplate)と呼ばれる`Pod`のテンプレートをもとに、`Pod`を指定された数(レプリカ数)に調整・管理を行う仕組みです。`Pod`がレプリカ数より足りない場合は`Pod`を追加し、多い場合はを`Pod`を削除します。この仕組みによってノードの障害やアプリケーションのクラッシュで`Pod`が足りなくなった際も自動的に`Pod`が追加され、セルフヒーリングが実現されています。

`Deployment`はコンテナイメージのバージョンアップなどアップデートがあった場合、新しい仕様の`ReplicaSet`を作成し、新旧の全体`Pod`数を調整しながら新しい仕様の`Pod`に置き換えていきます。これによりローリングアップデートが実現されます。ただし、レプリカ数のみ変更の場合は新しい`ReplicaSet`は作成せず、現在の`ReplicaSet`のレプリカ数を変更します。新旧の`Pod`ともに共通するラベルを持っておりローリングアップデート中は`Service`を通して新旧のバージョンが混ざってサービス提供される形になります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/100377/adb7acb1-72ef-5765-77bd-fae9d9c5cc09.png" width="777">

リソース | 役割
--- | ---
`Deployment` | `ReplicaSet`を生成、管理しローリングアップデートやロールバックといったデプロイ管理を行います。
`ReplicaSet` | 同じ仕様の`Pod`のレプリカ数を管理します。`ReplicationController`の後継に当たるものです。
`Pod`        | アプリケーションを動かすための最小単位。1つ以上のコンテナと共有されたボリュームで構成されます。

ローリングアップデートが終わった後もアップデート前の`ReplicaSet`は一定数保持されます。これによりロールバックが可能になっています。

<img src="https://qiita-image-store.s3.amazonaws.com/0/100377/050b6a40-fcb0-610f-1c9f-a753ad124d36.png" width="774">

## Deployment の定義

以下は`Deployment`の定義例になります。下記の内容を`deployment-example.yaml`という名前で保存したとします。

```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  # Deploymentの名前。Namespace内ではユニークである必要があります
  name: deployment-example
spec:
  # レプリカ数の指定
  replicas: 2
  # Podのテンプレート(PodTemplate)
  template:
    metadata:
      labels:
        # ラベル指定は必須
        app: deployment-example
    spec:
      containers:
      - name: nginx
        image: nginx:1.10
        ports:
        - containerPort: 80
```

`.spec.template`は[`PodTemplate`](https://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podtemplate)と呼ばれる`Pod`のテンプレートになります。この`PodTemplate`をもとに`ReplicaSet`がレプリカ数分の`Pod`を生成・管理します。また`Deployment`が配下の`ReplicaSet`と`ReplicaSet`が管理する`Pod`を管理するためにラベルが必要となり、`.spec.template.metadata.labels`で指定する必要があります。

## Deployment のデプロイ

定義した`Deployment`をデプロイをしてみます。ここでは操作履歴を残すために`--record`オプションを指定して`kubectl`のコマンドをアノテーションに保存するように指定しています。

```bash
$ kubectl create -f deployment-example.yaml --record
deployment "deployment-example" created
```

デプロイできたか確認してみます。

```bash
$ kubectl get deployments
NAME                 DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment-example   2         2         2            2           22m
```

`--selector`オプションで設定したラベルを指定して関連リソースを見てます。

```bash
$ kubectl get deployments,replicasets,pods --selector app=deployment-example
NAME                        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/deployment-example   2         2         2            2           30s

NAME                               DESIRED   CURRENT   READY     AGE
rs/deployment-example-2748039730   2         2         2         30s

NAME                                     READY     STATUS    RESTARTS   AGE
po/deployment-example-2748039730-k5c5j   1/1       Running   0          30s
po/deployment-example-2748039730-p85gg   1/1       Running   0          30s
```

## Deployment の変更

次に`Deployment`の情報を変更してみます。ここでは nginx のバージョンを変更してみます。先程の`deployment-example.yaml`を編集し、nginx のバージョンを 1.11 にしてみます。

```yaml
# 省略...
    spec:
      containers:
      - name: nginx
        image: nginx:1.11 # 1.10から1.11に変更
        ports:
        - containerPort: 80
```

変更を反映させます。

```bash
$ kubectl apply -f deployment-example.yaml --record
deployment "deployment-example" configured
```

変更されたか見てみます。ローリングアップデートで新旧の`Pod`の数を調整しながらアップデートされていきます。

```bash
$ kubectl get deployments,replicasets,pods --selector app=deployment-example
NAME                        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/deployment-example   2         2         2            2           7m

NAME                               DESIRED   CURRENT   READY     AGE
rs/deployment-example-2748039730   0         0         0         7m     # 古いバージョンのReplicaSet。レプリカ数が0に
rs/deployment-example-2829304371   2         2         2         27s    # 新しいバージョンのReplicaset。レプリカ数が2に

NAME                                     READY     STATUS    RESTARTS   AGE
po/deployment-example-2829304371-33x2x   1/1       Running   0          27s # Podは再生成される
po/deployment-example-2829304371-szbxm   1/1       Running   0          27s
```

アップデート中は下記の図のような状態になっています。アップデートが終わっても古いバージョンの`ReplicaSet`は履歴管理のために残ります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/100377/adb7acb1-72ef-5765-77bd-fae9d9c5cc09.png" width="777">


`Pod`のコンテナのバージョンが新しくなっていることを確認してみます。

```bash
$ kubectl describe pods --selector=app=deployment-example
...
Controllers:    ReplicaSet/deployment-example-2829304371
Containers:
  nginx:
    Container ID:       docker://a7adf603e653826d4092d8b1340f985584767080c47a5d23895c438ed5d4d813
    Image:              nginx:1.11 # コンテナのバージョンが新しくなっていれば成功
    Image ID:           docker://sha256:01f818af747d88b4ebca7cdabd0c581e406e0e790be72678d257735fad84a15f
    Port:               80/TCP
...
```

## ロールバック

`Deployment`の特徴にロールバックがあります。デプロイの履歴を見てみます。

```bash
$  kubectl rollout history deployment deployment-example
deployments "deployment-example"
REVISION        CHANGE-CAUSE
1               kubectl create -f deployment-example.yaml --record
2               kubectl apply -f deployment-example.yaml --record
```

リビジョンの詳細は下記のコマンドで確認できます。

```bash
$ kubectl rollout history deployment deployment-example --revision=2
deployments "deployment-example" with revision #2
  Labels:       app=deployment-example
        pod-template-hash=2829304371
  Annotations:  kubernetes.io/change-cause=kubectl apply -f deployment-example.yaml --record
  Containers:
   nginx:
    Image:      nginx:1.11
    Port:       80/TCP
    Volume Mounts:      <none>
    Environment Variables:      <none>
  No volumes.
```

リビジョンを指定してロールバックしてみます。`--to-revision`オプションを指定しない場合は直前のリビジョンになります。

```bash
$ kubectl rollout undo deployment deployment-example --to-revision=1
deployment "deployment-example" rolled back
```

nginx コンテナのバージョンが 1.11 から 1.10 にロールバックされていることを確認します。ロールバックまで少し時間がかかる場合があります。

```bash
$ kubectl describe pods --selector=app=deployment-example
...
Containers:
  nginx:
    Container ID:       docker://fae4d04753243119bf5840fefd820353766eddfd9dd75a628ea1e56673cf3828
    Image:              nginx:1.10  # 1.10に戻っている
    Image ID:           docker://sha256:c2d83d8cde8d6b13e774170f85d87829736e6614a8be33479bb7f8bd7f12dd06
    Port:               80/TCP
```

デプロイの履歴を見てみます。リビジョン1の行がなくなり、リビジョン3(最新)になっていることがわかります。

```bash
$ kubectl rollout history deployment deployment-example
deployments "deployment-example"
REVISION        CHANGE-CAUSE
2               kubectl apply -f deployment-example.yaml --record
3               kubectl create -f deployment-example.yaml --record # 1から3(最新)に変更された
```

なおデプロイの履歴は内部的には過去の`ReplicaSet`で管理されており、ロールバックは過去の`ReplicaSet`の切り替えることで行われています。リビジョンやコマンドの履歴といった情報は`ReplicaSet`のアノテーションとして保存されています。

<img src="https://qiita-image-store.s3.amazonaws.com/0/100377/050b6a40-fcb0-610f-1c9f-a753ad124d36.png" width="774">

## 補足: Deployment のラベル

`Deployment`では`spec.template.metadata.labels`設定したラベルが自動的に`Deployment`のセレクタ(`.spec.selector.matchLabels`)に設定されるようになっています。

```yaml
$ kubectl get deployment deployment-example -o yaml
apiVersion: extensions/v1beta1
kind: Deployment
...
spec:
  replicas: 2
  selector:
    # 自動的に設定される
    matchLabels:
      app: deployment-example
  ...
  template:
    metadata:
      creationTimestamp: null
      labels:
        # マニフェストで設定した値
        app: deployment-example
...
```

`Deployment`の`spec.template.metadata.labels`の値は`ReplicaSet`のラベルとセレクタの一部と`Pod`のラベルに使われています。また`ReplicaSet`ごとに配下の`Pod`を識別するために`pod-template-hash`という`PodTemplate`から生成したハッシュ値を持っています。

```yaml
$ kubectl get replicaset -l app=deployment-example -o yaml
...
- apiVersion: extensions/v1beta1
  kind: ReplicaSet
  spec:
    replicas: 2
    selector:
      # このセレクタでPodを選択する
      matchLabels:
        # deploymentで指定したラベル
        app: deployment-example
        # Podを識別するためのPodTemplateから作られたハッシュ値
        pod-template-hash: "2748039730"
    template:
      metadata:
        creationTimestamp: null
        labels:
          # セレクタと同じ値
          app: deployment-example
          pod-template-hash: "2748039730"
```

`Pod`の`ReplicaSet`が持つセレクタと同じラベルが設定されています。

```yaml
$ kubectl get pods -l app=deployment-example -o yaml
...
- apiVersion: v1
  kind: Pod
  metadata:
   ...
    labels:
      app: deployment-example
      pod-template-hash: "2748039730"
```

下記の図では`Deployment`の`spec.template.metadata.labels`が`app=myapp`だった場合のラベルの付け方を説明しています。新旧の`Pod`ともに共通の`app=myapp`というラベルがあるためローリングアップデート中も`Service`を通して断続なくサービス提供でき、新旧で異なる`pod-template-hashというラベルがあるため個別にも管理`できる仕組みになっています。

<img src="https://qiita-image-store.s3.amazonaws.com/0/100377/e817e2a7-000e-495d-924f-ad9ebf5190fc.png" width="795">

## まとめ

- `Deployment`はローリングアップデートやロールバックといったデプロイ管理の仕組みを提供します
- 同じ仕様の`Pod`を管理する`ReplicaSet`とアプリケーションを動作させる最小単位の`Pod`で構成されています
- 履歴が管理されておりロールバックが可能です

## 参考

- https://kubernetes.io/docs/user-guide/deployments/
- https://kubernetes.io/docs/user-guide/replicasets/