メドピア開発者ブログ

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

SREチームで「AIエージェント縛り」をやってみた

はじめに

こんにちは。SREチームの侘美です。
弊社ではLLM(大規模言語モデル)を活用したコーディング、特にDevinやClaude Codeのようなエージェント型ツールを積極活用する方針を打ち立て、各種ツールを利用できる環境を整備しています。 また、習熟やノウハウの獲得のため各チームで一定期間AIエージェント縛りで開発を行い、得られた知見や課題を共有する活動も進めています。

我々SREチームでも2週間にわたってAIエージェント縛りで開発する実証実験を行いました。 本記事では、この実証実験を通じて得られた、通常のプロダクト開発チームとは異なるSREチームならではの課題や知見を共有します。

前置き

弊社SREチームの特徴

  • 5人チーム
  • 5名で社内の全プロダクトを分担して担当
  • 主な業務:
    • AWS/GCP/Azureの構築・メンテナンス
    • GitHubや各種SaaSの管理
    • プロダクトのアラート対応やフォロー
    • データ分析基盤の構築
  • 使用する言語・ツール
    • HCL(Terraform)
    • JavaScript/TypeScript
    • Python
    • etc

利用したAIエージェントツール

  • Claude Code
  • Devin
  • Gemini CLI

AIエージェント縛りに関するルール

  • 手動でのコーディングは原則禁止
    • 障害対応や納期的に厳しいものは例外
  • 任意のAIエージェントツールを利用可能
  • コミットメッセージ、Git操作、PR作成等は人間が実施してOK
  • 週の終わりに振り返り会を実施する(2週で計2回)

AIエージェントの得意領域

メリットや有効に活用できた場面をメンバー内で共有した結果、いずれのメンバーもほぼ同じ結論となっていました。その中で意見が多かったものを以下に記載します。

1. シェルスクリプト生成

全メンバーが一致して評価したのは、シェルスクリプト生成の精度の高さです。

特に以下のような作業で威力を発揮しました。

  • AWS CLI一括処理: RDSメトリクス取得、複数アカウントの管理作業
  • データ分析スクリプト: CloudWatch metricsの統計処理、CDC スループット計算
  • コード生成用スクリプト: removed block一括生成、設定ファイル自動生成

Aurora MySQL から Confluent Cloud への CDC スループット分析という複雑な要件に対し、400行を超える高度なシェルスクリプトを一発で生成することもできました。(手動では2-3時間かかる作業が30分で完成)

2. ドキュメント・PR説明文の生成

文書作成系の作業でも有効に活用できました。

  • プルリクエスト説明文: コミット履歴から適切な説明を自動生成
  • 技術ドキュメント: 実装内容をもとにした分かりやすい手順書作成
  • コミットメッセージ: Conventional Commits準拠の体裁の良いメッセージ

以下のようにClaude Code用のカスタムコマンドを作成してPR説明文生成を自動化するとかなり捗ったと紹介しているメンバーもいました。

---
allowed-tools: Bash(git log:*), Bash(git diff:*), Bash(git fetch -a:*), Bash(cat:*), Bash(pbcopy:*), Read(*.md), Fetch(*)
description: "現在の作業ブランチからプルリクの説明文を作成します。"
---

* この作業ブランチに含まれるコミットとコミットメッセージをまとめて, プルリクエストに記載するdescriptionをmarkdownで作成してください。
  * 作業ブランチとorigin/masterもしくはorigin/mainブランチと比較することで, その作業ブランチに含まれるコミットとコミットメッセージを抽出します。
    * ローカルのorigin/masterもしくはorigin/mainブランチが古い可能性も考慮してgit fetch -aを事前に実行してください。
* descriptionの書式
  * Claude Codeで作成したことがわかりやすいように, description内にClaude Codeの署名を末尾に含めてください。
  * 可読性を上げるために以下の対応を行なってください。
    * またタイトル等で絵文字を多用してぱっと見で見やすい方にしてください。
    * 可能な限り箇条書きとして閲覧しやすくします。
    * 箇条書きにした項目の関連性によって, 適切な範囲で段落を下げてください。
  * 以下の情報は不要なので省略してください。
    * プルリクエストを見るとわかる, 変更ファイルの一覧や統計情報は冗長であるため不要です。
    * GitHub ActionsのCIによってチェックされる項目はdescriptionには不要です。
* 作成完了しましたら, クリップボードに格納してください。
  * 末尾にEOF等のdescriptionとは関係のない文字列が入らないようにしてください。

3. JSON・設定ファイルの編集

構造化データの編集作業においても手作業に比べてかなりの効率化が見られました。

  • Slack通知のJSONペイロード: 複雑な条件分岐やフォーマット調整
  • Lambda関数の軽微な修正: ESM形式への変更、パッケージ設定の調整

4. 小規模の修正を広範囲に実施するケース

大量のファイルに似た小さい修正を行うような作業でも効果的に感じました。

  • アカウント削除: 複数サービスからの特定ユーザー一括削除
  • 設定ファイル更新: 複数環境への同一設定適用
  • リソース名変更: 命名規則に合わせた一括リネーム

AIエージェントの苦手領域・課題

一方でうまく実装できない領域や課題も多く発見されました。 特にこの実証実験前から懸念していた、Terraformなどに代表される宣言的コードとの相性の悪さが浮き彫りになりました。

1. Terraformとの相性の問題

全メンバーが一致して指摘していました。

古いリソース・非推奨属性の提案

  • TerraformのProvider更新が頻繁なためか、AIの精度が低い
  • 推奨されない属性や廃止予定のリソースタイプを提案
  • 最新のベストプラクティスに従わないコード生成
  • MCPを利用してもそこまで改善されない

宣言的コードの特性による非効率性

Terraformなどの宣言的コードでInfrastructure as Codeを実現するケースにおいては、設計が完了すれば後はその設計をそのままコード化するだけなので実装上で悩むシーンはそこまで多くありません。

