Ingress設定のみでメンテナンス用レスポンス応答に切り替える方法


はじめに

最近寒くて趣味の自転車に乗るのが億劫になり、運動不足が深刻になってしまったので室内でも自転車を漕げるようにzwifterになりました。冬なのに窓全開で汗だらだら流しながら頑張ってます!

ZOZOTOWNは基本的にダウンタイム無しで日々更新していますが、やんごとなき事情により年1回あるかないかくらいの頻度でダウンタイムを伴うメンテナンスを行うことがあります。
本記事では、今年のメンテナンス時にIngress設定変更のみでメンテナンス用レスポンス応答に切り替えた方法を紹介します。

ALB周りのインフラ構成

ユーザからのリクエストを受け付けるALBに関するインフラ構成技術は投稿時点で以下のようになっております。本記事の内容はこちらの構成での事例になります。

  • Amazon EKS(v1.19)
  • Ingress(apiVersion: networking.k8s.io/v1)
  • AWS Load Balancer Controller(v2.2.0)

上記構成により、Ingressに設定した内容に基づきAWS Load Balancer ControllerがAWS Elastic Load Balancer(以下、ALBと記載)を構築します。 

やりたいこと

実運用であったり、安全にメンテナンスを進めることを考えると、メンテナンス用レスポンスを返す以外にも制御すべきことはあり、やりたいこととしては以下の3点になりました。

  • 1点目。主目的であるステータスコード503(Service Unavailable)でメンテナンス用のレスポンスを返すことです。
  • 2点目。メンテナンス後にサイトを開放して問題がないことをチェックするために、社内メンバーは事前に動作確認ができるとより安心して解放できますよね。動作確認用に社内VPNからのアクセスであれば正常なレスポンスを返せるようにしたいです。
  • 3点目。外形監視1のための定期的なリクエストも存在しますので、これに対しメンテナンス用レスポンスを返すと監視のアラートが鳴り響き、よろしくありませんのでこれもなんとかしたいです。メンテナンス期間中は外形監視をオフにすることも考えられますが、弊社の場合はその設定箇所がマイクロサービス別に分かれており、IaC化されているとはいえ個別にオフにするのが面倒でしたので、送信元が外形監視の環境で利用するNATGWである場合は正常なレスポンスを返してあげるのが良さそうです。

上記はいずれも送信元で振り分ける方針ですが、弊社はCDNを利用しておりますので送信元IPは必然的にCDNになってしまうため、判断情報としては送信元IPではなくX-Forwarded_Forヘッダーを参照する必要があります。但し、外形監視に関してはCDNに問題があるケースと、originサーバに問題があるケースの2つを切り分けるために、CDNへのリクエストとoriginサーバへのリクエストの2つの外形監視を行なってますのでそれぞれに対応する必要があります。

纏めると、要件は以下になります。

送信元 判定対象 対応内容
社内VPN X-Forwarded_Forヘッダーの先頭IP 正常なレスポンスを返す (=通常時と同様にバックエンドに流す)
外形監視環境のNATGW X-Forwarded_Forヘッダーの先頭IP 正常なレスポンスを返す (=通常時と同様にバックエンドに流す)
外形監視環境のNATGW 送信元IP 正常なレスポンスを返す (=通常時と同様にバックエンドに流す)
上記以外 X-Forwarded_Forヘッダーの先頭IP ステータスコード503でメンテナンス用レスポンスを返す

なお、弊社の場合バックエンドへの入り口はingress-gatewayになりますので、バックエンドに流すと記載している箇所はingress-gatewayに転送することを意味しております。

どうやるのか

上記の要件をALBのリスナールールで対応します。

リスナールールは上から順に判定され、条件に一致した処理を行います。
先述した要件をこのリスナールールに落とし込むと下記の表のようになります。

優先度 ルール名(service name) 条件(condition) 処理(action)
1 allow_vpn X-Forwarded_Forヘッダーが<社内VPN>* ingress-gateway:80へ転送
2 allow-synthetics-external X-Forwarded_Forヘッダーが<外形監視環境のNATGW>* ingress-gateway:80へ転送
3 allow-synthetics-origin 送信元IPが<外形監視環境のNATGW>* ingress-gateway:80へ転送
4 response-maintenance 条件無し ステータスコード503でメンテナンス用レスポンスを返す

Ingressで設定する

ALBは先述した通り、Ingressの設定内容に基づきAWS Load Balancer Controllerが構築する仕掛けになっておりますので、リスナールールを追加するための設定はIngressで行います。

Ingressの設定内容についてはこちらを参考にしました。
https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/#traffic-routing
なお、投稿時点で上記サイトの記載例はIngressのapiVersionはextensions/v1beta1で、これはv1.19のKubernetesで非推奨になりました。本記事ではapiVersion: networking.k8s.io/v1での記載例になります。

apiVersionによる構成の違いはこちらを参考にしました。
https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#ingress-v1-networking-k8s-io

設定ポイントをざっくり書くと以下になります。

  • リスナールールのcondition, actionをどこで定義するのか? -> metadata.annotationsにてアノテーションで設定
  • リスナールールのconditionとactionの紐付けは? -> アノテーションの以下の命名規則による紐付け
    • actionの場合、alb.ingress.kubernetes.io/actions.<service name>
    • condition場合、alb.ingress.kubernetes.io/conditions.<service name>
  • リスナールールの優先順は? -> spec.rules.http.paths内のpath記載順
    • conditionとactionを紐付けるservice nameはこのpath内で指定する

