gcpja
GoogleCloudPlatform
kubernetes
GoogleContainerEngine
GKE

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

More than 3 years have passed since last update.


はじめに

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

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

RC には、管理下の Pod を新しいイメージに1台ずつローリングでアップデートする機能があります。これはリリース作業で使われる機能ですが、対象とする 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

{

"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

{

"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 にアップデートします。

$ 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-containerfluentd-container をグルーピングした Pod を構成すると node-api-container から localhost:24224fluentd-container と疎通できます。実際の Spec ファイルを下記に示します。


Spec files


API Replication Controller Spec

{

"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

{

"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-containernode-api:1.0.1 にアップデートします。

$ 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 に変更してもう一度実行します。

$ 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-equalmatching key ... ? こんな時はソースで理解したほうが確実なので読んでみます。https://github.com/kubernetes/kubernetes/blob/v1.1.4/pkg/kubectl/cmd/rollingupdate.go##L248-L261

    // 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 の値が異なっている」パターンがひとつもない場合に生じるエラーのようです・・・って日本語で書いても難解なのでソース読んでくださいw

現状の 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

{

"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

{

"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-containernode-api:1.0.1 にアップデートします。今回は既存の RC 名が推測できないため kubectl コマンドを実行して既存の RC 名を取得する処理も行なっています。

$ 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