一方、Ruby等のプログラミング言語でバックエンドサービスを構築する場合、設計が完了した後もメソッドの分割の仕方や同じ処理系でも実装方法が無限にあるなど、詳細設計〜実装〜テストとまだまだ考慮すべき点が山程あります。この工程をAIに行わせることでかなりの生産性向上が見込めます。

そのため「自然言語で作業を指示する」というAIエージェントの利用方法とTerraformの相性の悪さを感じる結果となりました。

学習データ不足

Terraformのコードはそこまで複雑ではないのにもかかわらず、LLMによるコーディングの精度が悪いことは各所で言及されています。

一般的なプログラミング言語はOSSとして質の高いコードがいくつもWeb上で公開されています。 Terraformの場合もAWSが提供する公式モジュール等、良いコードがいくつも公開されています。 ですが私達サービスを構築するSREチームが参考にすべきような、『実際に運用されている大規模サービスのTerraformコード』が公開されている例は多くないと思います。

このように、実用に耐えうるレベルの質の高い学習のためのコード不足がそのまま他の言語と比べて精度が悪いといった結果を引き起こしているのでは?という考察もメンバー内で上がっていました。

2. 指示作成コストの高さ

複雑な要件の言語化が困難

  • 正確な実装を得るために、自然言語でterraformを記述することになる
  • プロンプト作成時間が実装時間を上回るケースが頻発
  • 大きい作業は1プロンプトより細かく小出しに指示した方が正確だが、これは結局、自然言語でコードを書いているようなもの

3. editorconfig・Linterルールの無視

設定ファイルの強制適用が困難

  • .editorconfigの設定をAIに従わせる効果的な方法がない
  • コード生成後に手動でフォーマット修正が必要
  • 自動修正(fix)ツールの不足

こちらはHooksの登場により、整備することである程度解消できる目処が見えてきましたね。 (実証実験中にHooksが発表されました)

一貫性のないコードスタイル

  • チームの規約に合わないインデントや改行
  • 命名規則の不統一
  • コメントスタイルの不一致

4. git操作の危険性と制御の困難さ

予期しない操作の実行

このあたりはAIエージェントあるあるで有名だと思いますが、実際に期間中にメンバーがいくつか遭遇していました。

  • rebase指示すると作業がループする
  • 追加実装したコードを消される
  • PRが勝手にクローズされる

(SREチームがコミットメッセージやコミットの粒度に厳しいメンバーが多く、より綺麗なコミットに修正しようとしたため遭遇率が高かった可能性もあります)

5. コストと経費処理の複雑さ

Claude Codeの支払いを会社側が行い、社員をメンバーとして利用させるようなチームプラン的なものが現状ありません。 そのため弊社では「AWS/GCPでClaudeを従量課金で動かして一人 100USD/月 を超えないように管理する」or「個人でProプランやMAXプランを契約して経費精算」という少々手間のかかる運用になっています。

その他知見

AWSコスト分析

AWS Cost Explorerから取得したデータを使い、アカウント別コスト比較や増減理由の分析を素早く行うことができました。

mise.toml活用

asdfからmiseに移行し、タスクランナー機能でvalidate, fmt, lint, trivyを統合実行できる環境を構築することで、指示やカスタムコマンドを簡略化できて効率的でした。

まとめ

2週間実装の全てをAIエージェントにすることで、得意不得意や有効的な活用方法が見えてきました。 特に現状の精度では、SRE領域のタスク全てを無理にAIエージェントに任せようとすると、かえって生産性が落ちる状況です。そのため、これらの知見をチームで共有できた点でも、かなり有用な実証実験だったと考えられます。

現状、ある程度の使い分けの方針は見えてきましたが、AIに関しては日進月歩で進化している領域なのでこれら苦手領域を克服してくる日も遠くないのでしょう。 随時エージェントの性能改善に関する情報をキャッチアップしつつ最も業務効率が上がる使い方を模索し続ける必要がありそうです。


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


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

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

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

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

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

YAML + Rakeタスクで実現する「権限漏れゼロ」なBI運用

こんにちは、サーバーサイドエンジニアの長谷川(@hasehiro25)です。

今回の『ClinPeerアプリ開発の裏側』連載では、BIツール運用の実践的なTipsをご紹介します。 tech.medpeer.co.jp

BIツールの運用における課題として、「テーブルやカラムが追加された際のメンテナンスに手間がかかる」があります。

参照できるカラムをリストで管理する「許可リスト方式」では更新漏れが起きやすく、逆に参照できないカラムを管理する「拒否リスト方式」では、意図せず個人情報などのカラムが参照可能になってしまうリスクがあります。

そこでClinPeerでは、テーブルやカラムが変更された際にCIでチェックを行い、BIで参照できるデータを安全に更新する仕組みを導入しています。これにより、権限設定の抜け漏れを防いでいます。

本格的な分析用BIツールと、エラー調査用の簡易BIツールとしてBlazerをClinPeerでは導入しており、今回は後者のBlazerをメインにご紹介します。なお、両ツールとも共通でBI用スキーマファイルを使用しています。

github.com

specによるschemaチェック

ClinPeerでは、schema_for_bi.ymlというファイルで、Blazerから参照できるスキーマ情報を管理しています。

ファイルの中身は以下のようなイメージです。

articles:
  - id
  - created_at
  - updated_at
  - title
  - body

users:
  - id
  - created_at
  - updated_at
  - nick_name
  # - email

YAMLファイルの構造は、articles:のようにまずテーブル名を記述し、その配下にカラムをリスト形式で記述するシンプルなものです。

ここで重要なのが、リストの中でも#でコメントアウトされているカラムの扱いです。これらは意図的に参照を許可しないカラムとして扱われます。

コメントアウトを用いる目的は、カラムの存在を把握しつつ、「これは意図的に許可してないよ」という状態を誰が見ても分かるようにするためです。 もし「許可リスト方式」の場合、それが意図的に書いてないのか、単なる追加忘れなのかを区別できません。コメントアウトによって、その曖昧さをなくしています。

