GKE におけるローリングアップデート

# はじめに

GKE では 1 台以上の Container をグルーピングする要素として Pod があります。その Pod のレプリカを管理する要素として Replication Controller (RC) があります。また、各 Pod に対して負荷分散を行う要素として Service があります。

実際のリクエストは Service を経由して各 Pod に負荷分散されます。それぞれの Pod はメタデータとしてラベル付けを行い、Service では負荷分散の対象とする Pod のラベルを指定します。

RC には、管理下の Pod を新しいイメージに１台ずつローリングでアップデートする機能があります。これはリリース作業で使われる機能ですが、対象とする RC が管理している各 Pod の Container 数によってローリングアップデートの方法が異なります。特に Multi Container Pod に対するローリングアップデートが難解なので、忘却メモとして投稿しました。

# 環境

現時点でのバージョンは GKE (Kubernetes) v1.1 です。

# Single Container

`api-pods` ラベルがつけられた Pod を selector として持つ RC と Service がある構成を例に挙げて紹介します。実際の Spec ファイルを下記に示します。

## Space files

### API Replication Controller Spec

```json
{
   "kind": "ReplicationController",
   "apiVersion": "v1",
   "metadata": {
      "name": "api-rc",
      "labels": {
         "name": "api-rc"
      }
   },
   "spec": {
      "replicas": 3,
      "selector": {
         "name": "api-pods"
      },
      "template": {
         "metadata": {
            "labels": {
               "name": "api-pods"
            }
         },
         "spec": {
            "containers": [
               {
                  "name": "node-api-container",
                  "image": "asia.gcr.io/xxxxx/node-api:1.0.0",
                  "ports": [
                     {
                        "containerPort":3000,
                        "protocol":"TCP"
                     }
                  ]
               }
            ]
         }
      }
   }
}
```

### API Service Spec

```json
{
   "kind": "Service",
   "apiVersion": "v1",
   "metadata": {
      "name": "api-srv",
      "labels": {
         "name": "api-srv"
      }
   },
   "spec": {
      "type": "LoadBalancer",
      "ports": [
        {
          "port": 3000,
          "targetPort": 3000,
          "protocol": "TCP"
        }
      ],
      "selector": {
         "name": "api-pods"
      }
   }
}
```

## Rolling Update

このような構成において `api-rc` が管理している各 Pod のイメージを `node-api:1.0.1` にアップデートします。

```shell-session
$ kubectl rolling-update api-rc --image=asia.gcr.io/xxxxx/node-api:1.0.1
Created api-rc-f9eed97d434b09b484ff9448ad82064b
Scaling up api-rc-f9eed97d434b09b484ff9448ad82064b from 0 to 3, scaling down api-rc from 3 to 0 (keep 3 pods available, don't exceed 4 pods)
Scaling api-rc-f9eed97d434b09b484ff9448ad82064b up to 1
Scaling api-rc down to 2
Scaling api-rc-f9eed97d434b09b484ff9448ad82064b up to 2
Scaling api-rc down to 1
Scaling api-rc-f9eed97d434b09b484ff9448ad82064b up to 3
Scaling api-rc down to 0
Update succeeded. Deleting old controller: api-rc
Renaming api-rc-f9eed97d434b09b484ff9448ad82064b to api-rc
replicationcontroller "api-rc" rolling updated
```

新しい RC が `api-rc-f9eed97d434b09b484ff9448ad82064b` として生成され、それぞれの Pod を一台ずつ increment / decrement し、正常に終わったタイミングで RC 名の変更が行われました。アップデート処理中においても各 Pod のラベルは `api-pods` として付けられているため Service からの負荷分散は停止することなくリリースできるようになっています。ただし、処理中に新旧が混在する状況となるため、あるユーザーがそれぞれのバージョンに接続する可能性があることには注意が必要です。

# Multi Container Pod

こっから本題。Pod は複数の Container で構成することができます。例えば `node-api-container` と `fluentd-container` をグルーピングした Pod を構成すると `node-api-container` から `localhost:24224` で `fluentd-container` と疎通できます。実際の Spec ファイルを下記に示します。

## Spec files

### API Replication Controller Spec

```json
{
   "kind": "ReplicationController",
   "apiVersion": "v1",
   "metadata": {
      "name": "api-rc",
      "labels": {
         "name": "api-rc"
      }
   },
   "spec": {
      "replicas": 3,
      "selector": {
         "name": "api-pods"
      },
      "template": {
         "metadata": {
            "labels": {
               "name": "api-pods"
            }
         },
         "spec": {
            "containers": [
               {
                  "name": "node-api-container",
                  "image": "asia.gcr.io/xxxxx/node-api:1.0.0",
                  "ports": [
                     {
                        "containerPort":3000,
                        "protocol":"TCP"
                     }
                  ]
               },
               {
                  "name": "fluentd-container",
                  "image": "asia.gcr.io/xxxxx/fluentd:1.0.0",
                  "ports": [
                     {
                        "containerPort":24224,
                        "protocol":"TCP"
                     }
                  ]
               }
            ]
         }
      }
   }
}
```

