ZOZOMATのgRPCリクエスト負荷分散をNLBからALBに移行した話

OGP

こんにちは、計測プラットフォーム本部バックエンド部の髙木(@TAKAyuki_atkwsk)です。普段はZOZOMATZOZOGLASSなどの計測技術に関する開発・運用に携わっています。ちなみにZOZOGLASSを使って肌の色を計測したところ、私のパーソナルカラーはブルーベース・冬と診断されました。

さて、本記事ではZOZOMATシステムで利用されていたNetwork Load BalancerをApplication Load Balancerに移行した事例をご紹介します。

ZOZOMATのシステム構成(2020年当時)に関しては、こちらの記事で詳しく説明されていますので合わせてご覧ください。 techblog.zozo.com

移行の背景

ZOZOTOWNアプリやZOZOTOWNシステムからZOZOMATシステムに対するリクエストの負荷分散のためにNetwork Load Balancer(以下、NLB)を利用していました。これは、ZOZOTOWNアプリからのリクエストがgRPCを利用することと、当時Application Load Balancer(以下、ALB)ではエンドツーエンドのHTTP/2対応がされていなかったことに拠ります。1年近くNLBを利用していましたが、2020年11月にエンドツーエンドのHTTP/2およびgRPCがALBにてサポートされたことを受け、NLBからALBに移行しようということになりました。

NLBを利用していたときの課題

NLBがALPN対応していないため、TCPリスナーで構成しターゲットにはEnvoyを配置してTLS終端とALPNの役割を担う構成にしていました。このため、TLS証明書はAWS Certificate Manager(ACM)を利用できず、自前で購入・管理する必要がありました。環境を複数用意したい場合、その都度証明書を購入するかワイルドカード証明書を購入するか、という観点でも悩みどころとなっていました。

また、NLBではTCPリスナーだとアクセスログを取得できないことやレスポンスタイムなどのメトリクスを取得できないことも運用にあたっては少し不便でした。実際にはターゲットに配置しているEnvoyのアクセスログを利用することで補っていました。ALBではこれらの悩みが解決されますが、急激なトラフィック増加が見込まれる際には事前に暖機申請をして備えることが求められます。今回の移行時にこの点が懸念としてありましたが私たちのユースケースでは特に問題ありませんでした。

移行に関する課題

移行はシステムを停止させることなく行いたいという要望がありましたが、知見が少なかったことと、このような移行作業の経験もなかったため、注意深く検討する必要がありました。そのため、不明確だった以下の点の調査・検証を行いました。

  • ALB経由でgRPCリクエストを受け付けられるか
  • 複数のIngressをグループ化して単一のALBでルーティングできるか
  • ExternalDNSを利用したDNSレコード変更ができるか
  • ロールバックできるか

本記事の後半では、そのなかでも複数のIngressをグループ化して単一のALBでルーティングできるか、ExternalDNSを利用したDNSレコード変更ができるか、の2点を紹介します。

移行方法

ZOZOMATシステムに紐付くドメインに対し、Route 53上でエイリアスレコードの値をNLBエンドポイントからALBエンドポイントへ切り替える方法で移行しました。NLB関連のリソースを残し、ALB関連のリソースを追加した状態で切り替えるため、切り替え後に不具合が起きた場合、すぐに切り戻しが可能な構成です。以下に切り替え前の構成図と切り替え時の構成図を示します。

切り替え前

切り替え時

構成図にはElastic Kubernetes Service(EKS)のクラスターや、Kubernetesのリソースも含めています。NLBやALB、そしてRoute 53のレコードがKubernetesリソースによって管理されるためです。

次に切り替えのために準備したものを紹介していきます。

ACMでのTLS証明書作成

ALBを利用する構成ではACMで発行したTLS証明書を使うことができるので、あらかじめ発行しておきました。あらかじめ発行しておく理由は、後ほど触れるIngressリソースを定義する際に証明書のARNを参照するためです。

AWS Load Balancer Controllerのインストール

AWS Load Balancer ControllerをEKSクラスターにインストールします。これにより、Ingressリソースを追加すると連動してALBが作成されるようになります。ちなみに、v2.1.0からgRPCワークロードに対応していて、事前検証中にこのバージョンがリリースされ、良いタイミングで利用できました。

Kubernetesリソースの追加

今回追加したのはIngressリソースと、既存のServiceとは別のServiceリソースです。

Ingressは、AWS Load Balancer Controllerでアノテーションを設定していくことでALBの振る舞いを変えることができます。設定可能なアノテーションは以下のドキュメントを参照ください。

kubernetes-sigs.github.io

ZOZOMATシステムでは、ZOZOTOWNアプリからはgRPCで、ZOZOTOWNサーバーからはHTTP/1.1(REST API)としてリクエストされることを考慮しなければなりませんでした。そのため、ALBを配置しても両方のプロトコルでリクエストを受けられるようにしておく必要があります。ALBでこれを実現するには、ターゲットグループを用意し、リスナーのルールによってそれぞれのターゲットグループにルーティングする方法を利用します。また、Ingressリソースの定義で実現できるかどうかも調査・検証しました。次章でご紹介します。

次に、Serviceリソースについて見ていきます。既存の構成では、LoadBalancerタイプのものを利用してNLBとして外部からアクセスする経路を作っていました。切り替え後の構成では、Ingressを利用してALBとして外部からアクセスする経路になるため、LoadBalancerタイプのServiceである必要はなくなります。そのため、新たにClusterIPタイプのServiceを用意してIngressと関連付けることにしました。

EnvoyのHTTPリスナーの作成

ロードバランサーのターゲットに配置されるEnvoyは、既存構成ではHTTPS用のリスナーのみ定義し、TLS終端やALPNを設定していました。切り替え後の構成では、ALBでこれらの役割を担うため、EnvoyにHTTP用のリスナーを追加しました。TLSの設定やポート番号以外は同じ定義です。

検証したこと

次に、今回検証した内容から2つのトピックをピックアップして紹介します。

検証1:複数のIngressをグループ化して単一のALBでルーティングできるか

AWS Load Balancer ControllerにIngress Groupという仕組みがあり、これを利用すると複数のIngressを単一のALBに統合できます。これを設定すると、ALBではリスナールールおよびターゲットグループとして現れます。ZOZOMATシステムで実現したいgRPC、HTTP/1.1両プロトコルの設定をグルーピングする例を以下に示します。

# gRPC用のIngress
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: zozomat-ingress-grpc
  namespace: default
  annotations:
    kubernetes.io/ingress.class: alb
    # グルーピングするIngress間で共通の値を使う
    alb.ingress.kubernetes.io/group.name: zozomat-ingress
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/backend-protocol-version: GRPC
    # ...
spec:
  rules:
  - http:
      paths:
      - path: /foo.BarService/*
        backend:
          serviceName: envoy-service
          servicePort: 80
      # ...
---
# HTTP/1.1用のIngress
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: zozomat-ingress-http1
  namespace: default
  annotations:
    kubernetes.io/ingress.class: alb
    # グルーピングするIngress間で共通の値を使う
    alb.ingress.kubernetes.io/group.name: zozomat-ingress
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/backend-protocol-version: HTTP1
    # ...
spec:
  rules:
  - http:
      paths:
      - path: /foo/*
        backend:
          serviceName: envoy-service
          servicePort: 80
      # ...

このマニフェストを適用することで、単一のALBに対して複数のターゲットグループが関連付けられることを確認できました。

検証2:ExternalDNSを利用したDNSレコード変更ができるか

ZOZOMATシステムではExternalDNSを利用しており、Kubernetesリソースを介してRoute 53のDNSレコードを制御する仕組みです。しかし、ExternalDNSに関して、既にService経由でDNSレコードが設定されている場合、Ingress経由で同じドメインのDNSレコードに対して操作が行えるかどうかが切り替える際に不確かな点でした。

LoadBalancerタイプのServiceに対してDNSレコードを作成するにはexternal-dns.alpha.kubernetes.io/hostnameアノテーションを設定します。詳しくはこちらのドキュメントに記載されています。

設定例を以下に示します。このマニフェストを適用するとNLBのエンドポイントをターゲットとするapi.example.comのエイリアスレコードが作成されます。

apiVersion: v1
kind: Service
metadata:
  name: envoy
  annotations:
    # 説明用のドメイン
    external-dns.alpha.kubernetes.io/hostname: api.example.com
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    service.beta.kubernetes.io/aws-load-balancer-internal: "false"
spec:
  type: LoadBalancer
  # ...

続いて、この状態でIngressリソースを作成した場合、既存のエイリアスレコードに変更が発生するのかを確認していきます。こちらのドキュメントを見ると、「Ingressのspec.rules[].hostを設定するとDNSレコードが作成される」と書いてあります。

設定例を以下に示します。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: zozomat-ingress-grpc
  namespace: default
  annotations:
    kubernetes.io/ingress.class: alb
    # あらかじめACMで発行したTLS証明書のARN
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:<region>:<account-id>:certificate/xxxxxx
    # ...
spec:
  rules:
  # Serviceのアノテーションで設定したドメインと同じ
  - host: api.example.com
    http:
      paths:
      - path: /foo.BarService/*
        backend:
          serviceName: envoy-service
          servicePort: 80
      # ...

これを適用したところ、既存のエイリアスレコードに変更はありませんでした。

内部の挙動を理解しておきたかったので、ExternalDNSのログを見ました。以下に示すように、DNSレコードに反映する候補となるIngressリソースを検知はしているようですが、変更は行われなかったと見て取れます。これに関連する処理のテストコードを見てみると、DNSレコードに反映する候補がいくつかある場合は、既存のDNSレコードと同じ値が含まれていれば変更しない挙動になっています。このことから、Serviceリソースのアノテーションで設定したドメインをhostとするIngressリソースを追加してもDNSレコードに影響を及ぼさないことが分かりました。

# ExternalDNSのログから抜粋(ドメイン名やhostedzoneを一部改編)
# Serviceに関連するログ
time="2020-12-02T09:11:59Z" level=debug msg="Endpoints generated from service: default/envoy: [api.example.com 0 IN CNAME  xxx.elb.ap-northeast-1.amazonaws.com []]"
# ここからIngressに関連するログ
time="2020-12-02T09:11:59Z" level=debug msg="Endpoints generated from ingress: default/zozomat-ingress-http1: [api.example.com 0 IN CNAME  yyy.ap-northeast-1.elb.amazonaws.com []]"
time="2020-12-02T09:11:59Z" level=debug msg="Endpoints generated from ingress: default/zozomat-ingress-grpc: [api.example.com 0 IN CNAME  yyy.ap-northeast-1.elb.amazonaws.com []]"
time="2020-12-02T09:11:59Z" level=debug msg="Removing duplicate endpoint api.example.com 0 IN CNAME  yyy.ap-northeast-1.elb.amazonaws.com []"
# 特定のhostedzoneについては全レコードが最新の状態であるというログ
# つまりapi.example.comのAレコード(エイリアスレコード)はNLBのエンドポイントに設定されたまま
time="2020-12-02T09:11:59Z" level=debug msg="Considering zone: /hostedzone/ZZZZZZ (domain: example.com.)"
time="2020-12-02T09:11:59Z" level=info msg="All records are already up to date"

さらに、この状態からNLBと紐付くLoadBalancerタイプのServiceのアノテーションを削除するとどうなるのかを検証しました。その結果、エイリアスレコードのターゲットがNLBのエンドポイントからALBのものに切り替わりました。

作業時のログを以下に示します。DNSレコードに反映する候補としてServiceリソースは検知されなくなり、これによってIngressリソースの値が同じドメインのエイリアスレコードに反映されています。

以上の調査・検証で切り替え方が分かったので、最後にある程度の負荷を掛けながら今までの作業を試しました。特にリクエストが失敗することなくDNSレコードの値を切り替えることができました。

# ExternalDNSのログから抜粋(ドメイン名やhostedzoneを一部改編)
# Serviceに関しては検知されなくなった
time="2020-12-02T09:18:02Z" level=debug msg="No endpoints could be generated from service default/envoy"
# ここからIngressに関連するログ
time="2020-12-02T09:18:02Z" level=debug msg="Endpoints generated from ingress: default/zozomat-ingress-http1: [api.example.com 0 IN CNAME  yyy.ap-northeast-1.elb.amazonaws.com []]"
time="2020-12-02T09:18:02Z" level=debug msg="Endpoints generated from ingress: default/zozomat-ingress-grpc: [api.example.com 0 IN CNAME  yyy.ap-northeast-1.elb.amazonaws.com []]"
time="2020-12-02T09:18:02Z" level=debug msg="Removing duplicate endpoint api.example.com 0 IN CNAME  yyy.ap-northeast-1.elb.amazonaws.com []"
# 先ほどとは違いレコードの変更が行われたログが記録されている
time="2020-12-02T09:18:03Z" level=debug msg="Considering zone: /hostedzone/ZZZZZZ (domain: example.com.)"
time="2020-12-02T09:18:03Z" level=debug msg="Adding api.example.com. to zone example.com. [Id: /hostedzone/ZZZZZZ]"
time="2020-12-02T09:18:03Z" level=debug msg="Adding api.example.com. to zone example.com. [Id: /hostedzone/ZZZZZZ]"
time="2020-12-02T09:18:03Z" level=info msg="Desired change: UPSERT api.example.com A [Id: /hostedzone/ZZZZZZ]"
time="2020-12-02T09:18:03Z" level=info msg="Desired change: UPSERT api.example.com TXT [Id: /hostedzone/ZZZZZZ]"
time="2020-12-02T09:18:03Z" level=info msg="2 record(s) in zone example.com. [Id: /hostedzone/ZZZZZZ] were successfully updated"

ここまでの一連の流れを図示したものを以下に示します。 DNSレコード変更の流れ

さいごに

本記事で紹介した事前の準備や調査・検証により、本番環境での移行作業を滞りなく、かつシステムを停止せずに行うことができました。ALBに切り替えた後はTLS証明書の更新作業から開放され、新しい環境が必要になった際もACMで証明書を発行できるので、手軽に環境を構築できるようになりました。

これは個人的な副産物なのですが、一連の調査でAWS Load Balancer ControllerやExternalDNSのドキュメントおよびソースコードを読む機会がありました。少しずつ理解をしていくうちに、どちらのコンポーネントもあるリソースの状態をチェックして別のリソースをあるべき状態にするパターンなんだなと分かりました。さらに調べるとKubernetesのカスタムコントローラーという概念であることを知りました。作業に入る前はExternalDNSというものが「なんか良い感じにDNSレコードの制御をやってくれている」くらいの認識でしたが、その中身や周辺知識を知る良い機会になりました。

最後に、計測プラットフォーム本部バックエンド部では、サーバーエンジニア、SREエンジニア、それぞれでファッションにおける計測にまつわる課題解決を共に進めてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

www.wantedly.com www.wantedly.com

カテゴリー