CLINICS の認証サーバ刷新で「何を解決しているのか」を問い直した話

こちらの記事は「MEDLEY Summer Tech Blog Relay」の6日目の記事です。

はじめに

医療プラットフォーム本部 プラットフォーム開発室 SRE グループの山田です。医療機関向け SaaS である CLINICS の安定稼働とシステム信頼性の向上に取り組んでいます。

本記事では、CLINICS が長きにわたって使用してきたクライアント認証サーバを OpenResty から Nginx へ移行するに至った経緯について紹介します。 特に実際に直面したビジネス課題や技術選定にフォーカスを当ててお話しします。

なお、本記事の主眼は「OpenResty vs Nginx」のツール比較ではありません。比較検討を進めるなかで「そもそも電子署名が要らないのではないか」という気づきに至り、結果として認証サーバ単体ではなくアーキテクチャ全体を見直すことになりました。本記事ではその評価軸と判断の過程を共有します。

  • 想定読者: 既存システムの技術選定・刷新に取り組むエンジニア
  • 検証環境: AWS (ALB / NLB / NAT Gateway / VPC) を前提
  • 執筆時点: 2026年3月。OpenResty / Nginx のバージョン記述は当時の情報

本記事のポイント

本記事は CLINICS のクライアント認証サーバを OpenResty から Nginx へ移行した事例ですが、話の中心はツール置き換えそのものではなく、「そもそも認証サーバは何を解決しているのか」を問い直した過程にあります。

  • 入口の問い: OpenResty の後継として何を選ぶか
  • 見直した問い: 認証サーバに残すべき役割は本当に何か
  • 辿り着いた答え: 経路をネットワーク層で分離すれば、アプリ層の電子署名は不要となり、認証サーバの役割は mTLS 検証のみに縮む
  • 技術選定の決め手: 「新規ユーザーがクライアント証明書なしでアクセスできる導線が必要」という CLINICS 固有の仕様が、ALB mTLS ではなく Nginx 単体を選ぶ根拠となった

ここに至れた理由をひとことで言えば、「OpenResty の代替を探す」ことをやめて、アーキテクチャの前提そのものを問い直したためです。ツール比較は入口にすぎず、本質は技術スタックの外側 — アーキテクチャの前提とサービスの使われ方 — にありました。

前提: なぜクライアント認証 (mTLS) が必要なのか

本題に入る前に、CLINICS のような医療プロダクトにおいて、なぜクライアント認証 (mTLS) を行う認証サーバが必要なのかを整理します。

CLINICS は医療機関で利用される情報システムであり、厚生労働省「医療情報システムの安全管理に関するガイドライン 第6.0版」(2023年5月) の対象となります。同ガイドラインの システム運用編 [Control] 13. ネットワークに関する安全管理措置 では、オープンなネットワークを介した通信について次のように明記されています (厚生労働省 ガイドライン公式ページ)。

⑥ オープンなネットワークにおいて、IPsec による VPN 接続等を利用せず HTTPS を利用する場合、TLS のプロトコルバージョンを TLS1.3 以上に限定した上で、クライアント証明書を利用した TLS クライアント認証を実施すること。ただしシステム・サービス等の対応が困難な場合には TLS1.2 の設定によることも可能とする。その際、TLS の設定はサーバ/クライアントともに「TLS 暗号設定ガイドライン 3.0.1 版」に規定される最も安全性水準の高い「高セキュリティ型」に準じた適切な設定を行うこと。

— 医療情報システムの安全管理に関するガイドライン 第6.0版 システム運用編 [Control]

つまり、CLINICS がオープンなネットワーク経由でクライアントと通信する以上、TLS 1.3 (または高セキュリティ型設定の TLS 1.2) + mTLS はガイドライン上の要請になります。IPA「TLS 暗号設定ガイドライン」の高セキュリティ型については、公式ページ (IPA 公式ページ) を参照してください (執筆時点の最新版は第3.1.1版 (2025年4月25日公開)。上記引用文中の「3.0.1 版」は医療情報ガイドライン第6.0版本文の記載に基づきます)。