このYAMLファイルがデータベースの現状と一致していることを担保するために、CIで以下のようなspecを実行しています。

it "テーブル定義順がABC順であること" do
  tables = YAML.load_file("db/schema_for_bi.yml").keys
  expect(tables).to eq(tables.sort)
end

it "全てのスキーマ情報が記載されていること", aggregate_failures: false do
  expect_tokens = []
  ActiveRecord::Base.connection.tables.sort_by(&:itself).each do |table|
    expect_tokens << table
    columns = ActiveRecord::Base.connection.columns(table).map(&:name)
    columns.each { |column| expect_tokens << column }
  end

  yml_lines = File.readlines("db/schema_for_bi.yml")
  yml_lines.fill("", yml_lines.size..expect_tokens.size - 1).zip(expect_tokens).each do |actual_line, expect_token|
    expect(actual_line).to include(expect_token)
  end
end

内容はシンプルで、ActiveRecord::Base.connection.tables でテーブルとカラムの情報を全て取得し、File.readlinesで読み込んだYAMLファイルの内容と一致するかを一行ずつ確認します。(コメントアウト部分含む)

このspecがパスすれば、YAMLファイルがデータベースの現状を正しく反映していることが保証され、安全にBlazerの権限設定に進めます。

Rakeタスクによる権限更新

specのチェックを通過したYAMLファイルを使い、実際に権限を更新するためのRakeタスクがこちらです。

namespace :bi do
  task initialize_user: :environment do
    exec = ->(sql) { ActiveRecord::Base.connection.execute(sql) }
    exec["DROP USER IF EXISTS #{Setting::Blazer.db_user_name};"] # 権限リセット
    exec["CREATE USER #{Setting::Blazer.db_user_name} IDENTIFIED BY '#{Setting::Blazer.db_user_password}';"]
    exec["GRANT SHOW VIEW ON * TO #{Setting::Blazer.db_user_name};"]
  end

  task grant_select_columns: :initialize_user do
    YAML.load_file("db/schema_for_bi.yml").each do |table, columns|
      # 「read」など、MySQLの予約語に定義されたカラム名を、テーブル登録するカラム名として認識されるように、バッククォートで囲う
      columns = columns.map { |s| "`#{s}`" }.join(",")
      ActiveRecord::Base.connection.execute("GRANT SELECT (#{columns}) ON #{table} TO #{Setting::Blazer.db_user_name};")
    end
  end
end

initialize_userで古い権限設定を無効化して、新たな権限を適用する初期化処理を実行します。

そのあとgrant_select_columnsではYAML.load_file を使ってYAMLファイルを読み込み、それぞれのテーブルとカラムに対して権限設定を行なっています。コメントアウト部分は無視されるため、結果として参照を許可したいカラムにのみ SELECT 権限を付与する GRANT 文が実行される仕組みになっています。

grant_select_columnsの実行タイミングですが、他のデプロイ用タスクと合わせて追加しており、デプロイ時に自動で実行されるように設定しています。

namespace :deploy do
  task pre_hook: %i[setting:validate db:create db:migrate db:seed bi:grant_select_columns]
end

まとめ

BIツールの権限設定は後回しにされがちで、「いざデータを見たい!」という時に参照できない、といったことが起こりがちです。

今回ご紹介したように、テーブル構造の変更と同時に権限設定もチェックする仕組みをCIに組み込むことで、最新かつ安全な状態でデータを参照できる環境を維持できます。

「いつかやろう」と溜まりがちな作業は、CIを活用して日々コツコツと対応していくことで、少しずつ快適な開発環境に繋がっていくと思いますので、ぜひ参考にしてみてください。


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


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

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

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

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

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

Notion のタスクのメモはどこに書く?コメント機能よりも「ページ下部」がオススメな理由と実践方法

こんにちは。メドピア内 Slack チャンネル 「#club_notion」 部長の佐藤太一(@teach_kaiju)です。

今回の「ClinPeerアプリ開発の裏側連載記事」では Notion を用いたタスク管理におけるメモの取り方を紹介します。

tech.medpeer.co.jp

目次

はじめに

Notion はその自由度の高さから、様々な情報を集約できる万能ツールとして扱うことができます。特にタスク管理においては、詳細情報だけでなく、関連するメモやアイデア、Slack でのやりとりのリンクなどを一緒に残しておきたい場面も多いのではないでしょうか。

そんなとき、「タスクに関するメモ、どこに書いていますか?」

よく使われるのは Notion の画面上部のコメントだと思います。

コメント(画面上部)

手軽に書ける反面、他タスク管理ツールとの挙動の違いで戸惑った経験はありませんか?

今回は ClinPeer チームの開発タスクで実際に行われている、メモはコメント機能ではなく「タスクページの下部」に書くという文化を紹介したいと思います!

コメント機能のよくある課題点

手軽に使えるコメント機能ですが、タスクのメモを残す場所としては、いくつかの課題があります。 情報は2025年5月時点のものです。

課題1: コメントのリンクを取得することができない

コメントのスレッドのリンクは取得できますが、2つめ以降のコメントはリンクが取得できません。

コメントへのリンクがない

課題2: Enter で送信

誤って書き途中のまま送信してしまいがち。
(Enter は改行という挙動に切り替えられるようになってほしい)

課題3: 場所が画面上部で折り畳まれるデザイン

画面上部のコメントは数が多いと折り畳まれます。中身を確認するために開く必要があり、これが手間です。

コメントの折りたたみ

ページ下部へのメモの仕方

ページ下部にメモをとることで、上記の課題を解決できる他、ブロックを用いた見やすいデザインを作ることができる等のメリットも得られます。

その具体的な方法がこちらです。

ページ下部にメモ

  1. 「Memo」という見出しを作る
  2. 日付を書く
  3. 誰が書いたのかを示すために自身の絵文字のアイコンを出す(省略することはよくあります)
  4. 内容を記述

主なメモの内容

  • タスクの進捗
  • 関連するやりとりへのリンク
  • 思ったこと・気づき
  • 課題
  • 簡易的な議事録

特にタスクに関連するやりとりへのリンク(Slack等)はメモっておくと後から見返す時に非常に助かります。

タスクに関するMTGを行う場合は、最近出た AI ミーティングノートをタスクページに作るのも良さそうです。

Q & A

Q. 他のメンバーへの通知はどうする ?
A. 画面上部のコメント使います。内容が多い時は Memo のリンクをコメントに貼ります。ページ内メンションは使っていません。

Q. ページがどんどん長くなって見づらくならないか?
A. 画像を大量に貼ったりすると長くなりますが、トグルで折り畳めば気になりません。

まとめ

今回はタスクのメモをページ下部にとる具体的なやり方を紹介しました。

タスクにメモをとっておくと、タスクページそのものがドキュメントの役割を果たしたりすることが可能になります。情報を追いやすくなるためとてもオススメです。

また、本手法はチームでのタスク管理だけではなく、個人でのタスク管理でも使えます。
ぜひ試して見てください!


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


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

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

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

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

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

SwiftUIにおけるEnvironmentの活用法

こんにちは!メドピアにてモバイルアプリエンジニアをしている王です。 今回の「ClinPeerアプリ開発の裏側連載記事」では、SwiftUIのEnvironmentについてお話しできればと思います。 tech.medpeer.co.jp

SwiftUIのEnvironmentは、ビュー間でデータを共有するための強力な依存性注入(DI)の仕組みです。多くのSwiftUIプロジェクトで活用されており、ビュー間のデータ伝達を簡素化し、アプリ設計の柔軟性を高めます。ClinPeerでは、ほぼすべての画面をSwiftUIで構築しており、Environmentの活用について考察しています。

SwiftUIにおけるUI専用の依存性注入

依存性注入(Dependency Injection、DI)は、モダンなソフトウェア開発において重要な技術であり、以下の利点があります。

  • 疎結合:コンポーネント間の依存関係を最小限に抑えることで、モジュールの再利用性と保守性が向上します。
  • 保守性:依存関係が明示的になることで、コードの理解と修正が容易になります。
  • テスト容易性:依存関係を差し替えることで、ユニットテストが容易になります。

SwiftUIのEnvironmentは、ビューがロードされた後にのみ値を取得できるという特徴があります。この設計は、SwiftUIが宣言型UIフレームワークであることを反映しており、ビューの構築と更新がデータや環境と一貫性を保つようになっています。

値型以外の活用

Environmentは、単なる値型の注入にとどまらず、関数、ファクトリーメソッド、プロトコル制約など、さまざまな形式の依存性を注入できます。実際、SwiftUIの標準Environment値には、editModedismissmanagedObjectContextなど、非値型の例も含まれています。

開発者は、Environmentの活用を値型に限定せず、SwiftUIの特性を活かして、より柔軟で疎結合なコンポーネントを構築することが重要です。

Observationフレームワークの活用

EnvironmentObjectを使用する際、依存性の注入を忘れるとアプリがクラッシュするリスクがあります。一方、Environmentはデフォルト値を要求するため、より安全で信頼性の高い設計が可能です。iOS 17以降、Observationフレームワークの導入により、可観測オブジェクトの注入がさらに容易になりました。

extension EnvironmentValues {
    @Entry var article: Article = .init()
}

@Observable
class Article {
    // プロパティやメソッドを定義
}

struct ContentView: View {
    @Environment(\.article) var article
    var body: some View {
        // ビューの構築
    }
}

この方法では、クラッシュのリスクを回避し、同じ型の可観測インスタンスを複数使い分ける柔軟性も得られます。

extension EnvironmentValues {
    @Entry var article: Article = .init()
    @Entry var article1: Article = .init()
    @Entry var article2: Article = .init()
}

Environmentの最適化

Environmentを使用してアプリの状態を管理する際、ビューの更新効率がユーザー体験に影響を与えることがあります。以下の最適化戦略を採用することで、不要なビューの再描画を抑えることができます。

精密な注入

複数のサブ状態を含む複合値型に対して、特定のプロパティのみを注入することで、不要な更新を回避できます。ビューが実際に必要とする部分の状態だけを購読することで、より効率的なリアクティブUIを構築できます。

struct UserState {
    var height = 175 // 単位: cm
    var weight = 75  // 単位: kg
}

extension EnvironmentValues {
    @Entry var userState = UserState()
}

struct HeightView: View {
    // heightのみが更新対象
    @Environment(\.userState.height) var height
    var body: some View {
        Text("身長: \(height) cm")
    }
}

struct WeightView: View {
    // weightのみが更新対象
    @Environment(\.userState.weight) var weight
    var body: some View {
        Text("体重: \(weight) kg")
    }
}

struct BMIView: View {
    @Environment(\.userState) var userState
    var body: some View {
        let heightInMeters = Double(userState.height) / 100.0
        let bmi = Double(userState.weight) / (heightInMeters * heightInMeters)
        return Text(String(format: "BMI: %.2f", bmi))
    }
}

struct RootView: View {
    @State var userState = UserState()
    var body: some View {
        List {
            Button("身長を変更") {
                userState.height = Int.random(in: 130...220)
            }
            Button("体重を変更") {
                userState.weight = Int.random(in: 35...120)
            }
            HeightView()
            WeightView()
            BMIView()
        }
        .environment(\.userState, userState)
    }
}

条件付きの更新

transformEnvironmentを使用すると、特定の条件を満たす場合にのみ環境値を更新できます。これにより、更新頻度を減らし、アプリの応答性と滑らかさを向上させることができます。

struct RootView: View {
    @State var userState = UserState()
    @State var height = 175
    var body: some View {
        List {
            Button("身長を変更") {
                height = Int.random(in: 130...220)
            }
            HeightView()
        }
        .transformEnvironment(\.userState) { state in
            guard height > 150 else {
                print("無視: \(height)")
                return
            }
            state.height = height // height > 150 の場合のみ更新
        }
    }
}

Environmentとサードパーティ製DIフレームワークの併用

SwiftUIのEnvironmentは優れた機能を提供しますが、ビューのライフサイクルに厳密に制限されます。ビジネスロジックをViewModel層に分離したり、ユニットテストを行う場合、サードパーティ製のDIフレームワークの使用が有効です。ClinPeerでは、FactoryというSDKを使用しています。

@Observable
class UserState {
    var height: Int = 175
    var weight: Int = 75
}

extension Container {
    var userState: Factory<UserState> {
        Factory(self) { UserState() }
            .scope(.shared)
    }
}

// 使用例
@Injected(\.userState) private var userState

ハイブリッドアーキテクチャの利点

Environmentとサードパーティ製DIツールを併用することで、以下の利点が得られます。

  • 柔軟なアーキテクチャ:ビュー層ではSwiftUIのEnvironmentを使用し、ビジネスロジック層ではFactoryなどのDIツールを活用できます。
  • テストの容易性:ビジネスロジックをUIから完全に切り離すことで、ユニットテストやモック化が簡単になります。
  • 保守性の向上:特定のUIフレームワークへの依存を最小限に抑え、コードベースの長期的な安定性と再利用性を高めます。

まとめ

SwiftUIのEnvironmentは、依存性注入とビューのライフサイクルを一体化した設計であり、視覚的なUIコンポーネントの境界を明確にしながら、ビュー階層におけるデータの効率的かつ制御可能な伝達を実現します。

また、精密な注入や選択的な変更によって、不要なビュー更新を避けることでアプリ全体の効率を高めることができます。 さらに、SwiftUIのエコシステムにおいては、柔軟にサードパーティ製のDIフレームワークを取り入れることで、より複雑なユースケースにも対応可能です。

Environmentの設計に対する深い理解は、拡張性が高く品質の高いSwiftUIアプリを構築するための確かな基盤となります。


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


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

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

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

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

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

環境ごとの設定管理が可能な ClinPeer のフィーチャーフラグの紹介

こんにちは。サーバーサイドエンジニアの佐藤太一(@teach_kaiju)です。

今回の「ClinPeerアプリ開発の裏側連載記事」ではサーバーサイドにおける、フィーチャーフラグの実装方法を紹介します。

tech.medpeer.co.jp

目次

フィーチャーフラグとは

フィーチャーフラグ(機能フラグ、別名フィーチャートグル)は、機能のオン・オフを制御する仕組みです。
ClinPeer ではフィーチャーフラグを用いることで、開発中の機能の細かいリリースや、社内IPのみ機能を有効にするなどの柔軟な制御を実現しています。

フィーチャーフラグに関する詳細は以下の記事に書かれています。こちらもぜひご覧ください。

tech.medpeer.co.jp

機能の有効化 Feature#enabled?

ClinPeer では Flipper gem をラップした Feature クラスを用いています。

基本的な使い方

特定の機能(例:allow_access_to_new_sugoi_feature)が有効かどうかを調べるには、以下のように記述します。

if Feature::ALLOW_ACCESS_TO_NEW_SUGOI_FEATURE.enabled?

特定の条件で有効化

enabled? メソッドには、オプションで引数を渡すことができます。この引数を使うことで、「特定のユーザーだけに機能を有効にする」「特定のIPアドレスからアクセスされた場合のみ機能を有効にする」といった、より細かい制御が可能になります。

例えば、特定のIPアドレスからのアクセスに対してのみ機能を有効にしたい場合は、そのIPアドレス文字列を渡します。

if Feature::ALLOW_ACCESS_TO_NEW_SUGOI_FEATURE.enabled?(request.remote_ip)

許可するIPアドレスは、Flipper UI で設定します。

flipper_ui_allow_ip

フラグの運用

フィーチャーフラグの定義や操作は、主に config/features.yml ファイルと Flipper UI を通じて行います。

features.yml

フィーチャーフラグの設定は、config/features.yml ファイルで一元管理されます。このファイルには、各フラグの識別子 (kind)、説明 (description)、そして環境ごとの設定を記述します。

# config/features.yml の例
- kind: allow_access_to_new_sugoi_feature
  description: 新しいスゴイ機能へのアクセスを許可する
  development: flipper
  test: true
  staging: flipper
  production: flipper

各項目の意味は以下の通りです。

  • kind: フラグの一意な識別子です。コード中では Feature::KIND_NAME のようにして参照できます。
  • description: フラグの説明です。この内容は Flipper UI のダッシュボードにも表示されます。
  • 環境名 (development, test, staging, production など):
    • flipper: Flipper UI でフラグの有効/無効を制御する場合に指定します。(デフォルト: 無効)
    • true: その環境では常にフラグを有効にします。
    • false: その環境では常にフラグを無効にします。

ClinPeer ではデプロイ時にseed を実行し、その中で features.yml の内容をもとに差分を更新します。

# app/models/feature.rb の抜粋

class Feature < ActiveYaml::Base
  include ActiveHash::Enum

  # 略
  
  set_root_path Rails.root.join("config")

  enum_accessor :kind

  scope :flipper_controllable, -> { where(Rails.env => FLIPPER_VALUE) }

  FLIPPER_VALUE = "flipper"
  private_constant :FLIPPER_VALUE

  # 略
end
# seed の処理

features = Feature.flipper_controllable.pluck(:kind) # ymlからFlipper制御対象のkindを取得
current_features = Flipper.features.map(&:name) # 現在Flipperに登録されている機能名を取得

# ymlにあってFlipperにないものを追加
(features - current_features).each { |f| Flipper.add(f) }
# Flipperにあってymlにないものを削除 (ymlから削除されたフラグ)
(current_features - features).each { |f| Flipper.remove(f) }

フラグの新規追加

config/features.yml に新しいフラグの定義を追加します。 デプロイ時に seed で features.yml の内容をもとに差分を更新します。

フラグの有効・無効の切り替え

config/features.ymlflipper と設定されているフラグの有効/無効は、Flipper UI (社内用管理画面) 上で操作します。

flipper_ui_on_off

フラグの削除

不要になったフィーチャーフラグを削除する際は、以下の手順で行います。

  1. フラグの参照箇所をコード上から削除
  2. 上記対応をリリース
  3. config/features.yml から該当フラグの定義を削除

ポイント
「フラグ参照箇所の削除」と「ymlからのフラグ定義の削除」を同一のリリースに含めないようにしています。

  • Flipper は、存在しないフラグを参照した場合、無効 (false) として扱われます。
  • ymlの変更(フラグ定義の削除)を反映するデプロイタスクは、アプリケーションコードの反映よりも先に実行される場合があります。

もし同一リリースに含めてしまうと、ymlからフラグが削除された後、まだ古いコードがそのフラグを参照しているわずかな時間帯に、意図せず機能が無効化されてしまう可能性があります。

条件付き有効化の実装

Flipper には対象をflipper_idで識別し、一致した場合のみ機能を有効化するという機能があります。 具体的には以下の2つを比較し、一致した場合機能を有効化します。

箇所
enabled? の第二引数 puts some_obj.flipper_id # 127.0.0.1
Flipper.enabled?("allow_access_to_new_sugoi_feature", some_obj)
Flipper UI で設定した actor
flipper_ui_allow_ip

(actor は flipper_id という識別子を持ったオブジェクト。この識別子を比較することで actor が同一であるかどうかを判断しています。そして、actor が同一であれば機能を有効化します。)

https://www.flippercloud.io/docs/features/actors

ClinPeerでは StringFlipperActor クラスを導入することで、任意の文字列を直接Actorの識別子として扱えるように拡張しています。

# app/models/feature.rb の抜粋

class Feature < ActiveYaml::Base

# 略

  class StringFlipperActor
    attr_reader :value

    def initialize(value)
      @value = value
    end

    alias flipper_id value
  end

# 略

  def enabled?(obj = nil)
    case value
    when FLIPPER_VALUE
      obj = StringFlipperActor.new(obj) if obj.is_a?(String)
      Flipper.enabled?(kind, obj)
    else
      !!value
    end
  end

  private

  def value
    public_send(Rails.env)
  end
end

これにより、Feature#enabled? メソッドに文字列を渡すと、その文字列がそのまま flipper_id として扱われます。

Feature::ALLOW_ACCESS_TO_NEW_SUGOI_FEATURE.enabled?("127.0.0.1") # "127.0.0.1" がflipper_idとなる

この仕組みを利用することで、IPアドレスや特定の識別文字列など、モデルオブジェクトが存在しないようなケースでも柔軟にActorベースのフラグ制御を行うことができます。

生成AIを活用したフラグ削除

フィーチャーフラグは、機能のリリースサイクルを柔軟にする強力なツールですが、役目を終えたフラグは適切に削除していく必要があります。フラグが増えすぎると、コードの複雑性が増し、管理コストも増大するためです。

従来、フラグの参照箇所の削除は以下の手順で行っていました。

  1. コードベース全体から、削除対象フラグの参照箇所を検索する。
  2. 特定された参照箇所を一つ一つ手動で修正・削除する。

このプロセスは、特に view 等の分岐が複雑な場合、時間と手間がかかり、見落としのリスクも伴いました。

そこで、現在はフラグの参照箇所の削除に生成AIを使用しています。プロンプトの例を以下に示します。

Feature::{フラグ名}は常に{true or false}なフラグとなりました。
上記を参照しているすべての条件分岐を削除してください。
features.ymlから対象のフラグの削除はしないでください。
features.ymlの該当のフラグに「TODO: 参照箇所削除済み、削除予定」というコメントを追加してください。
参考:
 - kind: allow_access_to_new_sugoi_feature # TODO: 参照箇所削除済み、削除予定
 
その後 features.ymlをコミットしてください
コミットメッセージ: Git履歴を残すために削除予定のコメント追加

AIを活用すると、手動と比較して、以下のようなメリットが見込めます。

  • 参照箇所の自動特定: AIがコードを解析し、削除対象のフィーチャーフラグが使用されている箇所を迅速に特定します。
  • 修正コードの提案: 特定された箇所に対して、AIが適切な修正案(フラグ参照の削除や、条件分岐の恒久化など)を提案してくれる場合があります。
  • 作業時間の短縮とミスの削減: 手作業による検索や修正と比較して、作業時間を大幅に短縮し、ヒューマンエラーによる見落としや修正ミスを減らすことができます。

最終的なコードの確認とテストは開発者自身が行う必要がありますが、AIツールを補助として利用することで、フィーチャーフラグのライフサイクル管理をよりスムーズかつ安全に行えるようになると考えています。

おわりに

本記事では、ClinPeerにおけるフィーチャーフラグの実装と運用方法について紹介しました。 Flipperという強力な基盤ライブラリを利用しつつ、Feature というActiveHashモデルでラップすることにより、アプリケーション固有の事情や、より使いやすいインターフェースを開発チームに提供しています。

このように、フィーチャーフラグシステムを適切に抽象化(ラップ)することには、多くのメリットがあります。

  • 管理の容易化: フラグの定義を一元化 (features.yml) し、環境ごとの挙動を明確にすることで、管理コストを低減します。
  • 利用の簡便化: Feature::KIND_NAME.enabled? のような直感的で統一されたインターフェースを提供することで、開発者が迷うことなくフラグを利用できます。
  • 将来的な拡張性: 例えば、将来的に別のフィーチャーフラグ管理システムに移行する場合でも、Feature クラス内部の実装を変更するだけで済み、アプリケーションコードへの影響を最小限に抑えることができます。
  • 独自のロジックの追加: StringFlipperActor のように、特定のニーズに合わせた独自のロジックを組み込みやすくなります。

フィーチャーフラグは、アジャイルな開発、安全な機能リリース、そしてA/Bテストなど、現代的なソフトウェア開発において非常に有効なプラクティスです。 ClinPeerでは、このような仕組みを活用し、ユーザーにより良い価値を迅速に届けられるよう、日々改善を続けています。

この記事が、フィーチャーフラグの導入や運用を検討されている方の一助となれば幸いです。


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


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

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

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

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

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

GitHub Copilot を味方につける:AI に渡すコンテキスト整備の工夫

こんにちは。事業本部開発部 MISP グループのフロントエンドエンジニアの小林和弘(@kzhrk0430)です。

メドピアでは「AI ファーストカンパニー」を目指すことを全社で掲げています。実際に社内では、AI ツールを活用して業務を効率化する動きが活発に行われています。たとえば、Gemini を使って Google Meet の文字起こしや議事メモを作成したり、Notion AI で要件定義とテストケースの整合性を確認したり、スライド作成を AI に任せたりと、日々の業務に AI を積極的に取り入れています。

今回は、開発環境をより快適にするために GitHub Copilot(VS Code 拡張)を活用した取り組みをご紹介します。

AI に渡すコンテキストを整備する

Copilot は多様な機能を提供していますが、それぞれ異なる種類のコンテキストを参照している印象があります。そのため、意図通りに動作させるには、各機能ごとに適切なコンテキストを整備することが重要です。

今回は、以下の 3 点にフォーカスして取り組みました:

  • VS Code におけるコード生成
  • GitHub におけるコードレビュー支援
  • VS Code におけるコミットメッセージ自動生成

VS Code におけるコード生成

まず、コード生成機能に関しては、.github/copilot-instructions.md というファイルをプロジェクトに追加しました。このファイルには、プロジェクト特有の文脈や設計方針、命名規則などを記載しています。

作成時には、まず Copilot に初稿を書かせ、その内容を人間がレビュー・修正してブラッシュアップするという流れを取りました。なお、社内では AI ツールの Cline も利用しているため Cline 用にコンテキストを渡す.clinerules ファイルには .github/copilot-instructions.md を参照させるよう設定し、Cline 経由でも文脈が共有されるようにしています。

.clinerules の中身はこの用になっています。

# やくばとシステム開発ガイドライン

## 注意事項

このファイルは参照用として保持されていますが、最新かつ詳細な開発ガイドラインは `.github/copilot-instructions.md` に移動しました。
開発作業を行う際は、`.github/copilot-instructions.md` を参照してください。

GitHub Copilot をはじめとする開発ツールは、`.github/copilot-instructions.md` を参照してコード提案やガイドラインの適用を行います。

## リンク

開発ガイドラインの詳細は [.github/copilot-instructions.md](.github/copilot-instructions.md) を参照してください。

当初は .clinerules.github/copilot-instructions.md と同様に Cline に生成させていたのですが、コンテキストが二重管理になっていたため Copilot Agent に 2 つのファイルを統合させて .clinerules の中身を書き換えています。

GitHub におけるコードレビュー支援

GitHub の Pull Request テンプレートにも Copilot 活用の工夫を加えました。具体的には、Copilot のレビューがわかりやすくなるように、PR テンプレート内にレビューのコンテキストとなる情報を明記するようにしています。

今はまだレビューを日本語で書かせて、レビューコメントの表示がバグる HTML タグのコメントのルールを書いているだけですが、今後コンテキストを増やしてレビュー支援の質を高めて、より有用なフィードバックが得られるようにしたいと考えています。

<details>
<summary>このブロックは Copilot レビューのためのコンテキストです。Copilot は下記の命令を守ってください。</summary>

