Google によるフルマネージドサービスである GKE では Kubernetes のマスタやノードのことは理解しなくとも Kubernetes の API を使って実現したいことに集中できる。
しかし、本番運用をはじめてから「なぜ動いているのかもなぜ動かないのかも分からない」というような状態ではいざという時に困る。
特に Service がどのようにクラスタの外から Pod へのアクセスを提供しているのかはブラックボックスになっていたため、今回は GKE での Service の L3(〜L4) の挙動について Kubernetes の内外を調査した。
(後で図や参照情報へのリンクを追加予定) 図解が出てきたので追加しなかった。
追記(2018年10月5日)
Kubernetes Engine の Network Overview が良いのでこちらも読むと良いでしょう。この記事を書いた当時は存在しなかった Alias IP が GA となり、 GCE Route に成り代わっています。
https://cloud.google.com/kubernetes-engine/docs/concepts/network-overview
追記(2017年3年22)
この記事が書かれた当時は GKE での LoadBalancer Service の動作についてドキュメントがなく、私を含めて動作を疑問に思っている人が多かった。よって、ドキュメントとソースコードを照らし合わせつつ挙動を確認する必要があった。
現在は Google Cloud Next 2017 にてこの記事の内容の概要をカバーする Kubernetes の Co-founder & Tech-lead の Googler である Tim Hockin と Michael Rubin
による下記の発表が公開されているので、これを観ることが正しくかつ近道である。この記事で触れていない localOnly 等も解説されている。
The ins and outs of networking in Google Container Engine and Kubernetes (Google Cloud Next '17)
https://www.youtube.com/watch?v=y2bhV81MfKQ
https://speakerdeck.com/thockin/the-ins-and-outs-of-networking-in-google-container-engine
kube-proxy によって出力される iptables の中身を把握したい人には参考になるかもしれない。
追記2
この内容と上記の Google のセッションを元にスライドにして Kubernetes Meetup Tokyo #4 で発表しました。
https://speakerdeck.com/apstndb/kubernetes-false-service-hatoudong-iteirufalseka
環境
Kubernetes ver. 1.5.2(Master & Node)
GKE クラスタの初期設定
今回はネットワークを作ってその中で作業する。
$ gcloud compute networks create kube-sandbox
(省略)
$ gcloud compute firewall-rules create default-allow-ssh --network kube-sandbox --allow tcp:22
(省略)
上で作った kube-sandbox
ネットワーク内に GKE クラスタを作成する。今回は単純化のためノードは1とする。
$ gcloud beta container clusters create --machine-type=g1-small --num-nodes=1 --network=kube-sandbox kube-sandbox
(省略)
NAME ZONE MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS
kube-sandbox asia-east1-a 1.5.2 104.199.251.51 g1-small 1.5.2 1 RUNNING
現時点で kube-sandbox
ネットワークには下記のルートが自動的に設定されている。具体的なノード名等は環境ごとに違うので読み替えてほしい。
$ gcloud compute routes list --filter=network=kube-sandbox
NAME NETWORK DEST_RANGE NEXT_HOP PRIORITY
default-route-0febf5d565eb04b9 kube-sandbox 10.146.0.0/20 1000
default-route-61d48fa98847c47f kube-sandbox 10.138.0.0/20 1000
default-route-73c1d7230a9f1444 kube-sandbox 10.128.0.0/20 1000
default-route-7cc26e9102aa6708 kube-sandbox 10.142.0.0/20 1000
default-route-7de16d017205e5de kube-sandbox 10.132.0.0/20 1000
default-route-e3201908a50d2b01 kube-sandbox 0.0.0.0/0 default-internet-gateway 1000
default-route-fccedbefa32ce294 kube-sandbox 10.140.0.0/20 1000
gke-kube-sandbox-5bb78bc1-3ca8c4dd-edb8-11e6-90da-42010af00188 kube-sandbox 10.76.0.0/24 asia-east1-a/instances/gke-kube-sandbox-default-pool-88bb8656-26d1 1000
いかにも GKE な感じの gke-kube-sandbox-5bb78bc1-3ca8c4dd-edb8-11e6-90da-42010af00188
というルートができており NEXT_HOP
に指定されている GCE インスタンスが作成した GKE クラスタのノードであることがわかる。
該当するインスタンスを確認する。
(表示内容の調整のため format 内でprojection を使っている。)
$ gcloud compute instances describe gke-kube-sandbox-default-pool-88bb8656-26d1 \
--format='table(name,networkInterfaces[0].networkIP, networkInterfaces[0].accessConfigs[0].natIP, tags.items.map(0).join(","):label=TAGS)'
NAME NETWORK_IP NAT_IP TAGS
gke-kube-sandbox-default-pool-88bb8656-26d1 10.140.0.2 104.199.254.163 gke-kube-sandbox-5bb78bc1-node,goog-gke-node
対応する Kubernetes 側の Node 情報を確認する。
(デフォルトでは表示されないフィールドを抜き出すために custom-columns
の中で JSONPath を使う。)
$ kubectl get node -o custom-columns='NAME:.metadata.name,EXTERNAL-IP:.status.addresses[?(@.type=="ExternalIP")].address,INTERNAL-IP:.status.addresses[?(@.type=="InternalIP")].address,POD-CIDR:.spec.podCIDR'
NAME EXTERNAL-IP INTERNAL-IP POD-CIDR
gke-kube-sandbox-default-pool-88bb8656-26d1 104.199.254.163 10.140.0.2 10.76.0.0/24
GCE ルートの DEST_RANGE
と Kubernetes Node の POD-CIDR
, GCE ルートの NEXT_HOP
と Kubernetes Node の NAME
が対応していることが確認できた。
一応 Node の中の設定も見てみると、 POD-CIDR
のアドレス範囲は cbr0
ブリッジに割り当てられていることがわかる。
$ gcloud compute ssh user@gke-kube-sandbox-default-pool-88bb8656-26d1 -- ip address show cbr0
4: cbr0: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1460 qdisc htb state UP group default qlen 1000
link/ether 0a:58:0a:4c:00:01 brd ff:ff:ff:ff:ff:ff
inet 10.76.0.1/24 scope global cbr0
valid_lft forever preferred_lft forever
inet6 fe80::a42a:16ff:fe06:38a5/64 scope link
valid_lft forever preferred_lft forever
この記事では L2 レイヤのコンテナネットワークについては説明しないが、このブリッジにそれぞれの Pod の veth がアタッチされる。
(/24
ということは同時にIP アドレスが割り当てられる Pod は1 Node につき254個ということを意味する。 GKE に限って言えば nodeIpv4CidrSize フィールドは変更不可能なようだから、他のリソースに余裕があっても Pod の数に応じて Node をスケールする必要があるように見える。)
ここまでで Pod の IP アドレスから対象の GCE インスタンスへのルーティングが行われていることが分かる。
これらの設定は何を意味するのだろうか。
Networking によると Kubernetes のコンテナ間ネットワークの基本要件は下記の3つがある。
- 全てのコンテナは別の全てのコンテナと NAT なしで通信できる
- 全てのノードは全てのコンテナと NAT なしで通信できる
- 逆も同様(全てのコンテナは全てのノードと NAT なしで通信できる)
- コンテナは自分自身の IP アドレスを他のコンテナから見えるのと同じ IP アドレスとして見ることができる
flannel 等のオーバレイネットワークでこれらの要件に対応している環境もあるが、 GCE ではオーバレイネットワークが不要としているのは、この GCE ルートの設定によるものであるようだ。
Kubernetes の世界
ここからは Kubernetes の Service がどのように動いているかについて「Debugging Services - Is the kube-proxy working?」を参考に確認していく。
kube-proxy
Service は kube-proxy による仮想 IP アドレスと kube-dns による名前解決によって実現されているが、この記事では L3(〜L4) にフォーカスするため kube-dns については特に説明しない。
kube-proxy は Kubernetes 1.2 以降は動的に iptables のルールを操作 (Proxy-mode: iptables) することで、 Linux カーネルの netfilter を使って動き、仮想 IP アドレスへのアクセスを適切に Pod に転送する。
なお、過去は名前通りユーザスペースのプロキシ (Proxy-mode: userspace) として動いていたらしい。
SSH 経由で GKE ノードの iptables のルール全体を取得するには下記のようにする。
$ gcloud compute ssh user@gke-kube-sandbox-default-pool-88bb8656-26d1 -- sudo iptables-save
操作ごとに差分のみ示す。なお著者は iptables に詳しくないので、 iptables 自体の解釈については他の記事も参考にしてほしい。
(注: Kubernetes の外に転送する ExternalName Service と 複数 IP アドレスを A レコードで返す Headless Service は DNS ベースで仮想 IP アドレスを持たないのでこの記事では触れない。)
Service なし
まずは kubectl run
で作った Deployment から Pod を起動する。
$ kubectl run hostnames --image=gcr.io/google_containers/serve_hostname \
--labels=app=hostnames \
--port=9376 \
--replicas=1
deployment "hostnames" created
下記のような IP アドレスを持った Pod が作られた。この時点だと特に iptables には変化がない。
$ kubectl get pod -o wide -l app=hostnames
NAME READY STATUS RESTARTS AGE IP NODE
hostnames-3799501552-jpmdq 1/1 Running 0 6m 10.76.0.9 gke-kube-sandbox-default-pool-88bb8656-26d1
ClusterIP Service
先程の Pod に対応する の ClusterIP Service を作ってみよう。
$ kubectl expose deployment hostnames --port=80 --target-port=9376
service "hostnames" exposed
作られたものを確認してみる。 Service だけでなく Endpoints ができていることが分かる。
$ kubectl get pod,endpoints,service -o wide -l app=hostnames
NAME READY STATUS RESTARTS AGE IP NODE
po/hostnames-3799501552-jpmdq 1/1 Running 0 42m 10.76.0.9 gke-kube-sandbox-default-pool-88bb8656-26d1
NAME ENDPOINTS AGE
ep/hostnames 10.76.0.9:9376 33m
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
svc/hostnames 10.79.255.4 <none> 80/TCP 33m app=hostnames
この時点で iptables には次のルールが追加されていた。
-A KUBE-SERVICES ! -s 10.76.0.0/14 -d 10.79.255.4/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.79.255.4/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-SVC-NWV5X2332I4OT4T3
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-EV2NQKURMPKCP5UI
-A KUBE-SEP-EV2NQKURMPKCP5UI -s 10.76.0.9/32 -m comment --comment "default/hostnames:" -j KUBE-MARK-MASQ
-A KUBE-SEP-EV2NQKURMPKCP5UI -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.76.0.9:9376
部分ごとに見ていこう。
-A KUBE-SERVICES ! -s 10.76.0.0/14 -d 10.79.255.4/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
10.76.0.0/14
の中以外から ClusterIP への通信は KUBE-MARK-MASQ
に回されるようだ。上で確認した cbr0
のアドレス空間は 10.76.0.0/24
だが、等しくないのは何のためで、どこで設定されているのか?
設定箇所は kube-proxy
の起動オプションが答えだった。ノードごとの Pod のアドレス空間の上にクラスタの Pod のアドレス空間があることが分かる。
$ gcloud compute ssh user@gke-kube-sandbox-default-pool-88bb8656-26d1 -- ps ax | grep kube-proxy
1625 ? Sl 1:48 kube-proxy --master=https://104.199.251.51 --kubeconfig=/var/lib/kube-proxy/kubeconfig --cluster-cidr=10.76.0.0/14 --resource-container= --v=2
KUBE-MARK-MASQ
は次のように定義されている一連のルールを読むと POSTROUTING
を経由して KUBE-POSTROUTING
からマスカレード(NAT)するかどうかの判断のためのフラグをつける設定だとわかる。
よって、クラスタ境界を跨ぐ通信は NAT されることがわかった。
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
次のルールを見てみよう。Cluster IP への通信は KUBE-SVC-*
チェインに飛ぶことが分かる。
-A KUBE-SERVICES -d 10.79.255.4/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-SVC-NWV5X2332I4OT4T3
KUBE-SVC-*
チェインは Endpoints に対応する KUBE-SEP-*
に飛ぶ。
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-EV2NQKURMPKCP5UI
KUBE-SEP-*
は対象の Pod の ContainerPort に宛先の IP アドレスとポート番号を転送する。
-A KUBE-SEP-EV2NQKURMPKCP5UI -s 10.76.0.9/32 -m comment --comment "default/hostnames:" -j KUBE-MARK-MASQ
-A KUBE-SEP-EV2NQKURMPKCP5UI -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.76.0.9:9376
ここまでで、 Cluster IP へのアクセスが Service を構成する Pod へと転送されることがわかった。
Pod をスケールする
Pod をスケールした場合の変化を確認する。
$ kubectl scale deployment hostnames --replicas 2
deployment "hostnames" scaled
複数の Pod に対応する Endpoints が出来ていることがわかる。
$ kubectl get pod,endpoints,service -o wide -l app=hostnames
NAME READY STATUS RESTARTS AGE IP NODE
po/hostnames-3799501552-jpmdq 1/1 Running 0 39m 10.76.0.9 gke-kube-sandbox-default-pool-88bb8656-26d1
po/hostnames-3799501552-nw2gz 1/1 Running 0 21m 10.76.0.10 gke-kube-sandbox-default-pool-88bb8656-26d1
NAME ENDPOINTS AGE
ep/hostnames 10.76.0.10:9376,10.76.0.9:9376 30m
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
svc/hostnames 10.79.255.4 <none> 80/TCP 30m app=hostnames
この時点でできた差分は下記のようになる。
-A KUBE-SEP-BWZ646KDVNVKXICT -s 10.76.0.10/32 -m comment --comment "default/hostnames:" -j KUBE-MARK-MASQ
-A KUBE-SEP-BWZ646KDVNVKXICT -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.76.0.10:9376
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-BWZ646KDVNVKXICT
KUBE-SEP
については、新しく追加された Pod に対しても同様にチェインを追加している。
-A KUBE-SEP-BWZ646KDVNVKXICT -s 10.76.0.10/32 -m comment --comment "default/hostnames:" -j KUBE-MARK-MASQ
-A KUBE-SEP-BWZ646KDVNVKXICT -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.76.0.10:9376
注目すべきは statistic module の random モードを使って 50% ずつ振り分けを行っているところだ。これにより ClusterIP はロードバランサとして働く。
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-BWZ646KDVNVKXICT
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-EV2NQKURMPKCP5UI
単純化のためにレプリカ数は1に戻しておこう。
$ kubectl scale deployment hostnames --replicas 1
deployment "hostnames" scaled
NodePort Service
これから外に公開する Service を作ってみる。まずは Node であるインスタンスのポートを通して Service にアクセスできるようにする NodePort Service だ。
前回作った ClusterIP Service を消してから作成する。
$ kubectl delete service hostnames
service "hostnames" deleted
$ kubectl expose deployment hostnames --port=80 --target-port=9376 --type=NodePort
service "hostnames" exposed
作られたリソースを見てみる。 Service のポート 80 がノードのポート 31358 に割り当てられていることが分かる。
$ kubectl get pod,endpoints,service -o wide -l app=hostnames
NAME READY STATUS RESTARTS AGE IP NODE
po/hostnames-3799501552-jpmdq 1/1 Running 0 50m 10.76.0.9 gke-kube-sandbox-default-pool-88bb8656-26d1
NAME ENDPOINTS AGE
ep/hostnames 10.76.0.9:9376 1m
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
svc/hostnames 10.79.245.174 <nodes> 80:31358/TCP 1m app=hostnames
CLUSTER-IP
が設定されていることからも、 ClusterIP Service の拡張であることがわかる。
iptables には下記のようなルールが追加された。
KUBE-NODEPORTS
に来たもので NodePort に割り当てられたポートあてのパケットを対応する KUBE-SVC-*
で処理するルールだとわかる。
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/hostnames:" -m tcp --dport 31358 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/hostnames:" -m tcp --dport 31358 -j KUBE-SVC-NWV5X2332I4OT4T3
KUBE-NODEPORTS
チェインとは何かというと、 KUBE-SERVICES
に来たものでノードのローカルアドレス宛てのパケットだ。
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
これにより、ノードの NodePort に来たものが Service の Endpoints を構成する Pod の適切なポートに転送されることが分かる。
LoadBalancer Service (GKE 依存)
最後に LoadBalancer Service がどのように動くのかを見る。
$ kubectl delete service hostnames
service "hostnames" deleted
$ kubectl
kubectl expose deployment hostnames --port=80 --target-port=9376 --type=LoadBalancer
作られたリソースを確認する。 EXTERNAL-IP
が設定された。
% kubectl get pod,endpoints,service -o wide -l app=hostnames
NAME READY STATUS RESTARTS AGE IP NODE
po/hostnames-3799501552-jpmdq 1/1 Running 0 55m 10.76.0.9 gke-kube-sandbox-default-pool-88bb8656-26d1
NAME ENDPOINTS AGE
ep/hostnames 10.76.0.9:9376 1m
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
svc/hostnames 10.79.250.127 104.199.232.159 80:30675/TCP 1m app=hostnames
PORT
を見ると、 NodePort Service の上に実装されていることが分かる。
iptables には次のようなルールが追加された。
-A KUBE-SERVICES -d 104.199.232.159/32 -p tcp -m comment --comment "default/hostnames: loadbalancer IP" -m tcp --dport 80 -j KUBE-FW-NWV5X2332I4OT4T3
-A KUBE-FW-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames: loadbalancer IP" -j KUBE-MARK-MASQ
-A KUBE-FW-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames: loadbalancer IP" -j KUBE-SVC-NWV5X2332I4OT4T3
-A KUBE-FW-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames: loadbalancer IP" -j KUBE-MARK-DROP
まず、下記のルールから、EXTERNAL-IP
の対象宛ての Service のポート宛てのパケットは全て KUBE-FW-*
で処理されることが分かる。
-A KUBE-SERVICES -d 104.199.232.159/32 -p tcp -m comment --comment "default/hostnames: loadbalancer IP" -m tcp --dport 80 -j KUBE-FW-NWV5X2332I4OT4T3
次のルールから KUBE-FW-*
に来たパケットはは対応する KUBE-SVC-*
で処理されることが分かる。 KUBE-MARK-DROP
は何らかの理由で KUBE-SVC-*
がない場合にパケットを捨てるためのものだろう。
-A KUBE-FW-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames: loadbalancer IP" -j KUBE-MARK-MASQ
-A KUBE-FW-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames: loadbalancer IP" -j KUBE-SVC-NWV5X2332I4OT4T3
-A KUBE-FW-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames: loadbalancer IP" -j KUBE-MARK-DROP
KUBE-SVC-*
に来たら今までに見てきた Service と同様なので、 EXTERNAL-IP
に来たパケットは結果的には Pod に転送されることが分かる。
LoadBalancer Service の GCE 側
GCE 側も確認する。最初に確認した通り現在存在する GKE ノードの GCE インスタンスには下記のようなタグが設定されている。
$ gcloud compute instances list --format='table(name,tags.items.map(0).join(","):label=tags)'
NAME tags
gke-kube-sandbox-default-pool-88bb8656-26d1 gke-kube-sandbox-5bb78bc1-node,goog-gke-node
GCE ファイアウォールを確認してみるとノードのタグに対して Service が export しているポートへの通信が許可されている。
$ gcloud compute firewall-rules list --filter=network=kube-sandbox
NAME NETWORK SRC_RANGES RULES SRC_TAGS TARGET_TAGS
k8s-fw-a0d5edc74edc111e690da42010af0018 kube-sandbox 0.0.0.0/0 tcp:80 gke-kube-sandbox-5bb78bc1-node
更に Service の EXTERNAL-IP
の IP アドレスに対応する転送ルールが作られている。
$ gcloud compute forwarding-rules list --filter IPAddress=104.199.232.159
NAME REGION IP_ADDRESS IP_PROTOCOL TARGET
a0d5edc74edc111e690da42010af0018 asia-east1 104.199.232.159 TCP asia-east1/targetPools/a0d5edc74edc111e690da42010af0018
転送ルールの転送先は上で確認したインスタンスを含むターゲットプールに向いていることが確認できる。
$ gcloud compute target-pools list a0d5edc74edc111e690da42010af0018 --format='table(name,region,instances.map().basename().join(","))'
NAME REGION INSTANCES
a0d5edc74edc111e690da42010af0018 asia-east1 gke-kube-sandbox-default-pool-88bb8656-26d1
なおこれらの転送ルール及びターゲットプールの生成は GCE Cloud Provider Addon が行っている。
ターゲットプールを指す転送ルールは GCE の L4 ロードバランサである Network LoadBalancing として機能するので、転送ルールに設定された外部 IP アドレスへのパケットが Node に転送されることが分かる。
なお、GCE の Network LoadBalancing は Maglev として知られる DSR 型のロードバランサなので、クライアントが送信した際の送信先 IP アドレスがそのまま iptables で処理される。(参考)
まとめ
GKE/GCE/Kubernetes を通して、下記のように Service から作られたロードバランサからどのように Pod までパケットが届くかを確認した。
- GKE のノード間のネットワークは GCE のルートでルーティングするためオーバレイネットワークは不要
- Kubernetes のコア機能である Service の IP レイヤは kube-proxy が動的に生成した iptables のルールを使って Linux カーネルで処理される
- GCE Network Load Balancing 等との連携部分については Cloud Provider Addon が処理する
なお、 HTTP ロードバランサに対応する Ingress の実装は今回説明した LoadBalancer Service のものとは別になっている。
(具体的には現在 NodePort Service で公開されるポートを使って実現されている。GCE Ingress Controllerの実装を参照してほしい。)
参考
Service: https://kubernetes.io/docs/user-guide/services/
ネットワークモデル全般: https://kubernetes.io/docs/admin/networking/
kube-proxy の iptables 処理部分の実装: https://github.com/kubernetes/kubernetes/blob/v1.5.2/pkg/proxy/iptables/proxier.go
GKE に頼らずに GCE 及び AWS でクラスタを構成するチュートリアル: https://github.com/kelseyhightower/kubernetes-the-hard-way
etcd と照らし合わせて確認している: http://containerops.org/2017/01/30/kubernetes-services-and-ingress-under-x-ray/