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

proxy-onlyサブネットを塞ぐと、リージョンALBはHEALTHYのまま504を返した

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

概要
#

ロードバランサのトラブルは「ヘルスチェックが赤い」から入ることが多く、そこを直せば通ります。リージョン外部 Application Load Balancer には、緑のまま通らない入り方があります。

バックエンドから見ると、同じロードバランサからの通信がヘルスチェックと実トラフィックの2つに分かれています。片方だけを塞ぐと、こうなります。

今回測ったのは、IPv4のVMを GCE_VM_IP_PORT 型のゾーンNEGに登録した構成です。ハイブリッドNEGやバックエンドバケットでは経路が変わるので、そのまま当てはめられません。

何が来るか 送信元
ヘルスチェック 35.191.0.0/16
実トラフィック proxy-only サブネットの範囲

ファイアウォールを片方しか開けていないと、片方だけ通ります。ヘルスチェックだけ開けていれば、バックエンドは HEALTHY のまま、クライアントには 504 が返ります。

ファイアウォールを1本だけ外す対照実験で、これを実際に起こします。

Google Cloud のロードバランサを触ったことがある人向けです。ALB の種類の選び方や、Cloud Armor・セッションアフィニティは扱いません。

検証環境
#

  • Terraform v1.14.3、hashicorp/google v7.46.0
  • Google Cloud SDK 562.0.0

使用するコードは Advanced-Examples/08-regional-external-alb-neg です。

cd Advanced-Examples/08-regional-external-alb-neg
cp terraform.tfvars.example terraform.tfvars   # project_id と iap_member を書き換える
terraform init
terraform apply
Apply complete! Resources: 33 added, 0 changed, 0 destroyed.

Outputs:
vip = "34.146.203.171"

バックエンドVMに外部IPはありません。インターネットからの入口はロードバランサだけです。

構成
#

