GKEの内部負荷分散機能を使ってInternal Load Balancerを構築する

※ 2020/04時点でIngress for internal load balancingは公開されたものの、未だプレリリースのステージにあります。Rapid channelのクラスターでしか動かないため、Stableに降りてきたら手順を更新予定です。

# はじめに

この記事は[ZOZOテクノロジーズ #1 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech) 22日目の記事です。
昨日の記事は @takanamitoさんによる「[teyuに届いたPullRequestで使われているRubyの高速化手法](https://takanamito.hateblo.jp/entry/2019/12/21/000000)」でした。

みなさん、GKE完全に理解していますか？僕は定期的に完全理解しています。

今回は、GKEに内部ロードバランサーをデプロイする方法についてご紹介します。

## Kubernetesのネットワークの世界のおさらい

内部ロードバランサーの話をする前に、まずはKubernetesのネットワークについて簡単に振り返ります。詳しくは、青山真也氏著のKubernetes完全ガイドなどに詳しく掲載されています。

Kubernetesでは、以下のような形でPodのネットワークとNodeのネットワークが論理的に分離されています。

実際の通信はノードのネットワークを通して行われますが、iptablesなどのファイアウォールを使って通信を隔離しているため、ノード側の通信(外部からの通信の入り口)からPodのネットワークに直接入る方法はありません。

<img width="1324" alt="スクリーンショット 2019-12-21 15.33.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100275/46eb5490-62dd-dd0d-cdb6-94c10318b7f8.png">

しかし、ネットワークの中継役を担うServiceというリソースを使うことで、Pod上のアプリケーションを外部に公開することができます。
※Serviceリソースにはいくつか種類(type)がありますが、ここでは簡単のため代表的なCusterIPとLoadBalancerのみ取り上げます。

他にも、L7ロードバランサー実装であるIngressも外部公開を行うための手段の1つです。

<img width="1304" alt="スクリーンショット 2019-12-20 17.22.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100275/5012ac3d-12f7-22e5-7e4e-92dedc599cb7.png">

上図の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オブジェクト」と[定義](https://kubernetes.io/docs/concepts/services-networking/ingress/)されており、通常内部ネットワークの通信を制御することは要件に入っていません。また、GKEにおけるIngressもServiceも、デフォルトでは外部IPを持つ動きをします。

外部からの通信をさせるとネットワーク的にも無駄が大きいですし、何より内部ネットワークだけで通信を完結させたいクラスターの場合はセキュリティ的にもリスクでしかありません。

そこで内部負荷分散機能があると便利ですよね。

## GKEにおける内部負荷分散機能のはなし

現在、GKEには大きく分けて2種類の負荷分散機能があります。

- [Internal TCP/UDP Load Balancing](https://cloud.google.com/kubernetes-engine/docs/how-to/internal-load-balancing)
- [Internal HTTP(S) Load Balancing for GKE pods](https://cloud.google.com/load-balancing/docs/l7-internal/set-up-gke-pods)

1つ目のTCP/UDP LBによる内部負荷分散、2つ目のHTTP(S) LBによる内部負荷分散です。

### L4の内部負荷分散

前者はServiceリソースを以下のように作成することで簡単に実現できます。

`cloud.google.com/load-balancer-type: "Internal"`がミソです。

```yaml:ilb-service.yaml
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がリリース](https://github.com/kubernetes/ingress-gce/releases/tag/v1.7.0)されており、GKEの1.15から*Alpha*で利用可能になっているためです。Alphaの場合ホワイトリストへの追加が必要になるので今は一般公開されていませんが、*Beta*になった頃には公式の手順も出てくると思います。

#### Ingressを使わないL7の内部負荷分散

上記の通り、現状の構成ではIngressを使って直接構成することはできません。そこで、Ingressを使わずにうまいことHTTP(S)のロードバランサーを展開するために、GCPのネットワークリソースであるNetwork Endpoint Groups(NEGs)を用います。

NEGを使うとコンテナのネットワークとコンテナポートを直接負荷分散の対象に出来るようになるため、Kubernetesの持つ複雑なネットワークを無視して「ロードバランサーとコンテナ」を直接紐付けて負荷分散できるようになります。

GCPではこの機能を「コンテナネイティブロードバランシング」と呼んでいます。

![](https://storage.googleapis.com/gweb-cloudblog-publish/original_images/Ingress_support_KJr9zxS_7NIvwPd.gif)

Ref: https://cloud.google.com/blog/ja/products/containers-kubernetes/container-native-load-balancing-on-gke-now-generally-available

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クラスターは既に構成済みとします。

このとき、[ドキュメントに従い](https://cloud.google.com/load-balancing/docs/l7-internal/set-up-gke-pods)以下のような構成を追加します。

##### 1. DeploymentとServiceを作成

まず、クラスターに対してアプリケーションをデプロイします。

```yaml:nginx.yaml
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番)

```yaml:service.yaml
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とコンテナの関係性は以下の画像が参考になりそうです。

![](https://cloud.google.com/load-balancing/images/neg-container.svg)

Ref: https://cloud.google.com/load-balancing/docs/negs/

##### 2. Proxy subnetの作成

次に、Proxy Subnetを作成します。ここでの「proxy network」はプロキシー専用に作られたサブネットで、ロードバランサーがPodに通信する時に使うアドレス空間として使用されるようです(ドキュメント参照)。これを作成するにはサブネット作成時にプロパティ`purpose = "INTERNAL_HTTPS_LOAD_BALANCER"`を有効にします。

他にも、このサブネットに対する通信の許可設定などを合わせて作成しておきます。

```proxy.tf
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について意識すべきことはありません。

```bash:create_ilb.sh
#/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. 構築がメチャクチャ簡単<br>2. ただのServiceなので構成がシンプル | 1. Kubernetesの内部DNSが使えない<br>2. Stackdriverでモニタリングできる項目が少ない | ☓(そもそもL4) |
| Internal HTTP(S) Load Balancing for GKE pods | 1. Stackdriverで監視できる項目多い<br>2. 一応HTTPSは使える | 1. 手順が煩雑 | ☓ |

# さいごに

Internal Load Balancerは、内部で完結するワークロードをより幅広いサービスと連携して使うためのいいアップデートです。

まだ課題はありますが、今後もアップデートが続くことを期待しています。

ありがとうございました！

明日の記事は @sonots さんの「[書き込みがあるワークロードにおける ZOZOTOWN マルチクラウド構想とその検討停止について](https://qiita.com/sonots/items/f125d3394ee102522aee)」です。ご期待ください！