- レビューコメントは日本語で行う
- レビューコメントの HTML タグはマークダウンの Code spans (`) でラップする

</details>

GitHub 側でコンテキストを設定する機能は用意しているようですが、Copilot Enterprise プランでのみ利用でき、現在は一部のユーザーしか利用できない状態なので、今回の PR の説明文にコンテキストを注入する方法は一時的なハックになっています。

VS Code におけるコミットメッセージ自動生成

Copilot Chat 拡張機能のひとつに、コミットメッセージを自動生成してくれる機能があります。これに対しても、プロジェクトの文脈を反映させる設定を行いました。

具体的には、以下のように VS Code の settings.json.github/copilot-instructions.md を指定しています:

{
  "github.copilot.chat.commitMessageGeneration.instructions": [
    {
      "file": ".github/copilot-instructions.md"
    }
  ]
}

.github/copilot-instructions.md にはコミットメッセージにおけるルールを下記のように記載しています。

## 提案すべきコミットメッセージ

- コミットメッセージは日本語で書く
- git log で参照した過去のコミットメッセージを参考にする
- Conventional Commit を基本とする
- 1 行目のコミットの下には空白の行間を設ける
- 複数行の詳細なコミットメッセージを書く
  - 詳細なコミットメッセージは `## 背景``## 修正内容` などのマークダウンの見出しをつける

この設定により、コミットメッセージの生成時にもプロジェクト固有の背景が反映されるようになり、精度の高い出力が得られるようになりました。

Copilot で生成したコミットメッセージの一例

簡単なコミットメッセージであれば Copilot でコミットメッセージをジェネレートさせてさっとレビューするだけでコミットを作成しています。

VS Code における Copilot 設定の展望

VS Code の Copilot の instructions の設定は Experimental で提供されていますが、下記の 5 つの機能にコンテキスト設定ができるようになっています。

  • Review Selection: Instructions
  • Code Generation: Instructions
  • Commit Message Generation: Instructions
  • Pull Request Description Generation: Instructions
  • Test Generation: Instructions

VS Code の Copilot の設定画面のキャプチャー

GitHub 側も、.github/copilot-instructions.md にすべてのユースケースの情報を統合することが難しいと判断したのか、将来的には用途ごとにマークダウンファイルを分けることを検討しているのかもしれません。

AI に渡すコンテキスト整備の重要性

最近公開された Devin Wiki は、AI のコード理解能力を強く印象づけるものでした。

メドピアでも Devin AI を導入しており、社内で Devin Wiki の生成結果を確認する機会がありました。その中で、医療機関向けおよび薬局向けの Nuxt アプリを管理しているモノレポ構成のリポジトリに対して、Devin が自動的に、医療機関・薬局・患者間の処方せんの流れを示すフローチャートを生成していたのを目にし、大きな驚きがありました。

ただし、すべての機能について完璧に Wiki 化されているわけではなく、ハルシネーション(誤生成)が起きそうな領域については、あえてページを生成しないようにしているケースも見受けられました。

このように、AI ツールの進化によって、今後さらに多くの業務が AI によって支援・代替されるようになると感じています。これはエンジニアの仕事を奪うということではなく、むしろ課題発見力や判断力といった本質的な能力に集中できる環境が整っていく、というポジティブな変化だと考えています。

この点については、VPoE の保立さんも以下のインタビュー記事で言及しています: style.medpeer.co.jp

現時点では、AI がどこからどのようにコンテキストを取得しているのかを意識し、AI が誤解しないようなデータセット(明確な変数名や整理されたコード構造など)を整備することが、エンジニアに求められていると強く感じます。

おまけ:AI 活用と執筆の裏側

「AI に渡すコンテキストを整備する」セクションは、まず箇条書きで要点を整理し、それを ChatGPT に文章化してもらったうえで、内容を加筆・修正して仕上げました。

その他のセクションは、最初に自分で文章を書き、その後 ChatGPT にレビューを依頼して改善点を洗い出しました。

また、OGP 画像も記事をレビューさせたついでに ChatGPT に出力させています。

このように、試せるところから積極的に AI を活用し、自分なりに現在地を確認していくことが、AI 時代を前向きに生きる上で大切な姿勢だと考えています。

今後も、実務に即した形での AI 活用について、実験と発信を続けていきたいと思います。


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


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

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

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

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

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

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

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

RubyKaigi 2025に参加されていた皆さん、お疲れ様でした。

今回、内藤がRubyKaigi 本編に登壇しました。 発表内容の詳細は以下の記事にまとめておりますのでよろしければご覧ください。

naitoh.hatenablog.com

セッションレポート

RubyKaigi のセッションの中で特に印象に残ったセッションをご紹介します。 タイムテーブルは下記から確認ください。

rubykaigi.org

Make Parsers Compatible Using Automata Learning

rubykaigi.org

オートマトン理論は理解していなかったのですが、「オートマトンと正規表現は相互に変換できる」とのことなので、正規表現が数学的にオートマトンで表現できるのは美しく非常に良いですね!

RubyKaigi に来るたびに、自分の知らないコンピュータサイエンスの知識を思い知らされます。

聞きたいセッションがあれば、タイムテーブルに書かれている説明を予習しておくとセッションをもっと楽しめるんですよね。 続きものであれば、同じ人の昨年以前のセッション動画を見ておくのもお勧めです。

Goodbye fat gem 2025

rubykaigi.org

様々な gem をメンテナンスされている須藤さんの公演で、fat gem*1 の辛みを面白おかしく共感を呼ぶ形でお話されるトークでした。 自分のトークもこのように、会場の反応を楽しみながらできれば良いのですが、なかなかハードルが高いです。

公演の内容は、fat gem はユーザー視点だと利用するだけなら楽で良いけど、これって実は開発者側に多大なコストがかかっているので、持続可能性の意味で厳しいんですよね。 例えば nokogiri gem の場合、Ruby 3.4 がリリースされたその日に、対応する11プラットフォーム(内部的にサポート対象の Ruby のバージョンは4つ)が用意されています。ユーザーとしては非常にありがたいのですが、開発者目線で見ると nokogiri は頑張りすぎだと思います。 自分の gem でこんな事求められたら無理ですと断るレベル。

なので、須藤さんの提案は、

  1. C拡張 gem でもユーザー自身にビルドしてもらいましょう
  2. ビルド環境を用意するのが手間(よくビルドエラーになる)なので、ビルド環境を自動で準備できれば良さそう
  3. Windows 環境には RubyInstaller2 という先駆者がいる
    • Devkit というビルド環境がセット
    • パッケージマネージャもセット
    • 依存パッケージを自動インストール
    • 依存パッケージが存在せずインストールに失敗するということはない
  4. インストール時に自動で外部依存もインストールする rubygems-requirements-system gem を用意

このように、ユーザー自身でビルドする世界になれば持続可能性が高まり、みんなハッピーになるだろうということです。

ユーザーの環境で毎回ビルドのコストがかかる点と、ユーザーの環境に依存パッケージをインストールする必要がある点がデメリットですが、前者は許容できるコストで、後者はローカル環境を汚染したくない場合、Docker 上で実施するのが良いのではないでしょうか。(この gem がもし主流になれば、Dockerfile に依存パッケージ名を記載する手間がなくなる可能性もあるかもしれません。)

RuboCop: Modularity and AST Insights

rubykaigi.org

精力的に開発が続いている RuboCop のモジュール性のお話です。 これまで RuboCop は公式のプラグイン API を提供していなかったため、inject と呼ばれるモンキーパッチを利用する形で実現されており、それがデファクトスタンダードだったそうです。末恐ろしい状況ですね。

体系的なプラグインシステムが提供されるとユーザーとしても安心して使えるし、開発者としても貢献しやすくなりますよね!

また、RuboCop のバックエンドパーサーとして機能してきた Parser gem の代わりに、今後は Prism が採用されるとのことで、Ruby エコシステムの世代交代が進んでいますね。 Ruby の最新の機能が実用段階に来ているということで、どんどん最新版を使っていきましょう!

おわりに

3日間にわたる RubyKaigi 2025が終了しました。 非常に魅力的な公演が目白押しでしたが、3トラックのため、視聴できるセッションが限られていた点と、自身の発表の裏番組だった ZJIT を聞けなかったのが残念です。

次回のRubyKaigiは 2026年4月22日から4月24日、場所は北海道函館市です。


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


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

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

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

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

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

*1:事前にビルド(コンパイル)された C拡張バイナリを同梱した gem。新しい Ruby が出るとその Ruby のバージョンにあわせた対応バイナリが必要。