メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

Claude Enterprise全社導入で何を検討し、何をやらなかったか

こんにちは。セキュリティエンジニア兼SREの侘美です。

2026年6月、メドピアではClaude Enterpriseの全社導入を完了しました。正社員・契約社員あわせて約130名にアカウントを配布しています。

今回の導入は、自分(セキュリティ)とコーポレートIT(社内のIT機器・SaaS管理を担当)で協力して設計・構築しました。セキュリティ方針やリスク評価は自分が担当し、SSO・SCIM設計やクライアント管理、コネクタ運用といった実装面の細かい検討・構築はコーポレートITのメンバーが中心になって進めています。

全社導入にあたっては「何を使えるようにするか」と同じくらい「何をやらないか」の判断が多く、この記事ではその検討プロセスを共有します。導入を検討している企業の参考になれば幸いです。

全社導入の背景

PoC期間で見えたこと

メドピアではエンジニアが先行してClaude Codeを業務に活用していました。2026年3月からはビジネスサイドを含む5〜10名の選抜メンバーでPoCを開始し、オペレーション業務を1/3に削減する目標を掲げていました。

PoCを進める中で2つの課題が見えてきました。

1つ目は非エンジニアの利用リスクです。PoCメンバーから「パッケージのインストールを案内されるとよくわからずにYesを押してしまう」という声があがりました。git操作やNode.jsの設定をAIに指示されるまま実行してしまうケースも報告されています。

2つ目はアカウントの格差です。Claude IDを持っている人と持っていない人とで情報格差・業務格差が発生し、事業推進部ではSkills化が進んでオペレーションメンバーへの展開が必要な段階になっていました。

全社員にアカウントを配布しつつ、安全に使える仕組みが必要。これが全社導入の出発点です。

セキュリティ面の状況

導入検討と並行して、AIエージェント周りのセキュリティインシデントが立て続けに報告されていました。

  • Claude Code設定ファイル経由のRCE(CVE-2025-59536)
  • Coworkでの間接プロンプトインジェクションによるファイル流出
  • Desktop ExtensionsのゼロクリックRCE(CVSS 10.0)
  • 野良Skills(ToxicSkills)の36%にプロンプトインジェクション検出

これらのリスクを理解した上で「だから使わない」ではなく「どう使うか」を設計する必要がありました。

導入までのスケジュール

全体で約3ヶ月、以下の流れで進めました。

時期 フェーズ
2026年3月 PoC開始、ガバナンスポリシー策定
4月 セキュリティ方針共有、プロダクト整理、部長会での合意取得
5月前半 管理設計・方針凍結
5月後半 設定構築、SCIM連携、SSO・端末認証設定
6月1日 全社展開完了

設計の基本方針

Tier方式のリスク分類

Claudeのプロダクトを2つのTierに分類しました。

  • Tier 1(Chat等): ユーザーがデータを明示的に渡す。従来のチャット型AIと同等のリスク
  • Tier 2(Cowork / Code / Chrome等): AIが許可された範囲で自律的に操作する。固有のリスクあり

この分類が「誰に何を使わせるか」の判断基軸になっています。

「事前制御」主軸

Cowork、Claude in Chrome、Officeエージェント、Claude in Slackは監査ログに対応していません。標準機能では「誰が・いつ・何を入力し・何を取得したか」の追跡がほぼ不可能です。

OpenTelemetryで理論上は追跡可能ですが、コスト・ROIの問題があります。

事後検証に頼れない以上、そもそもリスクが発生しない環境を作る「事前制御」を主軸に据えました。

リスクへの基本姿勢

リスクをゼロにすることは現実的ではなく、これはClaude固有の話ではなくあらゆるITツールに共通です。「リスクがある=利用禁止」ではなく、状況に応じた柔軟な判断を方針としています。

導入時のルール

職種ごとのプロダクト使い分け

Claudeの主要プロダクトは4つあります。Chat(Web/デスクトップ/モバイル)、Code(CLI/IDE)、Cowork(デスクトップアプリ)、Design(プロトタイプ作成)です。下にいくほど自律性が高く、リスクも増します。

機能 エンジニア 非エンジニア
Chat 利用可 利用可
Cowork 利用可 許可制
Code 利用可 利用不可

DesignはまだResearch Previewのため、ここでは割愛しています。

非エンジニアのClaude Code利用については社内でも議論がありました。結論としては「非エンジニアがCodeを使うのではなく、そのタスクはエンジニアが担当すべき」「Codeを使うなら実質的にエンジニアになるべき」という方針です。セキュリティ面で中堅エンジニア程度の知識がないと危ないというのもあります。

Coworkは想定以上に多機能で、Notion整理やSlack連携、CSV分析といったタスクはCoworkで十分にこなせます。安全にしたCodeと素のCoworkの機能差が小さくなってきている現状では、いったんシンプルな線引きで、無理に仕組みを作らずにプロダクトを使い分ける方が無難だと判断しました。

認証: SSO + 端末認証

Google WorkspaceをIdPとしたSAML認証に加え、Context-Aware Access(CAA)で会社管理端末のみアクセスを許可しています。BYODからのアクセスは排除する設計です。SSO・CAAの設定構築はコーポレートITが担当しました。

検証中に2点ほど問題が見つかりました。

OTPログイン経路がCAAを迂回する問題。メール+OTPでのログインはSAML/CAA経由しないため、端末認証が効きません。「Require SSO for Claude」設定をONにしてSSO以外の認証経路を封鎖しました。検証段階で気づけたのはよかったです。

iOSアプリのSSOログイン失敗。アプリ内WebViewではCAAのデバイスシグナルが渡せず、Googleサポートにも「サードパーティアプリのWebViewはGoogleエンドポイント管理と統合されていない」と言われました。社用iPhoneではChrome経由でアクセスする運用としています。モバイルでClaude使う場面は限定的なので、業務影響は軽微です。

SCIM連携によるアカウント・権限の自動管理

アカウント管理は4つのプロビジョニングモード(Invite only / JIT / SCIM / SCIM + Group Mappings)から最も自動化度が高い「SCIM + Group Mappings」を採用しました。Googleグループへの所属・離脱だけでアカウント発行・削除・権限変更が完結します。

グループ設計とSCIM構築はコーポレートITが中心に進め、7つのGoogleグループで運用しています。

グループ 用途
claude-owner 管理コンソール全権限
claude-admin 管理コンソール(請求除く)
claude-base Claude.aiの基本利用
claude-cowork Cowork追加利用
claude-code Claude Code追加利用
claude-seat-premium Premiumシート
claude-seat-standard Standardシート

シート種別はStandard($40/月)とPremium($200/月)の2種類があり、PremiumはClaude CodeやCoworkが利用可能です。誰にどちらを割り当てるかの設計もグループで管理しています。

ポイントはカスタムロールの加算式仕様です。claude-baseに所属するメンバーにclaude-coworkグループを追加すると、base + coworkの権限の和集合が適用されます。「制限のためにグループを作る」というアプローチはこの仕組みでは取れません。ベース+積み上げ式の設計になります。

運用上の落とし穴がいくつかあります。

  • Group Mappings有効化前に全メンバーをいずれかのロールグループに所属させる必要がある。未所属のメンバーは自動削除されてしまう
  • Owner権限グループが存在しないまま有効化するとロックアウトする
  • シート種別を割り当てないとPremiumに自動振り分けされる。コスト増の罠
  • メールアドレス変更はSCIM上では新規作成+旧アカウント削除として処理される

地味にハマるポイントなので、導入時は注意してください。

コネクタ(MCP)の管理

利用を許可するコネクタは限定しています。

コネクタ 読取り 書込み 備考
Notion 個人情報NG
Slack 書込み不可
GitHub エンジニア向け
Figma / Datadog / Sentry 調整中 調整中

Slackへの書込みを不可としている理由は、後述のClaude in Slack OFFと共通です。端末認証の迂回経路になるのを防いでいます。

コネクタ管理で厄介だったのは、有効化すると組織全体に影響する制約です。「特定部署だけで使う」といった細かい制御ができませんでした。当初は組織全体のon/offしかできないため、コーポレートITが申請フローを整備して統制していました。

その後、Role単位でコネクタを制御できる機能がリリースされたため、今後は許可されたRoleにはある程度の裁量を持たせるルールに変更予定です。こうしたプロダクト側の進化でルールの前提が変わるのは日常的に起きます(「新機能リリースへの対応」で詳しく触れます)。

制限・無効化した機能

Claude in Slack → OFF

OFFにした理由は複合的です。

  • SlackはBYODからアクセス可能なので、Claudeの端末認証を迂回する
  • SlackのガバナンスとClaudeのガバナンスが矛盾する(情報の流通経路が二重になる)
  • Claude Code in SlackでCode起動が可能。非エンジニアCode利用NG方針と衝突する
  • チャット内容がClaudeの監査ログに記録されない

Admin ConsoleでのOFFに加え、コーポレートIT側でSlackのClaudeアプリをRestrictにして二重防御としています。