Ingressの設定例

例えば、以下のリスナールールを設定したい場合、Ingressの設定は次のようになります。
(関連箇所のみ抜粋)

ルール名(service name) 条件(condition) 処理(action)
allow_vpn X-Forwarded_Forヘッダーが<社内VPN>* ingress-gateway:80へ転送
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    alb.ingress.kubernetes.io/actions.allow-vpn: >
      {"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"<ingress-gatewayのサービス名>","servicePort":"80","weight":100}]}}
    alb.ingress.kubernetes.io/conditions.allow-vpn: >
      [{"field":"http-header","httpHeaderConfig":{"httpHeaderName": "X-Forwarded-For", "values":["<社内VPNのIP1つ目>*", "<社内VPNのIP2つ目>*"]}}]
spec:
  ingressClassName: alb
  rules:
  - http:
      paths:
      - pathType: ImplementationSpecific
        path: /*
        backend:
          service:
            name: allow-vpn
            port:
              name: use-annotation

注意点として、ALB側の制約で1つのリスナールールで指定可能なワイルドカード(*)の数は5つまでという制限があります。
今回のようにX-Forwarded_Forヘッダーの先頭IPで判定したい場合、先頭以外のIPをワイルドカードで判定する必要があり、上記の例では社内VPNのIPアドレスが2つあったため、それぞれで利用するとそれだけで2つ消費することになります。また、適用するパスの指定でも/*のように記載したい場合は1つとしてカウントされるためご注意ください。

メンテナンス用のIngress設定内容

改めて、以下のメンテナンス用のリスナールールを適用する場合、Ingressの設定は次のようになります。
(関連箇所のみ抜粋)

優先度 ルール名(service name) 条件(condition) 処理(action)
1 allow_vpn X-Forwarded_Forヘッダーが<社内VPN>* ingress-gateway:80へ転送
2 allow-synthetics-external X-Forwarded_Forヘッダーが<外形監視環境のNATGW>* ingress-gateway:80へ転送
3 allow-synthetics-origin 送信元IPが<外形監視環境のNATGW>* ingress-gateway:80へ転送
4 response-maintenance 条件無し ステータスコード503でメンテナンス用レスポンスを返す
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:

    ### allow_vpn
    alb.ingress.kubernetes.io/actions.allow-vpn: >
      {"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"<ingress-gatewayのサービス名>","servicePort":"80","weight":100}]}}
    alb.ingress.kubernetes.io/conditions.allow-vpn: >
      [{"field":"http-header","httpHeaderConfig":{"httpHeaderName": "X-Forwarded-For", "values":["<社内VPNのIP1つ目>*", "<社内VPNのIP2つ目>*"]}}]

    ### allow-synthetics-external
    alb.ingress.kubernetes.io/actions.allow-synthetics-external: >
      {"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"<ingress-gatewayのサービス名>","servicePort":"80","weight":100}]}}
    alb.ingress.kubernetes.io/conditions.allow-synthetics-external: >
      [{"field":"http-header","httpHeaderConfig":{"httpHeaderName": "X-Forwarded-For", "values":["<外形監視環境のNATGWのIP1つ目>*", "<外形監視環境のNATGWのIP2つ目>*", "<外形監視環境のNATGWのIP3つ目>*"]}}]

    ### allow-synthetics-origin
    alb.ingress.kubernetes.io/actions.allow-synthetics-origin: >
      {"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"<ingress-gatewayのサービス名>","servicePort":"80","weight":100}]}}
    alb.ingress.kubernetes.io/conditions.allow-synthetics-origin: >
      [{"field":"source-ip","sourceIpConfig":{"values":["<外形監視環境のNATGWのIP1つ目>/32", "<外形監視環境のNATGWのIP2つ目>/32", "<外形監視環境のNATGWのIP3つ目>/32"]}}]

    ### response-maintenance
    alb.ingress.kubernetes.io/actions.response-maintenance: >
      {"type":"fixed-response","fixedResponseConfig":{"contentType":"application/json","statusCode":"503","messageBody":"{\"code\": 503, \"message\": \"メンテナンス中です\"}"}}

spec:
  ingressClassName: alb
  rules:
  - http:
      paths:
      - pathType: ImplementationSpecific
        path: /*
        backend:
          service:
            name: allow-vpn
            port:
              name: use-annotation
      - pathType: ImplementationSpecific
        path: /*
        backend:
          service:
            name: allow-synthetics-external
            port:
              name: use-annotation
      - pathType: ImplementationSpecific
        path: /*
        backend:
          service:
            name: allow-synthetics-origin
            port:
              name: use-annotation
      - pathType: ImplementationSpecific
        path: /*
        backend:
          service:
            name: response-maintenance
            port:
              name: use-annotation

メンテナンス当日の切り替え

上記のメンテナンス用のIngressマニフェストファイルを事前に準備しておくことで、メンテナンス当日に切り替えとしてやることは以下の2点のみになります。

  • メンテナンス開始時、メンテナンス用Ingressマニフェストファイルをapplyする
  • メンテナンス終了時、元のIngressマニフェストファイルをapplyする

おわりに

今回はメンテナンスの事例を紹介しましたが、送信元で処理を切り替える仕組みは他のことにも応用できると思います。本記事を見ていただいた誰かのお役に立てれば幸いです。


  1. 外形監視にはAmazon CloudWatch Syntheticsを利用