Istio で使っていた SSL 証明書システムを変更しました

はじめに

Knile(発音は “ナイル")という、リクルートのデータ利活用基盤のチームでアプリケーション開発、SRE をやっている多田です。

Knile とは、以前 CET と呼ばれていたチームが開発するデータ利活用基盤です。

以下の過去記事が関連するシステムになります。

Knile はかつて「株式会社リクルートライフスタイル」所属のプロダクトでしたが、2021年4月の組織改編 により「全社横断」としての役割も期待されており、活用シーンの多様化と要求されるシステム性能のバランスにうれしい悲鳴をあげています。

この記事では、Knile のコンポーネントの1つである「Knile API」の SSL 証明書を変更した経緯やその際に検討・考慮したことを共有します。

目次

Knile API とは?

Knile API は、リクルートの旅行事業・飲食事業・ビューティ事業といった様々なサービスでデータ活用施策に利用される社内 PaaS / FaaS のような API プラットフォームです。

呼び出し元は、各サービスのバックエンドサーバなこともあれば、サービスの利用ユーザーのブラウザやアプリから直接呼ばれることもあります。

Knile api 1

Knile API には 5 つのサブコンポーネントがあり、GKE 上の Istio によるサービスメッシュにより連携されています。

Knile api 2

図内の Istio ロゴは 以下のページより引用: https://istio.io/

これらは基盤として社内に開放されており、アプリケーション開発のスキルに自信がないデータサイエンティストでも開発や運用の負担を最小限に API を本番反映できることを目指しています。

Istio と cert-manager と Let’s Encrypt

Knile API の GKE クラスターを構築し始めたのは 2 年前の 2019 年夏でした。

その頃はどこまで社内で利用されるか、どれくらい重要な施策が基盤上で稼働するか不透明であったことから運用負担の削減を最重要とし、 API のための SSL 証明書に Let’s Encrypt を採用しました。

この Let’s Encrypt 証明書は、クラスターで動作する cert-manager により、一定間隔で更新するように設定されていました。

Istio Gateway は直接インターネットに面しており、グローバル IP を持っています。

cert-manager が停止しても直ちに証明書が壊れるということはないので比較的気分は楽ですが、万が一壊れた際に気付きにくいので証明書の有効期限チェックに Datadog の SSL チェック機能 を利用しました。

Datadog ssl

このような運用を 1 年以上続けていましたが、 2020 年 4 月ごろ「9 月に Let’s Encrypt がルート証明書を変更する」というニュース が飛び込んで来て騒然となりました。

Letsencrypt root cert will change

雲行きの怪しい Let’s Encrypt と決断

上述の通り、 Knile API は社内のサーバ/一般ユーザー問わずリクエストされるシステムであるため、ルート証明書が変更されると影響範囲が広大になることが予想されていました。

例えば、社内サーバでは古くから隔離されたオンプレで稼働している物がいくつかあり、TLS 接続において新ルート証明書 (ISRG 発行) が信頼されるか不透明でした。

また一般ユーザーからのリクエストにおいて基盤の仕様として「Android ver.x 以上であること」という要件を定義していなかったため、古いブラウザからの TLS 接続がそもそも失敗するというリスクを孕んでいました。

そこで、以下の論点について複数の選択肢を検討することにしました。

プラン 検討結果
社内利用において、新ルート証明書を信頼するように設定変更をお願いする 基盤都合で利用システムに証明書インストールしてもらうのは多大な工数がかかるため最終手段
Android などの古い端末からの接続をサポートしないよう仕様を変更する 基盤都合でサポート端末を狭めることはシステムの信頼性にも繋がるため難易度が高い
Let’s Encrypt の新ルート証明書変更が起こらない方法を探る Let’s Encrypt の方針が ISRG 証明書を普及させる事にあるので、長期的にみるとその場凌ぎにしかならない

複数検討しましたがどれも打ち手に欠け、実施する際のハードルが非常に高い物でした。

また、対応策を検討している間にも次々と Let’s Encrypt 側から移行スケジュール変更のお知らせが届きました。

個人的には「DST ルート証明書」(これは以前の証明書のことで、多くの端末に信頼されています)のハックを利用した対応には疑問があり、これを本番環境で検証することが難しいことがわかっていました。

まず、 SSL 証明書はカナリアリリースをできません。なぜなら、TCP/TLS 接続は接続元でプールされている可能性があるので「1%適用」という設定を行っても、ずっと失敗し続けるということがあり得るからです。

また、開発環境で新しいルート証明書に強制的に更新することで疎通テストをすると言う方法もありますが、新しいルート証明書への変更タイミングは Let’s Encrypt の対応を待つ他なく、更新した後に万が一疎通に失敗すると元に戻すことができないと言う問題がありました。

さらに、「DST ルート証明書のハック」を確実にテストするためには、2021 年 9 月 29 日を待つ必要があり、コンチプランを設計することもできません。

そして何より「DST ルート証明書のハック」が本番環境でどのような挙動をするのか把握するコストを見積もることができません。

詳しい解説は以下の記事に譲りますが、期限切れのルート証明書という不安定な存在に業務システムが頼るのは SRE としても看過できませんでした。

一方 Let’s Encript は相次ぐ方針転換により予め定められていたルート証明書の変更計画を何度も変更していて、このままでは安定的な運用を行う上でリスクが大きくなるのではないかと思われました。

2021 年 3 月頃、コンチプランを含めた複数の移行計画をそろそろまとめないといけないという事になり、 Let’s Encrypt を継続するリスクに対して得られるメリットがそぐわないと判断し、 Let’s Encrypt の利用を停止する判断をしました。

代替案の検討

cert-manager + Let’s Encrypt を利用しない場合、どこの証明書を利用するか、どこに設置するかという自由度が生まれます。

リクルートの場合自前で証明書を購入する必要はなく、会社で利用される証明書を一括で代理購入・サポートしてくれるチームがいます。この証明書は社内の多くのシステムで利用されているので、社内のサーバ/一般ユーザーから信頼されないという問題はクリアされます。

また、証明書を設置する箇所としてはクラウドプロバイダー、この場合は Cloud Load Balancing (以下、GCLB) を利用するという案もあります。この場合、Google の発行する証明書を利用するマネージド証明書か、自前で用意した証明書を利用するセルフマネージド証明書が選べます。

結果取り得る選択肢としては、以下のようなパターンがあります。

Istio + 自前証明書 GCLB + マネージド証明書 GCLB + セルフマネージド証明書
証明書更新の運用負担 手動 自動 手動
ワイルドカードドメイン 可能 不可 可能
TLS 終端箇所 Istio Gateway GCLB GCLB

Knile API では、API 毎に異なるサブドメインを設定するのでワイルドカードドメインが必要です。そのため、 GCLB + マネージド証明書は利用できません。

となると、証明書は自前で用意する事は確定となります。

あとは証明書を設置する場所になりますが、 Let’s Encrypt 利用時と同様に Istio Gateway に置くか、 GCLB にアップロードするか選ぶ必要があります。

TLS 終端処理はそれなりに負荷が高くできるだけ管理したくありません。また GCLB を通す事でリクエストのログを集約でき問題発生のヒントに利用することもできます。

これらの理由から、最終的に「GCLB + セルフマネージド証明書」を選択しました。

NEG と Kubernetes

ここで、Network Endpoint Group (NEG) という仕組みを紹介します。

詳細はリンク先の記事をご覧いただくとして簡単に説明すると、NEG とは Kubernetes の Service のように複数の Pod をグループ化し GCP 側から直接扱えるようにする仕組みになります。

この NEG を活用すると、GCLB のバックエンドとして GCE VM ではなく GKE Pod を直接指定できるようになりパフォーマンスが向上します。 これをコンテナネイティブの負荷分散と言います。

Blue green 1

しかも、GKE では Kubernetes annotation を設定することで NEG を構成できる方法が提供されており、Configuration as Data としてインフラを構成できます。

具体的な設定方法は以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  namespace: istio-system
spec:
  components:
    ingressGateways:
    - name: old-istio-ingressgateway # 古い Istio Ingress Gateway
      enabled: true
      k8s:
        overlays:
        - kind: Service
          name: old-istio-ingressgateway
          patches:
          - path: spec.loadBalancerIP
            value: ${OLD_IP_ADDRESS:?}
    - name: new-istio-ingressgateway # 新しい Istio Ingress Gateway
      enabled: true
      label:
        app: new-istio-ingressgateway
        istio: new-ingressgateway
      k8s:
        service:
          type: ClusterIP
          ports:
          - name: status-port
            port: 15021 # Istio ヘルスチェックポート [a]
            targetPort: 15021
          - name: http2
            port: 80 # Service 公開ポート [b]
            targetPort: 8080
        serviceAnnotations:
          cloud.google.com/neg: '{"ingress": true}' # Service を NEG として構成します
          cloud.google.com/backend-config: '{"default":"new-istio-ingressgateway"}' # BackendConfig を参照します
          cloud.google.com/app-protocols: '{"http2":"HTTP"}'
        overlays: # Istio Operator が作るマニフェストの上書き
        - apiVersion: apps/v1
          kind: Deployment
          name: new-istio-ingressgateway
          patches:
          - path: spec.template.spec.readinessGates
            value:
            - conditionType: cloud.google.com/load-balancer-neg-ready # GCLB からのヘルスチェック条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  namespace: istio-system
  name: new-istio-ingressgateway
spec:
  healthCheck:
    checkIntervalSec: 5
    timeoutSec: 1
    healthyThreshold: 1
    unhealthyThreshold: 2
    type: HTTP
    requestPath: /healthz/ready
    port: 15021 # [a] と同じ
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  namespace: istio-system
  name: new-istio-ingressgateway
  annotations:
    kubernetes.io/ingress.allow-http: "false" # HTTP (80) での接続を許可しない。HTTPS にリダイレクトしたい場合は記載せず別途設定すること
    kubernetes.io/ingress.global-static-ip-name: "${NEW_IP_ADDRESS_NAME:?}" # 事前設定されたグローバル IP 名
    ingress.gcp.kubernetes.io/pre-shared-cert: "${NEW_CERTIFICATE_NAME:?}" # 事前設定された証明書名
spec:
  backend:
    serviceName: new-istio-ingressgateway
    servicePort: 80 # [b] と同じ

この設定により古い Istio Ingress Gateway を維持しつつ、新しい Istio Ingress Gateway を NEG として構成でき、 GCLB のバックエンドとして直接 Pod にアクセスできるようになります。

Blue green 2

ここまで設定できれば、あとは DNS の切り替えを徐々に行うことで Let’s Encrypt から GCLB への更新をカナリアリリースで安全かつより良い構成において行うことができます。

まとめ

Istio + cert-manager + Let’s Encrypt という、当初はリリース最優先で運用負担が少ない構成を選択しましたが、システムが稼働し始めたことで様々なシステム要件や社内事情から Let’s Encrypt の利用を停止する必要がありました。

複数の選択肢の中から最も要求に合い現実的な手段として GCLB + セルフマネージド証明書という構成を選択し、ゼロダウンタイム更新ができる手法を模索しました。

また、GKE で NEG が構成できるという記事を見た時から Configuration as Data と GitOps の可能性に未来を感じ、いつかは採用したいと調査を続けてきました。

最終的に 2021 年初夏、 Let’s Encrypt のルート証明書更新という影響範囲の大きい問題を無事回避し、以前よりもモダンで効率的な構成にマイグレーションが完了しました。

一緒に働きませんか?

弊社では、事業を加速させるデータプロダクトの開発を始めとする様々な職種のエンジニアを募集しています。 興味のある方は以下の採用ページからご応募ください。

一緒に、モダンでチャレンジングなデータ基盤を創っていきましょう!