※ 2020/04時点でIngress for internal load balancingは公開されたものの、未だプレリリースのステージにあります。Rapid channelのクラスターでしか動かないため、Stableに降りてきたら手順を更新予定です。
はじめに
この記事はZOZOテクノロジーズ #1 Advent Calendar 2019 22日目の記事です。
昨日の記事は @takanamitoさんによる「teyuに届いたPullRequestで使われているRubyの高速化手法」でした。
みなさん、GKE完全に理解していますか?僕は定期的に完全理解しています。
今回は、GKEに内部ロードバランサーをデプロイする方法についてご紹介します。
Kubernetesのネットワークの世界のおさらい
内部ロードバランサーの話をする前に、まずはKubernetesのネットワークについて簡単に振り返ります。詳しくは、青山真也氏著のKubernetes完全ガイドなどに詳しく掲載されています。
Kubernetesでは、以下のような形でPodのネットワークとNodeのネットワークが論理的に分離されています。
実際の通信はノードのネットワークを通して行われますが、iptablesなどのファイアウォールを使って通信を隔離しているため、ノード側の通信(外部からの通信の入り口)からPodのネットワークに直接入る方法はありません。
しかし、ネットワークの中継役を担うServiceというリソースを使うことで、Pod上のアプリケーションを外部に公開することができます。
※Serviceリソースにはいくつか種類(type)がありますが、ここでは簡単のため代表的なCusterIPとLoadBalancerのみ取り上げます。
他にも、L7ロードバランサー実装であるIngressも外部公開を行うための手段の1つです。
上図のService Dを見るとわかるように、ClusterIPは内部通信用のインターフェイスを提供するだけなので、実際に外の世界とKubernetes内部の世界を繋ぐためには
- ServiceリソースのType: LoadBalancer
- Ingressリソース
の2種類を使うのが一般的です。
IngressとLoadBalancerの違い
KubernetesにおけるIngressリソースとService(LoadBalancer)リソースの違いに関してはここでは詳しく説明しませんが、大きく分類するとServiceはL4(TCP)の世界を、IngressはL7(HTTP)のロードバランシングを行ってくれます。
特に、GKEの世界ではそれぞれのリソースを作成すると、GKEクラスターと同一のVPCにおいてGCLBのTCP/HTTPロードバランサーが展開されます。
なぜ内部ロードバランサーが必要なのか
上記でも説明したように、Kubernetesではクラスター内でサービス間の通信を行うための「ClusterIP」という機能が提供されています。
こちらを使うと同一Kubernetesクラスターの範囲においてはPodのロードバランシングなどを行ってくれて非常に便利なのですが、VPC内の他のリソース(例えばGCEインスタンスとか、VPC Peering/Interconnect経由で対向側からアクセスしたいケースとか)からアクセスすることはできません。
また、Ingressは公式ドキュメントでも「クラスター内のサービス(通常はHTTP)への外部アクセスを管理するAPIオブジェクト」と定義されており、通常内部ネットワークの通信を制御することは要件に入っていません。また、GKEにおけるIngressもServiceも、デフォルトでは外部IPを持つ動きをします。
外部からの通信をさせるとネットワーク的にも無駄が大きいですし、何より内部ネットワークだけで通信を完結させたいクラスターの場合はセキュリティ的にもリスクでしかありません。
そこで内部負荷分散機能があると便利ですよね。
GKEにおける内部負荷分散機能のはなし
現在、GKEには大きく分けて2種類の負荷分散機能があります。
1つ目のTCP/UDP LBによる内部負荷分散、2つ目のHTTP(S) LBによる内部負荷分散です。
L4の内部負荷分散
前者はServiceリソースを以下のように作成することで簡単に実現できます。
cloud.google.com/load-balancer-type: "Internal"
がミソです。
apiVersion: v1
kind: Service
metadata:
name: ilb-service
annotations:
cloud.google.com/load-balancer-type: "Internal"
labels:
app: hello
spec:
type: LoadBalancer
selector:
app: hello # set your app name
ports:
- port: 80
targetPort: 8080
protocol: TCP
L7の内部負荷分散
Ingressを使ったL7の内部負荷分散
こいつが厄介で、現時点で公開されているドキュメントにおいて、L7の内部負荷分散を直接Ingressで公開する方法はまだありません。
ドキュメントを見ていただくとわかると思いますが、L4の方法ではGKE側のドキュメントであるのに対し、L7はGKEではなく一般的なネットワークリソースを使った方法が書かれています。
Kubernetesネイティブな方法はおそらくもうすぐリリースされるはずですが、現状はできないので面倒です。
なぜそれが言えるかというと、ILBに対応したGCPのIngress Controllerがリリースされており、GKEの1.15からAlphaで利用可能になっているためです。Alphaの場合ホワイトリストへの追加が必要になるので今は一般公開されていませんが、Betaになった頃には公式の手順も出てくると思います。
Ingressを使わないL7の内部負荷分散
上記の通り、現状の構成ではIngressを使って直接構成することはできません。そこで、Ingressを使わずにうまいことHTTP(S)のロードバランサーを展開するために、GCPのネットワークリソースであるNetwork Endpoint Groups(NEGs)を用います。
NEGを使うとコンテナのネットワークとコンテナポートを直接負荷分散の対象に出来るようになるため、Kubernetesの持つ複雑なネットワークを無視して「ロードバランサーとコンテナ」を直接紐付けて負荷分散できるようになります。
GCPではこの機能を「コンテナネイティブロードバランシング」と呼んでいます。
GKEでコンテナネイティブロードバランシングを使うためには、GKEクラスターをVPC-nativeに構成する必要があります。VPC-native clusterを使うとKubernetesのネットワークをVPCに割り当てたネットワークアドレスを使って通信するため、VPC内にある他のリソースとGKE上のコンテナが同一VPC内で通信できるようになります。
逆に言えば、VPC-native clusterでない場合はPodに割り当たるIPアドレスがKubernetesによって決定される(Kubernetes内部で独自に構成されたネットワークのアドレスを持つ)ため、通信にはiptablesなどのファイアウォールを介す必要があります。これではノードとPodに偏りがあった場合に通信も偏ってしまい、負荷分散がそもそも成立できません。
HTTP ILB(Internal Load Balancer)の構成を作ってみる
VPCと、それに付随したKubernetesクラスターは既に構成済みとします。
このとき、ドキュメントに従い以下のような構成を追加します。
1. DeploymentとServiceを作成
まず、クラスターに対してアプリケーションをデプロイします。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
run: api
name: api
spec:
minReadySeconds: 60
replicas: 3
selector:
matchLabels:
run: api
template:
metadata:
labels:
run: api
spec:
containers:
- name: api
image: nginx:mainline-alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
failureThreshold: 5
periodSeconds: 5
livenessProbe:
httpGet:
port: 80
path: /
failureThreshold: 5
periodSeconds: 5
terminationGracePeriodSeconds: 60
続いて、NEGを有効にしたClusterIPのServiceをデプロイします。
こうすると、NEGが作られ、バックエンドであるコンテナの既定ポートと通信できるように構成されます(この場合は80番)
apiVersion: v1
kind: Service
metadata:
annotations:
cloud.google.com/neg: '{"exposed_ports":{"80":{}}}'
labels:
run: api
name: api-http
namespace: default
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8080
selector:
run: api
type: ClusterIP
このとき作成されるNEGとコンテナの関係性は以下の画像が参考になりそうです。
Ref: https://cloud.google.com/load-balancing/docs/negs/
2. Proxy subnetの作成
次に、Proxy Subnetを作成します。ここでの「proxy network」はプロキシー専用に作られたサブネットで、ロードバランサーがPodに通信する時に使うアドレス空間として使用されるようです(ドキュメント参照)。これを作成するにはサブネット作成時にプロパティpurpose = "INTERNAL_HTTPS_LOAD_BALANCER"
を有効にします。
他にも、このサブネットに対する通信の許可設定などを合わせて作成しておきます。
variable "vpc_subnetwork_proxy_ip_range" {
default = "10.129.0.0/26"
}
resource "google_compute_subnetwork" "vpc-proxy-subnetwork" {
provider = google-beta
name = "vpc-proxy-subnetwork"
ip_cidr_range = var.vpc_subnetwork_proxy_ip_range
region = var.region
network = google_compute_network.vpc-network.self_link
purpose = "INTERNAL_HTTPS_LOAD_BALANCER"
role = "ACTIVE"
depends_on = [google_compute_network.vpc-network]
}
# Ref. https://cloud.google.com/load-balancing/docs/l7-internal/setting-up-l7-internal
resource "google_compute_firewall" "fw-allow-subnet-for-self" {
name = "fw-allow-subnet-for-self"
network = google_compute_network.vpc-network.self_link
direction = "INGRESS"
allow {
protocol = "icmp"
}
allow {
protocol = "tcp"
}
allow {
protocol = "udp"
}
source_ranges = [var.vpc_subnetwork_ip_cidr_range]
}
resource "google_compute_firewall" "fw-allow-subnet-for-google-healthcheck" {
name = "fw-allow-subnet-for-google-healthcheck"
network = google_compute_network.vpc-network.self_link
direction = "INGRESS"
allow {
protocol = "tcp"
}
target_tags = ["load-balanced-backend"]
# Ref. https://cloud.google.com/load-balancing/docs/https/#troubleshooting
# These IPs are used for LB healthcheck from Google
source_ranges = ["130.211.0.0/22", "35.191.0.0/16"]
}
resource "google_compute_firewall" "fw-allow-subnet-for-proxy" {
name = "fw-allow-subnet-for-proxy"
network = google_compute_network.vpc-network.self_link
direction = "INGRESS"
allow {
protocol = "tcp"
ports = ["80", "443"]
}
target_tags = ["load-balanced-backend"]
source_ranges = [var.vpc_subnetwork_proxy_ip_range]
}
3. Internal HTTP LoadBalancerの作成
1で設定したアプリケーションのNEGに対して通信の向いたロードバランサーを作成します。
Proxy subnetは自動的に割り当てられるため、この手順においてproxy subnetについて意識すべきことはありません。
#/bin/bash
set -xe
export DEPLOYMENT_NAME=$(gcloud compute network-endpoint-groups list \
--filter="asia-northeast1-a AND api-http" \
--format="get(name)")
echo $DEPLOYMENT_NAME
gcloud compute health-checks create http l7-ilb-gke-basic-check \
--region=asia-northeast1 \
--request-path="/" \ # ヘルスチェックエンドポイント
--use-serving-port
gcloud compute backend-services create l7-ilb-gke-backend-service \
--load-balancing-scheme=INTERNAL_MANAGED \
--protocol=HTTP \
--health-checks=l7-ilb-gke-basic-check \
--health-checks-region=asia-northeast1 \
--region=asia-northeast1
gcloud compute backend-services add-backend l7-ilb-gke-backend-service \
--network-endpoint-group=$DEPLOYMENT_NAME \
--network-endpoint-group-zone=asia-northeast1-a \
--region=asia-northeast1 \
--balancing-mode=RATE \
--max-rate-per-endpoint=5
# 複数ゾーンにまたがっている場合は追加する
# gcloud compute backend-services add-backend l7-ilb-gke-backend-service \
# --network-endpoint-group=$DEPLOYMENT_NAME \
# --network-endpoint-group-zone=asia-northeast1-b \
# --region=asia-northeast1 \
# --balancing-mode=RATE \
# --max-rate-per-endpoint=5
gcloud compute url-maps create l7-ilb-gke-map \
--default-service=l7-ilb-gke-backend-service \
--region=asia-northeast1
gcloud compute target-http-proxies create l7-ilb-gke-proxy \
--url-map=l7-ilb-gke-map \
--url-map-region=asia-northeast1 \
--region=asia-northeast1
gcloud compute forwarding-rules create l7-ilb-gke-forwarding-rule \
--load-balancing-scheme=INTERNAL_MANAGED \
--network=vpc-network \
--subnet=vpc-subnetwork \
--address=10.146.32.13 \ # お使いのIPアドレス空間から1つ任意のアドレスを選ぶ
--ports=80 \
--region=asia-northeast1 \
--target-http-proxy=l7-ilb-gke-proxy \
--target-http-proxy-region=asia-northeast1
完成。
いまの内部HTTPロードバランサーに足りない機能
- マネージドのHTTPS証明書が使えない
- 内部IPアドレスが割り当てられるのでIPアドレスの予約が難しい
- 外部DNSとの紐付けが難しい(Internalなので、KubernetesのExternalDNSは使えない)
- VPC PeeringやInterconnect越しに通信するときにFQDNを指定するのは難しく、現状はIPアドレス直指定が必要
- そもそも手順が煩雑(クラスターを作成→アプリケーションをデプロイ→Serviceで作られたNEGに対してLBをアタッチの順番でやらないといけない)
特にDNSがやりづらいのと、Ingressでサクッと作れないのが不便です。Googleさんなんとかしてください(小声)
L4 ILBとL7 ILBの比較表
今回紹介した2つの内部LBの特徴について比較表をまとめました
方法 | メリット | デメリット | マネージドTLS使える? |
---|---|---|---|
Internal TCP/UDP Load Balancing | 1. 構築がメチャクチャ簡単 2. ただのServiceなので構成がシンプル |
1. Kubernetesの内部DNSが使えない 2. Stackdriverでモニタリングできる項目が少ない |
☓(そもそもL4) |
Internal HTTP(S) Load Balancing for GKE pods | 1. Stackdriverで監視できる項目多い 2. 一応HTTPSは使える |
1. 手順が煩雑 | ☓ |
さいごに
Internal Load Balancerは、内部で完結するワークロードをより幅広いサービスと連携して使うためのいいアップデートです。
まだ課題はありますが、今後もアップデートが続くことを期待しています。
ありがとうございました!
明日の記事は @sonots さんの「書き込みがあるワークロードにおける ZOZOTOWN マルチクラウド構想とその検討停止について」です。ご期待ください!