はじめに
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-container
と fluentd-container
をグルーピングした Pod を構成すると node-api-container
から localhost:24224
で fluentd-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-container
を node-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-equal
で matching 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-container
を node-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