CLINICS にクライアント認証サーバが存在する根本理由はここにあります。「ベストプラクティスとして導入している」のではなく、医療情報を扱うサービスとして満たすべき要件として、ネットワーク経路上に mTLS 検証を担うコンポーネントが配置されています。

この「mTLS をどこで終端し、誰が検証するか」が、本記事で扱う設計判断の出発点となります。

旧構成と OpenResty 採用の背景

CLINICS のクライアント認証サーバは、約4年にわたって OpenResty で運用されてきました。サービスがスケールアップしていく時期に設計され、その後も大きな改修を加えずに動き続けてきたものです。

なぜ当時 OpenResty が選ばれたのか、要件と設計判断の順に追ってみます。

要件: mTLS によるクライアント認証

前項で述べたとおり、CLINICS がオープンなネットワーク経由でクライアントと通信する以上、医療情報ガイドラインの要請として mTLS の実装が必要です。クライアント認証サーバは、その mTLS 検証を担うコンポーネントとして配置されています。

設計判断: Web サーバの共通化

一方、CLINICS の Web サーバは、性質の異なる複数経路からのリクエストを単一で捌く構成になっていました。ここでいう「複数経路」とは、次の3種類のユーザーからのリクエストです。

  • 社内オペレーター: サポートデスクなど、社内のオペレーション業務を担うユーザー
  • melmo ユーザー: 患者向けアプリ melmo の利用者
  • 医師 / 医療事務 (本記事では「カルテ利用ユーザー」とも表記): CLINICS カルテをご利用いただいている医療機関のユーザー

追加要件: リクエスト経路の識別

Web サーバを共通化した結果、受け取ったリクエストが「どの経路を辿ってきたか」を判定する必要が生まれました。

設計判断: 電子署名によるリクエスト経路の識別

CLINICS ではこの「経路識別」を、クライアント認証サーバが付与する電子署名で実現していました。

旧構成

つまり、認証サーバは「mTLS の実装」と「リクエスト経路の識別」の2つの要件を同時に満たす必要がありました。特に経路識別を電子署名で実現するには、リクエストの各種情報を Lua スクリプトから Nginx の変数経由で取り出す必要があります。両方を1つの技術スタックで実現できる選択肢として OpenResty を採用していました。当時の要件に対しては、合理的な選択だったと言えます。

ただし、システムは要件と一緒に古びるものです。約4年の間に、サービスのスケール、ユーザー属性の多様化、AWS 側で利用できる機能の進化があり、当初の前提が少しずつズレてきていました。

旧構成で見えてきた2つの課題

長年運用するうちに、構成上の課題が顕在化してきました。

1. NAT Gateway 依存による経路構成の課題

Web サーバの ALB が Internet-facing スキームのため、ドメイン解決ではパブリック IP が返されます。OpenResty から proxy_pass で ALB へリクエストを送る経路は、結果として VPC 外を経由するルーティングとなり、NAT Gateway を必ず通る構成になっていました。

加えて、OpenResty の Docker Hub 上で公開されている公式イメージ (openresty/openresty) は執筆時点で 1.29.2.3 (Nginx 1.29.2 ベース) までが公開されており、Nginx 本家で upstream への keep-alive がデフォルト化された 1.29.7 の変更 (NGINX Community Blog) を取り込むには、自前で Docker イメージをビルドする必要がありました。proxy_pass の度に新規 TCP セッションが張られるオーバーヘッドも、サービスを提供する上でのパフォーマンス課題の一つでした。

NAT Gateway で障害