とはいえ、SlackはメドピアのコミュニケーションHub なので、ここでClaudeが使えない不便さは正直大きいです。セキュリティ上の課題をクリアする方法がないか、継続的に検討しています。

プラグイン・デスクトップ拡張 → 原則不可

プラグイン経由で意図しないコネクタやスキル、MCPが持ち込まれるリスクがあるため、許可リスト方式で管理しています。業務上必要なものはIT宛に申請する運用です。

Claude in Chrome → 許可制

Claudeがブラウザ上で読み取り・クリック・入力・送信を代行する機能です。不許可条件として、要配慮個人情報を含むサービス、誤操作時に不可逆な影響があるもの、代替手段があるものを設定しています。承認フローは部長→SaaS管理者→情シス→セキュリティの4者直列です。Google WorkspaceのChrome拡張管理でインストール自体を制御しています。

Officeエージェント → 保留

M365コネクタの判断と一体で扱う方針です。SharePoint内の要配慮個人情報の実態確認や、顧客契約上の「外国保管禁止」条項の確認が必要で、まだ結論が出ていません。

新機能リリースへの対応

「デフォルトON」問題

Anthropicは新機能をデフォルトONで提供します。管理者の明示的な同意なく機能が有効化される構造です。

実際に起きた事例をいくつか挙げます。

  • Officeエージェントが有効化された状態で提供開始された(2026年5月7日)
  • Claude in Slackが管理者の同意なく有効化されていた(2026年5月13日確認)
  • Claude Security機能が利用上限設定を無視して課金されるバグがあった(5月初旬、5/7修正)

「まずONで配って、利用しないなら個別にOFFしてください」がAnthropicのスタンスです。これは「たまたま起きた個別事象」ではなく構造的な問題なので、今後も継続的に発生することを前提に統制を設計しています。

検討中に前提が変わる

先述のコネクタ管理のように、ルールを設計している最中にプロダクト側の仕様変更で前提が変わることが日常的に起きます。Coworkも当初はResearch Previewでしたが、急速に進化して想定以上の機能を持つようになりました。

完璧なルールを最初に作ろうとするとすぐに陳腐化します。段階的に整備して変化に追従する方針で運用しています。

検討したがやらなかったこと

導入時のルールと同じくらい、「やらない判断」にも時間を使いました。

