【GKE】Ingressのヘルスチェックで All backend services are in UNHEALTHY stateが出る場合の原因と解決方法


前提としてNuxt.jsをGKEにデプロイしている

前提は表題の通りです。

Nuxt.jsのアプリをGKEにデプロイしています。

Ingressの使い方が分からない人は、以下の公式ドキュメントを読みましょう。

参考:https://cloud.google.com/kubernetes-engine/docs/concepts/ingress?hl=ja

Dockerfileの作り方が分からない人は、以下にまとめているので参考にしていただけますと幸いです。

参考:https://qiita.com/arthur_foreign/items/fca369c1d9bde1701e38

また、今回の起きたことを一言でまとめると「Ingressを利用するならヘルスチェックちゃんと通せよ」って話です。

GKEのIngressで「All backend services are in UNHEALTHY state」が発生

GKEのIngressで「All backend services are in UNHEALTHY state」が発生しました。

Ingressの外部IPアドレスにアクセスすると、常に「502 Server Error」が表示されます。

GKEのIngressにおけるヘルスチェックの仕様がエラーの原因

All backend services are in UNHEALTHY state」の発生は、Ingressのヘルスチェックによる仕様が原因でした。(ヘルスチェックの設定を切り替えたことで、ちゃんとトラフィックが飛んだことも確認できています。)

以下の内容をGoogleの公式ドキュメントの引用ベースでまとめます。

  • GKEのIngressの仕様
  • ヘルスチェックの仕様

GKEのIngressの仕様

GKEのIngressの仕様は以下です。

Ingress を通して公開される Service は、ロードバランサからのヘルスチェックに応答する必要があります。
引用:https://cloud.google.com/kubernetes-engine/docs/concepts/ingress?hl=ja

ヘルスチェックに応答するためには、以下のような操作が行われます。

負荷分散されたトラフィックの最終的な送信先であるコンテナは、正常であることを示すために次のいずれかの操作を行います。

/ パスでの GET リクエストに対して、HTTP 200 ステータスのレスポンスを返す。

引用:https://cloud.google.com/kubernetes-engine/docs/concepts/ingress?hl=ja

上記について、デフォルトだと引用通り/のパスに対してヘルスチェック(GETリクエスト)が飛びます。
※デフォルトのパスにヘルスチェックを飛ばす以外の方法は、別の見出しにまとめました。

ヘルスチェックの仕様

まず、ヘルスチェックの仕様について前提を引用すると以下です。

ヘルスチェックとロードバランサは連携して機能します。プローブが連続して正常終了または失敗した回数に基づいて(このしきい値もユーザーが個別に設定できます)、GCP はロードバランサの各バックエンドについて全体的な健全性を計算します。正常応答の回数が設定値に達した場合、バックエンドは正常であると判定され、同様に、応答に失敗した回数が設定したしきい値に達した場合、バックエンドは正常ではないと判定されます。
引用:https://cloud.google.com/load-balancing/docs/health-check-concepts#method

また、Ingressを利用するとロードバランサが作成されますね。

Ingress オブジェクトを作成すると、GKE Ingress コントローラによって Google Cloud Platform HTTP(S) ロードバランサが作成され、Ingress および関連する Service の情報に従って構成されます。
引用:https://cloud.google.com/kubernetes-engine/docs/concepts/ingress?hl=ja

ヘルスチェックはロードバランサと連携するので、Ingressを利用する場合においてもヘルスチェックが行われます。

それこそ、Ingressは自動的にヘルスチェックを構成しています。

HTTP(S) ロードバランサ: Ingress リソースを使用して作成できます。HTTP(S) ロードバランサは HTTP(S) リクエストを終了させるように設計されており、状況に応じてより適切に負荷分散の判断を下すことができます。提供される機能には、カスタマイズ可能な URL マップや TLS ターミネーションなどがあります。GKE は、HTTP(S) ロードバランサのヘルスチェックを自動的に構成します。
引用:https://cloud.google.com/kubernetes-engine/docs/tutorials/http-balancer?hl=ja

HTTP・HTTPS・HTTP/2 の正常終了の条件について

先ほどのIngressの説明を引用していましたが、IngressはHTTPに関するプロトコルを利用します。

HTTP、HTTPS、または HTTP/2 プロトコルを使用するヘルスチェック プローブからのレスポンスが正常終了したとみなされるのは、GCP が送信したリクエストに対して HTTP 200 (OK) のレスポンスを受信し、そのレスポンスがプローブ タイムアウトの前に到着した場合のみです。
引用:https://cloud.google.com/load-balancing/docs/health-check-concepts#criteria-protocol-http

ロードバランサの種類ごとの違いについて知りたい人は、下の記事を読むとより本記事の内容が理解できるかもしれません。