### API Service Spec

```json
{
   "kind": "Service",
   "apiVersion": "v1",
   "metadata": {
      "name": "api-srv",
      "labels": {
         "name": "api-srv"
      }
   },
   "spec": {
      "type": "LoadBalancer",
      "ports": [
        {
          "port": 3000,
          "targetPort": 3000,
          "protocol": "TCP"
        }
      ],
      "selector": {
         "name": "api-pods"
      }
   }
}
```

## Rolling Update

このような Multi Container Pod を管理している RC に対するローリングアップデートは Single Container Pod で使用したイメージファイルを指定した方法は利用できません。Multi Container Pod に対しては新しい RC の Spec ファイルを指定する必要があります。今回は `api-rc` が管理している各 Pod のイメージのうち `node-api-container` を `node-api:1.0.1` にアップデートします。

```shell-session
$ cat << EOS | kubectl rolling-update api-rc -f -
{
   "kind": "ReplicationController",
   "apiVersion": "v1",
   "metadata": {
      "name": "api-rc",
      "labels": {
         "name": "api-rc"
      }
   },
   "spec": {
      "replicas": 3,
      "selector": {
         "name": "api-pods"
      },
      "template": {
         "metadata": {
            "labels": {
               "name": "api-pods"
            }
         },
         "spec": {
            "containers": [
               {
                  "name": "node-api-container",
                  "image": "asia.gcr.io/xxxxx/node-api:1.0.1",
                  "ports": [
                     {
                        "containerPort":3000,
                        "protocol":"TCP"
                     }
                  ]
               },
               {
                  "name": "fluentd-container",
                  "image": "asia.gcr.io/xxxxx/fluentd:1.0.0",
                  "ports": [
                     {
                        "containerPort":24224,
                        "protocol":"TCP"
                     }
                  ]
               }
            ]
         }
      }
   }
}
EOS
error: - cannot have the same name as the existing ReplicationController api-rc
see 'kubectl rolling-update -h' for help.
```

既に同じ名前の RC が存在すると怒られました。Single Container Pod の場合は良い感じにリネーム処理までやってくれましたが Multi Container Pod の場合はダメなようです。実際の selector で使用している Pod の `name` ラベルが同じであれば Service からの負荷分散は停止することなくアップデートできるため、新しい RC の名前を `api-rc-v2` に変更してもう一度実行します。

```shell-session
$ cat << EOS | kubectl rolling-update api-rc -f -
{
   "kind": "ReplicationController",
   "apiVersion": "v1",
   "metadata": {
      "name": "api-rc-v2",
      "labels": {
         "name": "api-rc-v2"
      }
   },
   "spec": {
      "replicas": 3,
      "selector": {
         "name": "api-pods"
      },
      "template": {
         "metadata": {
            "labels": {
               "name": "api-pods"
            }
         },
         "spec": {
            "containers": [
               {
                  "name": "node-api-container",
                  "image": "asia.gcr.io/xxxxx/node-api:1.0.1",
                  "ports": [
                     {
                        "containerPort":3000,
                        "protocol":"TCP"
                     }
                  ]
               },
               {
                  "name": "fluentd-container",
                  "image": "asia.gcr.io/xxxxx/fluentd:1.0.0",
                  "ports": [
                     {
                        "containerPort":24224,
                        "protocol":"TCP"
                     }
                  ]
               }
            ]
         }
      }
   }
}
EOS
error: - must specify a matching key with non-equal value in Selector for api-rc
see 'kubectl rolling-update -h' for help.
```

別のエラーが出てきました。そして、いまいち意味が分からない。既存の RC の selector の値が `non-equal` で `matching key` ... ? こんな時はソースで理解したほうが確実なので読んでみます。https://github.com/kubernetes/kubernetes/blob/v1.1.4/pkg/kubectl/cmd/rollingupdate.go##L248-L261

```go
	// To successfully pull off a rolling update the new and old rc have to differ
	// by at least one selector. Every new pod should have the selector and every
	// old pod should not have the selector.
	var hasLabel bool
	for key, oldValue := range oldRc.Spec.Selector {
		if newValue, ok := newRc.Spec.Selector[key]; ok && newValue != oldValue {
			hasLabel = true
			break
		}
	}
	if !hasLabel {
		return cmdutil.UsageError(cmd, "%s must specify a matching key with non-equal value in Selector for %s",
			filename, oldName)
	}
```

「旧 RC の selector で使用している key が、新 RC の selector の key として定義されていて、かつ、その key の値が異なっている」パターンがひとつもない場合に生じるエラーのようです・・・って日本語で書いても難解なのでソース読んでくださいｗ 

現状の Spec ファイルだと各 Pod には `name` ラベルのみを定義しています。このエラーを回避するためには、新しい RC で selector の `name` ラベルの値を変更する必要がありますが、同様に各 Pod の `name` ラベルの値も変更する必要があります。しかしながら、既存の Service でも selector を `name` ラベルで指定しているため、負荷分散が停止します。