さらに問題なのは、この経路上にいる NAT Gateway が単一障害点として機能してしまうことです。検討当時、AWS の NAT Gateway は AZ 単位 (Zonal) 動作のみで、CLINICS の構成では認証サーバから Web App への経路が単一の NAT Gateway を経由していました (執筆時点では複数 AZ へ自動展開する Regional NAT Gateway も選択肢に加わっています)。これが落ちると、次の2方向が同時に止まります。

  • インバウンド方向: 認証サーバ → Web App へのリクエスト経路が遮断され、カルテ利用ユーザーのリクエスト受付が停止する
  • アウトバウンド方向: Web App → 外部 SaaS への連携が不能になる

本来であれば、この経路を NAT Gateway のような単一障害点に依存させることは避けたいところでした。

2. 障害発生時の影響範囲の広さ

Web サーバで障害

社内オペレーター / melmo ユーザー / カルテ利用ユーザーが同じ Web サーバを共有しているため、Web サーバで障害が起きると、性質の異なる全ユーザーに同時に影響が波及する構造になっていました。サービスの成長に対して、リスクが線形以上に膨らんでいた状態とも言えます。

解決の方向性 — 経路の分離と VPC 内閉域化

2つの課題を、打ち手に対応させると次のようになります。

  • NAT Gateway 依存による経路構成の課題 → ALB を Private Subnet に配置し、認証サーバから Web App までの通信を VPC 内に閉じる
  • 障害発生時の影響範囲の広さ → カルテ利用ユーザーの経路を、社内オペレーター・melmo ユーザーの経路から分離する

つまり今回の刷新では、「カルテ利用ユーザーの経路を分離し、VPC 内で完結させる」というアーキテクチャの方針が先に決まります。認証サーバをどう作り直すかは、この方針のあとに続く問いです。

分離の帰結 — 電子署名の存在理由が消える

方針が決まったところで、OpenResty が担ってきた役割を分解し直します。

  1. mTLS によるクライアント認証 — 医療情報ガイドラインの要請
  2. 電子署名による経路識別 — 単一の Web サーバが全経路を捌くための仕組み

1 は外せない。では 2 はどうか。

電子署名で経路を識別しなければならなかったのは、「社内オペレーター / melmo ユーザー / カルテ利用ユーザーのすべての経路を、単一の Web サーバが捌いている」からです。経路がネットワーク的に区別できないからこそ、アプリ層の電子署名で区別していました。経路をネットワーク層で分けるなら、電子署名そのものが要らなくなります。

これは何かを工夫して削った結果というより、分離という方針の論理的な帰結です。ただし、この帰結が持つ意味は大きく、認証サーバに残る役割は mTLS によるクライアント認証ただ1つになります。「OpenResty の後継に何が必要か」という問いは、この時点で「mTLS だけなら、何で実装するか」という、ずっと小さな問いに変わっています。

検討した選択肢 — mTLS だけなら、何で実装するか

認証サーバに残る役割が mTLS のみに絞れたことで、選択肢は次の2つになります。

  • 選択肢 A: AWS ALB の mTLS 機能 — クライアント認証をマネージド機能で代替し、認証サーバそのものを廃止する
  • 選択肢 B: Nginx 単体 — 認証サーバは残し、Lua スクリプト (OpenResty) を抜いて標準機能のみで再構成する

まず魅力的に見えたのは選択肢 A でした。mTLS しか担わないコンポーネントのために、サーバを自前で運用し続ける理由はないかもしれません。運用負荷を下げ、構成も単純にできると考えていました。

選択肢 A: AWS ALB の mTLS 機能

AWS は ALB に mTLS 機能を提供しています (ALB Mutual TLS 公式ドキュメント)。ALB の mTLS には verify モード と passthrough モード の2つがあり、それぞれ動作と責務範囲が大きく異なります。

A-1. verify モード

ALB がクライアント証明書を直接検証するモードです。クライアントが提示した X.509 証明書を、ALB に紐付けた Trust Store の CA 証明書および CRL (失効リスト) と照合し、検証を TLS ハンドシェイク内で完結させます。検証結果は X-Amzn-Mtls-Clientcert-Serial-NumberX-Amzn-Mtls-Clientcert-IssuerX-Amzn-Mtls-Clientcert-SubjectX-Amzn-Mtls-Clientcert-Validity などのヘッダでバックエンドに渡されます。

verify モード

  • 検証・失効確認まで ALB に委任できる: Web App 側に証明書処理コードが不要となり、認証関心をネットワーク基盤側に閉じ込められる
  • 検証失敗の負荷がバックエンドに及ばない: ハンドシェイク段階で検証が完結するため、不正な証明書が Web App に到達することがない
  • 証明書を提示しないクライアントは TLS ハンドシェイクが成立しない: これは「不正アクセスを早期に遮断できる」というメリットでもあり、「正当な理由で証明書を持たないクライアント」を一切通せないという制約でもある

A-2. passthrough モード

passthrough モード

  • 検証ロジックをアプリ側で自由にカスタマイズできる: Subject や Issuer に応じた認可、組織固有のポリシー適用などが、アプリのコードベースで完結する
  • 検証ポリシーをコードでバージョン管理しやすい: Trust Store の更新を ALB の管理画面ではなくデプロイパイプラインで扱える
  • 証明書チェーン検証・失効確認のコストはすべて Web App 側に乗る: 検証ロジックの実装責任に加え、性能チューニング (チェーン検証のキャッシュ、CRL/OCSP の取得経路) も Web App で持つ必要がある
  • セッション再開 (Session Resumption) はサポートされない: AWS 公式ドキュメント上、passthrough と verify の両モードで Session Resumption は無効化される旨が明記されている (Mutual authentication with TLS - Before you begin)

選択肢 B: Nginx 単体

電子署名を捨てたあとの認証サーバに必要なのは、TLS の終端、クライアント証明書の検証、バックエンドへの proxy_pass だけです。これらはすべて Nginx の標準機能で完結します。

そもそも OpenResty を採用していた理由は「電子署名のためにリクエスト情報を Lua で柔軟に扱える」ことでした。Lua を使う動機がなくなった以上、本家 Nginx に戻るのが自然な選択肢になります。本家に戻れば、upstream への keep-alive がデフォルト化された 1.29.7 のような、本家のリリースにも直接追従できます。

比較と評価 — ALB mTLS を採用しなかった理由

ALB mTLS は魅力的に見えましたが、CLINICS のアプリケーション仕様と照らし合わせると採用できませんでした。

CLINICS の仕様 — カルテ利用ユーザーでも mTLS なしで通る経路が必要

CLINICS には、カルテ利用ユーザーであっても、新規ユーザーのクライアント証明書発行時はクライアント証明書を持たない状態でサービスにアクセスできる必要があるという仕様があります。

ALB の verify モードを採用すると、証明書を持たないクライアントは TLS 接続自体が確立しないため、ユーザー登録の導線が壊れてしまいます。

この仕様は、QA チームと「現状どんなパターンのアクセスを通しているか」を一緒に棚卸ししたタイミングで整理できたものでした。コードを読んだだけでは見えてこない「使われ方」が、QA チームの実機検証ノウハウから浮き上がってきた形です。「機能要件」ではなく「サービスの使われ方」の文脈で捉え直す必要がありました。

passthrough モードの場合の難点

では passthrough モードはどうか。ALB は証明書チェーンをヘッダで渡すだけなので、実際の mTLS 検証は Web App 側で行うことになります。これは次のような別種のオーバーヘッドを生みます。

  • 認証ロジックを Web App に持ち込むため、認証関心とビジネス関心が混ざる
  • mTLS の検証コスト (証明書チェーンの検証、失効確認) が Web App の応答時間に直接影響する

「認証サーバを廃止して ALB に寄せる」つもりが、結局 Web App 側に複雑性が移るだけの結果となります。

こうして、mTLS の実装は選択肢 B の Nginx に決まりました。方針 (経路の分離と VPC 内閉域化) と技術選定 (Nginx) を合わせた結果が、次の新構成です。

新構成

新構成

各コンポーネントの役割と設計意図

NLB (L4 ロードバランサ)

NLB は TCP のままパススルーする L4 ロードバランサとして残しました。理由は、TLS 終端を Nginx に置きたかったためです。ALB を最前段に置くと L7 で TLS を終端することになり、mTLS 検証の選択肢は ALB の verify/passthrough モードに限定されてしまいます。NLB を前段にして TCP のまま Nginx に届けることで、TLS の終端と mTLS 検証を Nginx 側で完結できます。

Nginx (TLS 終端 + mTLS 検証)

Nginx を TLS 終端の位置に置いた設計意図は次のとおりです。

  • Web App を TLS 終端にしない: Web App 側に TLS 終端と mTLS 検証ロジックを持たせると、認証関心とビジネス関心が混ざる (ALB passthrough モードで指摘した課題と同じ理由)
  • 認証ロジックを「単一の場所」に閉じ込める: 認証は Web App より手前で完結させ、Web App はビジネスロジックに集中させる
  • OpenResty 時代の Lua スクリプトが不要になる: 経路識別 (= 電子署名) を捨てたことで、Nginx の標準機能のみで構成できる

Private Subnet ALB (L7 ルーティング + ヘルスチェック)

ALB は Internet-facing から Private Subnet 配置に変更しました。これにより、Nginx から ALB への通信が VPC 内に閉じ、NAT Gateway を経由しなくなります。

ALB を完全に外して Nginx から Web App に直結する案もあり得ましたが、ALB を残したのは次の役割分担を意図したためです。

  • L7 ルーティング (パスベース・ホストベース) を ALB に任せ、Nginx は認証に集中させる
  • ヘルスチェックとデプロイ時のローテーションを ALB に委ねる

Nginx は認証、ALB は配送 — 関心の分離をネットワーク上の役割としても明示する設計としました。

変更がもたらしたもの

整理すると、新構成は次の4点を同時に実現しています。1 と 3 が2つの課題への直接の回答、2 は分離の帰結、4 はその副産物です。

  1. スコープを「カルテ利用ユーザーからのリクエスト」に限定。社内オペレーター・melmo ユーザーは別経路へ分離
  2. 経路がネットワーク的に分かれたため、電子署名による経路識別が不要に。OpenResty で Lua を使う動機がなくなり、Nginx 単体で十分な構成に
  3. NAT Gateway を経由しなくなり、構成上の単一障害点が消滅
  4. VPC 内閉域通信になったことで、Nginx 1.29.7 のデフォルト upstream keep-alive (NGINX Community Blog) の恩恵もそのまま享受できる構成に

移行で苦戦した点 — 「設計図に書かれていない経路」が一番怖い

新構成を絵に描くこと自体は比較的早く終わりました。本当に時間がかかったのは、その後の「現状どう使われているか」の棚卸しです。

CLINICS には、カルテ利用ユーザーであっても、新規ユーザーのクライアント証明書発行時はクライアント証明書を持たない状態でアクセスできる必要があるという仕様があります。 これは設計図ではなく、サービスの使われ方として存在する仕様です。

この経路の存在に気づくことができたのは、QA チームに依頼した E2E テストの結果を確認したときでした。 SRE 側で設計図やコードを追いかけていた段階では拾えなかった仕様が、その資産を通した検証で表に出てきた、というのが正直なところです。机上の設計図だけを追いかけていたら、リリース後に登録導線が壊れていたかもしれません。

ここで大きかったのは、他チームが積み上げてきた資産を、SRE の移行検証としてそのまま活用できたことです。QA チームが整備している E2E テストは、この移行のために作られたものではなく、日常的な品質担保のなかで育ってきたものです。それを移行検証にも転用できるという、チームをまたいで資産にアクセスできる状態が、机上の設計だけでは見えない仕様を移行前に洗い出すセーフティネットになっていました。

加えて、デグレ検証も大きな工数を割いた工程でした。「動いている既存サーバを置き換える」とは、既存のすべての挙動を保証する必要があるということでもあります。本番と等価な振る舞いをするか、QA チームに新環境での機能の総ざらいを依頼して確認しました。SRE 単独で同等の観点をゼロから組み立てようとしていたら、この検証は成立していなかったと思います。

教訓 — ツール比較の前に、役割の分解があった

今回の移行を通じて残った教訓は、次の2つです。

1. 技術選定の前に、役割の分解がある

「OpenResty の移行」として検討を始めると、現行サーバが担っている役割をすべて引き継ぐことが暗黙の前提になります。その前提のままでは、「Lua 相当の柔軟性を持つ後継」を探し続けていたはずです。

実際には、アーキテクチャの前提 — 単一の Web サーバが全経路を捌く — を先に見直したことで、引き継ぐべき役割は mTLS だけに縮みました。そこまで来れば、ツールの比較はほとんど自明です。言い換えると、OpenResty の代替を探すのをやめたら、OpenResty が要らなくなったということです。

「OpenResty vs Nginx」「ALB mTLS を使うか否か」というツールの比較は、入り口にすぎませんでした。本質は「何を解決しているのか」を問い直すことであり、その答えは多くの場合、技術スタックの外側 — アーキテクチャの前提 — にあります。

2. それでも、最後の決め手は「使われ方」

役割を mTLS だけに絞っても、「ALB mTLS か Nginx か」という選択は残りました。これを決めたのは性能でもコストでもなく、「新規ユーザーのクライアント証明書発行時は、証明書なしでアクセスできる必要がある」という仕様でした。そしてこの仕様は、コードや設計ドキュメントではなく、QA チームの実機検証ノウハウのなかにありました。

ALB mTLS は単体で見れば優れた機能ですが、CLINICS の仕様と組み合わせると採用できません。技術選定の良し悪しは、選定対象の優劣ではなく、選定対象とサービス仕様の組み合わせで決まります。

アーキテクチャ刷新における技術選定は、技術的な正しさだけでなく、アプリケーションの仕様やユーザーがどのようにサービスを使っているかを理解した上で行う必要がある。

移行検証の工程も含めて、今回の刷新で最も時間を要したのはこの理解でした。

まとめ

CLINICS のクライアント認証サーバを OpenResty から Nginx へ移行した事例を、背景にあるアーキテクチャ見直しの過程とあわせて紹介しました。

  • 旧構成: 単一の Web サーバが全経路を捌くため、アプリ層の電子署名で経路識別 → NAT Gateway 依存による経路構成や障害波及範囲の広さに課題を抱えていた
  • 方針: カルテ利用ユーザーの経路を分離し、VPC 内に閉じる → 分離の帰結として電子署名が不要になり、認証サーバの役割は mTLS のみに縮小
  • 技術選定: ALB mTLS は CLINICS の仕様 (証明書なしで通る導線が必要) と組み合わせると採用できず、Nginx 単体を採用
  • 教訓: ツール比較の前に役割の分解があり、それでも最後の決め手はサービスの使われ方の理解にあった

今回はカルテ利用ユーザーの経路から着手しましたが、社内オペレーター・melmo ユーザーの経路についても同じ考え方で整理を進めていく予定です。

「動いているもの」を変えるのは怖いものですが、設計時の前提が変わったまま放置すると、いつの間にか「動いているだけのもの」になってしまいます。その差分を埋める作業を、これからも続けていきたいと考えています。

We’re hiring

メドレーでは、SRE をはじめ「医療ヘルスケアの未来」を共に創っていくエンジニアを募集しています。ご興味をお持ちいただけましたら、ぜひご応募ください。

※カジュアル面談も大歓迎です!ご希望の際は、「その他の項目(希望記入欄)」にてその旨をご記載ください。

MEDLEY Summer Tech Blog Relay 7日目の記事は斎藤さんです!お楽しみに!!