Managed Settings(managed-settings.json

Claude Codeには設定ファイルが4階層あり、その最上位がmanaged-settings.jsonです。Jamf等のMDMで配布すればユーザーは変更できず、Hooksの無効化やMCPの制限、危険コマンドのdenyを強制できます。

非エンジニア向けの厳格な設定まで設計していましたが、費用対効果から見送りました。

  • managed-settings.jsonの対象はClaude Code利用者に限られ、全社員のうちの一部。その人数に対して設計・配布・メンテナンスの工数が見合わない
  • エンジニアはMacの管理者権限を持っているため、本人またはAIがmanaged-settings.jsonを削除・改ざんできてしまう
  • 無効化の検知はCrowdStrike FileVantage等で理論上は可能だが、その仕組みの構築自体にもコストがかかる

セキュリティリスクと工数を天秤にかけた結果です。ただし完全に捨てたわけではなく、なるべく工数をかけずにクリティカルな部分を抑制できる設定がないか、検証は継続しています。

Cloudflareでのapi.anthropic.comブロック

非エンジニアのClaude Code利用をネットワーク層で遮断する案も検討しました。managed-settings.jsonで統一的に制御した方がシンプルという判断で不採用に。最終的にはライセンス制御で十分に対応できています。

CrowdStrike FileVantage

.claude/配下やmanaged-settings.jsonのファイル変更をリアルタイム検知する案です。Managed Settingsの無効化検知にも使えます。ただ現状のCrowdStrike契約に含まれておらず、価格調査・動作検証が必要なため保留としています。

パッケージスキャンツール(Socket.dev等)

npm install前にパッケージの安全性を自動チェックするツールです。有効なのは間違いないですが、導入コスト・運用負荷との兼ね合いで保留中です。

代替として、サプライチェーン攻撃対策を全社で水準統一しています。Actionsのhash固定(pinact等)、ロックファイルの厳密な適用、ライブラリアップデートのクールダウンタイム(3〜7日)に加え、Takumi Guard(GMO Flatt Security)をレジストリプロキシとして導入し、npm install時に悪性パッケージをインストール前にブロックする仕組みも取り入れています。

まとめ

Claude全社導入はセキュリティリスクとの戦いというより、リスクを正しく理解した上での設計の連続でした。監査ログに頼れない現状では「事前制御」が軸になりますし、Anthropicの「デフォルトON」方針を前提とした運用設計も必要です。

今回はセキュリティ側からリスク評価と方針を出し、コーポレートITが実装・運用面を細かく詰めていく分担でうまく回りました。プロダクトの進化が速いので完璧なルールは作れませんが、段階的に整備して変化に追従していく方針が現実的だと感じています。

Claudeは新機能が高頻度でリリースされるプロダクトなので、一度作った設計が正解であり続けることはありません。この記事の内容も、数ヶ月後には見直しが必要になっているはずです。導入して終わりではなく、設計を常にアップデートしていく前提で運用することが大事だと思います。

同じようにClaude Enterpriseの導入を検討している方の参考になれば幸いです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら hrmos.co

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

10年もののモノリスから「認証」を切り出す 〜メドピア共通基盤プロジェクトの裏側〜

こんにちは。三村(@t_mimura39)です。

皆さんは、長年運用されてきたモノリシックなRailsアプリケーションから「認証」のような根幹機能を切り出した経験はありますか?
言葉にすると一行ですが、実際にやろうとすると胃が痛くなるテーマですよね。

今回はメドピアで2025年4月に走り出した「共通基盤プロジェクト」について、その背景と設計の裏側をご紹介します。

※ 本記事は共通基盤プロジェクトの立ち上げ(2025年4月)以降、執筆時点(2026年6月)までの取り組みをまとめたものです。プロジェクトは現在も進行中のため、今後さらに形が変わっていく可能性がある点はご留意ください。

目次

🙋 はじめに 🙋

本記事では、メドピアが抱えていた共通機構(特に「ユーザー管理」と「認証」)を切り出して再構築する 共通基盤プロジェクト の全体像と、設計上工夫したポイントについて記述します。

個別の実装コードの詳細までは踏み込まず、「なぜそういう判断をしたのか」という意思決定の部分を中心にお伝えできればと思います。「社内サービスの認証基盤を再構築したい」という似た境遇の方に、何かしらの参考になれば幸いです。

🏚️ 背景 — 10年もののモノリスとClinPeerの狭間で 🏚️

弊社の医師集合知プラットフォーム「MedPeer」(medpeer.jp)は、元々PHPで実装されていたシステムを2016年頃からRubyに移植し始めました。

tech.medpeer.co.jp

今現在は(社内専用の一部の管理画面を除いて)すべてRubyへの移植が完了しています。
……が、Ruby移植が始まってからもう 10年近く 経過しています。しかもDBは前システムのDB(なんなら前の前のシステムのDBもあったり)を参照している、なかなかに歴史のあるモノリシックなアプリケーションです。

こうなると、機能拡張、特に認証などの基盤となる部分の変更は慎重にならざるを得ません。触るたびに「ここを変えて大丈夫だろうか」と冷や汗をかく、そんな状況です。

そんな中、2024〜2025年にかけてメドピアでは新サービス「ClinPeer」を立ち上げました。ClinPeerの詳細な紹介は割愛しますが(興味がある方は ClinPeer 紹介ページ をご覧ください)、要件の一つに「MedPeerと同一のアカウントでログインできること」がありました。

MedPeerには他サービスへ認証状態を連携する仕組みが備わっているため、その仕組みに乗っかるだけでSSO的な挙動は実現できました。が、1点だけ厄介なことがありました。

MedPeerは医師向けサービスで、「医師であることが確認できたユーザー」のみが利用できるよう制限されています。一方のClinPeerも医師向けサービスではあるのですが、「医師であることの確認が十分でなくとも利用は可能とする」という要件でした。

一見すると些細な仕様差異に見えますよね。私も最初はそう思っていました。ところがこれがなかなかに厄介だったのです。

MedPeerの認証連携は「まずMedPeerにログインすること」を前提としているため、「医師であることの確認が十分でないユーザー」はそもそもMedPeerにログインできず、認証連携の処理を実行できなかったのです。

これは負債だと認識しつつも、ClinPeerの開発が急務だったため、苦肉の策として if clinpeer? のような判定処理をMedPeerのリポジトリにいくつか埋め込み、パズルのように本問題を凌ぎました。

しかし、誰かがいずれこの負債には向き合わなくてはいけません。

そんなこんなで、MedPeerから「認証」などの共通処理を切り出し、再構築するプロジェクトが2025年4月に走り出しました。その名も「共通基盤プロジェクト」です。(そのまま)

🏗️ 共通基盤プロジェクトとは 🏗️

共通基盤プロジェクトは、MedPeerが中心となって抱え込んでいた共通機構を外に切り出し、MedPeer自身もClinPeerなどと同列の位置付けにする ことを目的としています。

切り出す対象として想定しているのは、以下のようなものです。

  • ユーザー管理
  • 認証
  • お問い合わせ
  • ポイント

などなど。

その中でも、まずは「ユーザー管理」、そして「認証」から共通基盤に切り出していきます。本記事でもこの2つを順に見ていきましょう。

👥 ユーザー情報連携 👥

現状、MedPeer・ClinPeer・MedPeer Careerはそれぞれが独立したRailsシステムであり、それぞれのDBにユーザー情報が格納されています。位置付けとしては MedPeerのユーザー情報がマスタ で、他サービスにはそれが転記されている、という形でした。

これを、共通基盤をマスタ とする形に置き換えていきます。

共通基盤DBにユーザー情報を格納する

まずは共通基盤DBに、MedPeer相当のユーザー情報を同期します。

システム間連携の仕組みとして、当初はApache Kafkaなども検討しました。が、最終的には お手軽で、かつ柔軟に対応しやすい ように、専用の同期APIを共通基盤側に実装し、MedPeerでユーザー情報が更新されるたびにその同期APIを呼び出す、という素朴な作りにしました。きらびやかな仕組みより、まず確実に動いて運用しやすいものを選んだ形です。

そして今回、特に大事にしていたのが「過去の負債を引き継がない」という点です。

前述の通り、MedPeerには10年では効かないような前々世代のDBが現役で動いています。こういったテーブルはRailsと相性が悪いものが多く、また「MedPeerに特化した設計」になっています。

たとえばMedPeerでは「会員ステータス(サービス利用可能、一部サービス利用可能、退会 など)」を管理していましたが、これは「医師であること」を前提に設計されていました。そのため、薬剤師のような医師以外の職種でもサービスを利用できるようにすると、会員ステータスという概念そのものが破綻してしまう状況だったのです。

そこで共通基盤では 会員ステータスという概念をあえて持たない ことにしました。代わりに、

  • 退会しているかどうか
  • 本人確認が完了しているか否か
  • 職種

といった情報を バラバラに保持 し、それらを連携先の各サービスが必要に応じて判定する形としました。

そして、この「バラして持ち直す」という整理、見た目以上に骨が折れました。

元の会員ステータスはenum的に管理されており、しかも管理画面からそのenumをコロコロと切り替えられる機能まで備わっていました。 お察しの良い方はもうお気づきかもしれませんね。蓋を開けてみると、ステータスと会員属性の関係性に矛盾が生じているユーザーデータがたくさん 存在していたのです。

「ユーザー属性的にはMedPeerの全ての機能が利用できて良いのだが、諸事情により機能を制限している」など

そこで一つひとつ事情を紐解き、必要に応じて仕様を変更したり、ユーザー属性を新設するなどをした後に最終的には「いくつかのユーザー属性から算出できるもの => 会員ステータス」という位置付けに整理し直すことができました。

「会員ステータス」という一見便利だが特定サービスに密結合した概念を一度ほどき、より素朴な事実の集合として持ち直す。地味ですが、ここを妥協しなかったことが後々効いてくると考えています。

共通基盤の情報を各サービスへ展開する

無事に共通基盤DBがMedPeer相当のユーザー情報を持つようになったら、次は他サービスへの同期の切り替えです。

これまで「MedPeer → ClinPeer」だったデータの流れを、まずは「MedPeer → 共通基盤 → ClinPeer」のように共通基盤が仲介する形へと、徐々に切り替えていきます。

共通基盤から他サービスへのユーザー同期は、シンプルに次の2つの仕組みで成り立っています。

  • ユーザー情報更新通知
    • インターネット経由で送信し、署名検証の仕組みあり。通知ペイロードには「更新が発生したユーザーの識別子」のみが含まれます。
    • 共通基盤でユーザー情報が更新されたタイミングで、共通基盤 → 各サービスの向きに通信されます。
  • ユーザー情報取得API
    • 隔離されたVPC内でのみ疎通可能。
    • 上記の更新通知の受信時や、後述する認証成功時、あるいはサービス都合の任意のタイミングで本APIを呼び出し、最新のユーザー情報をサービスDBに格納します。

通知では「誰が更新されたか」だけを伝え、実際の中身は各サービスがVPC内のAPIで取りに来る。センシティブな情報をインターネット経由で流さずに済むため、この役割分担はなかなか上手くハマったと感じています。

💡 ちょこっと工夫: heartbeatで「通知の消失」を防ぐ 💡

ここで一つ、設計上の工夫を紹介させてください。

今回は とにかく各サービスの開発負担を小さくすること を念頭に設計していました。

たとえば「ユーザー情報更新通知」は、サービス側がメンテナンスなどでサーバーを停止していると、通知を受信できずにそのまま消失してしまいかねません。

これを防ぐには Amazon SQS のようなキューイングの仕組みを構築する手があります。が、それだと各サービスに「キューをポーリングする実装(Shoryuken など)」を強いることになってしまいます。負担を小さくしたいのに、これでは本末転倒ですよね。

そこで今回考えたのが、「各サービスから共通基盤に対して、heartbeat的なAPIを定期的に呼び出し続ける」という仕組みです。

sequenceDiagram
    participant SV as サービス (RP)
    participant CB as 共通基盤 (OP)
    SV->>CB: 1 定期的にheartbeat通知を送信
    SV->>SV: 2 メンテナンス開始(heartbeatを停止)
    CB-xSV: 3,4 更新通知を飛ばすが、メンテ中で受信できず
    CB->>CB: 5 一定回数リトライ後、通知内容をDBへ退避
    SV->>CB: 6 メンテ完了。heartbeat通知を再開
    CB->>SV: 7 退避していた更新通知を再送信

heartbeat通知の仕組み

  1. サービスは定期的にheartbeat通知を共通基盤に送信する
  2. メンテナンス開始のタイミングでheartbeat通知を停止する
  3. 共通基盤でユーザー情報が更新され、サービスへ「ユーザー情報更新通知」を飛ばす
  4. サービスはサーバーメンテナンス中のため、通知を受信できない
  5. 共通基盤は一定回数リトライした後、通知内容を共通基盤DBに退避的に記録しておく
  6. サービスのメンテナンスが完了し、再びheartbeat通知を共通基盤へ送信する
  7. 共通基盤はheartbeat通知を受信したタイミングで、退避していた「ユーザー情報更新通知」を再送信する

「heartbeat通知を定期実行する」ことはサービス側にお願いすることになりますが、cron的な仕組みはどのサービスにも既に備わっているため、さほどの労力にはならないと判断しました。

この仕組みのおかげで、いずれかのサービスでサーバーメンテナンスが発生しても、この通知の文脈では人間が手を介すことなく、メンテナンス終了とともに自然とユーザー情報連携が再開 されるようになっています。

キューを増やすのではなく、すでにある「定期実行」という最小の前提だけで消失対策ができる。シンプルですが、我ながら気に入っている設計です。

🔐 認証連携 🔐

続いて、本丸の「認証」です。

これまでの仕組みと課題

前述の通り、MedPeerには一部Cookieを共有することで認証状態を別システムに引き継ぐ仕組みがありました。長く使われてきた仕組みではあるのですが、いくつか課題を抱えていました。

  • 独自実装のためキャッチアップコストが発生する。新しく関わる人が仕組みを理解するまでに時間がかかります。
  • 認証状態を再現できるようなセンシティブな値をCookieで複数サービス間に共有している。そのため、各サービスの担当者がログマスキングなどを適切に気をつけ続けなければなりませんでした。

独自実装は、書いた本人や当時のチームにとっては自然でも、サービスが増えるほど「全員が正しく気をつける」ことを要求してしまいます。これはスケールしません。

OpenID Connectへの移行

そこで今回、認証連携を 標準的な OpenID Connect に切り替えました。

  • 標準規格が定められているため キャッチアップコストが低い。そしてこれは個人的に大きいと感じているのですが、AIコーディングとの相性も良い です。世に情報が溢れている標準規格は、AIにとっても扱いやすい題材ですよね。
  • セキュリティ面でも、独自実装より厳格にできます。

なお、後述しますが 一部のCookie共有は残す ことになりました。

「標準規格である」という価値

「キャッチアップコストが低い」と一言で書きましたが、ここは今回の判断の肝なのでもう少し掘り下げさせてください。

元々の独自実装の認証機構では、RP側の実装を共通化するために 社内Gem が作られていました。OmniAuthのカスタムストラテジや、専用API呼び出し用のクライアントライブラリです。独自仕様で会話する以上、その手順を各サービスで再実装させるわけにはいきませんから、ライブラリにまとめて配布するのは自然な発想です。

今回も当初は「同じようにクライアントライブラリを作るか」と検討しました。が、ここで一度立ち止まりました。

OpenID Connectは標準規格です。 わざわざ自分たちで独自にライブラリを管理するのは、むしろ過剰なのではないか? と。最終的には、独自ライブラリで囲い込むのではなく、各RPがそれぞれ規格に準拠した形で実装する という方針にしました。

この判断を後押ししたのが、社内の技術スタックの広がりです。

弊社はRuby on Railsなシステムが大半ですが、最近はフロントエンド・バックエンドともにTypeScriptで構成されたシステムや、LLM活用度合いの高いプロダクトでPythonを採用するシステムも徐々に増えてきています。

こうなると、言語ごとにクライアントライブラリを自作してメンテし続ける のは、なかなかに大変です。それなら、業界標準であるOpenID Connectという「共通言語」で会話してもらう方が、長い目で見て筋が良いと考えました。

独自実装は、自分たちで自由に作れる代わりに、自分たちで一生面倒を見続ける宿命を背負います。標準規格に乗るというのは、その面倒の多くを世界中のエコシステムに肩代わりしてもらう、という選択でもあるのだと改めて感じています。

迷い

本設計・実装を進めている中で以下の黒曜さんのセッション(「ドメイン指定Cookieとサービス間共有Redisで作る認証基盤サービス Kaigi on Rails 2025より」)が目に留まりました。これはMedPeerの旧システムに近い仕組みで、Cookieでセッション情報を共有するといったものです。 正直、自社サービス内だけであればOpenID Connectは過剰であるという意見は大いに同意するものの上述の「標準規格のメリット」そして「外部への認証機構の提供の可能性」を考慮し、今回はOpenID Connect準拠の決断をしました。

kaigionrails.org

主な実装内容

主な実装内容は、大きく3つです。

1. Authorization Code Flow(認可コードフロー)への準拠

基本となる認可フローは Authorization Code Flow に準拠しました。ただし、アクセストークン・リフレッシュトークンの発行は行っていません。隔離されたサーバー間通信が前提のため、認可コード/IDトークンの交換以降は、ユーザー情報を前述の「ユーザー情報取得API」でサーバー間に取りに行く構成としたためです。標準に忠実であることと、自分たちの構成で本当に必要な範囲を見極めることの、バランスを取った形です。

2. シングルログアウト(SLO)

実はOpenID Connectには、シングルログアウト(SLO)についても規格が定められています。お恥ずかしながら、私は今回まで知りませんでした。「ログインの規格」という印象が強かったのですが、ログアウトまでちゃんと面倒を見てくれるのですね。

SLOは、以下二つの組み合わせで実現されます。

  • Back-Channel Logout
    • 共通基盤(OP)がログアウトしたら、つながっている全サービス(RP)もログアウトする
  • RP-Initiated Logout
    • RPがログアウトしたら、OPもログアウトする

本件についてはこれといった工夫も特になく、それぞれの規格に準拠する形で実装しました。 SLOの規格としてもう一つ Front-Channel Logout というものがありますが、これはサービスが増えれば増えるほど安定性に欠けると判断し採用しませんでした。

3. シームレスなシングルログイン体験

一方で、こんな体験も求められました。

「MedPeerにログインしているユーザーが、初めてClinPeerにアクセスしたら、明示的なログイン操作なしでログイン状態としたい」

これは OpenID Connectに該当する規格がないため、独自実装 です。標準に乗れるところは素直に乗り、乗れないところだけ最小限で作る。今回の認証連携は、終始この方針でした。

そして、ここで先ほどの「一部Cookie共有」が登場します。logged_in = yes / no という 情報だけ を持つCookieをサービス間で共有します。

各RP(連携先サービス)へ未ログイン状態でアクセスしたタイミングでこのCookieを参照し、logged_in = yes(つまり共通基盤側ではログイン済み)の場合は即座にOpenID Connectのフローを発火させ、自動的にログイン状態とします。

センシティブな認証情報の共有はやめつつ、yes / no という当たり障りのない一片の情報だけは共有することで、ユーザーから見たときの「気づいたらもうログインできている」というシームレスな体験は維持する。課題だったCookie共有を完全にゼロにするのではなく、残すべき最小限まで削ぎ落とした という落とし所です。

📍 現在地 — すべての医師向けサービスがつながった 📍

ここまで設計の話を続けてきましたが、現在の状況にも触れておきます。

共通基盤プロジェクトが開始しておおよそ1年が経過した頃、すべての医師向けサービス(MedPeer / ClinPeer / MedPeer Career / みんコレ!※)が共通基盤に接続し、ユーザー・認証を一元管理できる状態 となりました。

みんコレ! は共通基盤プロジェクトの最中に、新規で共通基盤への接続が決まったサービスです。ユーザーの移行作業などが不要だったため、驚くほどスムーズに共通基盤に接続することができました。

裏側ではこれだけ大きな切り替え作業を実施しているにもかかわらず、ダウンタイムなし・大きな障害なしで完遂 できました。これは我ながら上出来だったのではないかと、ちょっとだけ自負しています。(こういうものは、何事もなく終わったときほど語られないものですが今回は自分で言わせてください。)

そして現在は、次のステップとして「利用規約の同意状況の一元管理」や「問い合わせ対応システムの統合・刷新」を進めています。

これらはいずれも「単なるリプレイス」では終わらせず、「共通基盤として提供するべきものは何か」を一つひとつ再定義しながら設計しています。「今必要だから」という理由だけでなく、「今後サービス展開が拡大していく上で必要になるか」を意識して仕様を定義しているのです。

これは YAGNIの原則に一定反している、という自覚はあります。「必要になってから作れ」という教えは、普段の機能開発であれば私も賛成です。

ですが、他のあらゆるサービスの基礎となる共通基盤 においては、こういった先読みはむしろ必要だと考えています。土台を後から作り直すのは、その上に乗っているものすべてを揺らすことになります。原則は原則として尊重しつつ、それが効く文脈とそうでない文脈を見極める。この塩梅こそが共通基盤づくりの難しさであり、面白さなのかもしれません。

👋 まとめ 👋

長くなりましたが、メドピアの共通基盤プロジェクトについて、ユーザー情報連携と認証連携を中心にご紹介しました。

10年もののモノリスから根幹機能を切り出すのは、正直なところ気の重い作業の連続です。それでも、

  • 過去の負債を引き継がない という意思を持ってデータの持ち方から見直す
  • 各サービスの開発負担を小さくする ことを軸に、heartbeatのような素朴で確実な仕組みを選ぶ
  • 独自実装を 標準規格(OpenID Connect) に寄せてキャッチアップコストとセキュリティリスクを下げる

といった一つひとつの判断が、これからのサービス展開を支える土台になると信じて進めています。

共通基盤プロジェクトはまだ道半ばです。今後、お問い合わせ管理やポイント管理の切り出しなど、続きをまたどこかでご紹介できればと思っています。似た課題に立ち向かっている方の参考に少しでもなれば幸いです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら hrmos.co

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

メドピアは TSKaigi 2026 にシルバースポンサーとして協賛しました 〜印象に残ったセッションを紹介します〜

こんにちは。エンジニアリング推進室の小林和弘(@kzhrk0430)です。

2026 年 5 月 22 日(金)〜 23 日(土)の 2 日間にわたって開催された TSKaigi 2026 にメドピア株式会社はシルバースポンサーとして協賛しました。私自身も個人スポンサーとして協賛させていただき、当日現地にお邪魔させてもらったので印象に残ったセッションや現地の様子をいくつかご紹介させていただきます。

会場入口に展示されたスポンサーロゴを指さしている四人の男性

今年は会場をベルサール羽田空港に移しての 2 日間開催で、TypeScript をテーマにした国内最大級のカンファレンスとして、今年も大変盛況でした。

TypeScript の言語処理系そのものの話から AI 時代の型設計の話まで幅広く聞けたのはとても良い刺激になりました。今回はその中でも個人的に印象に残ったセッションをいくつかピックアップして紹介したいと思います。

業務に残された「よくない型」で考える「TypeScriptの難しさ」(Saji)

speakerdeck.com

まず最初に紹介したいのが、Saji さんによる「業務に残された『良くない型』で考える『TypeScript の難しさ』」です。

「成功にパターンはないが失敗にはパターンがある」という格言から始まり、実際のプロダクトのコードベースに残ってしまった anyas@ts-ignore といった「良くない型」を収集・分類(パターン化)することで、TypeScript の難しさの一端に迫っていく、という構成のトークでした。

冒頭で「これは『こんな型を書くなんてダメ!』というトークではありません」と強調されていたのが個人的にとても好印象でした。誰だって好んで as@ts-ignore を書いているわけではなく、自分だって多少はやったことがあるはずで、その上で「なぜそうなってしまうのか」をパターンとして捉え直そう、という姿勢です。

3 つのパターンに分類する

最初のパターンは 動的な境界での型落ち です。DOM イベントや catch (e) のような、どうしても型が広くなってしまう箇所の話です。e.target as HTMLButtonElement(e as Error).message のような as がここに該当します。これらは currentTarget を使ったり instanceof で narrow したり型ガード関数を共通化することで、ある程度安全側に倒せるという話でした。

次のパターンは 動的な配列・Object 変換 です。Array.prototype.includesreadonly な配列だとリテラル型を要求してしまう問題や、Object.fromEntries / Object.keys の戻り値が構造的部分型の限界で劣化してしまう問題など、標準メソッド単体ではどうしても解決が難しいケースです。これらは自作のヘルパーや型ガードを書くことで、ある程度効率的に安全側へ寄せられる一方、narrowing や型ガードはユーザー次第な部分が大きいので慎重に、というまとめでした。

最後は Union の扱いの難しさ です。本来 Discriminated Union にできるはずの型を、typevalue を別々の式で組み立ててしまって直積型になってしまう例や、引数の Union から戻り値を推論しづらい例、ジェネリックな引数でハンドラを呼ぶ例などが紹介されていました。データ生成において Discriminated Union にできないのは設計の問題で、対応させたい型は常に一緒に生成することを忘れない、という指摘が特に印象に残りました。

「押し戻せるか」という分類軸

個人的に一番なるほどと思ったのが、これらのパターンを「外部との境界由来か、内部由来か」「安全圏に押し戻せるか、押し戻せないか」という 2 つの軸の 4 象限で整理していたところです。

境界由来で押し戻せるものはスキーマ(Zod 等)を含む型ガードで対応し、「型ガードを書ける場所では書く」をチーム規約にする。内部起因で押し戻せるものはチームで型ヘルパー・型ガード集を持つ。そして内部起因で押し戻せないものは、ある程度諦めて局所的に as@ts-expect-error を理由コメント付きで使い、TypeScript の進化(e.g. 5.5 導入された Array.prototype.filter の型推論改善)を追っていく。こういった対策が分類ごとに整理されていました。

このセッションを通して感じたのは、これは「TypeScript の難しさ」というよりは「TypeScript の型システムが完璧ではない」という話なのではないかということです。業務で書かれてしまう「良くない型」には、TypeScript の不完全さによって生まれてしまったケースと、実装の歪みによって生まれてしまったケースの両方がありそうで、これは発表での「境界由来 / 内部由来」という分類軸ともよく重なります。

そして個人的に得た気付きとして、TypeScript の「良くない型」はレガシーコードやアンチパターンの検知につながるのではないか、という視点があります。「良くない型」が集まっている箇所は、TypeScript の限界というより実装の歪みが出ている箇所である可能性が高く、そこを起点にコードの危ういゾーンを炙り出していけるのではないか、と考えています。

プラグインで拡張されるContextをtype-safeにする難しさと設計判断(kazupon)

speakerdeck.com

次に紹介するのは、kazupon さんによる「プラグインで拡張される Context を type-safe にする難しさと設計判断」です。

kazupon さんといえば Vue.js 日本ユーザーグループの代表で、Vue Fes Japan のオーガナイザーでもあり、弊社の Vue Fes Japan 2023 After Meetup にも特別ゲストとしてお越しいただいた方です。今回は JavaScript の CLI 開発ライブラリ「Gunshi」の v0.27.0 でリリースされたプラグイン機能を題材に、プラグインによって動的に拡張される Context をどうやって type-safe にするか、という型設計のお話でした。

Gunshi にプラグインという機能拡張を入れるにあたって、現在の型の状況・プラグインという機能に求められる型の条件・複数の型設計の検討という流れで話が進んでいき、型を使って Gunshi の開発者・利用者・プラグイン開発者の体験をよくするために何が最適なのかを、どう考え抜いたのかが詳細に語られていました。TypeScript の型システムを最大限活用していることがよく伝わってくるセッションでした。

値が作られるタイミングと、型が必要なタイミングがずれる

Gunshi では、コマンドを定義する define() を起点に CommandContext の型が決まり、run(ctx) まで型が伝播していく仕組みになっています。ここにプラグインが加わると ctx.extensions に値やロジックが動的に追加されます。

ctx.extensions の中身は CLI を起動してプラグインを適用した後に作られるのに対し、プラグインの型はコマンドを定義する define() 実行時に必要になる、というタイミングのずれが発生します。つまり、実行時に注入された値を定義時に型として参照する必要があります。この課題を解くために、プラグイン ID・プラグイン同士の依存・コマンド側が期待する extension という 3 つを型でつなぐ必要がある、という整理が非常に明快でした。

4 つの設計アプローチとトレードオフ

発表では、この問題を解くための設計アプローチとして、Module Augmentation・Fluent Interface・Explicit Context Type・Compositional Type Augmentation の 4 つが比較されていました(名称の一部は kazupon さんによる命名とのことです)。

Gunshi では Explicit Context Type をベースに採用し、プラグイン ID と Extension 型のペアを Map として表現する設計を選んだ、という結論でした。「宣言ベースのインターフェース」「コマンドモジュールとプラグインが別々の場所に存在する」という Gunshi の特性に合わせた判断だそうです。プラグイン作者にプラグイン ID と Extension 型の責務を持たせているのも面白いです。

応用できる知見

このセッションが良かったのは、最後に提示された「応用できる知見」が、Gunshi に閉じない普遍的な話に着地していたところです。Context のような動的に値を追加できる構造を設計するときは、「追加された値の由来を API に残すか」「型を global に広げるか scope を限定するか」「自動推論を優先するか明示的な契約を優先するか」といった観点で判断することになる、という整理でした。

動的な拡張を type-safe にする設計では、型の表現だけでなく、境界と責務の置き方が重要になるということを学びました。普段ライブラリの利用者側にいることが多い自分としては、こうした設計判断の裏側を覗けたのはとても貴重で、Vue の Composable やプラグイン的な仕組みを設計するときにも効いてくる視点だと感じました。

TS 7: How We Got There(Jake Bailey)

jakebailey.dev

続いては、Microsoft の Jake Bailey さんによる基調講演「TS 7: How We Got There」です。

ご存知の方も多いと思いますが、TypeScript チームは Go 実装のコンパイラである tsgo の開発を進めています。このセッションでは、なぜ Go へ移植するのか、その過程をどう支えたのか、そしてどのように世に出していくのか、といった話が語られていました。

なぜ Go が採用されたのか

このセッションで一番細かく語られていたのが、TypeScript 7.0 でなぜ Go が採用されたのか、という点でした。聞いていて自分なりに整理すると、理由は大きく次の 4 点に集約されると感じました。

  • TypeScript と Go は言語としての構造が似ているということ
  • ガベージコレクションを備えていること
  • async/await のような関数の色分けなしに並列処理を素直に扱えること
  • 学習容易性

聞いていて印象的だったのは、これらが単なる実行速度の話に留まっていなかったことです。移植のしやすさや、チームがその言語にどれだけ早く慣れて開発を進められるか、といった現実的な観点まで含めて検討されていることが伝わってきて、言語選定というものの考え方として勉強になりました。

TypeScript 6.0 の立ち位置と破壊的変更

Go への移植の話とあわせて、TypeScript 6.0 の立ち位置についても触れられていました。聞いた限りでは、6.0 は次のメジャーバージョンへの橋渡しとなるリリースで、いきなり大きく切り替わるのではなく、段階的に移行できるように位置づけられているという印象を受けました。破壊的変更を一度に押し付けるのではなく、移行の道筋を用意してくれているあたりに、このプロジェクトの進め方の丁寧さを感じました。

型チェックのデモが分かりやすかった

個人的に一番おもしろかったのが、型チェックのデモでした。CPU のコアと、その利用率を従来の tsc と新しい tsgo で並べて見せてくれたのですが、これがとても分かりやすかったです。

tsc のときは一部のコアしか働いていないのに対し、tsgo では複数のコアがしっかり使われている様子が目で見て取れて、なぜ型チェックが速くなったのかが直感的に理解できました。数字で「速くなった」と説明されるよりも、リソースの使われ方の違いを視覚的に示してもらえたことで、腑に落ちる感覚がありました。

いつテストを書くか?―ソフトウェア開発における安心と不安について考える(lacolaco)

docs.google.com

次は、lacolaco さんによる「いつテストを書くか?―ソフトウェア開発における安心と不安について考える」です。

AI コーディングでテストを作成することが増えた昨今、テストを書く目的を見失いつつあったので、改めてテストを書く意義を再認識するためにセッションを聞いていました。

テストという観点から開発の本質を問いかける

このセッションはテストを切り口にしながら、保守性の高いソフトウェア開発とは何か、そしてソフトウェアの変更容易性をどうやって担保していくか、という問いを投げかけてくるものでした。テストを「書くべきか・書かざるべきか」という話ではなく、開発者がコードの変更に対して抱く安心や不安という、もっと深層の感覚から捉え直していく流れがとても印象的でした。

感じたこと

今回の TSKaigi では TypeScript の歴史や型システム、ツールといった話題が多く語られていましたが、このセッションはそうした話題からは一歩離れた内容でした。だからこそ、ソフトウェア開発において本当に大事なことは何か、という普遍的なテーマに正面から向き合っていて、聞いていて深く考えさせられるセッションだったと思います。

そして個人的に一番おもしろかったのは、これはテストに関する話ではあるものの、TypeScript の型定義を設計するときにも似たような感性や着眼点が必要なのかもしれない、と感じたことです。「この設計に手を入れるのは怖くないか」「この構造に無理がないか」という変更に向き合うときの感覚は、テストを書くかどうかを判断するときにも型を設計するときにも同じように働いている気がします。テストの話を通して自分がコードとどう向き合っているのかそのものを見直すきっかけになりました。

次世代リンターで探る、tsgo 時代における型認識カスタムルールの現実解(Yuta Takahashi)

speakerdeck.com

最後は、髙橋佑太さんによる「次世代リンターで探る、tsgo 時代における型認識カスタムルールの現実解」です。

tsgo が来る時代に、型認識カスタムルール(Type-Aware Linting)とどう向き合っていくか、という非常に実務的なテーマでした。

tsgo で型情報へのアクセス境界が変わる

tsgo では型チェック処理が Go 実装側へ移るため、これまで TypeScript 本体から呼び出していた型情報へのアクセスが JavaScript 側からはできなくなります。

@typescript-eslint/no-floating-promises のように式の型を見て検出する型認識ルールは、今は typescript-eslint が担っています。parser services 経由で TypeChecker を引いて型情報をもとに診断する仕組みです。開発環境のエコシステムのひとつである Lint には、こうした TypeScript の型情報に依存したルールが存在します。

ところが tsgo では型チェック本体が Go 実装・ネイティブプロセス側に移るため、JS / TS で書かれた既存の ESLint カスタムルールは、従来の parser services 経由では tsgo 側の型情報を参照できなくなります。つまり各種 Lint ツールにとっては、型に依存する Lint ルールで「型情報を Go からどのように呼び出すか」が課題になる、というわけです。

次世代リンターの対応状況の整理

発表では、Oxlint(tsgolint をバックエンドに採用)、Rslint(Go 製で typescript-go を直接組み込み)、Biome(独自型推論の Biotype)という次世代リンターの対応状況が、表で丁寧に整理されていました。標準の型認識ルールはそれぞれ実装が進んでいる一方、型認識のカスタムルールはいずれも現状では未対応、という状況が一望できる内容でした。

その上で、Rslint にパッチを当てて型認識カスタムルールを Go バックエンドルールとして実装する PoC まで踏み込んでいたのが素晴らしかったです。実際に「書けるか」は検証できた一方で、typescript-go の internal API(GetAwaitedType などの unexported な API)に shim で依存することになり、内部実装の変更を自前で追従し続ける必要がある、という運用リスクも正直に共有されていました。「書けるだけでは代替できない」という指摘がとても誠実だと感じました。

各種 Lint ツールが tsgo にどのようにアプローチしているのかが表で整理されていたので、今後 Lint ツールを選定する上でも参考になるセッションでした。

まとめ

TSKaigi 2026 は、TypeScript 7 の tsgo という言語処理系の大きな節目と、AI とどう向き合って開発するかという、2 つの大きなテーマが並ぶカンファレンスだったように思います。

今回紹介したセッションはテーマこそ異なるものの、「AI がコードを書く時代に、人間は何を設計し、どこで責任を持つのか」という問いに向き合っていたように感じました。型設計、プラグイン設計、テスト、Lint ルールのいずれも、単なる実装技術ではなく、ソフトウェアを継続的に進化させるための意思決定そのものです。TSKaigi 2026 は、TypeScript の未来だけでなく、AI 時代のエンジニアリングのあり方についても考えさせられるカンファレンスでした。

紹介しきれなかったセッションもたくさんあるので、YouTube のアーカイブ動画が公開されたらぜひチェックしてみてください。

運営スタッフの方々、カンファレンスの開催・運営ありがとうございました。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら hrmos.co

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

#RubyKaigi 2026 で発表されたRubyのAOT Compiler Spinelで作ったものを自慢しよう!

こんにちは、メドピアのサーバーサイドエンジニアの草分( @Lni_T )です。

RubyKaigi 2026参加者の皆さん、お疲れ様でした。 今回は北海道は函館での開催ということで、名所にグルメに楽しいことが目白押しでしたが、皆さん満喫できましたでしょうか?

草分は函館だけでなく小樽や札幌まで足を伸ばしていました。北海道、また行きたいですね。

海の幸を満喫

Spinel

さて、今回の本題はSpinelです。RubyKaigi最終日、Matzさんのキーノートで発表され、話題となったツールですね。

github.com

SpinelはRubyのAOT Compilerで、Rubyコードをスタンドアロン実行可能なネイティブ実行ファイルにコンパイルすることが可能です。

処理の流れとしては以下のように説明されています。

Ruby ソースコード (.rb)
    ↓
spinel_parse:Prism を使用した解析
    ↓
AST テキストファイル
    ↓
spinel_codegen:型推論および Cコードの生成
    ↓
C ソースコード (.c)
    ↓
cc -O2 -Ilib -lm:標準の Cコンパイラ + ランタイムヘッダー
    ↓
ネイティブバイナリ:スタンドアロン(実行時の依存関係なし)

そう、RubyのインストールなしにRubyコードが動作するようになります。

とはいえ、もちろん制約はあり、evalやメタプログラミングなどはサポートされていない旨がREADMEに記載されています。 「Railsサーバーがそのまま動く!」といったソリューションではないようですね。

作ったものを自慢しよう!のコーナー

それでは、Spinelにはどこまでのパワーがあるのでしょうか? ちょっとひねったものを実装して確かめてみましょう。

1. ○×ゲーム(Tic Tac Toe)

実装

ソースコード

気づき

Ruby版/Spinel版で挙動が変わるケースがある

# array.rb
ary = Array.new(3, nil)
puts ary.inspect

上記のコードは、Rubyで実行した場合とSpinelでコンパイルした場合で結果が異なります。

$ ruby array.rb
[nil, nil, nil]
$ ./array
[0, 0, 0]

nilで初期化した配列ですが、Spinel版では0が出力されます。RubyにはNIlClassがありますが、C言語のnullにそのような概念はないせいか、どうも0として扱われてしまうようです。
この辺りの挙動はC言語に変換されている以上は致し方ない所でしょうか。

※2026/05/19追記: 上記の挙動は修正されました!🙇 最新版をfetchしてmakeしましょう!

適宜、Rubyコードとコンパイル済バイナリの実行結果を比較しつつ実装すると安全に開発できそうですね。

型の制約

Rubyでは型宣言などを書かず、やりたい放題プログラムを実装できますが、 Spinelを利用する場合、少々型の制約を受けてしまうようです。

# apple.rb
ary = Array.new(3)
ary[0] = "apple"
puts ary.inspect

例えば、上記のコードはSpinelではコンパイルエラーになります。

$ ../spinel apple.rb
/tmp/spinel_out.xxxxxx/out.c:23:34: error: incompatible pointer to integer conversion passing 'char *' to parameter of type 'mrb_int' (aka 'long long') [-Wint-conversion]
    sp_IntArray_set(lv_ary, 0LL, (&("\xff" "apple")[1]));
                                 ^~~~~~~~~~~~~~~~~~~~~~

int配列と認識されている配列の値に文字列を代入しようとしたため、エラーになってしまうようですね。

以下のように、型を合わせることでコンパイルを通すことができます。

ary = Array.new(3, "")
ary[0] = "apple"
puts ary.inspect

2. スネークゲーム

実装

ソースコード

気づき

system が使える

Rubyで外部コマンドを実行する Kernel.#system も動作します。

Spinel単体ではsleepが整数秒単位でしか動作しないなどの制限もあり、このデモコードでは外部コマンドに頼っています。
※2026/05/19追記: ↑こちらも修正されました!

しかし、この方法は環境依存要因が増えてしまいますね。せっかくスタンドアロン実行可能形式にコンパイルしたのに、これでは本末転倒かもしれません。

おわりに

ちょっとしたツールを作って運用する場合「Goでビルドする」手段が強力です(最近ならRustも選択肢に)。また、さらに前の時代であれば「Perlでスクリプトを書く」というのも有力で、Rubyでツールを作るケースは少なかったように思います。

こうしたユースケースに対してRubyも参入できるのかも!と思うと、なかなか夢が広がるツールだなぁと感じています。今後の動向が楽しみですね!


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら hrmos.co

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

#RubyKaigi 2026 セッションレポート

皆様こんにちは、メドピアのサーバーサイドエンジニアの内藤(@naitoh)です。

RubyKaigi 2026 に参加されていた皆さん、お疲れ様でした。 開催直前の地震や羽田空港の管制システムトラブルで最初はどうなることかと思いましたが皆様無事に辿り着けましたでしょうか。 自分は飛行機が欠航しましたので、急遽、新幹線に振替を行い北海道新幹線のありがたさを噛み締めました。

RubyKaigi 2026

いろいろありましたが、RubyKaigi のセッションの中で特に印象に残ったセッションをご紹介させて頂きます。

タイムテーブルは下記から確認ください。

rubykaigi.org

Portable and Fast - How to implement a parallel test runner

最初に Red Data Tools での開発の取り組みで「テストを高速化できないか」という気づきが発端になったという Portable and Fast - How to implement a parallel test runner を紹介させて頂きます。

rubykaigi.org

Rails な皆さんはテスティングフレームワークとしてたいてい RSpec (もしくは Rails 標準の minitest)を使っていて、test-unit はご存知ないのではと思いますが、今回は gem として開発が続いている test-unit の並列化を並列実行させて早期にテスト完了させる取り組みになります。

参考: Rubyのテスティングフレームワークの歴史(2014年版)

ちなみに、並列テストランナーとして、RubyKaigi 2023 の The Resurrection of the Fast Parallel Test Runner が今回の話の参考になります。

その中で、ActiveSupoort::TestCase で Minitest を使用している場合はスレッドベースで並行テストが可能。 Rails 6 以降で ActiveSupoort::TestCase で Minitest を使用している場合はデフォルトでプロセスベース(fork)で並列テストが有効になります。 プロセス間の同期には drb を使用しています。 ただ皆さんがよく使われている RSpec では並列テストはサポートされていないのが課題ということでした。

そこで並列テストランナーの test-queue は、RSpec や Minitest、test-unit などの各種テスティングフレームワークに対して、キューを分割させる形でプロセスベースでUNIX domain Socket or TCP/IP Socket を用いてプロセス間の同期を行い並列で動作させる事で、テストを早期に完了させることができるという話でした。

test-queue を使えば、test-unit も並列動作可能なのですが、各種テスティングフレームワークの内部APIを使用しているので内部APIが変更になると動作しなくなるリスクがあるので、最終的には各種テスティングフレームワーク自身で並列テストできる事が望ましいと最後に述べられています。

というわけで、前置きが長くなりましたが、今回は test-unit 単体で並列テストを可能にする取り組みです。

slide.rabbit-shocker.org

Portable (互換性) と Fast (テスト全体を効率良く早く完了させる)事を念頭に開発されたそうです。

  • Portable : test-unit 自身がサポートしている Windows もサポートする必要がある。(fork ではなくWindows でもサポートされている spawn を使用する)
    • spawn は親プロセスの状態を引き継がないので、親プロセスとの通信の仕組みを作る。
    • プロセス間通信の仕組みで drb があるが、Ruby 3.3 以降で Bundled gem になっているため、外部ライブラリへの依存しない方針のため不採用。
    • pipe を使ってプロセス間通信を行う。
      • Unix: IO.pipe を用いた片方向通信のペアでの MainプロセスとWorkerプロセス間の個別の双方向通信。 (Marshal#dump/load でオブジェクトをやり取り)
      • Windows: TCP/IP Socket を使った通信。
  • Fast : 各Workerの処理状況を知っているのは各Worker自身なので、キューに並べられたテスト対象を、各Workerが自分でPullしテストを実行するため、遊んでいるWorkerが発生しない形にしている。

スレッドベースではなくプロセスベースにすることで、各Workerが効率良くテストを実行できる点や、テストを各Worker自身がPull する仕組みが効率的良さそうな点と、外部依存を極力排除しているので導入しやすさの点でよく考えられている印象でした。

ただ、Windows で TCP/IP Socket を使うのであれば、test-queue の様に UNIX domain Socket & TCP/IP Socket の組み合わせ方が処理的に似ているのでメンテナンスし易そうに思いました。というわけで、いろいろなプロセス間通信の特徴が理解できました。

Exploring RuboCop with MCP

次は MCPサーバー & クライアントの Ruby-SDK のお話です。

rubykaigi.org

speakerdeck.com

MCP は JSON-RPC を用いるのは知っていましたが、トランスポートの仕組みに stdio と Streamable HTTP (HTTP + SSE の拡張)という仕組みを用いるんですね。 ここで Rack 3 の streaming body を使って SSEに対応しているのが面白いですね。

Rubocop x MCP の取り組みの紹介で、cli での rubocop -A で自動修正 や、 cli の結果をAIエージェントが解釈すればいいのでは? 🤔 と思ったのですが、 Support built-in MCP server by koic · Pull Request #14911 · rubocop/rubocop · GitHub を確認するとエージェントはCLI出力を解析するのではなく、構造化された方法で(定義されたスキーマを使用して) RuboCopに問い合わせる事ができるので、より確実に処理する事が狙いだそうです。AI専用の応答を MCP経由で返すインターフェースですね。

弊社内でも管理画面にDB参照用のMCPサーバーを組み込むことで、AIエージェントからDBに対してSQLを発行*1し障害調査に使えるようになっており、今回の開発は大変ありがたいです。

Surviving Black Friday: 329 billion requests with Falcon!

続いてFalconを2025年のBFCM*2に本番投入したお話です。

rubykaigi.org

speakerdeck.com

Falcon はFiberベースで、I/Oバウンドなリクエストを効率良く処理するwebサーバーです。CPUバウンドなリクエストには効果はありません。*3

本番導入するにあたって、コードがFiber Safeである必要があります。

  • スレッドをブロックするC拡張(rdkafka, gRPC, etc)は、そのWorker上の全てのFiberをブロックする。

ただ、どのようなケースで問題が発生するか事前には不明だったため、スケールテストや、カナリアリリースで問題が発生時は即座にUnicorn に切り戻す形で検証を実施することで、上記の問題を検出 & 修正しアップストリームにフィードバックされたとのことなので、途中から Falcon に切り替える場合は同様の検証が必要になりそうです。

なお、最初はUnicornモード(Worker毎に1リクエスト)で実施したとこのこと。これは Unicorn から Puma への移行でも同じ感じですね。

最終的なスループットの改善はUnicorn と比較して1割程度と控えめですが、Shopify ならその削減は相当なものになるでしょうし、Shopify レベルの環境で Falcon の本番導入実績ができたのはすごいですね。

おわりに

3日間にわたる RubyKaigi 2026 が終了しました。 最後は AOT コンパイラの Spinel に話題を持って行かれた気がしましたが、Ruby にはまだまだ進化の余地がありそうなので楽しみですね!

次回のRubyKaigiは 2027年4月14日から4月16日、場所は宮崎県宮崎市です。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら hrmos.co

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

*1:GRANT で個人情報等は参照できないように制限しています。

*2:Black Friday, Cyber Monday

*3:複数コアを活用するなどCPUバウンドなリクエストに効果がありそうなのは Ractor です。

第二創業期のメドピアが定義する、次世代エンジニアの「三つの責任」

こんにちは。三村(@t_mimura39)です。

メドピアはいま、大きな変革の真っ只中にいます。 2024年の創業20周年や代表交代、そして2025年のMBO。こうした大きな節目を経て、自らを「第二創業期」の只中にあると定義しています。 そんな中で私は、プリンシパルエンジニアの一人として「プロダクト開発組織の未来図」を描き、その実現をリードしています。評価制度や開発フロー・ルールの刷新など、あらゆる変革の根底にあるのは、一つの極めてシンプルな問いです。

「AIが実装を担う未来において、エンジニアは何に責任を持つべきか?」

今回はその問いに対しての一つの答えを書き記したいと思います。

目次


1. はじめに「なぜ今、この責任を定義するのか」

生成AIの普及により、コードを「書く」という行為は急速に自動化されつつあります。これからのエンジニアの存在意義は、実装という作業そのものではなく、「技術によって、託されたプロダクトの品質を保証すること」に一本化されていきます。

事業の推進者がエンジニアを信頼し、プロダクトという事業の心臓部を託すための共通言語として、「プロダクト開発」を主眼に置くエンジニアが果たすべき「三つの責任」を定義します。


2. 三つの責任

① 構造の責任

「正しく、AIや人間が文脈を理解しやすい秩序を保つ責任」

  • 責任の本質
    • 変化に強い骨格を設計し、AIが生成する膨大なコードによってシステムの秩序が失われるのを防ぐ。状況に応じて最適な結合度を選択し、開発速度と保守性のバランスを制御する。
  • 必要なスキル例
    • ドメインモデリング: ビジネスの本質を、シンプルで強固なデータ構造に落とし込む力。
    • アーキテクチャ設計: 規模やフェーズに応じ、構成の分割単位と依存関係を定義する力。保守性と速度を天秤にかけ、チームの認知負荷に適した「最適な複雑さ」を選択する。
    • 技術的ガバナンス: 命名規則や設計思想の一貫性を守り、AIや人間が迷わず文脈を理解できる「ノイズのない場」を維持する力。
  • 具体的なアクション例
    1. 解決するべき複雑な事象を整理し、単なるデータの保存ではない「ビジネスルールを表現する」強固なデータ構造を定義することで、システムの整合性を担保する。
    2. 「疎結合」を目的化せず、チームの状況を考慮して、あえてシンプルさを優先するか、抽象化を導入して独立性を高めるかの損益分岐点を見極め、構成を決定する。
    3. 設計思想と実装の乖離を埋めるリファクタリングを主導し、AIや後続のエンジニアが「推測」なしに改修できる透明性の高いコードベースを維持し続ける。

② 価値成立の責任

「技術をプロダクトの価値へ翻訳し、確実な形として具現化する責任」

  • 責任の本質
    • 「仕様書通り」の遂行に留まらず、技術の力で事業の可能性を拡張する。ビジネスの意図を咀嚼した上で、プロダクトが進むべき理想の姿を自ら定義し、最短・最良の形で価値へと昇華させる。
  • 必要なスキル例
    • 技術要素の統合力: 自身の専門を軸としつつ、インフラからフロントエンドまでを地続きに捉え、ユーザー体験を成立させるために必要な要素を繋ぎ合わせる力。
    • プロダクト価値の定義: 要求の背景にある課題を技術的に解釈し、プロダクトとしての「あるべき品質や機能」を逆提案する力。
    • 仮説検証の設計力: 最初から完璧を目指すのではなく、ビジネスの仮説を最速で検証するための「価値の核」を特定し、素早いフィードバックループを回すための構成を提案する力。
  • 具体的なアクション例
    1. 自身の担当領域を仕上げた上で、APIの挙動や画面の操作感に矛盾がないかE2Eで点検し、必要であれば領域外の修正まで踏み込んで「体験」を完成させる。
    2. 不確実性の高い新機能において、フルスペックの開発に入る前に「価値の核」を確認するための最小実装(プロトタイプ)を提案・構築し、実戦での検証結果をもとに素早く軌道修正を行う。
    3. 実装段階で仕様の不備(あるべきプロダクト価値との乖離など)に気付いた際、専門家として代替案を提案・議論し、最適化する。

③ 継続性の責任

「プロダクトが社会的に生存し続け、信頼を維持するための責任」

  • 責任の本質
    • セキュリティや信頼性、そして経済的合理性を後回しにせず、プロダクトが持続するための絶対条件として維持する。
  • 必要なスキル例
    • 経済的最適化: インフラ構成やリソース消費をモニタリングし、パフォーマンスを維持しつつコストを最小化する力。
    • レジリエンスと可観測性: 異常の予兆を検知し、復旧プロセスをエンジニアリングで自動化する力。
    • セキュリティ・バイ・デザイン: 扱うデータの重要性やリスクを予見し、設計段階から堅牢な保護の仕組みを組み込む力。
    • ライフサイクル管理: 技術負債を適切に管理し、計画的なアップデートによってシステムの陳腐化を防ぐ力。
  • 具体的なアクション例
    1. リソースの利用効率を追求し、モデルの選定やキャッシュ戦略の最適化を通じて、事業利益を圧迫しない技術構成を実現する。
    2. SLO/SLIを定義し、「人が介入すべき真の異常」のみを抽出するアラート設計と、復旧手順の自動化を推進する。
    3. 認証基盤やデータ保護など、社会的な信頼に応えるためのセキュリティ基準を満たすアーキテクチャへの継続的な投資と改善を行う。

3. 実装者から「価値の設計者」へ

これらの責任は、生成AIが登場する以前からエンジニアリングの根幹を成すものでした。しかし、AIがコード生成の大部分を肩代わりする現在、その重要性はかつてないほど高まっています。

これからのエンジニアは、コードを書くという「手段」の習熟から解放され、エンジニアリングの「本質」に対してのみ注力する時代へと突入します。

  • 「作る」から「成立させる」へ

要件をコードへ翻訳するだけの段階は終わりました。これからのエンジニアリングとは、実装の先にある「構造の妥当性」や「経済的な持続性」を束ね、プロダクトを事業として成立させることそのものを指します。実装は責任を果たす過程で生まれる成果物であり、目的そのものではありません。

  • 「何を託すか」から「誰に託すか」へ

AIがコードを書ける時代だからこそ、事業推進者が抱く問いは「何ができるか」から「誰にこの事業の心臓部を託すべきか」へと変わります。三つの責任を引き受け、プロダクトの命運を担える「設計者」であること。それこそが、変化の時代において私たちが信頼され続けるための確かな指針です。


4. 専門性を起点とした、境界なきオーナーシップ

私たちは、個々のエンジニアが持つ深い専門性(モバイル、バックエンド、SREなど)を、三つの責任を果たすための強力な「武器」であると定義します。しかし、専門性を磨くことは、自らの貢献を特定の領域に閉じ込めることではありません。

  • 専門性は「目的を達成するための起点」

自身の得意とする領域において、卓越した品質で構造を保ち、価値を成立させ、継続性を守ることはエンジニアとしての「ベースライン」です。しかし、私たちの真の目的は技術の出力そのものではなく、プロダクト価値の完遂にあります。

  • 「価値の完遂」が行動の射程を決める

例えば「② 価値成立」の責任を果たす上で、自身のメイン領域の修正だけでなく、APIの仕様変更やデータベースの調整、あるいはインフラの構成変更が必要であれば、技術スタックの境界を自ら越えて課題解決に当たることを求めます。

  • 技術領域に安住しない

「ここから先は自分の領域ではない」という心理的な境界線を設けず、「プロダクトを成功させるために、今、解決すべき課題は何か」を最優先する姿勢が重視されます。専門性は、自身の貢献を規定するための枠組みではなく、より広範な課題を解決するための確固たる足場として機能させます。


5. 結びに「なぜこれらの責任が信託の基礎となるのか」

事業の推進者はエンジニアに「コード」を頼むのではありません。「技術を通じて、この事業を成功させてほしい」という願いを託します。

  • 構造があるから、明日も加速できる。
  • 価値成立があるから、ユーザーに届く。
  • 継続性があるから、安心して使い続けられ、事業が成り立つ。

この三つの責任を果たすこと。それこそが、私たちが専門家として信頼を受け、プロダクトに命を吹き込むための証明です。コードの先にある、事業の未来を共に創りましょう。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら hrmos.co

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

Rails8.0にアップグレードしたらinsert_allの振る舞いが変わった

こんにちは。メドピアのお手伝いをしている@willnetです。最近寒くなってきましたね。毎年この時期に風邪をひくので最近は手洗いうがいを怠らないようにしています。このたび久しぶりにテックブログに寄稿します。

insert_allの仕様変更に関するPR

メドピア内のとあるRailsアプリケーションのバージョンを7.2から8.0にアップグレードしたときにinsert_allの仕様変更に気づいた話をします。 insert_allの仕様変更に該当するのは次のPRです。

Fix active record insert values of type cast and serialize by OuYangJinTing · Pull Request #48139 · rails/rails

上記PRではsaveメソッドとinsert_all(及びinsertinsert_all!)メソッドで値の変換方法が一致していないのを修正しています。PRではString型のカラムにArrayやHashをアサインして変更内容を検証していますが、この変更でDateTime型やTime型のカラムの振る舞いも変わることがわかりました。

変更内容

前提として、次のようにRailsのタイムゾーンを"Tokyo"にしています。

class TestApp < Rails::Application  
  config.time_zone = "Tokyo"
end

そのうえで次のようにstart_timeカラム(Time型)、start_atカラム(DateTime型)に値をいれます。どちらも文字列を利用しているのがポイントです。

Post.insert_all([{ start_time: "12:00:00", start_at: "2025-10-29 12:00:00" }])

するとRails7.2では次のようなクエリが発行されます

INSERT INTO "posts" ("start_time","start_at") VALUES ('2000-01-01 12:00:00', '2025-10-29 12:00:00') ON CONFLICT  DO NOTHING RETURNING "id"

Rails8.0ではこうです。

INSERT INTO "posts" ("start_time","start_at") VALUES ('2000-01-01 03:00:00', '2025-10-29 03:00:00') ON CONFLICT  DO NOTHING RETURNING "id"

INSERTする時刻が9時間ズレてしまいました。

もともとRailsはsave時にタイムゾーン設定を考慮して、DBにはUTCに変換した時刻を入れるようになっています。しかしinsert_allを利用したときはsaveと振る舞いが違っていました。ActiveSupport::TimeWithZoneオブジェクトがアサインされたときはUTCへの変換が行われますが、文字列がアサインされたときにはタイムゾーン変換を行わずにクエリを発行しています。Rails8.0ではこの問題が解消されてinsert_allsaveが同じように振る舞います。

感想

これはバグフィックスとしては妥当な変更だと思いますが、結果として発行するクエリが変わるので、アップグレードガイドなどに変更する旨を記載するか設定で振る舞いを切り替えられるとより良かったように思えます。Rails8.0がリリースされる前にこの仕様変更に気づいてPRを出すべきだったので、やっぱり「定期的にRailsのedgeでCIを実行する」は実施すべきプラクティスだよな〜という気持ちを新たにしました。

この記事が今後Rails8.0にアップグレードする方の参考になれば幸いです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp