Memorystore for Redis移行のためのRedis HAProxy

Memorystore for Redis移行のためのRedis HAProxy

はじめに

SREグループ・ヒロチカです。 GO株式会社では、サービスのクラウドインフラの設計から構築・運用までを担当しています。 あるKubernetesのサービスを別のGCPプロジェクトにあるKubernetes環境に移す計画の中で、そのサービスが利用してるGCPプロジェクトのMemorystore for Redisについても別のGCPプロジェクトに移す必要が出てきました。 Memorystore for Redisは内部ネットワークからのみアクセスが可能なサービスのため、移行元のサービスがあるGCPプロジェクトの内部ネットワークから移行先の内部ネットワークにアクセスできるようにRedis用のHAProxyサービスを構築しました。 この記事では、そんな冗長化等で利用するのとはまた違ったRedis用HAProxyの構成・構築内容について紹介できればといます。

経緯

はじめにの項でも記載した通り、ある稼働中のKubernetesのサービスを別のGCPプロジェクトにあるKubernetes環境に移す計画の中で、そのサービスが利用してるGoogle CloudのマネージドなキャッシュサーバであるMemorystore for Redisについても移行が必要となりました。 このMemorystore for RedisはパブリックIPを持てずプライベートIPをエンドポイントにした通信を行う仕様のため、内部ネットワーク経由でのみのアクセスが可能なマネージドサービスです。 また今回、サービスが利用してるMemorystore for Redisは他の複数の重要なアプリケーションから参照されているため、移行作業中・移行期間中についても参照側アプリケーションが常に最新の状態を取得できることも考慮する必要がありました。

この条件の中で可能な限りサービスを止めずに移行する方法を考えるとき、Memorystore for Redisが内部ネットワークからしかアクセスできないにも関わらずGCPプロジェクトまたぎで別のネットワークにあるMemorystore for Redisと同じ最新データが存在する状態を作るという部分に、何かいいアイデアを出さなければなりませんでした。

まず、Google Cloudの提供する共有VPCサービスを利用しVPCを共有する形で内部ネットワーク同士のアクセスを可能にする案を検討しましたが、他複数のサービスも動いているVPC同士であったため一部IPレンジが被っている箇所があったのと、プライベートアクセスのために既存のサービスが動いている内部ネットワークの再構築が必要な箇所があったため選択肢から除外しました。

次にGoogle Cloudの提供するVPCピアリングを利用し内部ネットワーク同士を繋ぎアクセス可能にする案を検討したところ、実はMemorystore for Redisサービスのマネージド化された通信環境部分にてVPCピアリングを利用しているという罠があり、VPCピアリング経由越しの内部ネットワークのアクセスが意図せずVPCピアリングの推移的ルーティングの制約に引っかかり通信できない事がわかりました。 つまりVPCピアリングの経路が複数のVPCピアリングを越える通信とみなされてしまう部分が問題ということです。

そこでVPCピアリングは張った状態で移行元のネットワークから1つ目のVPCをまたいだところにプロキシを噛ませ、そのプロキシから再度コネクションを張る形でRedisアクセスを行うことで通信としては多重にVPCピアリングを越えることなくアクセスできると考え、今回のHAProxyを使った構成が採用となりました。

また新旧Redis Clusterで同じ最新データの状態にする条件については、Memorystore for Redisがマネージドなサービスであるため異なるクラスター同士で簡単にレプリケーション構成を作る事が難しい点を踏まえ、アプリケーション側が新旧Redisに対しダブルライトを行うことで同一のデータとなる状態を作るようにしています。

注) HAProxy ... TCP/HTTPベースのアプリケーション向けプロキシ・ロードバランサーOSS

移行は移行準備段階〜移行完了まで以下の4フェーズを経ての作業となります。以下が、構成と移行の流れを示した図です。

移行準備段階

旧アプリケーションがダブルライトを実施。従来通りのRedisへとHAProxyを経由した新Redisへの両方に書き込みを行う。

参照側移行段階

各参照側アプリケーションが順次、新アプリケーションへと接続先を変更。

書き込み側移行段階

クライアントが新アプリケーションへと書き込むように変更。

移行完了段階

構築内容

前項に示した移行準備段階の構成図に基づき、HAProxy関連の設定を行います。

Memorystore for Redisについては特殊な設定等は行っていませんので、公式ドキュメントなどをもとに構築しました。 なお、弊社の構築ルールに則りMemorystore for RedisはAUTHパスワード認証付きで構築しています。後述しますが、AUTHパスワードの要否によってHAProxyのヘルスチェックオプションが一部異なる点にご留意ください。

VPCピアリングについても同様に公式ドキュメントなどを参考に対象のネットワーク同士が内部ネットワークで通信できるよう設定しました。