Multi Container Pod を使用する場合は RC を作成する段階でローリングアップデートを考慮する必要があることが分かりました。様々な方法がありますが、今回は作成時の UnixTime を RC 名の語尾に追加し、各 Pod に `version` ラベルとして追加します。実際の Spec ファイルを下記に示します。

## Spec files [fixed]

### API Replication Controller Spec

```json
{
   "kind": "ReplicationController",
   "apiVersion": "v1",
   "metadata": {
      "name": "api-rc-1453323827",
      "labels": {
         "name": "api-rc-1453323827"
      }
   },
   "spec": {
      "replicas": 3,
      "selector": {
         "name": "api-pods",
         "version": "1453323827"
      },
      "template": {
         "metadata": {
            "labels": {
               "name": "api-pods",
               "version": "1453323827"
            }
         },
         "spec": {
            "containers": [
               {
                  "name": "node-api-container",
                  "image": "asia.gcr.io/xxxxx/node-api:1.0.0",
                  "ports": [
                     {
                        "containerPort":3000,
                        "protocol":"TCP"
                     }
                  ]
               },
               {
                  "name": "fluentd-container",
                  "image": "asia.gcr.io/xxxxx/fluentd:1.0.0",
                  "ports": [
                     {
                        "containerPort":24224,
                        "protocol":"TCP"
                     }
                  ]
               }
            ]
         }
      }
   }
}
```

### API Service Spec

```json
{
   "kind": "Service",
   "apiVersion": "v1",
   "metadata": {
      "name": "api-srv",
      "labels": {
         "name": "api-srv"
      }
   },
   "spec": {
      "type": "LoadBalancer",
      "ports": [
        {
          "port": 3000,
          "targetPort": 3000,
          "protocol": "TCP"
        }
      ],
      "selector": {
         "name": "api-pods"
      }
   }
}
```

## Rolling Update

準備が整ったため、実際にローリングアップデートを行います。以前と同様に `api-rc` が管理している各 Pod のイメージのうち `node-api-container` を `node-api:1.0.1` にアップデートします。今回は既存の RC 名が推測できないため `kubectl` コマンドを実行して既存の RC 名を取得する処理も行なっています。

```shell-session
$ CURRENT_RC_NAME=$(kubectl get rc | grep '^api-rc' | cut -d ' ' -f 1)
$ CURRENT_UNIXTIME=$(Date +'%s')
$ cat << EOS | kubectl rolling-update ${CURRENT_RC_NAME} -f -
{
   "kind": "ReplicationController",
   "apiVersion": "v1",
   "metadata": {
      "name": "api-rc-${CURRENT_UNIXTIME}",
      "labels": {
         "name": "api-rc-${CURRENT_UNIXTIME}"
      }
   },
   "spec": {
      "replicas": 3,
      "selector": {
         "name": "api-pods"
         "version": "${CURRENT_UNIXTIME}"
      },
      "template": {
         "metadata": {
            "labels": {
               "name": "api-pods"
               "version": "${CURRENT_UNIXTIME}"
            }
         },
         "spec": {
            "containers": [
               {
                  "name": "node-api-container",
                  "image": "asia.gcr.io/xxxxx/node-api:1.0.1",
                  "ports": [
                     {
                        "containerPort":3000,
                        "protocol":"TCP"
                     }
                  ]
               },
               {
                  "name": "fluentd-container",
                  "image": "asia.gcr.io/xxxxx/fluentd:1.0.0",
                  "ports": [
                     {
                        "containerPort":24224,
                        "protocol":"TCP"
                     }
                  ]
               }
            ]
         }
      }
   }
}
EOS
Created api-rc-1453400999
Scaling up api-rc-1453400999 from 0 to 3, scaling down api-rc-1453323827 from 3 to 0 (keep 3 pods available, don't exceed 4 pods)
Scaling api-rc-1453400999 up to 1
Scaling api-rc-1453323827 down to 2
Scaling api-rc-1453400999 up to 2
Scaling api-rc-1453323827 down to 0
Scaling api-rc-1453400999 up to 3
Update succeeded. Deleting api-rc-1453323827
replicationcontroller "api-rc-1453323827" rolling updated to "api-rc-1453400999"
```

正常に完了しました。Single Container Pod のように同じ RC 名でのローリングアップデートはできなくなったので複雑になりましたが、既存 Service の selector は `name` ラベルのみを指定しているため負荷分散を停止することなくリリースできるようになっています。実際に既存の Service の `:3000` に対してリクエストを送信すると正常に動作しました。複雑だったけど、もっと良い方法がありましたらご指摘いただければ幸いです。

# まとめ

GKE におけるローリングアップデートは Single Container Pod の場合は凄くシンプル。一方で Multi Container Pod の場合は Spec ファイルを指定する必要があり、新旧 RC で値を変更できるバージョンのようなラベルを付与する必要があります。

# おわりに

この内容を gcp ja night #31 で軽く触れましたが、補足版として解決するまでの流れをそのまま書いてみました。発表資料も併せてご覧ください！

http://gcpja.connpass.com/event/23874/
http://www.slideshare.net/katsutoshinagaoka/gke-57322091