↓メインコンテンツへスキップ
  1. Blogs/

TerraformでGKEのPodをInternal HTTP LB(NEG)でポート別に振り分けてみた

3 分
Terraform GoogleCloud GKE LoadBalancing
0222-nnn
著者
0222-nnn
猫が好き
目次
Terraform-GoogleCloud - この記事は連載の一部です
パート 26: この記事

概要
#

GKEプライベートクラスタと踏み台を作った後、複数のアプリケーションをクラスタに同居させたくなる場面があります。リクエストをアプリごとに振り分けたい、という場面です。GKEのServiceだけではVPC内部からの経路が用意されません。Ingressを使わない場合、Podへの入り口をどう作るかが課題になります。

Internal HTTP LB(INTERNAL_MANAGED)は、GKEのPodをNetwork Endpoint Group(NEG)というバックエンドの形で直接受け取れます。Terraformが手動で作るNEGとは異なる点です。GKEのServiceにcloud.google.com/negというアノテーションを付けるだけです。GKEコントローラがNEGを自動的に作成します。

Terraformの基本操作と、前回のGKEプライベートクラスタ+踏み台構成は前提としています。

Session Affinity(Cookie)は本記事では扱いません。外部ALB向けの構成例がterraform-gcp-examplesの10-regional-alb-session-affinityにあります(Terraformコードのみ)。Ingress(GKEのL7 LBコントローラ)や外部公開も扱いません。

検証すること
#

  • GKEのServiceアノテーションから、Network Endpoint Group(NEG)が自動作成されること
  • Internal HTTP LBで、共有VIPの3つのポート(:81/:82/:83)を、それぞれ異なるバックエンドへ振り分けられること
  • 踏み台からVIPの3ポートへ実際に接続し、意図したバックエンドが返ること(LB作成の確認とは別)

前提環境
#

  • Google Cloud CLI、Terraform、Docker
  • Billingが有効な検証用Google Cloud Project
  • GKE、Compute Engine、IAMを操作できるGoogleアカウント
  • 検証時のバージョン:Terraform 1.14.3、google 7.46.0

今回の構成
#

GKEのServiceにcloud.google.com/negアノテーションを付けると、GKEコントローラはクラスタが配置されているゾーンごとにNEGを作成します。この構成では3ゾーンにノードプールを配置しているので、1つのServiceにつき3つのNEG(ゾーンごと1つ)が対応します。