ダブルライトについても特別な実装なくアプリケーション側に機能を追加しました。

HAProxyの前段となるロードバランサーはレイヤ4のネットワークロードバランサーを採用し、RedisのTCPベースで送られてくる通信を単純にプロキシするようにだけ設定しています。このロードバランサーがリクエストの受け口となります。 ロードバランサーのIPが変わると通信ができなくなってしまうため、ロードバランサーには内部のStaticIPを設定しておきます。今回、弊社の環境ではドメインも設定しました。

ネームスペースなどはサービスに合わせて調整します。

apiVersion: v1
kind: Service
metadata:
  name: my-l4lb-proxy
  namespace: my-namespace
  annotations:
    networking.gke.io/load-balancer-type: Internal
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  loadBalancerIP: xxx.xxx.xxx.xxx
  ports:
    - port: 6379
      targetPort: 6379
      protocol: TCP
  selector:
    app: my-app

下記が、ネットワークロードバランサーが受けた通信を流す先のHAProxyの実体です。

対象のKubernetesサービスと同じネームスペースの中のpodとして起動させており、利用するimageはHAProxyの出している公式imageから取得しています。

負荷が低いためサービスで受けるトラフィックにはpod1台で充分ですが、単一障害点を作らないよう弊社では2台に冗長化させて運用します。 HAProxyのconfigについてはConfigMapにて設定します。

Deployment例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: my-namespace
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: haproxy
        image: haproxy:3.0.2-alpine  # 使用するhaproxyのイメージ
        ports:
        - containerPort: 6379        # haproxyがリッスンするポート
        volumeMounts:
        - mountPath: /usr/local/etc/haproxy
          name: haproxy-config
        env:
          - name: AUTH_PASSWORD
            value: "my-auth-password"          # ここに実際の認証パスワードを設定
          - name: REDIS_PRIMARY_ENDPOINT
            value: "redis-server.example.com"  # ここに実際のRedisエンドポイントを設定
      volumes:
      - configMap:
          name: my-app-haproxy-config
        name: haproxy-config

ConfigMap例

apiVersion: v1
kind: ConfigMap
metadata:
  name: haproxy-config
  namespace: my-namespace
data:
  haproxy.cfg: |
      global
        log stdout format raw local0
      defaults
        mode tcp
        option tcplog
        log global
        retries 3
        timeout connect 10s
        timeout client 1m
        timeout server 1m
        timeout check 10s
        timeout queue 5m
        maxconn 1000
      frontend frontend_redis
        bind *:6379
        default_backend backend_redis 
      backend backend_redis
        option tcp-check
        tcp-check connect
        tcp-check send AUTH\ "$AUTH_PASSWORD"\r\n
        tcp-check send PING\r\n
        tcp-check expect string +PONG
        server redis_server "$REDIS_PRIMARY_ENDPOINT" check

HAProxy全体の動作を指定するのがglobalセクション、パラメータのデフォルト値を指定するのがdefaultsセクション、通信の待ち受け時の挙動の指定をするのがfrontendセクション、プロキシ先の設定をするのがbackendセクションになります。

この設定では、HAProxyに届いた6379ポートの全ての通信をbackendセクションに指定したredis_serverに送ります。

backendの中にあるoption部分でセクション自体が行う機能を定義してパラメータやコマンドを設定するのですが、今回はtcp-checkオプションというTCP通信のヘルスチェックのオプションを使っています。 optionの中にはredis-checkというredis用のヘルスチェックオプションも存在するのですが、AUTHパスワード付きの通信に対応していないため今回はやむなくtcp-checkオプションでRedisのパスワードを突破する形のヘルスチェックを設定しています。

その他、HAProxyのconfigの詳細は公式ドキュメントのチュートリアルなどを参照のうえ、利用するRedisの仕様に合わせコネクション数やタイムアウト時間を設定いただければと思います。

ここまで全て正しく設定できれば、ネットワークロードバランサーに設定したドメイン(もしくはStaticIP)とポートに対して、Redisのリクエストが可能となります。

おわりに

今回、異なるプロジェクトからのRedis通信を可能にする構成のHAProxy設定をご紹介しました。 レイテンシーについては正確には測っていませんがサービス上全く問題になるレベルではなく、負荷についてもかなり軽量でした。 この辺りがよりシビアなサービスであればあらかじめ負荷試験等で確認されておくのが良いかと思われます。

また今回のHAProxyのサービスは弊社としてはサービスの移行期間中の数ヶ月間のみ一時利用するためのサービスとして設定しています。 恒久的な利用を想定していないため運用面の考慮などは熟慮していない部分も多いです。本記事の設定については、あくまで自己責任にてお願いいたします。

この記事がどなたかのお役に立てば幸いです。