参考:https://www.kimullaa.com/entry/2019/12/01/135430

正常・異常応答のしきい値到達時の挙動

先ほど、ヘルスチェックについて、正常・異常応答のしきい値部分を引用しました。

まずは、正常なしきい値に到達した時の挙動を公式ドキュメントから引用します。

この正常しきい値に達すると、GCP はバックエンドを正常であるとみなします。正常なバックエンドは新しい接続の受信に適格であるとされます。
引用:https://cloud.google.com/load-balancing/docs/health-check-concepts#criteria-protocol-http

正常なしきい値に到達した場合ついては特になにも言うことはありません。

次は、異常なしきい値に到達した場合の挙動を見てみましょう。

異常しきい値に達すると、GCP はバックエンドが正常でないと見なします。正常でないバックエンドは新しい接続を受信に不適格とされます。ただし、既存の接続がすぐに終了することはありません。すぐに終了するのではなく、タイムアウトが発生するか、トラフィックが破棄されるまで、その接続は開かれたままで保持されます。動作の詳細は、使用するロードバランサの種類に応じて異なります。

プローブ失敗の原因によっては、既存の接続がレスポンスを返せない場合があります。正常でないバックエンドが、正常しきい値を再び満たすことができると、正常に復帰できます。
引用:https://cloud.google.com/load-balancing/docs/health-check-concepts#criteria-protocol-http

どうやら、正常なしきい値に再び戻すことが出来るまで、レスポンスを返せない場合があるようですね。

原因がわかったのでエラーの対策をしてみましょう。

ちなみに、原因についてはGitHubのIssueとかにも掲載されているようですね。

参考:https://github.com/kubernetes/ingress-gce/issues/42

All backend services are in UNHEALTHY stateの解決方法

All backend services are in UNHEALTHY state」を解決するにあたって、以下の対応を実施しました。

  • Nuxt.jsのExpressで/healthのパスにリクエストが飛んだら200のステータスコードを返す
  • プローブで/healthのパスを指定する
  • ヘルスチェックで名前解決しているホスト名(URL)とパスを指定する

Nuxt.jsのExpressで/healthのパスにリクエストが飛んだら200のステータスコードを返す

※Expressの詳しい説明は抜きにして、ソースコードのみを共有します。

nuxt.config.js
export default {
  serverMiddleware: ['~/server/health.js'],
}
~/server/health.js
const express = require('express')

const app = express()

app.get('/health', (req, res) => {
  return res.sendStatus(200)
})

module.exports = {
  path: '/',
  handler: app
}

これで、/healthのパスにリクエストが飛んだら、ステータスコード200を返す実装になっています。

Nuxt.jsのServerMiddlewareの使い方は、以下のドキュメントを参考にしました。
参考:https://ja.nuxtjs.org/api/configuration-servermiddleware/

Expressの実装(200のステータスコードを返す実装)については、以下のドキュメントを参考にしました。
参考:https://expressjs.com/ja/api.html#res.sendStatus

わざわざExpressを使ってまでパスを指定している理由

私が開発しているNuxt.jsのプロジェクトだと、/にアクセスした場合は、普通にindexページが表示されるんですよね。

ヘルスチェックのためにアクセスされる都度、indexページが返されるのも負荷が重いかなと思いました。

そのため、ステータスコードだけ返す処理を書いたというわけです。

また、インデックスページにアクセスすると、axiosでAPIを叩いて認証が通らずに、403を返してしまうような状況になってしまう場合に、ヘルスチェックの飛ばす先を変えてあげるみたいな使い方も可能です。

プローブで/healthのパスを指定する

※プローブについてlivenessProbereadinessProbeの違いについては言及しませんので、以下のドキュメントを参考にしてください。

参考:https://kubernetes.io/ja/docs/concepts/workloads/pods/pod-lifecycle/#%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AEprobe

apiVersion: apps/v1
kind: Deployment
// 省略
spec:
  // 省略
  template:
    // 省略
    spec:
      containers:
      - image: gcr.io/project_name/app_name:v1
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /health
            port: 3000
            scheme: HTTP
          initialDelaySeconds: 15
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1

これで、ヘルスチェックのリクエスト先パスを/healthに変更出来ました。

※とりあえずヘルスチェックに成功すればいいと思って、適当な設定にしているのでご了承ください。

ヘルスチェックで名前解決しているホスト名(URL)とパスを指定する

Ingressの外部IPに対して、DNSを使って名前解決していると思います。

ホストHTTPヘッダー」欄で解決しているURLを入力しましょう。

また、先ほどヘルスチェックを飛ばすパスとして指定した「/health」を「リクエストパス」に入力してあげてください。

これで、ヘルスチェックが通るはずです。(Ingressを見ると緑マークになってるはず)