flowchart TB
    subgraph GCP["Google Cloud"]
        subgraph Project["Project"]
            subgraph Region["asia-northeast1"]
                subgraph VPC["VPC"]
                    subgraph GkeSubnet["GKE Subnet"]
                        PodA["Pod(app-a)"]
                        PodB["Pod(app-b)"]
                        PodC["Pod(app-c)"]
                    end
                    Proxy["proxy-only Subnet
/23"] VIP["共有VIP
:81 / :82 / :83"] end end end end VIP -->|":81"| PodA VIP -->|":82"| PodB VIP -->|":83"| PodC Proxy -.->|"Envoyプロキシ処理"| VIP

VIPは1つですが、ポートごとに異なるURL Map・Target HTTP Proxy・Backend Serviceを割り当てています。結果として、異なるバックエンドへ振り分けられます。

sequenceDiagram
    participant Bastion as 踏み台
    participant VIP as ILB VIP
    participant PodA as Pod(app-a)

    Bastion->>VIP: curl http://VIP:81/
    VIP->>PodA: :81のBackend Serviceへ転送
    PodA-->>VIP: "backend-a"
    VIP-->>Bastion: "backend-a"

使用するTerraformコード
#

GKEクラスタをリージョナル・3ゾーンにする
#

NEGはゾーンごとに作られるため、前回のゾーナル・1ノード構成のままでは複数ゾーンのバックエンドを確認できません。今回はGKEクラスタをリージョナルにし、3ゾーンにゾーンごと1ノードずつ配置します。

resource "google_container_cluster" "primary" {
  name     = var.cluster_name
  location = var.region  # 前回はvar.zone(ゾーナル)
  # ...
}

resource "google_container_node_pool" "primary" {
  name           = "${var.cluster_name}-np"
  location       = var.region
  cluster        = google_container_cluster.primary.name
  node_count     = 1               # ゾーンごとに適用される値
  node_locations = var.gke_zones   # 3ゾーンで合計3ノード
  # ...
}

ノード数が前回の3倍になるため、コストも高くなります。確認後はすぐにdestroyする前提の構成です。

GKEのServiceからNEGを自動作成する
#

apiVersion: v1
kind: Service
metadata:
  name: app-a
  namespace: default
  annotations:
    cloud.google.com/neg: '{"exposed_ports":{"8080":{"name":"neg-app-a"}}}'
spec:
  type: ClusterIP
  selector:
    app: app-a
  ports:
    - name: http
      port: 8080
      targetPort: 8080
      protocol: TCP

app-b・app-cも同じ形で、名前とNEG名だけを変えて用意します。バックエンドのアプリにはhashicorp/http-echoを使い、それぞれbackend-a・backend-b・backend-cという固定文字列を返すだけにしています。

Internal HTTP LB(NEGを参照する側)
#

resource "google_compute_region_backend_service" "bs_app_a" {
  name                  = "${var.cluster_name}-bs-app-a"
  region                = var.region
  load_balancing_scheme = "INTERNAL_MANAGED"
  protocol              = "HTTP"
  health_checks         = [google_compute_region_health_check.hc_app_a[0].id]

  dynamic "backend" {
    for_each = toset(var.gke_zones)
    content {
      group                 = "https://www.googleapis.com/compute/v1/projects/${var.project_id}/zones/${backend.key}/networkEndpointGroups/neg-app-a"
      balancing_mode        = "RATE"
      max_rate_per_endpoint = 100
      capacity_scaler       = 1.0
    }
  }
}

NEGはTerraformが作るリソースではなく、GKEコントローラが作る側のリソースです。Backend Serviceは名前を組み立てたURL文字列でNEGを参照しているだけです。Terraformの計画時点ではNEGの実在を確認できません。存在しないNEGを参照したままapplyすると、Terraformの計画エラーではなく、実際のAPI呼び出し時に404で失敗します。

設定と実行(2段階apply)
#

NEGはPodやServiceを作った後にしか存在しないため、applyを2段階に分けます。

cd Advanced-Examples/14-gke-bastion-ilb-multi-neg
cp terraform.tfvars.example terraform.tfvars
project_id = "YOUR_PROJECT_ID"
iap_member = "user:YOUR_ACCOUNT"
terraform init
terraform fmt -check
terraform validate
terraform plan
terraform apply

1段階目(enable_ilb=false、デフォルト)はVPC・踏み台・GKEクラスタ(3ゾーン)を作ります。25リソースです。

Apply complete! Resources: 25 added, 0 changed, 0 destroyed.

続けて、踏み台へapp-a/b/cのマニフェストを転送し、kubectl applyします。

for f in nginx-deployment.yaml ilb-app-a.yaml ilb-app-b.yaml ilb-app-c.yaml; do
  gcloud compute scp "k8s/$f" \
    "$(terraform output -raw bastion_name):/tmp/$f" \
    --zone="$(terraform output -raw zone)" --tunnel-through-iap \
    --project="$(terraform output -raw project_id)"
done
# 踏み台の中で実行
gcloud container clusters get-credentials "$(terraform output -raw cluster_name)" \
  --region="$(terraform output -raw region)" --project="$(terraform output -raw project_id)"
kubectl apply -f /tmp/ilb-app-a.yaml -f /tmp/ilb-app-b.yaml -f /tmp/ilb-app-c.yaml

NEGの作成を1〜2分待ってから、ローカルで3ゾーン分できているか確認します。

gcloud compute network-endpoint-groups list --filter="name~neg-app"
NAME       LOCATION           ENDPOINT_TYPE   SIZE
neg-app-a  asia-northeast1-a  GCE_VM_IP_PORT  1
neg-app-b  asia-northeast1-a  GCE_VM_IP_PORT  1
neg-app-c  asia-northeast1-a  GCE_VM_IP_PORT  1
neg-app-a  asia-northeast1-b  GCE_VM_IP_PORT  0
neg-app-b  asia-northeast1-b  GCE_VM_IP_PORT  0
neg-app-c  asia-northeast1-b  GCE_VM_IP_PORT  0
neg-app-a  asia-northeast1-c  GCE_VM_IP_PORT  0
neg-app-b  asia-northeast1-c  GCE_VM_IP_PORT  0
neg-app-c  asia-northeast1-c  GCE_VM_IP_PORT  0

Podはレプリカ数1のため実際には1ゾーンにしか配置されませんでしたが、NEG自体はクラスタが持つ3ゾーンすべてに作られていました。Pod未配置のゾーンはSIZE=0の空のNEGとして存在します。3ゾーン分そろったところで、2段階目のapplyへ進みます。

terraform apply -var="enable_ilb=true"
Apply complete! Resources: 19 added, 0 changed, 0 destroyed.

Outputs:

ilb_vip = "10.40.0.250"

踏み台からVIPの3ポートへ実際に接続できることを確認する
#

ILBが作成できたことと、意図したバックエンドへ実際に振り分けられることは別です。

# 踏み台の中で実行
VIP=$(terraform output -raw ilb_vip)
curl -s "http://$VIP:81/"
curl -s "http://$VIP:82/"
curl -s "http://$VIP:83/"
backend-a
backend-b
backend-c

同じVIPの異なるポートに対して、それぞれ意図したバックエンドの応答が返ってきました。1つのVIPを複数ポートで使い分ける構成が、実際に機能していることを確認できました。

後片付け
#

terraform destroy
Destroy complete! Resources: 44 destroyed.

3ゾーンのGKEクラスタとILBは利用中に料金が発生します。11〜13より単価が高い構成のため、検証後は必ずすぐにdestroyします。

まとめ
#

  • GKEのServiceにcloud.google.com/negアノテーションを付けると、GKEコントローラがゾーンごとにNEGを自動作成する
  • NEGはTerraform管理外のリソースのため、Backend ServiceはURL文字列で参照する。存在しないNEGへの参照は、plan時ではなくapply時のAPI呼び出しで404になる
  • NEGを使う構成では、Podが実際に配置されていないゾーンにも空のNEGが作られる
  • 1つのVIPでも、ポートごとに異なるURL Map・Backend Serviceを割り当てれば、複数のバックエンドへ振り分けられる
  • LB作成の確認とは別に、意図したポート・バックエンドの組み合わせで実際に応答が返ることまで確認できる

参考資料
#

次回
#

GKEのコントロールプレーンをプライベート+パブリックの両方から接続します。

Terraform-GoogleCloud - この記事は連載の一部です
パート 26: この記事

関連記事

TerraformでGKEのコントロールプレーンをプライベート+パブリックの両方から接続してみた
2 分
Terraform GoogleCloud GKE
TerraformでGKEプライベートクラスタからMemorystore for Redis Clusterへ接続してみた
3 分
Terraform GoogleCloud GKE Redis
TerraformでGKEプライベートクラスタからMemorystore for Redisへ接続してみた
2 分
Terraform GoogleCloud GKE Redis
TerraformでGKEプライベートクラスタと踏み台VMを作成してみた
3 分
Terraform GoogleCloud GKE IAP
TerraformでGKE Workload IdentityからCloud Storageへアクセスしてみた
2 分
Terraform GoogleCloud GKE WorkloadIdentity
Terraform StateをCloud Storage Remote Backendへ移行してみた
2 分
Terraform GoogleCloud CloudStorage