flowchart LR
    C["クライアント"] -->|":81 / :82"| FR["転送ルール2本
同じ VIP"] FR --> ENV["Envoy プロキシ
proxy-only subnet
10.128.0.0/23"] HC["ヘルスチェック
35.191.0.0/16"] --> VM ENV -->|"送信元は 10.128.0.x"| VM["バックエンドVM ×3
10.80.0.0/24
tcp/8080"]

バックエンドに入る矢印が2本あります。ここが記事の全部です。

:81 はゾーンaの2台、:82 はゾーンcの1台に向きます。同じ VIP のポート違いで分けており、パスによる振り分けは使っていません。

$ gcloud compute forwarding-rules list --regions=asia-northeast1 \
    --format="table(name,IPAddress,portRange,loadBalancingScheme)"
NAME                IP_ADDRESS    PORT_RANGE  LOAD_BALANCING_SCHEME
tf-adv-elb08-fr-81  34.146.203.171  81-81       EXTERNAL_MANAGED
tf-adv-elb08-fr-82  34.146.203.171  82-82       EXTERNAL_MANAGED
$ curl -sS http://34.146.203.171:81/
backend-a
$ curl -sS http://34.146.203.171:81/
backend-a2
$ curl -sS http://34.146.203.171:82/
backend-b

proxy-only サブネットは何か
#

リージョン外部 ALB は Envoy で動きます。その Envoy が置かれるのが proxy-only サブネットです。

$ gcloud compute networks subnets list --regions=asia-northeast1 \
    --format="table(name,ipCidrRange,purpose,role)"
NAME                 RANGE          PURPOSE                 ROLE
tf-adv-elb08-proxy   10.128.0.0/23  REGIONAL_MANAGED_PROXY  ACTIVE
tf-adv-elb08-subnet  10.80.0.0/24   PRIVATE

普通のサブネットとは用途が違い、他には使えません。VMを置こうとすると断られます。

$ gcloud compute instances create tf-probe-in-proxy \
    --zone=asia-northeast1-a --subnet=tf-adv-elb08-proxy --machine-type=e2-micro
ERROR: (gcloud.compute.instances.create) Could not fetch resource:
 - Subnetwork must have purpose=PRIVATE.

ACTIVE は同じリージョン・同じVPCに2つ置けません。

$ gcloud compute networks subnets create tf-probe-proxy2 \
    --network=tf-adv-elb08-vpc --region=asia-northeast1 \
    --range=10.129.0.0/23 --purpose=REGIONAL_MANAGED_PROXY --role=ACTIVE
ERROR: (gcloud.compute.networks.subnets.create) Could not fetch resource:
 - The resource '.../subnetworks/tf-adv-elb08-proxy' already exists

エラーが指しているのは、作ろうとした名前ではなく既存のほうです。名前を変えても通りません。ドキュメントも、制限を active に限定しています。

In a given VPC network and region, only a single proxy-only subnet with purpose REGIONAL_MANAGED_PROXY can be active at any point in time.

BACKUP なら共存できます。 アドレス範囲を変えるときなどに使います。

$ gcloud compute networks subnets create tf-probe-proxy-backup \
    --network=tf-adv-elb08-vpc --region=asia-northeast1 \
    --range=10.129.0.0/23 --purpose=REGIONAL_MANAGED_PROXY --role=BACKUP
Created [...].

$ gcloud compute networks subnets list --regions=asia-northeast1 \
    --format="table(name,ipCidrRange,purpose,role)"
NAME                   RANGE          PURPOSE                 ROLE
tf-adv-elb08-proxy     10.128.0.0/23  REGIONAL_MANAGED_PROXY  ACTIVE
tf-adv-elb08-subnet    10.80.0.0/24   PRIVATE
tf-probe-proxy-backup  10.129.0.0/23  REGIONAL_MANAGED_PROXY  BACKUP

「2つ目は作れない」ではなく「ACTIVE は1つだけ」です。

同じ資料に、送信元についての記述もあります。

Packets sent from a proxy to a backend VM or endpoint has a source IP address from the proxy-only subnet.

送信元を実際に数える
#

バックエンドは python3 -m http.server で、アクセスログが /var/log/http.log に出ます。VMに外部IPが無いので、IAP 経由で入ります。

gcloud compute ssh tf-adv-elb08-a --project=YOUR_PROJECT_ID \
  --zone=asia-northeast1-a --tunnel-through-iap

tf-adv-elb08-a の上で、送信元だけを数えます。

sudo awk '{print $1}' /var/log/http.log | sort | uniq -c | sort -rn
     27 35.191.235.172
     27 35.191.235.169
     27 35.191.235.168
      1 10.128.0.4
      1 10.128.0.3
      1 10.128.0.2

きれいに分かれました。35.191.x がヘルスチェック、10.128.0.x が実トラフィックです。後者は proxy-only サブネットの 10.128.0.0/23 に入っています。

ヘルスチェック由来が81件、proxy-only サブネット由来が3件でした。これはログファイル全体の集計で、比率は観測時間と実リクエスト数で変わります。それでも、放っておけばログの大半がヘルスチェックになることは見て取れます。

ヘルスチェックの送信元はドキュメントにあります。

For IPv4 health checks to the backends: 35.191.0.0/16

このサンプルのファイアウォールは 130.211.0.0/22 も許可していますが、今回の観測ではすべて 35.191.x からでした。

ファイアウォールを1本だけ外す
#

3本あります。

$ gcloud compute firewall-rules list --filter="network~tf-adv-elb08" \
    --format="table(name,sourceRanges.list(),allowed[].map().firewall_rule().list())"
NAME                            SOURCE_RANGES                 ALLOW
tf-adv-elb08-vpc-allow-hc       130.211.0.0/22,35.191.0.0/16  tcp:8080
tf-adv-elb08-vpc-allow-iap-ssh  35.235.240.0/20               tcp:22
tf-adv-elb08-vpc-allow-proxies  10.128.0.0/23                 tcp:8080

allow-proxies だけを消します。ルートもバックエンドも転送ルールも触りません。

事前
#

Terraform の差分が無い状態から始めます。 これを確かめておかないと、あとの「1本だけ」が言えません。

$ terraform plan -detailed-exitcode
No changes. Your infrastructure matches the configuration.   # exit 0

$ gcloud compute backend-services get-health tf-adv-elb08-bs-a --region=asia-northeast1
HEALTHY;HEALTHY

$ curl -sS http://34.146.203.171:81/
backend-a

削除する
#

$ gcloud compute firewall-rules delete tf-adv-elb08-vpc-allow-proxies --quiet
Deleted [...].

$ terraform plan
  # google_compute_firewall.allow_proxies will be created
Plan: 1 to add, 0 to change, 0 to destroy.

差分は1件だけです。消したルール以外は動いていません。

60秒待ってから見ます。

$ gcloud compute backend-services get-health tf-adv-elb08-bs-a --region=asia-northeast1
HEALTHY;HEALTHY

$ curl -sS --max-time 60 -o /dev/null -w "HTTP %{http_code} / %{time_total}秒\n" http://34.146.203.171:81/
HTTP 504 / 30.025136秒

ヘルスチェックが何度も回ったあとも緑のまま、504 が30秒で返ります。

30秒はどこから来るか
#

バックエンドサービスの timeoutSec が30です。変えて確かめます。

$ gcloud compute backend-services update tf-adv-elb08-bs-a --region=asia-northeast1 --timeout=15

$ curl -sS --max-time 60 -o /dev/null -w "HTTP %{http_code} / %{time_total}秒\n" http://34.146.203.171:81/
HTTP 504 / 15.027065秒
HTTP 504 / 15.027665秒
timeoutSec 504 までの時間
30(既定) 30.025秒
15 15.027秒

設定した値がそのまま出ます。

戻す
#

$ terraform apply
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

$ gcloud compute backend-services get-health tf-adv-elb08-bs-a --region=asia-northeast1
HEALTHY;HEALTHY

$ curl -sS http://34.146.203.171:81/
backend-a

$ terraform plan -detailed-exitcode
No changes.   # exit 0

削除も復旧もファイアウォール1本で、前後とも差分ゼロ。 緑+200 → 緑+504 → 緑+200 と動きました。

「ヘルスチェックが緑だから、バックエンドまでは届いている」とは言えません。HEALTHY が示すのは、設定したプローブが正常判定の条件を満たしたことだけです。今回の構成ではプローブは 35.191.0.0/16 から届きますが、proxy-only サブネットからの実リクエストが通るかは見ていません。

ログで切り分ける
#

リージョン外部 ALB のログは、グローバルとはリソース種別もフィールド名も違います。

グローバル外部ALB リージョン外部ALB
resource.type http_load_balancer http_external_regional_lb_rule
失敗の理由 jsonPayload.statusDetails jsonPayload.proxyStatus

グローバルのつもりで resource.type="http_load_balancer" と書くと、このロードバランサのログは検索対象から外れます。エラーにはなりません。検索範囲にグローバル外部ALBのログがあれば、そちらが返ります。

gcloud logging read 'logName=~"external_regional_requests"' --freshness=2h --limit=40 --format=json

実験中のログです。

status=  200  latency=0.007180s   proxyStatus=(空)
status=  504  latency=30.001081s  proxyStatus=error="http_response_timeout"; details="client_timed_out"
status=   -   latency=9.991448s   proxyStatus=error="connection_terminated"; details="client_disconnected_before_any_response"

3行目はステータスが空です。クライアントが --max-time 10 で先に諦めた回で、ロードバランサは応答を返す前に切られたことを記録しています。

ドキュメントは proxyStatus の値を説明しています。

  • http_response_timeout —— バックエンドの応答を待って設定したタイムアウトに達した(504 / 408)
  • connection_terminated —— 完全な応答を受け取る前に接続が終わった(0 / 502 / 503)
  • destination_unavailable —— ヘルスチェック失敗などでバックエンドが使えない(500 / 503)

今回の 504 は destination_unavailable ではなく http_response_timeout でした。ロードバランサはバックエンドを使えると判断したうえで、応答を待って諦めています。ヘルスチェックが緑だったことと整合します。

ただし details は読み解けていません。同時に記録された details="client_timed_out" を、資料はクライアント接続のアイドルタイムアウトとして説明しています。

この組み合わせになる理由は確かめられませんでした。 約30秒という実測が timeoutSec=30 と一致するところまでが今回の裏づけです。切り分けるなら、timeoutSec だけを変えて504までの時間と両フィールドを取り直します。

つまずいたときの確認先
#

後片付け
#

$ terraform destroy
Destroy complete! Resources: 33 destroyed.

外部VIP・Cloud NAT・VM3台は、動いている間ずっと時間課金されます。 検証が終わったら消します。

VIP は転送ルールとして課金されます(Cloud Load Balancer Forwarding Rule ...)。VM の外部IPにも専用の SKU があり、Spot では最初の1時間から課金されます(External IP Charge on a Spot Preemptible VM)。使っていれば無料、ではありません。

まとめ
#

今回測ったのは、IPv4のVMを GCE_VM_IP_PORT 型のゾーンNEGに登録したリージョン外部ALBです。

  • バックエンドに入る経路は2つある。 ヘルスチェックは 35.191.0.0/16、実トラフィックは proxy-only サブネット
  • ヘルスチェックが緑でも通るとは限らない。 HEALTHY はプローブが条件を満たしたことしか示さない
  • ファイアウォール1本を外すと、緑のまま 504。timeoutSec を15にすると15秒で返った
  • proxy-only サブネットにVMは置けない。 purpose=PRIVATE を要求される
  • ACTIVE は1つだけ。 BACKUP は共存できる
  • リージョンとグローバルでログが違う。 http_external_regional_lb_rule と proxyStatus。グローバルの種別で書くと、このLBのログは対象から外れる
  • proxyStatus は切り分けに使えるが、値だけで原因は決まらない。 destination_unavailable はヘルスチェック失敗以外でも出る

参考資料
#

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

関連記事

GKEでPodの退避を起こしたら、監視に必要なメトリクスがCloud Monitoringに来なかった
8 分
Terraform GoogleCloud GKE Kubernetes CloudLogging CloudMonitoring
ドメイン認証がFAILEDになった9分後にAUTHORIZEDへ戻り、証明書はACTIVEになった
4 分
Terraform GoogleCloud LoadBalancing CertificateManager CloudDNS
terraform applyが成功してもVMの中は設定されていなかった話
4 分
Terraform GoogleCloud Ansible ComputeEngine CloudLogging
標準出力に1行JSONを書くだけなら、GKEではFluentdサイドカーが要らなかった
2 分
Terraform GoogleCloud GKE CloudLogging Fluentd
TerraformでGKEのスタンドアロンNEGを消したらLBのバックエンドが戻らなかった話
3 分
Terraform GoogleCloud GKE LoadBalancing
TerraformでGKEのPodをInternal HTTP LB(NEG)でポート別に振り分けてみた
3 分
Terraform GoogleCloud GKE LoadBalancing