有名テック企業の技術ブログを、ひとつのフィードで。
フィード
32件
【デジカルチーム ブログリレー 3日目】 クラウド型電子カルテデジカルチームの黒木です。 最近、私たちのチームで 「Pull Requestごとに、そのコードがそのまま動く環境が自動で立ち上がる」仕組み──いわゆる PR環境(ephemeral environment / preview environment)を構築しました。 なぜ作ったのか 設計の方針 実装のポイント 1:stateを「1 PR = 1 state」で完全分離する 2:DBはPRごと、クラスタは共有 3:ALB Listener Ruleの優先度をPR番号から決める 4:デプロイワークフロー(ラベル起点) 5:破棄ワークフロー(PR close + 毎朝の掃除) まとめ We are hiring! なぜ作ったのか 普段からプロダクト開発を続けるなかで、こんな不満がじわじわ溜まっていました。 レビューでコードは読めても、実際に触ってみないと分からない。特にフロントエンドの見た目や操作感 Storybookはあるものの、モックの限界がある かといってレビュアー全員が git fetch してブランチを切り替えてローカル起動……は何かと面倒 共有のステージング環境は限られた数しかなく、調整や、たまに順番待ちも発生する 「このPR、ちょっと触ってみたいんだけど」が気軽にできず、これを解決したいというのが出発点です。 完成後はPRに preview ラベルを付けると、数分後にBotがこんなコメントを返してきます。 ## 🚀 PR Dev Environment https://PR固有のID.preview.example.dev にデプロイしました (commit `a1b2c3d`)。 - DB は毎回リセットされます。手動で投入したデータは消えます - 再デプロイしたい場合は `preview` ラベルを一度外してから再付与してください - ラベルを外しても環境は残ります。最終デプロイから 3 日経過するか、PR がクローズされたタイミングで自動削除されます URLを開けば、そのPRのコードがそのまま動く環境にアクセスできます。レビュアーも、非エンジニアのステークホルダーも、URLを踏むだけで動作検証が可能です。 設計の方針 PR環境を作るうえで最初に決めたのは「何を環境横断で共有し、何をPRごとに作成するか」でした。 PRごとに VPC や RDS クラスタまで丸ごと作っていては、コストも起動時間もかかりすぎます。一方で、アプリのコンテナや DB のスキーマは PR ごとに完全に独立させたいので、次のように役割を割り当てました。 グループ 扱い 具体例 共有インフラ 全PRで使い回す VPC、ALB、Aurora(RDS)クラスタ、ECSクラスタ、ECR、CloudFront、ワイルドカードDNS per-PR リソース PRごとに生成・破棄 ECSサービス群、ALB Target Group / Listener Rule、DBスキーマ、他SQSキュー等 ルーティングは、共有ALBにPRごとのListener Ruleを足していく方式です。PR固有のID.preview.example.dev というホストヘッダーで振り分け、そのPR専用の Target Group(= ECSサービス)へ転送します。DNSは *.preview.example.dev のワイルドカードレコードを共有インフラ側に1つ用意しておけば、PRごとにレコードを作る必要はありません。 実装のポイント 1:stateを「1 PR = 1 state」で完全分離する PR環境の実体は、1つの Terraform スタックです。ポイントは state を PR ごとに完全に分けていることです。 # backend.tf (key は CI 側から動的に渡す) terraform { backend "s3" { bucket = "example-terraform-state" # key は terraform init -backend-config で指定する } } CI側では、PR番号から state のキーを組み立てて init します。 terraform init -backend-config="key=states/${PR_NUMBER}.tfstate" terraform apply -auto-approve \ -var="pr_number=${PR_NUMBER}" \ -var="image_tag=${IMAGE_TAG}" これにより、 PRごとのリソースが互いに干渉しない(state が別なので、他PRの apply / destroy の影響を受けない) 並列に何個でも PR環境を立てられる 破棄はそのPRの state に対して terraform destroy するだけ という性質が手に入ります。共有インフラの情報(VPC ID、ALBの Listener ARN、Aurora のエンドポイントや ARN など)は、別管理している共有スタックの state を terraform_remote_state で参照して取得します。 2:DBはPRごと、クラスタは共有 RDSクラスタを PR ごとに立てると、起動に時間がかかるうえコストもかさむので、CREATE DATABASE pr_123 のようにそのPR専用のDBを作成し、そこにマイグレーションとシードデータを流すだけにします。 これを Terraform の terraform_data リソース + local-exec プロビジョナで実現しています。apply 時に CREATE DATABASE、destroy 時に DROP DATABASE します。SQLの発行には RDS Data API(aws rds-data execute-statement)を使いました。 resource "terraform_data" "db_bootstrap" { triggers_replace = { pr_database_name = local.pr_database_name # 例: "pr_123" } # apply 時:存在しなければ CREATE DATABASE provisioner "local-exec" { interpreter = ["/bin/bash", "-c"] command = <<-EOT EXISTS=$(aws rds-data execute-statement ... \ --sql "SELECT 1 FROM pg_database WHERE datname = '${self.output.pr_database_name}'" \ --output json | jq '.records | length') if [ "$EXISTS" = "0" ]; then aws rds-data execute-statement ... \ --sql "CREATE DATABASE ${self.output.pr_database_name}" fi EOT } # destroy 時:接続を切ってから DROP DATABASE provisioner "local-exec" { when = destroy command = <<-EOT aws rds-data execute-statement ... \ --sql "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${self.output.pr_database_name}' AND pid <> pg_backend_pid()" aws rds-data execute-statement ... \ --sql "DROP DATABASE IF EXISTS ${self.output.pr_database_name}" EOT } } ※ ECSサービスがDBに繋ぎっぱなしだと DROP が失敗するので、DROP DATABASE の前に pg_terminate_backend で明示的に切断しています 3:ALB Listener Ruleの優先度をPR番号から決める 共有ALBに複数PRの Listener Rule がぶら下がるので、ルールの優先度(priority)が PR 間で衝突しないようにする必要があります。 これはシンプルに priority = PR番号 としました。PR番号が50000に到達すると問題になりますが、現状まだ大丈夫です。 4:デプロイワークフロー(ラベル起点) デプロイの起点は、PRへの preview ラベル付与です。opened / synchronize で毎回立てるのではなく、レビュアーや作者が明示的にラベルを付ける運用にしています。これでコストを必要なときだけに抑えられます。 on: pull_request: types: [labeled] jobs: deploy: if: github.event.label.name == 'preview' concurrency: # DB操作の途中でキャンセルされて環境が壊れるのを防ぐため、 # cancel-in-progress: false(キャンセルせず順番待ちにする) group: pr-dev-env-deploy-${{ github.event.pull_request.number }} cancel-in-progress: false ジョブの流れは次のとおりです。 PRのheadコミットをチェックアウト OIDCでAWSへ認証(長期キーは持たない) バックエンドのコンテナイメージをビルドしてECRへpush フロントエンドをビルド terraform init(PRごとのstateキーを指定)→ terraform apply フロントエンドの成果物をS3の pr-{N}/ プレフィックスへsync、CloudFrontをinvalidate ECSサービスが安定するまで待機(aws ecs wait services-stable) ECS Exec経由でDBを初期化 プレビューURLをPRにコメント(既存コメントがあれば更新) マイグレーションとシードデータ投入は、起動済みの api コンテナの中で実行しています。aws ecs execute-command でコンテナに入り込み、初期化 → マイグレーション → シード投入の順で叩きます。 ここで注意点があります。aws ecs execute-command は、リモートで実行したコマンドの終了コードを呼び出し元に伝播してくれません。セッションさえ確立できれば、リモート側が失敗していてもCLI自体は exit 0</code
【デジカルチーム ブログリレー 2 日目】 クラウド型電子カルテデジカルチームの井上 (@wtr_in) です。 言いたいことはタイトルでほぼ言い切ってしまいましたが、先日 BigQuery 上のデータをうっかり消してしまいそうになる事件がありました。原因はテーブルコピーに関する細かい仕様の勘違いだったのですが、もしかすると今後同じようにハマる人もいるかもしれないので、せっかくなので書き残しておきます。 BigQuery に関する前提知識 1. BigQuery のテーブル有効期限機能 2. 今や bq cp で異なるリージョンにもテーブルコピーできる 本題: テーブルコピーでの有効期限の引き継ぎに気をつけろ! 有効期限以外の違いはあるか? まとめ We are Hiring! BigQuery に関する前提知識 本題に入る前に、BigQuery についての前提知識をおさらいします。BigQuery に詳しい方は 本題 まで飛ばしていただいて問題ありません。 1. BigQuery のテーブル有効期限機能 BigQuery にはテーブル単位で有効期限を設定する機能があります。有効期限を設定した場合、期限を過ぎるとテーブルが自動削除されます。 docs.cloud.google.com 例えば、日次でテーブルを作っていて、1 ヶ月前までのテーブルは取っておきたいが、それ以前のものは不要なので削除したい、というケースを考えます。この場合、テーブル作成時に有効期限を 1 ヶ月後に設定してあげれば、わざわざ利用者側で定期削除などを行わなくて良いので便利です。 2. 今や bq cp で異なるリージョンにもテーブルコピーできる BigQuery では長年、リージョン(ロケーション)が異なる複数のデータセットを扱う場合に様々な制約がありましたが、近年のアップデートによって、その制約が解消されつつあります。 代表的な例として、リージョンが異なるデータセットにまたがって直接 JOIN などのクエリを実行できない、というのが長年の BigQuery の常識だったのですが、今年プレビュー提供が開始されたグローバルクエリ機能を使うと、直接クロスリージョンでクエリが実行できるようになりました*1。 またもう一つの例として、リージョンの異なるデータセット間でテーブルをコピーするのも、かつては GCS を経由したり、Data Transfer Service を使ったりとひと手間かかっていました。しかしこちらも、2023 年に bq cp コマンドでのリージョン間テーブルコピー 機能がプレビュー提供開始されたことで、割と気軽にコピーができるようになりました*2。 本題: テーブルコピーでの有効期限の引き継ぎに気をつけろ! さて、前提がおさらいできたところで本題です。 通常、テーブル単位での有効期限が設定されたテーブルを bq cp や各言語の SDK でコピーした場合、有効期限は引き継がれません*3。試してみるなら以下のような感じになります。 PROJECT_ID=<YOUR_PROJECT_ID> bq mk --dataset --location=US ${PROJECT_ID}:test_dataset_us # 1 時間後期限のテーブルを作成 bq mk --table --expiration 3600 ${PROJECT_ID}:test_dataset_us.source_table name:STRING,value:INTEGER bq show --format=prettyjson ${PROJECT_ID}:test_dataset_us.source_table | grep -A1 expirationTime => # 期限が設定されている "expirationTime": "1781233620000", "id": "${PROJECT_ID}:test_dataset_us.source_table", # テーブルをコピー bq cp ${PROJECT_ID}:test_dataset_us.source_table ${PROJECT_ID}:test_dataset_us.dest_table bq show --format=prettyjson ${PROJECT_ID}:test_dataset_us.dest_table | grep -A1 expirationTime => # 期限は引き継がれていないので何も出ない ところが、これを リージョンをまたいで bq cp した場合には、通常のリージョン内でのコピーと異なり コピー元テーブルで設定されていたテーブル有効期限が引き継がれる という挙動になります!*4 PROJECT_ID=<YOUR_PROJECT_ID> # test_dataset_us は最初の例で作成済みの前提(未作成の場合は最初の例を参照) bq mk --dataset --location=asia-northeast1 ${PROJECT_ID}:test_dataset_jp # 1 時間後期限のテーブルを作成 bq mk --table --expiration 3600 ${PROJECT_ID}:test_dataset_jp.source_table name:STRING,value:INTEGER bq show --format=prettyjson ${PROJECT_ID}:test_dataset_jp.source_table | grep -A1 expirationTime => # 期限が設定されている "expirationTime": "1781233624000", "id": "${PROJECT_ID}:test_dataset_jp.source_table", # テーブルをコピー(クロスリージョンコピーの警告が出る) bq cp ${PROJECT_ID}:test_dataset_jp.source_table ${PROJECT_ID}:test_dataset_us.dest_table_from_jp **** NOTE! **** Warning: This operation is a cross-region copy operation. This may incur additional charges and take a long time to complete. This command is running in sync mode. It is recommended to use async mode (-sync=false) for cross-region copy operation. cp: Proceed with cross-region copy of ${PROJECT_ID}:test_dataset_jp.source_table? [y/N]: y Waiting on bqjob_r405771497b263f8f_0000019eb9956f0e_1 ... (9s) Current status: DONE Table '${PROJECT_ID}:test_dataset_jp.source_table' successfully copied to '${PROJECT_ID}:test_dataset_us.dest_table_from_jp' bq show --format=prettyjson ${PROJECT_ID}:test_dataset_us.dest_table_from_jp | grep -A1 expirationTime => # source_table と同じ期限が設定されている! "expirationTime": "1781233624000", "id": "${PROJECT_ID}:test_dataset_us.dest_table_from_jp", 通常のリージョン内でのテーブルコピーとはかなり異なる仕組みでコピーされていることが推測されるので、それに起因する仕様の違いだとは思うのですが、そこそこ BigQuery を触っていて、有効期限は引き継がれないと思い込んでいると逆にハマってしまう罠です。 なお、この有効期限の引き継ぎについて公式ドキュメントのテーブルコピーの制限事項も確認してみましたが、明確な記載は見当たりませんでした(2026 年 6 月時点)。ドキュメント化されていない挙動のようなので、将来変わる可能性がある点にもご注意ください。 もしリージョン内コピーの挙動に合わせてコピー先のテーブルで期限を削除したい場合は、コピー後に明示的に期限を削除すれば OK です。 bq update --expiration 0 ${PROJECT_ID}:test_dataset_us.dest_table_from_jp bq show --format=prettyjson ${PROJECT_ID}:test_dataset_us.dest_table_from_jp | grep -A1 expirationTime => # 期限は消えたので何も出ない 有効期限以外の違いはあるか? おまけですが、テーブルの有効期限以外で挙動の違いはあるのかも気になったので確認してみました。 結論としては、リージョン内コピーとクロスリージョンコピーで挙動が変わるのは、テーブル有効期限だけでした。 設定項目 リージョン内コピー クロスリージョンコピー テーブル有効期限 ✕ 引き継がれない 〇 引き継がれる パーティション有効期限 〇 引き継がれる 〇 引き継がれる パーティション設定 〇 引き継がれる 〇 引き継がれる クラスタリング 〇 引き継がれる 〇 引き継がれる テーブル / カラムの description 〇 引き継がれる 〇 引き継がれる ラベル 〇 引き継がれる 〇 引き継がれる こう見ると、リージョン内でテーブルの有効期限が引き継がれないのがどちらかといえば例外的な挙動ではあるようです。 まとめ クロスリージョンでの bq cp では、リージョン内コピーと異なり、コピー元テーブルの有効期限がそのまま引き継がれる 引き継がれた有効期限が不要な場合は、コピー後に bq update --expiration 0 で削除すれば OK 有効期限以外の主要な設定は、リージョン内・クロスリージョンのどちらのコピーでも同様に引き継がれる bq cp でのクロスリージョンコピーは便利な機能ですが、テーブル有効期限を使っている場合は気をつけましょう。 We are Hiring! エムスリーでは、クラウド型電子カルテを開発したい方はもちろん、BigQuery などを使ったデータエンジニアリングが好きな方も絶賛募集中です! 興味を持っていただけた方、カジュアル面談や採用へのご応募をお待ちしています。 jobs.m3.com *1:追加料金がかかるなど、注意事項はあるので、利用の際は公式ドキュメントを参照ください。 *2:こちらも追加料金がかかるので、利用の際はドキュメントを確認ください。 *3:なお、コピー先データセットにデフォルトのテーブル有効期限が設定されている場合は、コピー元の有効期限の有無に関わらず、そのデフォルト有効期限がコピー先テーブルに適用されます。 *4:こちらはコピー先データセットにデフォルトのテーブル有効期限が設定されていても、それは無視されてコピー元の有効期限がそのまま引き継がれます。逆にコピー元に有効期限がない場合は、コピー先データセットのデフォルト有効期限も適用されず、有効期限なしのテーブルが作成されます。
【デジカルチーム ブログリレー1日目】エムスリーエンジニアリンググループ、デジカルチームの安江です。Scalaとマミさんが好きです。 双子が生まれ、1年間の育休を取りました。育休中に新しいチャレンジについて打診があり、自分でも希望してデジカルチームへ異動。この春に復帰したのですが、想像以上のギャップが待っていました。 この記事では「1年間のブランクからどう復帰したか」にテーマを絞って書きます。これから育休を考えている方、長期の離脱からの復帰に不安を感じている方の参考になれば幸いです。 育休中の技術との距離感 復帰したら何もかもが違った Claudeのおかげで未経験領域を突破した チームの力 変わるものと変わらないもの おわりに We are hiring !! エンジニア採用ページはこちら カジュアル面談もお気軽にどうぞ インターンも常時募集しています 育休中の技術との距離感 1年間、技術から完全に離れていたかというと、そうでもありません。 育休が長期になると会社PCは返却する必要があり、会社とのつながりは個人メールだけになります。エンジニアとしてのアイデンティティが揺らぐ感覚がありました。 その揺らぎを埋めるように、育児の空いた時間でScalaを触っていました。その中でコンパイラのバグを見つけて Scala 3 に貢献 できたのは、育休中のちょっとした誇りです。自宅のネットワーク整備にハマっていたのもこの時期です。 一方、育休期間中に世の中ではAIコーディングが爆発的に流行していましたが、自分はYouTube眺めてるだけでした。これが復帰後に効いてくるとは、この時はまだ知りません。 復帰したら何もかもが違った 自分で希望した異動とはいえ、実際に復帰してみると「昔話の浦島太郎どころじゃない」というのが正直な感想でした。エムスリーでは各チームが最適な技術を選定しているため、異動すると技術スタックや開発プロセスがガラッと変わります。 育休前 復帰後 プロダクト 医師向けメディアサービス 電子カルテ リポジトリ構成 複数リポジトリ モノレポ ワークフロー GitHub Flow Git Flow IaC Terraform AWS CDK これだけ環境が変わると不安もありましたが、育休中にAIコーディングが急速に進化していたことに救われました。育休前は「Copilotすげー」と叫んでいたのに、復帰したらClaudeがコードを書いている。このAIの進化が、キャッチアップを大きく助けてくれました。 Claudeのおかげで未経験領域を突破した 復帰前の得意領域はScalaでした。育休中もScalaを触っていたくらいで、React + TypeScriptは未経験です。 ところが復帰後、そのReact + TypeScriptのコードベースにすぐ機能追加ができてしまいました。Claudeのおかげです。 実際の時系列を振り返ると、こうなっています。 復帰初日〜: flakyテストの修正やログ設定の整理など、足元を固めるタスクから着手 復帰3日後: フロントエンドのUI改善タスクに着手。React + TypeScriptのコードに初めて手を入れた 復帰3週間後: 電子カルテの医療ドメインに踏み込んだ機能をReact + TypeScriptで実装した 以前なら React って何? TypeScript の型ってどう書くの? という勉強期間が必要だったのに、コーディングの重心が「自分で全部書く」から「AIと一緒に書く」に移ったことで、未経験の言語でも実戦投入までの時間が劇的に短くなりました。 その後もELT/Dataformによるデータパイプライン構築、バッチのメモリ最適化、Safariのポップアップブロック回避など、フロントエンドからインフラまで幅広く手を動かしています。「Scalaしか書かない人」はもういません。 最近は「AIに仕事を奪われる」という話をよく耳にしますが、1年間のブランクを経験した身からすると、むしろ逆でした。AIがブランクを埋めてくれました。育休のようなキャリアの中断がある人にとっても、復帰のハードルは確実に低くなっていると思います。 チームの力 もちろんAIだけで何とかなったわけではありません。 「Claudeと一緒にMR作ってみました」と復帰3日目に投稿した時も、特に驚かれることなく普通にレビューしてもらえました。AIコーディングがチームの中で当たり前になっていたことも、復帰のハードルを下げてくれた要因だと思います。 チームの雰囲気も大きかったです。復帰初日、環境構築でハマった時、Slackで質問したらチームメンバーがすぐに反応してくれて、その日のうちに開発環境が立ち上がりました。チームリーダーがとても優秀でドメイン知識の面でも迷いなく仕事を進められたし、復帰から2ヶ月後にはCIパイプラインの問題を自分が調査してチーム全体に共有・修正する側に回れました。助けてもらう側から、助ける側へ。この速度で立ち上がれたのは、チームの受け入れ体制があってこそでした。 変わるものと変わらないもの 復帰して驚いたことがもう1つ。 育休前に自分が作った「Googleカレンダーからチームメンバーの休暇予定を収集してスプレッドシートに一覧表示するツール」が、自分がいない間もずっと動き続けていました。しかも元のチームだけでなく、他のチームでも使われていました。 1年離れても残るものがある。一方で、AIコーディングのように業界のトレンドは完全に変わっている。変わるものと変わらないものがある、というのが復帰後の率直な感想です。 おわりに 今は毎朝6時に起きて子供たちの朝食を済ませ、車で保育園を回って帰宅する生活をしています。育休は終わりましたが、育児は終わっていません。 1年間の育休で学んだのは、「変わるものと変わらないもの」は技術だけでなく、自分自身にも当てはまるということです。コードの書き方は変わった。使うツールも変わった。でも、子供の寝顔を見て「今日も生きてるな」と思う感覚は、第一子の時から何も変わっていません。 これから育休を考えている方、特に「1年は長すぎるかな」と迷っている方へ。1年取ったからこそ見えた景色がありました。子供の成長を毎日隣で見られる時間は、キャリアの中のたった1年ですが、家族の中では二度と戻ってこない1年です。 We are hiring !! エムスリーでは一緒に医療の未来を変えるエンジニアを募集しています。育休も取れます。 エンジニア採用ページはこちら jobs.m3.com カジュアル面談もお気軽にどうぞ jobs.m3.com インターンも常時募集しています open.talentio.com
【マルチデバイスチーム ブログリレー6日目】 エンジニアリンググループ マルチデバイスチームの藤原です。 私たちのチームでは10近いiOSアプリを開発しています。各アプリには専任の開発者がおり、プロビジョニングプロファイルは fastlane match を使ってGitリポジトリで管理しています。 しかし、アプリごとに fastlane match の運用環境が独立していたため、テストデバイスの追加や証明書更新のたびに各担当者へ作業を依頼する必要がありました。さらに、スクリプトの配置や実行手順がアプリ間で微妙に異なることから、担当者以外が対応するには認知負荷が高いという課題を抱えていました。 本記事では、既存の fastlane match の仕組みを活かしつつ、トイル(手作業の繰り返し)となっていたデバイス追加運用を「誰でも簡単に対応できる」体制へと改善したプロセスをご紹介します。 Before:アプリ数の増加に伴い顕在化した課題 取り組み前の状態 典型的な対応フロー アプローチ:小さく始めて、段階的に育てる Phase 1: 専用リポジトリの構築 Phase 2: 全アプリへの横展開 YAMLベースの設定管理 ディレクトリ構造 Phase 3: GitHub Actionsを用いた自動化(CI/CD) 実装の詳細 1. YAMLからの設定読み込み 2. ローカルとCIで認証方式を分離 3. 統一されたインタフェース 効果:定性的な変化 Before → After 比較 副次的効果 まとめ 参考リンク We are hiring! Before:アプリ数の増加に伴い顕在化した課題 当初は各アプリのリポジトリ内で fastlane match を半自動化して運用していました。しかし、アプリ数が増加するにつれて、この運用フローが徐々にトイル化していきました。 特に、デバイス追加のたびに各担当者が個別対応する体制には無駄が多く、ボトルネックになっていました。 取り組み前の状態 サイロ化した運用: 10近いアプリごとに独立したリポジトリで管理 手順の属人化: アプリごとにスクリプトの場所や実行方法が微妙に異なる コミュニケーションコスト: 窓口担当 → Slackで各アプリ担当者へ依頼 → 個別作業という伝言ゲーム 横断対応の煩雑さ: 1台のテスト端末を複数アプリに登録する際、別々の担当者へ依頼が必要 担当者不在時の引き継ぎ: 休暇・離席時は、他メンバーが不慣れな手順で対応せざるを得ない 典型的な対応フロー 新しいテストデバイスの追加依頼を受領(多くの場合、複数アプリへの追加が必要) 代表者が Apple Developer Portal にデバイスを登録 Slack上で各アプリの担当者にプロビジョニングプロファイルの更新を依頼 各担当者が、自身のリポジトリで fastlane match を実行 各開発者に更新完了を連絡 アプリ数が少ないうちは許容できていたこのフローも、10近いアプリを抱える規模になると無視できない負担となります。特に、「1台のデバイスを複数アプリに登録するケース」では、アプリごとに異なる担当者へ連絡し、個別の手順で対応してもらう必要があり、非常に煩雑でした。 アプローチ:小さく始めて、段階的に育てる この課題に対して「最初から完璧な自動化」を目指すのではなく、段階的なアプローチで継続的に改善を重ねる方針をとりました。 Phase 1: 専用リポジトリの構築 まず、fastlane match が証明書やプロファイルを保管するリポジトリとは別に、更新処理を一元管理するための専用リポジトリを新設し、1つのアプリから試験運用を開始しました。 既存のアプリ側リポジトリから実行していた処理を、この専用リポジトリへ移譲する形です。将来的な複数アプリへの横展開を見据え、初期段階から「設定とロジックを分離したYAMLベースの拡張可能な構成」を採用したのがポイントです。 Phase 2: 全アプリへの横展開 最初のアプリで動作が安定したことを確認後、他のアプリへの適用を進めました。 ここでの目標は、「自分の担当外のアプリであっても、一元管理の専用リポジトリから同じ手順で更新できる状態」を作ることです。各アプリに散らばっていた更新作業が集約され、全アプリのプロファイル管理が1箇所で完結するようになりました。 Phase 1で導入したYAMLベースの構成が、この横展開で大きな効果を発揮しています。 YAMLベースの設定管理 Fastfileには共通ロジックのみを記述し、アプリ固有の情報をYAMLに切り出しています。これにより、新しいアプリを追加する際はYAMLファイルをコピーして編集するだけで済みます。また、アプリ情報とアカウント情報を分離することで、複数の Apple Developer Account にも柔軟に対応できるよう設計しました。 apps/sample-app.yml (サンプル): name: "Sample App" account: "company" git_url: "ssh://git@example.com/ios-fastlane-match.git" git_url_https: "https://example.com/ios-fastlane-match.git" git_branch: "master" app_identifiers: - "com.example.app" - "com.example.app.dev" accounts/company.yml (サンプル): name: "Company Account" team_id: "XXXXXXXXXX" apple_id: "developer@example.com" ディレクトリ構造 ├── apps/ # アプリ定義(YAML) │ ├── m3com.yml │ ├── lounge.yml │ ├── webinar.yml │ └── ... ├── accounts/ # Apple ID設定(YAML) │ └── m3.yml └── fastlane/ └── Fastfile # 共通ロジック Phase 3: GitHub Actionsを用いた自動化(CI/CD) ローカル環境での運用が定着したタイミングで、CI/CD環境への移行に着手しました。 導入した主な機能: App Store Connect APIによる認証(2要素認証の回避) HTTPS + Basic認証を利用したGit証明書リポジトリへのアクセス 手動トリガー(workflow_dispatch)による安全な実行制御 .github/workflows/update-m3com-devices.yml: name: Update m3.com Devices on: workflow_dispatch: jobs: update-devices: runs-on: ci-mac # 自前のMacをSelf-hosted Runnerとして登録 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup iOS Keychain # 一時キーチェーンの作成と破棄を行うカスタムActionを使用 uses: ./.github/actions/setup-ios-keychain with: keychain-password: ${{ secrets.CI_KEYCHAIN_PASSWORD }} - name: Install dependencies run: make bundle - name: Update m3.com devices run: rbenv exec bundle exec fastlane update_devices_ci app:m3com env: MATCH_PASSWORD: ${{ secrets.MD_MATCH_PASSWORD }} APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} APPLE_API_KEY_ISSUER_ID: ${{ secrets.APPLE_API_KEY_ISSUER_ID }} APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }} MATCH_REPOSITORY_ACCESS_TOKEN: ${{ secrets.MATCH_REPOSITORY_ACCESS_TOKEN }} 完全自動化(イベント駆動)ではなく手動トリガーを採用した理由は、意図しないタイミングでのプロファイル更新を防ぐためです。また、過渡期の対応としてローカルでの実行フローも残し、update_devices(ローカル用)と update_devices_ci(CI用)でレーンを分割しています。 実装の詳細 一元管理を実現するにあたり、工夫した実装のポイントを解説します。 1. YAMLからの設定読み込み Rubyの標準ライブラリを用いてYAMLから設定を動的に読み込み、fastlane match の引数に渡す仕組みです。 # YAMLファイルを読み込むヘルパー def load_app(app_name) file_path = "../apps/#{app_name}.yml" YAML.load_file(file_path) end def load_account(account_name) file_path = "../ac
【マルチデバイスチーム ブログリレー5日目】 マルチデバイスチームの小林(@bakobox)です。 2026年4月に Android CLI が公開されました。プロジェクトの作成から実機デバイス / エミュレータの操作までを行える CLI で、AI エージェントによるアプリ開発の操作を容易にすることを目的に開発されています。 この Android CLI と、昔からある ADB コマンドの使い方を AI エージェントに教えるだけで、公式ツールのみで Android 実機 / エミュレータを操作できます。 本記事では、Android CLI と ADB を使って Android 実機 / エミュレータを操作するエージェントスキルを、実際のコマンドとあわせて紹介します。 AI エージェントによる Android デバイス操作で何が変わるか 使用する Android CLI コマンド layout screen capture / screen resolve 使用する ADB コマンド スキル まとめ We are hiring! エンジニア採用ページはこちら カジュアル面談もお気軽にどうぞ インターンも常時募集しています AI エージェントによる Android デバイス操作で何が変わるか AI を使ったアプリ開発では、次のようなループが起こります。 AI が実装 -> 人間が動作確認 -> AI が修正 -> 人間が動作確認 -> AI が修正 -> ... このループの中で、動作確認は人間が担う必要があり、そこがボトルネックになっていました。 AI エージェントが Android デバイスを操作できるようになると、このループは次のように変わります。 AI が実装 -> AI が動作確認 -> AI が修正 -> AI が動作確認 -> AI が修正 -> ... 動作確認も含めたループを AI だけで回せるようになります。 次の動画は、ガジェット EC アプリのサンプルで Claude Code に動作確認をしてもらっている様子です。 商品一覧の表示・検索・カルーセルのスクロール・商品のカートへの追加といった操作を、人間が介入することなく自律的に行っています。 使用する Android CLI コマンド デバイスの UI 情報を取得するために使う Android CLI コマンドを紹介します。 layout android layout [--pretty] [--output] [--diff] このコマンドは、現在接続されている実機またはエミュレータのアクティブなアプリの UI レイアウトを JSON 形式で返します。 例えば、このサンプルアプリで layout コマンドを実行すると、次のようなデータが返されます。 [ { "interactions": [ "scrollable" ], "center": "[540,1335]", "bounds": "[0,310][1080,2361]", "resource-id": "product_list", "key": 3506402 }, { "interactions": [ "clickable", "focusable", "long-clickable" ], "center": "[540,404]", "resource-id": "search_field", "key": 3506402 }, { "text": "Featured", "center": "[151,710]", "resource-id": "header_Featured", "key": 3506402 }, { "interactions": [ "scrollable" ], "center": "[540,1095]", "bounds": "[42,774][1038,1417]", "resource-id": "featured_carousel", "key": 3506402 }, { "text": "All Products", "center": "[193,1491]", "resource-id": "header_All_Products", "key": 3506402 }, { "content-desc": "Search gadgets", "center": "[540,404]", "key": 3506402 }, ... ] 返される JSON は UI 要素の配列で、配列の各要素が画面上の1つの UI 要素に対応します。各要素には次のようなフィールドが含まれます。 text - 要素が持つテキスト resource-id - 要素を参照するための Android リソース ID content-desc - アクセシビリティツール向けの UI 要素の説明 interactions - 要素がサポートするユーザー操作の種類。checkable、clickable、focusable、scrollable、long-clickable、password のいずれか、または複数 state - 要素の現在の状態。checked、focused、selected のいずれか、または複数 bounds - 要素のバウンディングボックスの画面座標。[最小X,最小Y][最大X,最大Y] 形式 center - 要素の中心の画面座標。[x,y] 形式 off-screen - true の場合、要素は UI 階層に存在するが表示されていない。スクロールすることで表示される可能性がある これらのフィールドを手がかりに、目的の UI 要素の座標を特定して操作します。例えば次のように活用できます。 resource-id が search_field の要素は center が [540,404] なので、検索フィールドをタップしたい場合はこの座標を操作すればよいとわかります。このように、 UI 要素の座標さえ取得できれば、あとはその座標に対してタップなどを実行するだけです。 このコマンドは、 Jetpack Compose で構築した UI はもちろん、 Flutter で描画した UI でも使えます。 ただし、 WebView 内の要素は取得できない点に注意が必要です。アプリ内 WebView の要素の座標を特定したい場合は、次のコマンドを使います。 screen capture / screen resolve android screen capture [--output] [--annotate] このコマンドを使って、接続されたデバイスのスクリーンショットをキャプチャできます。この出力を AI が読むようにすれば、見た目の確認作業を AI に任せられます。 さらに --annotate オプションを付けて実行すると、画像内の UI 要素の周りに数字付きのバウンディングボックスを描画します。 この画像とバウンディングボックスの数値を次のコマンドで指定することで、対象要素の座標を特定できます。 android screen resolve --screenshot=<path> --string=<string> 例えば、先ほどのキャプチャの9番の要素の座標を特定する場合、 android screen resolve --screenshot=screenshot.png --string="input tap #9" を実行すると、 input tap 539 404 と出力されます。 これで、 WebView 内の目的の要素の座標も得られます。 まずは layout コマンドで UI 要素の座標の特定を試み、取得できなかったら screen capture / screen resolve ベースの手法にフォールバックすることで、ほぼすべての要素の座標を特定できます。 使用する ADB コマンド 入力操作に使う ADB コマンドを紹介します。 いずれも adb -s <serial> shell input から始まるコマンドです。-s <serial> で操作対象のデバイスを指定します(adb devices で確認できるシリアル番号)。複数のデバイスやエミュレータが接続されている場合に、どれを操作するかを明示するために付けています。 adb
【マルチデバイスチーム ブログリレー4日目】 こんにちは、マルチデバイスチームでモバイルアプリエンジニアをやっている八箇です。 2025年9月にエムスリーに転職して9ヶ月が経ちました。開発環境や文化の違いにもようやく慣れてきたところです。 昨年(2025年)はモバイルアプリの開発業務以外にも、iOSDC, DroidKaigi, FlutterKaigi, およびKotlin Festの4つのカンファレンスで弊社ブースのお手伝いをさせていただきました。 iOSDCのブースでの写真 入社直後だったためサポートを受けながらではありましたが、入社前の客観的な視点と、入社後実際に目にしたチームの様子、その両方を交えて社外の方にエムスリーのアプリ開発についてお話しできたのは、非常に良い経験になりました。 その際に、社外の方々とお話しする中で、多くの方がエムスリーに対して入社前の私と同じイメージを持っていると感じました。 本記事では、私が入社前に抱いていた不安と実際、そして入社してから気がついたエムスリーで働く上での魅力についてまとめています。 エムスリーに興味を持っている方の参考になれば幸いです。 入社前に抱いていた不安と実際 技術力への不安 複数アプリの運用体制 入社して初めて知ったエムスリーで働く上での魅力 おわりに We are hiring! 入社前に抱いていた不安と実際 技術力への不安 エムスリーのエンジニアについて調べていると、「全員CTOレベルを目指している」という言葉が出てくることもあり、入社前の私は、「極めて技術力の高いエンジニアで構成されている」というイメージを強く持っていました。 また、YouTubeの「エムスリー公式テックチャンネル」やテックブログなどで頻繁に技術発信を行っていることからも、エンジニア組織全体で技術的関心が高いことが伺えるため、そのイメージを強めていました。 www.youtube.com エンジニアとして、技術的関心が高いメンバーの中に身を置くことは、楽しいことである一方で、 本当に戦力として貢献できるのか、と転職先を決める際には不安で悩んでいました。 カンファレンスで会話した方の中にも「興味はあるが、自分ではついていけないのでは」という不安を話されている方がおり、共感したのを覚えています。 しかし、実際に入社して業務を経験した今では、入社前の技術力に対する不安は全く必要なかったと確信しています。 もちろん周りの技術力が高いのは事実ではあるのですが、エムスリーではエンジニアの組織が非常にフラットです。 特にマルチデバイスチームでは、一人一人が複数のアプリを担当する形でアサインされており、各人の裁量も大きく、開発環境も異なるため、他のメンバーとの技術力の違いが業務上問題になることはないように感じます。 他のメンバーとの技術力の差ではなく、自分自身でどのように課題を解決していけるか、自走してプロジェクトを推進できるかが重要だと感じています。 定期的に開催されているテックトークや技術ブログを楽しめる上に、日々の的確なレビューや議論を通して成長できており、周りの技術力や技術的関心が高いからこその良さを日々実感しています。 高い技術力に気後れすることなく、自分のペースで課題解決に向き合えるこの環境は、エンジニアとして働く上でこれ以上ない場所だと感じています。 複数アプリの運用体制 マルチデバイスチームでは、現在10個近いアプリの運用や新規開発を行っており、メンバー数以上のアプリの開発をしています。 入社前にこの話を聞いた時、たくさんのアプリに携われて楽しそうだと思う反面、前職では1つのアプリに対してチームで開発をしていたため、どのように運用が回っているのかが想像できませんでした。 前述の技術力の不安も相まって、自分に同じことができるのかという不安もありました。 しかし、実際に働いてみると、その心配は杞憂でした。 私も現在3つのアプリを担当していますが、忙しすぎるということはありません。 どの案件が最も事業的にインパクトが大きいかを考え、優先順位をエンジニアで判断して進める裁量があるため、自分のペースで実装を進められています。 加えて、多くのアプリに携わっているからこそリファクタリングや環境更新などやりたいことが尽きず、毎日新鮮な気持ちで業務に取り組めるという大きなメリットがあり、あっという間に9ヶ月が経過して、私自身驚いています。 また、それぞれのアプリは同じ技術スタックを使っているわけではなく、各アプリで異なるアプローチが採用されています。 昨年度時点でのアプリの技術は、ブログにまとまっておりますので、興味がある方は見てみてください。 www.m3tech.blog 採用の根拠さえあれば、技術スタックは担当者で自由に決められるため、新しい技術にも挑戦しやすい環境になっています。 チームメンバー間で異なるアプリを担当しており、得られた知見は適宜共有されるため、幅広いアプリ開発の知見を効率よく得られる環境だと感じています。 入社して初めて知ったエムスリーで働く上での魅力 エムスリーでは「くしゃみ」と呼ばれる行動規範が採用されています。 note.com この行動規範を意識している効果であると思いますが、エンジニアに限らず、エムスリーで働く方々は限られた時間の中で全体のパフォーマンスを最大化することを強く意識していると感じます。 複数のアプリの開発に関わっていますが、どのサービスもミーティングは最小限で簡潔にまとめられたものが多いです。 かといって、言われたものを作るだけということはなく、どのロールのメンバーでも、忌憚なく意見が言える環境であり、丁寧に議論を重ねながら案件を進行しています。 デザインやUXを決める際にもアプリエンジニアの観点が求められるため、入社して間もない私でも意見を出す機会が多くあります。 全員の当事者意識が高いため議論が停滞することもなく、短時間でも密度が高いやり取りができていると感じます。 必要な議論はしっかりやりつつ無駄がないからこそ、効率よく働ける環境だと感じています。 おわりに 自分が持っているアプリ開発の知見を十二分に発揮できること、多くの学びが得られていること、複数のアプリに触れることで日々新鮮な気持ちで働けることなど、本当に楽しみながら仕事ができています。 悩んだ末の選択でしたが、間違いありませんでした。 エムスリーやマルチデバイスチームの雰囲気が少しでも伝わっていれば幸いです。 We are hiring! エムスリーでは、スマホアプリエンジニアを募集しています!少しでも興味がありましたらお気軽にお問い合わせください! speakerdeck.com jobs.m3.com
【マルチデバイスチーム ブログリレー3日目】 エンジニアリンググループ マルチデバイスチーム(iOS/Androidアプリの開発を担当)の渡辺です。 マルチデバイスチームで開発している「臨床ポケット」アプリは、医師が臨床現場で行う判断をエビデンスに基づいて支援するアプリです。 実際の医療現場ではスマホの通信状況が芳しくないことも多く、インターネットに接続できる前提だといざというときに利用ができない場合があります。 臨床ポケットではあらかじめ必要なデータをアプリにバンドルしておくことで、オフラインでの利用を可能にしました。 iOSアプリの開発時は何も問題なくアプリをリリースできましたが、AndroidアプリはGoogle Play ConsoleでApp Bundleのアップロード時にエラーが出ました。 この記事では、ファイルサイズの大きなAndroidアプリを配信するために実施した対応を紹介します。 臨床ポケットアプリについて 制限を超えてアプリをリリースする手段 Play Asset Delivery (PAD) Play Feature Delivery Play Asset Delivery (PAD) の導入 アセットのダウンロードタイミング 1. アセット用モジュールの追加 2. アプリ本体に依存を追加 3. アセット用モジュールにアセットファイルを移動 参考 on-demand の場合 まとめ 参考 We are hiring! 臨床ポケットアプリについて 臨床ポケットアプリ Android版では、現在メインコンテンツとして表計算ツールを提供しています。 ツールはHTMLとJavaScriptで実装されており、WebViewで表示しています。 現時点では398個のツールが存在し、未圧縮状態で約844MB、リリースビルド生成時に圧縮され、aabファイルのサイズは約210MBになりました。 制限を10MB程度オーバーしているだけなので、プロジェクト構成を見直せば制限に収まるかもしれません。 しかし今後もツールを追加し続ける予定のため、一時的な対処でファイルサイズを減らすのではなく、200MBを超えても安定してリリースできるようにする方法を選択しました。 制限を超えてアプリをリリースする手段 Androidではファイルサイズの制限を超えてアプリをリリースするための手段として、 Play Asset Delivery (PAD) と Play Feature Delivery があります。 Play Asset Delivery (PAD) アセットのダウンロード方法とタイミングをカスタマイズでき、Google Play Consoleの標準の制限である200MBを超えるアプリの配信ができます。 アプリのサイズを最適化して Google Play のアプリのサイズ上限内に収める アプリ コンポーネント アプリのダウンロード サイズの上限 ベース モジュール 500 MB 個々の機能モジュール 500 MB 個々のアセットパック 1.5 GB すべてのモジュールとインストール時のアセットパックの合計 4 GB オンデマンドまたは fast-follow で配信されたアセットパックの合計 30 GB 補足: Googleのドキュメントでは各モジュールの上限は500MBとなっていますが、PADを導入していない場合は200MBを超えるとGoogle Play Consoleのアップロードでエラーになりました 圧縮後のアプリの合計ダウンロードサイズは34GBまで許容されます。 Play Feature Delivery アセットやコードを含むモジュールを、配信タイプに応じて分割配信できる仕組みです。 デバイス条件による配信制御や、ユーザーによる事後アンインストールを実現可能です。 分離したいのはアセットだけでKotlinのコードを含めたいわけではない 全ユーザーにアセットをバンドルしたく、デバイス条件による出し分けや事後アンインストールは不要 できるだけ低コストで対応したい これらの要件から、Play Feature DeliveryではなくPADを選択しました。 Play Asset Delivery (PAD) の導入 アセットのダウンロードタイミング 種類 タイミング install-time アプリのインストールと同時 fast-follow アプリのインストール後、バックグラウンドで開始 on-demand アプリの実行中、任意のタイミング アプリの要件から、インストール時にすべてのアセットがバンドルされている必要があるため、 install-time を選択しました。 1. アセット用モジュールの追加 アセットのみを持つモジュールのため、 assetpack という名前のモジュールを追加しました。 // assetpack/build.gradle.kts plugins { id("com.android.asset-pack") } assetPack { packName.set("assetpack") dynamicDelivery { // `install-time` , `fast-follow` , `on-demand` のいずれかを指定 deliveryType.set("install-time") } } // settings.gradle.kts rootProject.name = ... include( ":app", ":assetpack", ... ) 2. アプリ本体に依存を追加 通常、モジュールを新規追加する場合は dependencies ブロックに implementation(project(":module_name")) のように追加します。 アセット用モジュールは android ブロックに assetPacks を追加します。 // app/build.gradle.kts android { ... // `+= listOf(...)` で複数のアセット用モジュールを追加することも可能 assetPacks += ":assetpack" ... } 3. アセット用モジュールにアセットファイルを移動 アセット用モジュールの src/main/assets ディレクトリにアセットファイルを移動します。 参考 on-demand の場合 on-demand を指定した場合、アセットのダウンロード実行をコードでリクエストする必要があります。 val assetPackManager = AssetPackManagerFactory.getInstance(context) assetPackManager.fetch(listOf("{packName}")) まとめ ファイルサイズの大きなAndroidアプリをリリースするために Play Asset Delivery (PAD) を使用しました。 Kotlinのコードを変更することなく、新規のモジュールを追加し、依存を設定、アセットファイルを移動するだけで対応が完了しました。 同じような課題に直面している方の参考になれば幸いです。 参考 Play Asset Delivery (PAD) Play Feature Delivery アセット配信を統合する(Kotlin および Java) We are hiring! エムスリーでは、臨床ポケットアプリをはじめ様々なアプリを開発しています。もし興味がありましたらお気軽にお問い合わせください! jobs.m3.com
【マルチデバイスチーム ブログリレー2日目】 エンジニアリンググループ マルチデバイスチームの田根です。 日本最南端の波照間島のニシ浜にはウミガメがいっぱい 私は主に IntelliJ IDEA を使って開発しているので、AIエージェントは JetBrains Junie をメインで使っています。 Claude Code も IntelliJ IDEA のプラグイン Claude Code [Beta] - IntelliJ IDEs Plugin | Marketplace があり、IntelliJ IDEA 上で動かせます。 今回は、この両者を実際の開発プロジェクトでガッツリ使い倒して分かった「リアルな違い」を、率直に比較レビューします。 Junieの自律思考とClaude Codeの馬力、どちらがどんな場面で活きるのか? 使い分けのヒントをお届けします。 ※本記事では、Junieは最も安価な Gemini 3 Flash Previewモデル、Claude Codeは最新モデルを使用して比較しています。 JetBrains Junie とは 1分でわかる最大の違い 機能・挙動の徹底比較 【思考の深さ】気を利かせるJunie vs 実直なClaude Code Junie:文脈を読んで「関連クラス」まで自動修正 Junie:「テストファースト」へのこだわり 【パワーと規模】大局に強いClaude Code vs 迷子になりやすいJunie Claude Code:大規模なリファクタリングも力強く完遂 Junie:大きすぎる指示は迷子に 【開発UX】コード変更の「承認プロセス」の違い まとめ:私たちはどう使い分けるべきか? JetBrains Junie を使うべきシチュエーション Claude Code を使うべきシチュエーション We are hiring! JetBrains Junie とは JetBrains Junie は、JetBrains が提供する AI コーディングエージェントです。IntelliJ IDEA などの JetBrains IDE にネイティブ統合されており、コードの生成・修正だけでなく、テストの作成・実行やプロジェクト全体の文脈を踏まえた自律的なコード変更が特徴です。 Junie のモデルは複数から選べますが、モデルによって消費されるクレジットが異なります。 Junie で選択できる Model 1分でわかる最大の違い ひと言で表すなら、両者のキャラクターは次のように綺麗に分かれます。 Junie: プロジェクト全体を俯瞰し、テストを回しながら自律駆動する「自走型シニアエンジニア」 Claude Code: 指示された内容を圧倒的なパワーで超高速に完遂する「圧倒的馬力の実行特化型エージェント」 機能・挙動の徹底比較 比較項目 JetBrains Junie Claude Code 指示へのアプローチ 自動で気を利かせて関連箇所まで修正。ファイル指定しているのに他のファイルまで修正してしまうことも 指示されたこと(そのファイル)のみを修正 テストの扱い 実装前にテストを書く(テストファースト)、挙動確認もテストを実行して検証 指示しないとテストコードの修正・作成はしないことが多い 大規模な変更 苦手(Spring Bootのバージョンアップなどは頓挫しがち) 得意(大きな指示でもパワーで押し切れる) コード変更の安全性 修正後に確認(事後確認)。コンパイルエラーにはなりにくい IntelliJ上で事前にプレビューされ「承認」する形。指示次第ではコンパイルエラーのままになることも 修正方針の確認 選択肢を提示せず、自己判断で修正を進める 修正方針の選択肢を提案し、人間に判断を委ねる 他のプロジェクトの参照 Project Settings の Modules に追加しないと参照できない 可能 価格 AI Pro プラン 月額 2,800 JPY〜(法人) API従量課金(Vertex AI等)または Pro プラン 月額 $20〜 / Max プラン 月額 $100〜 【思考の深さ】気を利かせるJunie vs 実直なClaude Code この2つの決定的な差は、「プロンプトの読解力と、それに伴う周辺への気配り」にあります。 Junie:文脈を読んで「関連クラス」まで自動修正 例えば「このエンティティに項目を1つ追加して」と頼んだとします。 Junieの場合: 「項目を追加するなら、当然DTOやリポジトリ、マッパー、さらには関連するクラスも修正が必要だな」と自走し、関連ファイルを芋づる式に自動で修正してくれます。 Claude Codeの場合: 指示されたそのクラスに愚直に項目を追加して終了します。関連クラスの修正や、テストコードの追従も「指示されないとやらない」というスタンスです。ただし、修正方針に複数のアプローチがある場合は選択肢を提案してくれるため、人間が意思決定しやすいという利点があります。 Junie:「テストファースト」へのこだわり Junieの最も面白い特性が、「この場合はどうなりますか?」と質問したときの挙動です。 Junieは言葉で回答するだけでなく、実際に検証用のテストコードを書いて動作確認を行い、コンパイルエラーにならないことを確認した上で結果をレポートしてくれます。 この「テストを書いて実際に動かして確かめる」というプロセスを自動で行うため、最終的なコードや回答の信頼性はJunieが一歩リードしています。 Claude Code に比べ間違った回答をすることはかなり少ない印象です。 【パワーと規模】大局に強いClaude Code vs 迷子になりやすいJunie 一方で、タスクの規模が大きくなると評価は真逆になります。 Claude Code:大規模なリファクタリングも力強く完遂 「Spring Bootのバージョンを4.xに上げて、非推奨になったAPIを全部置き換えて」といった、プロジェクト全体に影響が及ぶ大きな指示はClaude Codeの独壇場です。コンテキストの広さと処理能力が高いため、大きな一歩をガツンと踏み出すのが得意です。ただし、大味な指示を出すとコンパイルエラーのまま作業を終えてしまうこともあるため、人間側の手綱引きが必要です。 Junie:大きすぎる指示は迷子に 逆にJunieは、大きすぎる指示を出すと試行錯誤を繰り返した結果、意図しない方向にコードが修正されてしまう(迷子になる)ケースが散見されます。Junieに依頼するときは、タスクをある程度細分化してあげるのがコツです。 【開発UX】コード変更の「承認プロセス」の違い 勝手にコードが変わるのが怖いエージェントツールにおいて、レビューのしやすさは重要です。 Claude Code (IntelliJ プラグイン):コードを変更する前に、IDE上にプレビューが表示され、人間が「承認(Approve)」ボタンを押して初めてコードが適用されます。勝手に書き換えられない安心感があります。 Junie:基本的には「修正後に人間が確認する」スタイルです。ただし、前述の通りテストを回してコンパイルエラーにならない状態まで仕上げてから出してくるため、事後確認でも破綻しにくいという自信の表れでもあります。 まとめ:私たちはどう使い分けるべきか? JetBrains Junie を使うべきシチュエーション 新機能の実装で、関連する複数のファイルをスマートに一括生成・修正してほしいとき。 「テストコードもセットで、絶対に動く状態」の綺麗なコードを担保したいとき。 定額の価格体系の中で、じっくりエージェントに自律思考させたいとき。 Claude Code を使うべきシチュエーション フレームワークのメジャーアップデートや、大規模なライブラリ刷新など、馬力が必要なとき。 「ここをこうして」という明確な指示があり、余計な忖度なしで爆速でコードを書き換えてほしいとき。 変更前に必ず手動でプレビューと承認を入れたい安全第一の開発。 どちらが優れているかではなく、「足元の泥臭い実装やテストはJunieに任せ、大きな構造変更はClaude Codeでゴリ押す」という、適材適所の併用スタイルが、現時点で最適な開発環境のひとつと言えそうです。 さらに、Junieで修正したコードはClaude Codeにレビューしてもらい、Claude Codeで修正したコードはJunieにレビューしてもらうという「クロスレビュー」も実践しています。それぞれ視点が異なるため、片方だけでは見落としがちな問題を補完し合えるのもこの併用スタイルの大きなメリットです。 JetBrains Junie は一番安い AI Pro プラン(月額 2,800 JPY)で利用していますが、Claude Code と使い分ければ余裕で足りています。 We are hiring! エムスリーでは AI を「道具」として使い倒し、爆速でプロダクトを育てる仲間を探しています! 「最新のAIツールを実務でガンガン試したい」 「退屈な定型業務や泥臭いリファクタリングはAIに丸投げして、自分はコアドメインの設計に集中したい」 そんな、AIエージェントと共に進化していきたいAIネイティブなエンジニア(または、これからそうなりたい方)を大募集しています! jobs.m3.com
【マルチデバイスチーム ブログリレー1日目】 エンジニアリンググループ・マルチデバイスチーム(以下「マルデバ」)の星野です。 私は普段マルチデバイスチームに所属し iOS/Android アプリの開発をしていますが、同時にエムスリーテクノロジーズにも出向という形で在籍し、グループ会社においてもモバイルアプリの開発・支援をしています。 今回は、現在私が進めているグループ会社のモバイルアプリのリファクタリング事例をご紹介し、エムスリーテクノロジーズにおける業務の様子をお伝えできればと思います。 リファクタリングイメージ図 サービスの紹介 リファクタリング 1. CocoaPods から SPM への切り替え 2. 画面毎のリファクタリング 2-1. 継承の解消 2-2. DI の導入 2-3. MVVM の整理 2-4. async / await への書き換え 3. AWS Code Commit から GitHub への移行 今後の予定 まとめ We are hiring! サービスの紹介 今、私が開発に携わっているサービスは介護領域で toB 向けに展開しているサービスです。導入いただいている企業は数千を超え、今なお拡大中です。サービスは主に管理サイト・Android / iOS アプリが存在し、管理サイトは主に介護事業者向けのサービス、Android / iOS アプリは介護業務に従事する方が利用するサービスとなっています。 グループ会社でリリースしているこのサービスは、スマホに慣れていないご高齢な介護従事者の方にも安心・安全・簡単に使っていただけるという大きな特徴があります。この唯一無二の利便性をより多くの企業・介護従事者の方々にお届けし、介護現場が抱える慢性的な人手不足や業務負荷を軽減し、誰もが本来の「介護ケア」に専念できる環境を支えていきたい——。エムスリーテクノロジーズではそのような更なる飛躍を目指すべく、開発スピードの引き上げ・新しい価値をよりスピーディに届ける方法の1つとして、土台・構造の整理 = リファクタリングを進めることなりました。 こちらについてはエムスリーテクノロジーズ取締役 / VPoE 藤原のインタビューにも記載されていますので、あわせてご参照ください。 www.m3t.co.jp リファクタリング サービスのリファクタリングでは管理サイト・インフラ・Android / iOS アプリなどさまざまな面のリファクタリングを並行していますが、今回のブログでは iOS アプリのリファクタリングについて紹介いたします。 昨年末頃に私が参画した当時の技術スタックは次のような状況でした。 - アーキテクチャ: MVVM - レイアウト: Storyboard / Xib ファイル - ライブラリ管理: CocoaPods - DI: なし - テスト: なし - その他: - MVVM ではあるが、V(View, ViewController)もロジックを持つ - すべての画面(ViewController) が共通したカスタム親クラスを継承していて、さらに多段階継承している場合もある - ゴッドオブジェクト、責務が不明瞭なマネージャクラス多数存在する - 通信の完了時の処理がハンドラ形式で書かれている 7-8年ほど前であれば、どの会社でも見かけるような一般的な技術スタック・状況ですが、今後もサービスを飛躍させたいと考えた場合に、未来に向けてのリファクタリングは必要だと判断し、リファクタリングを進めています。 1. CocoaPods から SPM への切り替え まず最初に行ったのが、ライブラリ管理の CocoaPods から SPM への切り替えです。現在はどのライブラリも SPM 版が用意されている為、大きな問題もなく2-3時間程度で切り替えられました。 2. 画面毎のリファクタリング その次に実施したのが、カスタム親クラスの排除・DI の導入・MVVM アーキテクチャの整理・モダンなコードへの書き換えを画面毎に進めることでした。アプリのマニュアルや開発関連ドキュメントはありますが、エムスリーテクノロジーズは途中からの参画のため、仕様を頭できちんと理解するにはやはり実際にコードを読んで動かすことが一番です。幸いにも画面毎にできること(機能・仕様)がしっかりと定まっていたため、画面単位でリファクタリングし、リファクタリングが完了したタイミングで適宜リリースする形が取れました。 2-1. 継承の解消 画面毎のリファクタリングの際に共通して最初に行うのが、カスタム親クラス(ここでは仮に BaseViewController と呼びます)の引き剥がしです。すべての画面が共通利用する親クラスは、実装時のお手軽さはありますが、「子クラスによっては不要な機能まで継承することになる」「本来関係ないはずの画面同士が親クラスを通じて結びついてしまう」など、保守運用の観点からはコードの見通しが悪くなるというデメリットの方が大きいため、本当に必要な共通処理は protocol + デフォルト実装に切り出す or 各画面毎に必要な機能を実装していく形で BaseViewController の継承を剥がしていきます。 子画面で必要な機能の解析・移行(protocol / 個別クラスへの実装)については Claude Code に任せるのが手っ取り早いので、ここは Claude Code に簡単な指示だけ出して、あとは出来上がったコードを確認するだけで簡単に進められます。 import UIKit // 共通の基底クラスによる NG なサンプル class BaseViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 良かれと思って、アプリがフォアグラウンドに戻った時の通知を設定 NotificationCenter.default.addObserver( self, selector: #selector(didReceiveAppWillEnterForegroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil ) } @objc private func didReceiveAppWillEnterForegroundNotification() { // 画面データの最新化など、共通で行いたい「重い処理」 print("【Base】アプリ復帰通知を受信:データをリロードします。") loadData() } func loadData() { // 各画面でオーバーライドされる想定の空メソッド } } // 1. 通知を受け取ってリロードしたいメイン画面(これは意図通り) class HomeViewController: BaseViewController { override func loadData() { print("ホーム画面の最新データをAPIから再取得します。") } } // 2. ❌ 通知処理が「不要」なはずの、固定文言を出すだけのヘルプ画面 class HelpViewController: BaseViewController { // 静的な画面なので、loadDataは必要ないためオーバーライドもしない // ⚠️ しかし、BaseViewControllerを継承しているせいで… // アプリが復帰するたびに、この画面も裏で通知を勝手に受け取ってしまう。 // 結果として、不要な処理が走ったり、思わぬバグやメモリリークの原因になる。 } また、一部のクラスで「UIViewController ← 親クラス ← 子クラス ← 孫クラス」という多段階継承された状況で、コード上では孫クラスを使うのに、レイアウトファイル(Storyboard)は子クラスしか存在しない」という特殊な状況の画面が複数ありました。これに関しては、仕方なく先に孫クラス用の Storyboard を新たに用意(複製)し、継承を解消した後、どこからも参照されなくなったタイミングで子クラスの Storyboard とコードを削除する形で継承を引き剥がしました。 2-2. DI の導入 クラスの継承をすっきりさせたところで、次に行うのは DI の導入です。DI については最近マルデバでよく採用している swift-dependencies を利用しました。軽量なコードで DI がかけますので、もしまだ使ったことのない人がいればぜひ試してみてください。エムスリーテックブログでも、一年前に取り上げられてますので、こちらもご参照ください。 www.m3tech.blog 2-3. MVVM の整理 次に MVVM アーキテクチャの整理です。画面毎に機能が異なるため、簡単な画面であれば Claude Code であっさり片付きますが、機能が複雑すぎる画面の場合は「副作用のあるメソッド」が他の「副作用のあるメソッド」を呼んでいたり、さらにそれらがいろいろな箇所から呼ばれていて、どのように整理するのが適切かすぐにわからない場合があります。 この場合は Claude Code に指示を出す前に、まず「何をやっているメソッドなのか」「依存関係はどうなっているのか」を解析してもらって、人間がある程度把握しておく必要があります。そうでないと、いくら指示を出したところで絡まったままのなんとなくの整理にしかならないため、意味のないリファクタリングになってしまいます。解析した内容や問題点を issue としてまとめ、それをもとに Claude Code に指示を出して修正を進めていきます。時間はかかりますが、今後の保守・運用を考えた場合にこの整理はとても重要なポイントなので、地道でも確実に進めていきます。 2-4. async / await への書き換え 通信の完了時の処理については GCD を使ったハンドラ形式の書き方になっていたのですが、このサービスでは通信を直列で順番に処理するシーンが多数あり、「ハンドラによるネスト」がコードリーディングの妨げとなっていました。そのため、あたらに通信クラスを作って「async に切り替える」+「ViewModel を MainActor 化することで、GCD によるスレッド操作を排除する」ことで、コードの可読性を向上させました。 // GCD のハンドラによるネストのサンプル class ViewModel { let networkManager = LegacyNetworkManager() var displayRecord: String = "" func loadDashboardData() { // ❌ GCDによる非同期ネスト地獄の始まり networkManager.fetchStaffInfo { [weak self] result in switch result { case .success(let staffId): // ネスト2階層目:入居者一覧の取得 self?.networkManager.fetchPatients(for: st
この記事は基盤開発チームブログリレー4日目の記事です。 こんにちは、エンジニアリンググループ基盤チームリーダー兼General Managerの横本(@yokomotod)です。 ありのまま今起こった事を話すぜ…おれは基盤チームブログリレーを走ると思っていたらいつのまにかOCamlブログリレーだった… 何を言ってるのかわからねーと思うがおれも何をされたのかわからなかった…頭がモナドになりそうだった… www.m3tech.blog www.m3tech.blog www.m3tech.blog 3日間、Web・CLI・機械学習と OCaml で攻め続ける先人達により、OCaml デビューする以外の道は閉ざされました。「じゃーまずはデプロイの仕方から学ぶかー。distroless で動くのかな?」ということで本記事が生まれました。 以前 distrolessコンテナイメージの中を覗いて「なんか軽くてセキュアらしい」より理解を深める という記事を書きました。公式には提供されていない OCaml の場合、どうするんでしょうか。 これはつまり、distroless × Python などで「distroless で依存不足で困った」という状況と同じようなものです。OCamlには馴染みのない方も「たまたまOCamlが題材なだけ」と思って安心してお読みください。*1 まずは hello world を distroless に載せてみよう なぜ scratch で動かないのか? HTTP サーバを載せてみる — cohttp-eio Dream を載せてみる distroless で .so が足りないとき チャレンジ1: LightGBM — ldd、信じていたのに ldd の限界 — dlopen base と cc の違い チャレンジ2: distrolessを超えてscratchへ hello world を static link してみる DNS 解決を試す Dream を static link pure OCaml TLS で HTTPS — scratch vs distroless/static scratch を目指してみて まとめ dynamic build glibc static build We are hiring! エンジニア採用ページはこちら カジュアル面談もお気軽にどうぞ インターンも常時募集しています まずは hello world を distroless に載せてみよう 何はともあれ、OCaml の hello world をビルドして distroless に載せてみます。 (* bin/hello.ml *) let () = print_endline "Hello, World!" ビルドは ocaml/opam:debian-13-ocaml-5.4 で行い、出来上がったバイナリを各 runtime イメージにコピーする multi-stage Dockerfile を書きます。 FROM ocaml/opam:debian-13-ocaml-5.4 AS build WORKDIR /home/opam/app RUN opam update COPY --chown=opam dune-project ocaml_distroless.opam ./ RUN opam install . --deps-only --yes COPY --chown=opam bin/ ./bin/ COPY --chown=opam lib/ ./lib/ RUN opam exec -- dune build --release ./bin/hello.exe # 調査用: file / ldd を使えるようにしておく RUN sudo apt-get update && sudo apt-get install --yes --no-install-recommends \ binutils file \ && sudo rm -rf /var/lib/apt/lists/* # ここから各種runtime FROM scratch AS runtime-scratch COPY --from=build /home/opam/app/_build/default/bin/hello.exe /hello ENTRYPOINT ["/hello"] FROM gcr.io/distroless/static-debian13:nonroot AS runtime-static COPY --from=build /home/opam/app/_build/default/bin/hello.exe /hello ENTRYPOINT ["/hello"] FROM gcr.io/distroless/base-debian13:nonroot AS runtime-base COPY --from=build /home/opam/app/_build/default/bin/hello.exe /hello ENTRYPOINT ["/hello"] 各ターゲットをビルドして実行してみます。 $ docker run --rm ocaml-distroless-hello:scratch exec /hello: no such file or directory $ docker run --rm ocaml-distroless-hello:static exec /hello: no such file or directory $ docker run --rm ocaml-distroless-hello:base Hello, World! scratch と distroless/static ではエラー。distroless/base で動きました。ほーん。 なぜ scratch で動かないのか? distroless イメージにはシェルもコマンドもないので、バイナリの調査にも builder を使えるように、Dockerfile で file と binutils を入れておいて使うことにします。 $ docker run --rm ocaml-distroless-hello:build file _build/default/bin/hello.exe ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ... dynamically linked 。Go の CGO_ENABLED=0 ビルドが statically linked になるのとは違い、OCaml のデフォルトネイティブビルドは動的リンクされたバイナリを生成するんですね。 ldd で依存を確認すると: $ docker run --rm ocaml-distroless-hello:build ldd _build/default/bin/hello.exe linux-vdso.so.1 libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2 libc と libm、そして動的リンカ(ld-linux-x86-64.so.2)に依存しています。distroless の公式 README*2にも「Statically compiled applications (Go) that do not require libc can use the gcr.io/distroless/static image」とある通り、distroless/static は glibc を含みません。distroless/base は static の内容に加えて glibc を含むので、納得の結果です<a href="#f-75ae1c8c" id="fn-75ae1c8c" name="fn-75ae1c8c" title="エラーメッセージの「no such file or directory」は /hello バイナリが見つからないのではなく、ELF interpreter /lib64/ld-linux-x86-64.so.2 が見つからないとい
長女と2人ポケパーク、世代を超えて愛されるものを作れるってすごいよね はじめに こんにちは、エムスリー株式会社 業務執行役員 VPoE 兼 基盤チーム チームリーダーの河合(@vaaaaanquish)です。 この記事は「基盤開発チーム ブログリレー3日目」の記事です。 先日、基盤チームで「うちもブログリレーやろう!」と盛り上がりまして、「いいね!私も書こうかな!」と気軽に思っていたのですが、何故か1日目と2日目の記事がOCamlの記事になっており「それをやられたらビッグウェーブに乗るしかないが…?」ということで私もOCamlで機械学習をやりました。 つまり本記事は、気合いと根性のやってみた記事になります。 はじめに LightGBMとは ということで出来ました 技術的に面白いなと思ったところ ctypes-foreign float32/float64問題 メモリ解放問題 char**のマーシャリング 動かしてみる おわりに We are hiring !! エンジニア採用ページはこちら カジュアル面談もお気軽にどうぞ インターンも常時募集しています LightGBMとは LightGBMは、勾配ブースティングというアルゴリズムを実装したフレームワークです。 2016年8月にMicrosoftが公開して以来、汎化性能の高さやライブラリとしての扱いやすさから人気を伸ばし、今尚Kaggleの表形式コンペでは「最も定番のフレームワーク」と言っても差し支えないほど使われています。 github.com 本家の中身はC++で、OpenMPでゴリゴリに並列化しつつメモリ書き込みの競合を避けるためにスレッドローカルなメモリ領域を確保したりと、面白い工夫がいくつも実装されている強いライブラリです。 私はLightGBMのRust版のBindingを作った経験があり、LightGBMにcommitする程度にはC APIを概ね把握しているため、OCamlでもFFIさえ掴めれば出来るはずです*1。 github.com 一方OCamlは初心者。一体、どうなっちゃうのー!? ということで出来ました 出来ました。 github.com なんとかなったので、実際の挙動はRepositoryを見てもらうとして、ここからは技術的に面白かった部分の話を書いていこうと思います。 技術的に面白いなと思ったところ ctypes-foreign 調べるとOCamlでC関数を呼ぶ方法はいくつか見つかりました。 今回は ctypes-foreign を使いました。 github.com ctypes-foreignは、libffiを利用した動的FFIを扱うライブラリで、OCaml側だけでC関数のバインディングを記述できそうだったので「これなら簡単じゃん…!」という事で進めました。 当然痛い目に遭いました。 具体的にはポインタ周りです。 ctypes-foreignでは、C関数名と型シグネチャを渡す書き方をします。 @-> 演算子で引数の型を連鎖させ、returning で戻り値の型を指定する、以下のようなコードです。 open Ctypes open Foreign let lgbm_dataset_create_from_mat = foreign "LGBM_DatasetCreateFromMat" (ptr void @-> int @-> int32_t @-> int32_t @-> int @-> string @-> ptr void @-> ptr (ptr void) @-> returning int) LightGBMは機械学習のライブラリなので行列演算が主たる操作になります。 つまり、ポインタのポインタのポインタを多く扱うライブラリでして、頭の中で「今@->@->@->@->だから次は@->@->で……つまり今俺は@->@->@->を触っている……?」という、AIもやってくれないパズルをすることになりました。 ctypes-foreignは、書き手にCのスタブコードを書かせないという意味で凄いライブラリで、型レベル DSLや動的libffiの実装は他のFFIバインディングでも使えるなというアイデアが詰まっています。一方普遍的な事実として、プログラミングにおいて初見の簡単さは実際の簡単さに比例しないという当たり前の事を思い出す、そんな令和の春になりました。 もうぶっちゃけほとんどこのパズルに時間使ったので、ここからはオマケと言っても過言ではないです。 float32/float64問題 LightGBMのC APIでは、機械学習において、入力となる行列(特徴量)がfloat32/float64、回答を表す行列(ラベル)がfloat32です。 Rustではf64とf32が別の型として存在するので自然に区別できますが、OCamlのfloatは常に64bitのdoubleです。 ここでctypesのCArrayを活用しています。 (* 特徴量: OCaml float → C double(自然な変換) *) let flat = CArray.make double (nrow * ncol) in Array.iteri (fun i row -> Array.iteri (fun j v -> CArray.set flat (i * ncol + j) v) row ) data; (* ラベル: OCaml float → C float(暗黙の精度切り捨て) *) let c_label = CArray.make float label_len in Array.iteri (fun i v -> CArray.set c_label i v) label; CArray.make doubleとCArray.make float でCの型を指定して、ctypesに適切なメモリレイアウトのバッファを確保させています。 ctypesのランタイム型記述子で作った事で、64bit→32bitの精度切り捨ても自動で行えて、個人的にニヤニヤしたお気に入りの実装です。 メモリ解放問題 C APIで確保したリソースは明示的に解放が必要になるのは当然のことです。 リソース解放においては、OCamlでは Gc.finalise という機能があります。 実装を始めた当初は決定論的に呼ばれるものかという意識をせず「Rustで言う所のDropトレイトみたいなもんかな」という安易な考えで組み込みました。 let from_mat ~data ~label = (* ... C APIでハンドルを取得 ... *) let t = { handle } in Gc.finalise free t; (* GCがtを回収する時にfreeが呼ばれる *) t ここでRustのDropにはない制約としてGc.finalise の「ファイナライザ内での例外処理」に当たりました。 A finalisation function may raise an exception; in this case the exception will interrupt whatever the program was doing when the function was called. 上記の公式ドキュメントの記載によると、ファイナライザは例外を投げると、その例外はGCのタイミングに依存した無関係なアロケーションポイントで再raise されるため、GCが発生した時点で実行中だった無関係なコードが中断されるみたいです。 そのため free関数でC APIの戻り値を無視するような実装になっています。 let free t = ignore (Lightgbm_raw.lgbm_dataset_free t.handle<span cl
基盤開発チームブログリレー2日目の記事です。 1日目は田尻さんがOCamlについて書いてくれました。そのOCaml熱にあおられ私も仕事で使い始めたので、私も便乗してOCamlについて書いてみます。 www.m3tech.blog 覚醒したラクダ はじめに OCaml、書いてますか。私も書いています。仕事の補助ツールとして。 こんにちは、基盤開発チームの林です。 以前まで、ちょっとしたCLIツールを作る言語としては、簡潔に書ける Ruby を使うことが多かったのですが、最近はAIエージェントをよく使うようになったこともあり、自分用のCLIツール開発には OCaml を使うようになりました。 もともと私はローカル環境や設定の作り込みをするタイプではなく、むしろクリーンな環境・ゼロ設定で仕事をするのが好みだったのですが、近頃はCLIツール開発も含めてローカル環境の作り込みをするようになってきたので、そのあたりで考えたことなどを記事にしてみます。 なお、最初にお断りをしておくと、関数型言語を仕事で使った経験はほとんどなく*1、OCaml 自体にも詳しくはないので、OCaml 初心者による初心者向けの記事です。 はじめに 自分用のCLIツールを開発したくなってきた経緯 CLIツール開発言語に求めたい性質を列挙したら OCaml が残った 実際に OCaml をCLIツール開発に使ってみて AIへの指示 型駆動設計 let* によるネスト改善 使っているライブラリ おわりに We're hiring! エンジニア採用ページはこちら カジュアル面談もお気軽にどうぞ インターンも常時募集しています 自分用のCLIツールを開発したくなってきた経緯 昨年から Claude Code を使い始めてからは、あらゆる手元の作業を Claude Code にお願いするようになりました。 また、Claude Code に作業を円滑にしてもらえるように、目的ごとに Skill ファイルもせっせと作るようになりました。 たとえばコードレビューの時は GitLab の公式CLIの glab コマンドを使って、コメントを取得する時はこのコマンドで、、と自然言語とコマンド例で書いていたりしたのですが、Skill を読ませても思った通りコマンド実行してくれず、何度もやり直しになることが多々ありました。 そもそも自然言語で書くとどうしても曖昧さが残りますし、Skill のメンテをするのにも疲れてきました。 そこで、定型の処理については、分かりやすいコマンド体系で決定的な動作をするCLIツールを自分用に作るのがよいと気付きました。 また、現在エムスリーでは Claude Team プランが導入されているので一定の上限枠までは定額利用でき、私はフルタイムで Claude Code を使っても枠が余っているので、CLIツール開発をAIにやってもらうのは実質費用なしでできてしまう状況です。 この状況を踏まえて、2026年に自分のローカル環境のCLIツール開発に使うならどの言語がいいかを検討しました。 CLIツール開発言語に求めたい性質を列挙したら OCaml が残った 代数的データ型とパターンマッチ プログラミングを安全に分かりやすく記述するための奥義は、代数的データ型とパターンマッチだと常々思っているので、これは必須で欲しい機能でした。 パターン記述が見やすいとわかりやすさに直結するので、簡潔に書けることも重要。 静的型付け、型推論 型を丁寧に書くのは昔は面倒だったこともありますが、AI以前でもIDEで楽になっていましたし、今はAIに書いてもらうので型はあるだけ良いと思っています。 型が合わなければコンパイルで落ちてくれるので、テストがなくてもある程度安全です。 また、読む時に不要な情報が少ない方がよいので、型推論によって簡潔に書けることも重要です。 ビルドや起動が遅くない せっかちなので、遅くないものがよいです。 メモリ管理はしたくはない Rust が上で挙げた要素の多くを満たす選択肢であるとは思いますが、メモリ管理を記述したいほどのCLIツールは作らないので、今回は Rust という気分ではありませんでした。 以上のような性質を満たす言語を検討した結果、Haxe と OCaml が候補に残り、最終的には基盤開発チームメンバーの田尻さんが OCaml を激推ししていることもあって、OCaml に決めたのでした。*2 実際に OCaml をCLIツール開発に使ってみて 具体的にどのようなツールを作っているかですが、たとえば次のようなサービス用のCLIツールを作ったりしています。 Confluence Jira Redmine Sentry 特にConfluenceやJiraは、同じAtlassian製でそれぞれ専用のクエリランゲージ(Confluenceの場合はCQL)があり、使いこなすと性能よく色々なことができるので便利です。*3 OCaml コードの大部分はAIに生成してもらっていますが、書き味というか読み味がよいです。 やはり代数的データ型をプログラムの上の方に宣言して、それのパターンマッチにより分岐を網羅して記述すると、AIも書き間違えないし、読むときも分かりやすくて気に入っています。 ただ、AIへの指示なしで書いてもらった時は、生成されたコードが期待とちょっと違ったので、指示を少しだけ CLAUDE.md に書いています。 AIへの指示 型駆動設計 代数的データ型とパターンマッチを重視して言語選定をしているので、それが最大限生かされるように次の指示を書いています。 ## 型駆動設計 - 代数的データ型とパターンマッチを最重視 - 型で表現できることは型で表現 - 型シグネチャを先に決めてから実装 関数を書き始める前にまず型を考えて定義してくれるようになりました。 let* によるネスト改善 OCaml ではエラー処理にいわゆる Result 型を使えますが、 最初AIに書いてもらった時に、次のようにネストが深くなる書き方をされました。 let fetch_user config_path = (* 設定ファイルを読み込む(ファイルが無ければ失敗) *) match File.read config_path with | Error e -> Error e | Ok config -> (* 読み込んだ設定からAPIのURLを組み立ててHTTP GET *) match Http.get (config.endpoint ^ "/user") with | Error e -> Error e | Ok body -> Ok body そこで次の指示を書いて、ネストしない見やすい書き方にしてもらっています。 ## ネストを浅く保つ - Result型の連鎖にはResult.Syntaxのlet*演算子を使う この指示があると、先ほどのコードは次のようにネストが深くならず見通しが良くなります。 let fetch_user config_path = let open Result.Syntax in let* config = File.read config_path in (* 設定ファイル読み込み *) let* body = Http.get (config.endpoint ^ "/user") in (* 読み込んだ設定でHTTP GET *) Ok body 使っているライブラリ 業務上必要なCLIツールの多くは、標準入出力、HTTPアクセス、JSON処理ができれば足りることが多いので、外部ライブラリはほとんど使っておらず、少数のライブラリのみ使っています。 JSON処理にYojson HTTP処理にocurl(libcurlのOCamlバインディング。バイナリ配布などでライブラリ依存が気になる場合は他のものを使ったほうがよいかも) おわりに 今回とりとめもなく、AI作業の効率化のためにCLIツール開発が必要だと思ったことや、CLIツール開発に OCaml を使っている話を書いてみました。 個人的には OCaml は認知負荷が低くて読みやすいし、AIもあまり書き間違えない気もしていて、型がしっかりあるとAIも書きやすいのかなと感じています。 もともとローカル環境整備に凝らない私が凝り始めているのだから、もともと凝るタイプの人が今どんなやり方をしているかもぜひ知りたいところですね。 We're hiring! 好きな開発言語・スタイルで仕事がしやすいエムスリーでは、ソフトウェアエンジニアを募集しています。 新卒・中途採用、カジュアル面談やインターンも募集しています! エンジニア採用ページはこちら jobs.m3.com カジュアル面談もお気軽にどうぞ jobs.m3.com インターンも常時募集しています <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2
【基盤開発チーム ブログリレー1日目】の記事です。 記事とはなんの関係もない自作の苔テラリウム OCaml、書いてますか。私は書いています。仕事として。 そんな話の詳細は今年の関数型まつり 2026 で話すとして、今回は「実際、OCaml で Web やれんの?」という疑問にお答えしようと思います。 実際に利用しているものから、なんとなく良さそうなものまで、Web アプリ開発の文脈で必要になりそうなものをさらっていきます。 この記事を見て、ぜひ、OCaml で Web アプリ開発にチャレンジしてみてください! はじめに OCaml で Web アプリを作るなら OCaml デファクトスタンダードライブラリの基礎知識 Base/Core/Async Batteries Included Lwt EIO Web FW Dream Vif HTTP ライブラリ cohttp httpaf JSON ライブラリ DB ライブラリ ロギング SSG OCaml 由来の AltJS まとめ We are hiring !! エンジニア採用ページはこちら カジュアル面談もお気軽にどうぞ インターンも常時募集しています はじめに OCaml が大好き、基盤開発チームの田尻です。 今年は、関数型まつり 2026 で話すことになりました。Track A でぼくと握手!*1 fortee.jp タイトルからも分かる通り、今は OCaml 製の Web アプリを開発しています。 正確には OCaml で API サーバーを書き、ReScript という OCaml に関連した AltJS で SPA を構成しています。 具体的な話は関数型まつりを楽しみにしていただくとして、今回は OCaml を 2026 年に利用しようとした時に、どんな技術選定があるのかについて記録しておこうと思います。 もちろんこれが全て、という訳ではないですが、特に最近の動向に注目して集めてみました。 これを見れば、あなたも今日から OCaml で Web アプリ開発できるはずです! OCaml で Web アプリを作るなら OCaml デファクトスタンダードライブラリの基礎知識 どの言語にも「標準ライブラリ」と呼ばれるものがあります。 当然、OCaml にも存在していますが、他言語と比べると、機能的に不足している部分もあります。 近年は標準ライブラリも充実してきており、十分使えるのですが、それでももうちょっと色々欲しいなんて声も聞こえてきそうです。 そのため、ユーザーの中で作成されたライブラリがいくつかあります。 これから紹介する Web のためのライブラリの前提にもなるので、先にここで紹介してしまいましょう。 Base/Core/Async GitHub - janestreet/base: Standard library for OCaml · GitHub GitHub - janestreet/core: Jane Street Capital's standard library overlay · GitHub GitHub - janestreet/async: Jane Street Capital's asynchronous execution library · GitHub 最初に紹介するのは、世界でも OCaml をよく利用している企業の1つ Jane Street による OSS ライブラリです。 Base/Core はその名の通り、標準ライブラリを置き換えるための「基礎」や「核」となるライブラリです。 特に、Base は多くの標準ライブラリに欲しいモジュールが実装されており、今後紹介するライブラリの依存になっていることも多くあります。 直接的にせよ、間接的にせよ、OCaml を書いていれば、これらのライブラリに触れることになるでしょう。 また、同様にして非同期のためのライブラリとして Async が存在します。 5.0 で並行並列が実装されるまで、後述する Lwt と並んでデファクトスタンダードなライブラリの1つでした。 今でも、いくつかの理由で十分に利用する価値のあるライブラリです。 Batteries Included GitHub - ocaml-batteries-team/batteries-included: Batteries Included project · GitHub 一方で、Batteries Included はコミュニティによる標準ライブラリを強化するライブラリです。 Core を使うのか、Batteries Included を使うのか、あるいは、言語標準のライブラリを使うのかは趣味が分かれるところです。*2 Lwt GitHub - ocsigen/lwt: OCaml promises and concurrent I/O · GitHub Lwt は ocsigen プロジェクトの一部です。 標準ライブラリではありませんが、並行処理のためのライブラリで、プロミスなどの実装があります。 Async と並ぶ2つ目の選択肢であり、ここにも選択の余地があります。 EIO GitHub - ocaml-multicore/eio: Effects-based direct-style IO for multicore OCaml · GitHub EIO はこの中で最も新しい、エフェクトを利用した I/O ライブラリです。 ocaml-multicore プロジェクトから生まれており、I/O のための並列並行ライブラリになります。 Algebraic Effects and Handlers を活用した最新の並列並行を体験したい時には選ぶと良いでしょう。 過去の記事: 実用 Algebraic Effects and Handlers ~本番環境で OCaml を利用するために~ - エムスリーテックブログ Web FW 私はあまりフレームワークを使わないのですが、OCaml にもいくつか Web フレームワークがあります。 まずはここから始めるのも良いでしょう。 今回は特に新しいものを紹介します。 Dream GitHub - camlworks/dream: Tidy, feature-complete Web framework · GitHub ocaml.org にも利用されているフレームワークです。 基本的な API サーバーやフロントエンドも含めたフルスタック構成だけでなく、WebSockets や GraphQL まで対応しています。 このフレームワークを使う場合には EIO 対応がまだないため、Async か Lwt を使うことになります。 Vif GitHub - robur-coop/vif: A simple HTTP server for OCaml 5 · GitHub GitHub - robur-coop/miou: A simple scheduler for OCaml 5 · GitHub Vif はまだ beta バージョンのフレームワークです。 miou というエフェクトによるスケジューラーを用いており、これからの開発が期待されます。 HTTP ライブラリ さて、もし、フレームワークを使わない場合、何らかの方法で HTTP を扱える必要があります。 EIO を使って自分で実装するには大変ですから、いくつかのライブラリを紹介しましょう。 cohttp GitHub - mirage/ocaml-cohttp: An OCaml library for HTTP clients and servers using Lwt or Async · GitHub mirage という unikernel プロジェクトによる HTTP ライブラリです。 Async/Lwt だけでなく、EIO の対応もあり、直近では私も利用しています。 httpaf GitHub - inhabitedtype/httpaf: A high performance, memory efficient, and scalable web server written in OCaml · GitHub こちらはパフォーマンスを意識した HTTP ライブラリになります。 dream でもこちらが利用されています。 JSON ライブラリ GitHub - ocaml-community/yojson: Low-level JSON parsing and pretty-printing library for OCaml · GitHub JSON を扱いたいのであれば yojson がデファクトスタンダードでしょう。 yojson は文字列をパースし、専用の型へ変換します。 一方で、実際に使う際にはドメインモデルへ楽に変換したいですよね? GitHub - ocaml-ppx/ppx_deriving_yojson: A Yojson codec generator for OCaml. · GitHub GitHub - janestreet/ppx_yojson_conv: [@@deriving] plugin to generate Yojson conversion functions · GitHub そんな時に使える機能として、OCaml には PPX という仕組みがあります。 マクロのようなものと認識しておくと良いでしょう。 yojson に対する PPX は現在、公式のものと Jane Street 社がメンテナンスしているものがあります。*3 これもお好きな方を使うと良いでしょう。 DB ライブラリ GitHub - paurkedal/ocaml-caqti: Cooperative-threaded access to relational data · GitHub 現在、OCaml で DB 操作をするなら現実的に caqti を使うことになるでしょう。 Async/Lwt をはじめ、EIO や Miou にも対応しようとしています。 ロギング GitHub - dbuenzli/logs: Logging infrastructure for OCaml · GitHub 癖のないロギングライブラリです。 今後はエフェクトを使ったようなものも出るかもしれませんが、ただログを記録するだけであれば、logs でもあまり困りません。 いくつかのライブラリでは integration もあるので、これを使うのが良いでしょう。 SSG GitHub - xhtmlboi/yocaml: YOCaml is a static site generator, mostly written in OCaml · GitHub 最後に紹介するのは SSG です。 Web サーバーではありませんが、このライブラリは ocaml.jp でも利用しています。 markdown からの変換やテンプレート機能など、欲しい機能が揃っています。 使ってみたい方はぜひ https://github.com/ocaml-jp/ocaml-jp.github.io を覗いてみてください! OCaml 由来の AltJS 最後に、幾つかの AltJS について触れておきます。 OCaml から派生した AltJS は複数あります。 Js_of_ocaml Reason · Reason lets you write simple, fast and quality type safe code while leveraging both the JavaScri
【QAチーム ブログリレー7日目】の記事です。 はじめに 前提:なぜPlaywrightへ移行したか 「まずコードを書けるようになろう」は諦めた 5つのステップで開発フローへ Step 1: Claude Codeに慣れる Step 2: シナリオを「読む」 Step 3: VS Code Codegenを使う Step 4: mablシナリオの移行ができる Step 5: 積み重ねで改良・省力化へ 現在地 AIだけでは足りなかった:地道な活動 レビューでテストの意図を確認する リファクタリングで品質の均一化を図る AI導入と同時に向き合っている3つの負債 1. 理解負債:AIが書いたコードを誰も理解しない問題 2. 技術的負債(新しい形):AIツール自体の不安定性 3. 認知負債:AIへの過依存でチームの判断力が落ちる おわりに We're hiring! はじめに こんにちは、QAエンジニアの末吉です。最近東京を離れ、楽器不可のマンションに引っ越したため、ピアノが弾けなくなりました。悲しいばかり。 これは浜松ICにあるローランド製の電子グランドピアノ 突然ですが、私が担当するUnit4(m3.com開発チーム)のQAエンジニアには、開発経験がほとんどないメンバーもいます。 そういったメンバーが今では、ブランチを切り、Playwrightのテストコードを修正し、CIの失敗を自分で直してマージリクエスト(MR)を出せるようになっています。この半年で、チームが関わるリポジトリの8割以上にPlaywrightを導入した取り組みの話です。 この記事では、そこに至るまでのプロセスと、AI導入に伴って見えてきた「理解負債・技術負債・認知負債」という3つの課題への向き合い方を書こうと思います。 前提:なぜPlaywrightへ移行したか mabl(ノーコードE2Eテストツール)からPlaywrightへの移行の背景についてはこちらの記事で詳しく書いています。 「まずコードを書けるようになろう」は諦めた 最初に考えたのは、チームメンバーにTypeScriptを学んでもらうことでした。ただ、現実的ではありませんでした。日常のQA業務をこなしながら、ゼロからプログラミングを習得するのは時間的にも心理的にもハードルが高い。そもそも作成すべきテストシナリオは山積みで、「学んでから始める」では間に合いません。 そこで発想を変えました。「コードを書けるようになる」のではなく、「AIを足場にして開発フローに参加できるようにする」という目標設定です。 5つのステップで開発フローへ Step 1: Claude Codeに慣れる まず取り組んだのは、生成AIそのものへの慣れです。Claude Codeをインストールし、日常業務の中で使う練習から始めました。 ここで大事にしたのは、「なんでも答えてくれる優しい先輩に聞く」感覚で使うことです。余談ですが、私自身も開発エンジニアに仕様を聞くとき、ちょっと恐る恐るになりがちです。忙しそうだし、初歩的な質問はどうかな、と。同じような感覚を持つQAメンバーは多いんじゃないかと思います。でもAIには何度聞いても怒られません。「こんなこと聞いていいのか」という壁がないことが、非開発者にとっては大きな入り口になります。うまくいかなければ「さっきの回答はうまくいかなかった、こういう状況なんだけど」と伝え直す。このやり取りの繰り返し自体が、後のステップで効いてきます。 Step 2: シナリオを「読む」 次に、Playwrightのテストコードを読めるようにしました。書くのはまだ先です。 ここで活用したのが、社内にすでにあったリポジトリです。エムスリーでは他チームが先にPlaywrightを導入しており、ある程度完成されたコードベースが存在していました。それを参考に、コードがどういう構造になっているかを把握していきました。 読む際にはClaude Codeを使いました。「このファイルは何をしているか」「この処理の意味は」と聞きながら読み進めると、ゼロから自力で読み解くより理解が早い。既存のテストシナリオと並べて「この操作がこのコードに対応している」と照らし合わせる作業を繰り返すことで、コードの構造が徐々に見えてきます。完全に理解できなくても、「だいたいこういうことをしているコード」が分かれば十分です。 Step 3: VS Code Codegenを使う ブラウザを実際に操作すると、その操作に対応するPlaywrightコードが自動生成されるVS CodeのCodegen機能を使い始めました。 「コードは手書きするもの」という思い込みが崩れる瞬間です。自分の操作がコードになる体験を通じて、「コードは操作の記録だ」という直感が生まれます。これがあると、既存のコードを読む解像度も上がります。 このCodegenは、もともと使っていたテストツールのレコーディング機能と同じ感覚で使えました。ブラウザ操作を記録してテストを作るやり方に慣れているメンバーには、特に導入しやすかったです。 Step 4: mablシナリオの移行ができる Step 1〜3が揃うと、mablのシナリオをPlaywrightに移行できるようになります。 実際の作業では改良の結果、Step 3以降の手順はマルチエージェントで一気通貫に実施しています。シナリオの解析・コード生成・レビューを自動化し、メンバーがやることはエージェントの出力を確認して意図通りかを判断すること。「書く」ではなく「確認して承認する」役割です。詳しい仕組みはこちらの記事をご覧ください。 Step 5: 積み重ねで改良・省力化へ マルチエージェントで一気通貫の仕組みができてからも、改善は続きます。「このパターンはエージェントが苦手だ」「ここはいつも同じ修正が必要だ」という知見をもとに、エージェントへの指示を見直したり、新しいエージェントを追加したりする段階に入ります。 現在地 現在、チームのQAメンバーは以下ができるようになっています。 ブランチを切ってコードを修正し、MRを作成する CIが失敗したとき、エラーを読んでAIと一緒に修正し、再度MRを出す mablシナリオをエージェントに渡して、Playwrightテストとして移行する コードをゼロから書くことはまだできません。ただ、開発フロー全体を完結させることはできます。これが「AIを足場にする」ということです。 AIだけでは足りなかった:地道な活動 ここまでは5つのステップの話です。ただ、AIは移行を速く進めますが、移行後のテストが元のmablシナリオの意図を正確に再現できているかは別の話です。並行して地道な活動を続けたことが、品質を保つ上で重要でした。 レビューでテストの意図を確認する AIが生成したコードはそれっぽく見えます。動いてもいます。でも「このステップは元のmablシナリオの何を検証しようとしていたか」を説明できないまま承認しているケースが出てきます。そこで、移行後のテストがmablの意図を正しく再現できているかをレビューで確認する工程を設けています。 リポジトリ導入時には開発者にもレビュアーとして入ってもらい、コメントをもらっています。テストコードへの開発者の視点は、QAだけで閉じていると気づけない観点を拾ってくれます。 リファクタリングで品質の均一化を図る Playwrightの導入時期によって、リポジトリ間で品質に差が出ていました。早期に導入したものは設計の試行錯誤の跡が残っており、後から入れたものの方が洗練されている、という状態です。 そこで、ログインフローや管理画面のPage Objectなど、複数のリポジトリにまたがる共通コンポーネントをサブモジュール化し、品質の均一化を進めました。AIが個々のリポジトリで生成したコードをそのまま放置せず、共通化できる部分を人間が判断して整理する——この作業がなければ、リポジトリが増えるほど負債も増える一方でした。 AI導入と同時に向き合っている3つの負債 ここまでは成果の話です。並行して、AI活用に伴う3つの負債に向き合ってきました。もともとコードが書けないQAエンジニアがAIに頼る構造は、これらの負債が溜まりやすい。その分、意識的な対策が必要になります。 1. 理解負債:AIが書いたコードを誰も理解しない問題 GoogleのエンジニアリングリードであるAddy Osmaniは、AI生成コードによる「Comprehension Debt(理解負債)」を提唱しています。コードが生まれるスピードに理解が追いつかなくなり、「なぜこうなっているか」を誰も説明できない状態が蓄積していく、という問題です。 Anthropicの研究(How AI Assistance Affects Coding Skills)でも、AIを使ったグループは使わなかったグループより理解度テストのスコアが17%低く、特にデバッグ能力に最大の差が出たと報告されています。QAエンジニアにとって肝である問題を切り分けるデバッグ能力がAI依存によって落ちていくとしたら、深刻なリスクです。 私たちの対策: 前述のレビューがその実践です。マルチエージェントの自動レビューに加えて人間が確認する工程を残すことで、「誰も理解していないコードがマージされる」状態を避けています。Step 2(シナリオを読む)をあえて学習プロセスに組み込んだのも、同じ理由からです。 2. 技術的負債(新しい形):AIツール自体の不安定性 私たちのプロセスはClaude Codeに大きく依存しています。そのClaude Codeが2026年3月〜4月にかけて品質低下のインシデントを起こしました(Anthropic公式ポストモーテム)。推論レベルの変更、キャッシングのバグ、システムプロンプトの変更——3つの独立した問題が重なり、ユーザーへの告知なしに品質が低下していた期間があります。 さらに構造的な問題として、マルチエージェントの補完には限界があります。 通常時: エージェントAが誤った出力 → エージェントBが検知 ✅ 推論劣化時: エージェントAが劣化した出力 → エージェントBも同じモデルで劣化したレビュー → 全体が静かに劣化 ⚠️ 役割を分担していても、基盤のモデルは共通です。モデルが劣化すれば、多層化した全体が一緒に劣化します。 実際、この問題はチームで体験しています。品質低下の期間中、チームで定めていたエージェントへの指示ルールが丸ごと無視される形でコードが生成されるケースが発生しました。マルチエージェントで生成しているにもかかわらず、どの層でも検出できませんでした。最終的に発覚したのは、人間が手動でレビューしたときです。 私たちの対策: 根本的な解決は、多層化で「分散を吸収する」のではなく「そもそも分散を小さくする」方向にあると考え、現在取り組んでいます。具体的には、skillsやエージェントへの指示ルールの記述を見直しています。自然言語による指示(しかも継ぎ足しされるタイプ)はAIの解釈に幅が生まれ、それが出力のブレになります。そこで、解釈余地を持たせない構造化されたデータ形式やルールへの置き換えを進めています。公式もxmlタグフォーマットの使用を推奨しています。(prompting-best-practices) 3. 認知負債:AIへの過依存でチームの判断力が落ちる ビクトリア大学のMargaret-Anne Storey教授は、生成AI・エージェントAIの普及によって技術的負債よりも「認知負債(Cognitive Debt)」が問題になると指摘しています。AIに設計や実装を任せるほど、開発者自身がシステム全体を理解できなくなっていく、という問題です。 Anthropicの研究が示すもう一つの示唆は、AIを使う「方法」によって結果が変わるという点です。受動的に「やっておいて」と丸投げするほど理解度は落ち、「なぜこうなるか」を問いながら使うほど理解が保たれる。自分で考えず、AIに判断を丸投げし続けることで、じわじわとチームの判断能力が下がっていく——これが認知負債の本質です。 私たちの対策: 現状は理解負債と同様に、レビューとリファクタリングの徹底が対策になると考えています。「この移行は元の意図を再現できているか」をレビューで確認し、共通化の判断を人間が行う。ブラックボックス化を避けるため、少しスピードを落とした地道な活動です。 おわりに 開発経験がほとんどないメンバーが中心のチームが、AIを足場にPlaywrightの開発フローに参加できるようになりました。 ただ、それは「うまくいった話」の半分です。AIを入れるだけでは負債が積み上がるという認識から、レビューやリファクタリングといった地道な活動を続けています。そういう積み重ねが、チーム全体の底上げにつながると感じています。 生成AIはテスト実行の外にも広がり、QAプロセス全体に及んでいくでしょう。その波に乗りながら、AIに任せた部分に「なぜ」を問い続けること。そのバランスを模索している方の参考になれば嬉しいです。 We're hiring! エムスリーでは、QAエンジニアを募集しています。Claude Codeに限らず、あらゆるツールが使える魅力的な環境が整っています!是非ご応募ください。 jobs.m3.com
ソフトウェアエンジニアの末永です。私は個人開発でFlutter製のモバイルアプリを開発しています。このアプリを開発している中でアプリのビルド周りでハマってしまったことがあり、その際ビルドシステムに関してしっかりと調査しました。この記事はその調査の際に書いたものです。*1 なお、本記事は次のバージョンを対象とした内容となっています。 Flutter: 3.38.0 Dart: 3.10.0 また、ビルド対象はiOSとAndroidのモバイルアプリのみとします。 「iOSのReleaseビルドだけ古いアプリが出ている」問題 アプリの最新版をAppStoreとGoogle Playにリリースした後、iPhoneユーザーの方に新機能を紹介したら「え?そんな機能まだ出てないよ?アプリのバージョン?言われた通りの最新だよ?」と返答がきました。 どんな問題が発生したか 次のような状態になっていました。 iOSのRelease版だけ「アプリ内で表示されるバージョンは最新だが古い画面のアプリが出ている」状態になっていた ビルド後のバージョンだけはアプリ側も最新バージョンが表示されていた flutter runではiOSもAndroidも同じ最新バージョンが表示されていた Androidアプリのビルドでは最新バージョンでビルドできていた iOS Android flutter run ⭕️ 新しい ⭕️ 新しい flutter build ❌ 古い ⭕️ 新しい 問題が発生した原因 結論として、次のような凡ミスでした。 フォルダ構成変更のタイミングでの設定の移行漏れ 旧ディレクトリを消さずに残していた モバイル・サーバーサイド・APIスキーマのリポジトリをモノレポにした時の名残 Xcodeのプロジェクト設定ファイル中の絶対パスが旧ディレクトリだった 書いてみたら「そうなればそうなるわな」という話ですが、完全に消し去ったと思っていたのとflutter runでは新しいバージョンで起動していたのでなかなか思い至らず結構な時間を溶かしてしまいました。 この問題を解決するためにFlutterのビルドシステムのコードまで見にいきました。最終的に解決するまでずっと「ビルドのキャッシュに問題がある」と思い込んでおり、そのためどのタイミングでどこにキャッシュが生成され、どのタイミングで読み込まれるか、を調査していました。結局私の凡ミスだったのでビルドキャッシュは無関係だったわけですが、ここからは調査しながらまとめたFlutterのビルドシステムとビルドキャッシュについて説明していきます。 Flutterアプリのアーキテクチャ ここでビルドシステムへの理解を深めるため、Flutterアプリのアーキテクチャについて触れておきます。 主要なコンポーネントはFramework (Dart) とEngine (C++) です。ざっくりいうと、Frameworkが画面の描画を行い、Engineがネイティブ側とAPIレベルでの橋渡しを行います。もっと低レイヤにはEmbedderというコンポーネントが存在し、これがモバイル端末でEngineをロードします。 Frameworkはアプリのコードがビルドされたもので、EngineはFlutter SDKに同梱されています。次の公式の図がかなり分かりやすいので引用します。 Flutterアプリのアーキテクチャ図。引用元: https://docs.flutter.dev/resources/architectural-overview ビルド時に走る処理 Flutterアプリをビルドする際は次のような処理が実行されます。 ビルド対象に応じた設定ファイルを読み込む EngineをFlutter SDKから抜き出して適切に配置 アプリ側のコードをコンパイルしてネイティブコードに変換する パッケージング アプリ起動時に走る処理 Flutterアプリが起動する際、次のような処理が実行されます。 OS がアプリを起動 Embedder が Flutter Engine をロード Engine の中で Dart VM (Runtime) が起動 VM が、アプリと一緒に同梱されている AOTコンパイル済みのネイティブコード(アプリ)をメモリ上に読み込み、実行を開始 Flutterのビルドシステム ここからは、Flutter製のアプリをビルドする際に行われる処理について説明します。 3つのモード まずはFlutterアプリを実行する時に指定する起動モードについて説明します。 Flutterのビルドシステムには3つのモードがあります。 Debug Profile Release それぞれのモードは flutter run コマンドや flutter build コマンドでflutter run --releaseのようにして指定できます。これらのモードはそれぞれにパフォーマンスなどの違いがあります。 3つのモードの違いを簡単に表にまとめると次のようになります。 パフォーマンス コンパイラ サイズ 成果物 Debug 低 JIT 大 JITコンパイラ入りのEngineと中間バイナリ(app.dill) Profile 高 AOT 中 バイナリ + 計測用コード Release 高 AOT 小 最適化済みバイナリ 3つのモードはJITコンパイルが行われるかAOTが行われるか、計測用コードが入っているかいないか、により区別されます。 Debugではapp.dillファイルがそのままVMの上でJITコンパイルされて動くのでホットリロードなどができる ProfileとReleaseではAOTコンパイルの結果最適化されたバイナリが生成される。その際gen_snapshotなどの特徴的な処理が行われる (後述) この辺りの話は次の2つの公式資料に分かりやすく書いてあるので気になる方は参照してください。 github.com docs.flutter.dev ビルドシステムの概要 ここではFlutterでアプリをビルドする仕組みの概要を説明します。アプリのコンパイルはFrameworkのバイナリを作る作業になります。Frameworkのコンパイルが終わるとビルドによりFrameworkとEngineとEmbedderを組み合わせます。EngineとEmbedderはFlutter SDKより提供されてアプリにバンドルされます。 具体的には、AOTコンパイルでは次のような対応のファイルになります。 Framework: App.framework (iOS), libapp.so (Android) Engine: Flutter.framework (iOS), libflutter.so (Android) Embedder: Flutter.framework (iOS), flutter.jar (Android) JITコンパイルではFrameworkが最終的なバイナリではなくapp.dillという中間バイナリになります。Debugモードでビルドすると、JITコンパイラがEngineに入った状態でアプリが起動し、JITコンパイラにapp.dillを読み込ませることでホットリロードが実現されています。 AOTモードでのEngineの挙動については次のドキュメントが詳しいです。 github.com iOSでのビルド (AOTコンパイル) ここではiOSのAOTコンパイルに絞って具体的に説明します。 FlutterアプリをiOS向けにビルドした時の成果物はApp.frameworkです。これがFramework層のバイナリで、Dartから生成したバイナリが含まれます。 また、Engine層のバイナリは共通のファイルとしてFlutter SDKに組み込まれています。実際のアプリにはFlutter.frameworkという名前で同梱されます。 ビルドの流れ ビルドを開始すると、gen_snapshotによりDartのコードを4種類のsnapshotにします。snapshotはDartのコードをコンパイルしたもので、Dart VMの状態とアプリの実行命令をマシン語として書き出したバイナリデータになります。これらをApp.frameworkに組み込みます。 次に、Xcodeのツールチェーンによって4種類のsnapshotを組み込みながら App.framework を生成します。 実際にアプリが起動するときは、EngineであるFlutter.frameworkがApp.frameworkの中からsnapshotを見つけて実行します。 iOSのAOTビルドと実行の流れ 直接バイナリを実行しないのは、iOSのバイナリ実行に関する制約が理由です。 iOSの制約 iOSには、実行時にメモリ上で動的にコードを生成して実行すること (つまりJITコンパイル) を禁止する制約があります。しかし、Flutterアプリを動かすにはDartで書かれたコードのバイナリを実行する必要があります。 この制約を回避するため、Flutterはビルド時にDartコードを4種類のスナップショットに変換しApp.framework の中に直接埋め込むことで、安全に実行できる仕組みをとっています。 Flutterアプリのビルドキャッシュ ここでやっと本題のFlutterアプリのビルドキャッシュについて具体的に見ていきます。まずは私の個人開発プロジェクトでiOS向けにDebugビルドとReleaseビルドをした際の .dart_tool/ ディレクトリ以下のファイルを次に示します。 iOS向けビルド後のビルドキャッシュ 中間生成物 Flutterアプリをビルドすると、中間生成物が .dart_tool/flutter_build に保存されます。この中にはビルドしたバイナリ本体や、差分検知に使うstampファイルなどが含まれます。ファイルの差分を検知する仕組みにより、差分がない場合はこの中間生成物がビルドキャッシュとなります。 次のような流れでキャッシュを用いたビルドが行われます。 ファイルごとの差分を確認する 差分がなければスキップ。該当するファイルのstampファイルを確認し前回ビルドの中間生成物をそのまま利用する 差分があればビルドしてstampや.filecacheの更新を行う 前回ビルド時の中間生成物をそのまま使い回すかを決定するために使われるのが、stampファイルと.filecacheです。 stamp stampファイルはFlutterアプリをビルドする上で入力となるファイルと出力となるファイルの対応を示すためのものです。このファイルによりファイルと出力された中間生成物の対象をマッピングします。 なお、stampの話はFlutter本体の build_system.dart にコメントとして一部説明が書かれているので引用します。 /// For each target, executing its action creates a corresponding stamp file /// which records both the input and output files. This file is read by /// subsequent builds to determine which file hashes need to be checked. If the /// stamp file is missing, the target's action is always rerun 参照: <a href="https://github.com/flutter/flutter/blob/3.8.0-0.0.pre/packages/flutter_tools/lib/src
こんにちは。エンジニアリンググループのAI・機械学習チームに所属している鴨田 です。弊チームでは毎週1時間の技術共有会を実施しており、各自が担当するプロダクトの技術や、最近読んだ論文を紹介しています。今週はICLR2026が開催されていることもあり、同学会の論文読み会となりました。1セッションにつき1名が担当し、各自が選定した論文の詳細について解説しました。本ブログではその一部として、セッションごとの「推し論文」を紹介します。 まだ読んでいない方は前回のAAAI2026の輪読会ブログも是非ご覧になってください www.m3tech.blog ICLR 2026からトップページバナーを引用 Is it Thinking or Cheating? Detecting Implicit Reward Hacking by Measuring Reasoning Effort 推しポイント はじめに 報酬のハッキング 暗黙のハッキング 論文の提案 感想 AnyUp: Universal Feature Upsampling 推しポイント 課題 感想 Neon: Negative Extrapolation From Self-Training Improves Image Generation 推しポイント RealPDEBench: A Benchmark for Complex Physical Systems with Real-World Data 推しポイント マスク学習によるダイナミクスの獲得 U-Netの有用性とコストパフォーマンス 実データ収集における計測規模 Invisible Safety Threat: Malicious Finetuning via Steganography 推しポイント ステガノグラフィによる攻撃 ステガノグラフィでの学習、推論の工夫 感想 We are hiring !! エンジニア採用ページはこちら カジュアル面談もお気軽にどうぞ インターンも常時募集しています Is it Thinking or Cheating? Detecting Implicit Reward Hacking by Measuring Reasoning Effort セッション: Oral Session 2D LLMs and Evaluation 著者: Xinpeng Wang, Nitish Joshi, Barbara Plank, Rico Angell, He He 論文リンク:https://openreview.net/forum?id=Gk7gLAtVDO 紹介者: 髙橋 論文中Figure 2から引用 推しポイント はじめに ある評価指標が目標に設定されると、その評価指標はもはや良い評価指標ではなくなる これはよく知られた格言でKPIデザインなどでよく言及されますが、LLMにも当てはまります。 最近のコーディングエージェントなどのLLMは、多ステップの推論を経て複雑な問題を解きます。こうした推論能力の最適化には主に強化学習が用いられ、最終的な正解や、そこに至る正しい推論ステップ(過程)に対して報酬を与えることでモデルの学習が進められます。 報酬のハッキング この学習の過程でモデルは設計者の期待から逸脱した行動によって報酬を「ハック」しようとすることがあります。 例えば、ユニットテストの通過のみを報酬として設定すると、モデルは要件から想定される実装の代わりに、テストの入出力をそのままハードコードして無理やりパスさせようとするケースがあります。 このような「明示的なハッキング」であれば、モデルの推論過程(CoT:Chain-of-Thought)を監視することで比較的容易に検知できます。なぜなら、モデルが抜け道を使おうとするプロセスや意図自体が、CoTのテキスト上にそのまま出力されてしまうためです。 暗黙のハッキング 今回の論文が問題提起しているのは、明示的なハッキングより巧妙な「暗黙のハッキング(Implicit Hacking)」と呼ばれる現象です。 暗黙のハッキングとは、モデルが実際には早期に抜け道を利用して答えを出しているにもかかわらず、CoT上では「いかにも正当な推論を順序立てて行っているかのような」もっともらしいテキストを生成して隠蔽している状態を指します。 暗黙のハッキングが行われていると、出力されたテキストを監視する従来の手法では見抜くことができなくなってしまいます。 Claude Codeを利用していても、実際には実行していないコマンドをあたかも実行したかのように報告することがあります。 論文の提案 論文中の実験ではタスクの中に意図的にLoophole(容易に報酬が得られる抜け道)が存在する環境を構築し、強化学習を通じてモデルがどのように振る舞うかを観察しています。 引用図のように特定の条件(問題のid番号や負の数を返せば報酬が得られる)を満たせば本来の推論をスキップして正答できる抜け道を与えた結果、モデルはその抜け道を利用して容易に正解を導き出しながらも、出力上はもっともらしく長い推論過程を生成し続けるという「暗黙のハッキング」を学習するモデルが実際に作られました。 論文ではこうした暗黙のハッキングを検出するための手法として、Truncated Reasoning AUC Evaluation (TRACE)を提案しています。 考え方としては、「ハッキングを利用していると推論のステップ数が少ない段階で本来解けないはずの問題を正解できてしまう」、という性質を逆に利用します。 具体的には、推論ステップの生成を10%、30%、50%といった任意の割合で強制的に打ち切り、その時点での文脈のみから最終解答を出力させます。 各打ち切り時点の推論ステップと正答率をグラフにプロットし、その曲線の下の面積(AUC: Area Under the Curve)を計算します。 暗黙のハッキングを行っているモデルは、推論の初期段階で打ち切られても高い正答率を保つため、AUCの値が大きくなる一方でハッキングを利用していないモデルでは、推論ステップ数に伴って正答率が向上するためAUCの値は比較して小さくとどまります。 この指標を用いることで、生成されたテキストのもっともらしさに騙されることなく、モデルの内部的な推論の早期終了を定量的に検知できる、と論文では主張されています。 感想 LLMの発展は常に目覚ましいもので、どうしてもモデルの規模に目が奪われがちですが、モデル能力の向上は評価側の発展に支えられているのだなと改めて感じさせてくれる論文でした。 AnyUp: Universal Feature Upsampling セッション: Oral Session 5E Learning in computer vision 著者: Thomas Wimmer, Prune Truong, Marie-Julie Rakotosaona, Michael Oechsle, Federico Tombari, Bernt Schiele, Jan Eric Lenssen 論文リンク:https://arxiv.org/pdf/2510.12764 紹介者: 鴨田 論文中Figure 3から引用 視覚基盤モデル(VFM)の発展により、画像全体の高度な意味理解が可能になりましたが、構造上出力される特徴量マップの空間解像度が劇的に低下してしまうため、ピクセル単位の精緻な予測(セグメンテーションや深度推定など)が難しいという根本的な課題があります。 この課題を解決し、低下した解像度を復元するアプローチとして特徴量アップサンプリングの研究がこれまでも盛んに行われてきました。しかし、既存の学習ベースの手法には、DINOやCLIPなどバックボーンとなるモデルを変更するたびに、アップサンプラー全体をゼロから再学習しなければならないという実用上の大きな壁がありました。 この論文の面白いところは、特定のモデルアーキテクチャに依存しない「特徴量非依存(Feature-Agnostic)」なアップサンプリングを推論時に実現している点です。 従来のように特定の特徴量に依存するのではなく、入力段に独自の「特徴量非依存レイヤー」を設け、画像のどこにエッジやテクスチャの境界があるかといったパターンを抽出するアプローチをとっています。これにより、未知の次元数や表現空間を持つ特徴量であっても、一度学習するだけで任意の解像度へ柔軟に拡張できるようになりました。 推しポイント 個人的に一番の推しなのは、「画像の解像度を復元(アップサンプリング)するには、わざわざ画像全体の特徴を計算しなくても、局所的(Local)な特徴を見るだけで十分ではないか」というアプローチです。 従来の手法は画像全体のクロスアテンションを計算しがちでしたが、AnyUpは局所的な構造変化さえ捉えられればよいと割り切り、ローカルなウィンドウアテンションを採用しています。さらに、複数の異なるモデル(DINOv2とSigLIPなど)でマルチバックボーン学習させることで、未知のモデルに対するゼロショット汎化性能を底上げしており、この直感的な設計と普遍性の組み合わせが見事です 。 課題 ただし、ウィンドウアテンションの副作用として、オブジェクトの境界分離能力(空間的識別性)が甘く、特徴量マップ全体が滑らかに一様化しやすい傾向があったり、アテンション計算の性質上、解像度が大きくなると計算コストとメモリ消費が二次関数的に爆発してしまうという構造的な弱点も抱えています。 このような課題が解決されていないので、計算の線形化を目指す「UPLiFT」や、学習効率と高倍率へのスケーリングに特化した「DiveUp」という論文が続々と登場しています。 感想 とはいえ、テーマ自体が「普遍的アップサンプリング」として面白いので後続の論文がたくさん出ている点と、この論文自体シンプルなアプローチで読みやすい点を考慮して推し論文としました! 後続の論文も読みやすいので合わせて読んでみてください! Neon: Negative Extrapolation From Self-Training Improves Image Generation セッション: Oral Session 1B Generative models I 著者: Sina Alemohammad, Zhangyang Wang, Richard Baraniuk 論文リンク:https://openreview.net/pdf?id=kpLRYtPGt3 紹介者: 中村 伊吹 論文中Figure 1から引用 推しポイント 生成AIの性能向上には大量のデータが必要ですが、高品質な実データは今後ますます貴重になります。そこで期待されるのが、モデル自身が生成した「合成データ」を使った自己学習です。ただし、合成データだけでFine-Tuningすると、品質や多様性が急速に崩れる「モデル崩壊」が起きやすい、という難しさがあります。 論文では、合成データによる崩壊の原因を、モデルがもともと出しやすい画像をさらに出しやすくしてしまう mode seeking にあると説明しています。つまり、自己生成データにはどうしても偏りがあり、その偏りでさらに学習すると、よく出るパターンばかりが強化され、生成の多様性が失われていく、という見方です。 この論文の面白いところは、そのモデル崩壊を「避けるべき失敗」として扱うのではなく、あえて利用する発想にあります。合成データで少しだけ自己学習したときの更新方向を、「モデルが崩壊していく方向」だとみなし、その逆向きにモデルを動かすことで性能改善を狙います。タイトルの Negative Extrapolation という名前も、
【QAチーム ブログリレー6日目】の記事です。 はじめに こんにちは。エンジニアリンググループ QA (QualityAssurance) チームの中塚です。 2週間ほど前からバジルの水耕栽培にチャレンジしていて、少しずつ大きくなる双葉を見守るのが毎朝の楽しみです。肥料を溶かした水だけで本当にあんなに大きく育つのか?自由研究の気分で楽しく観察しています。 画像はAI(Gemini)により生成されたイメージです。このブログ記事の内容から生成してもらいました。バジルの鉢植えもあってかわいい! さて、私は普段コンシューマ向けサービス全般のQAの設計、実施、計画、その他諸々の活動をしています。 その中で、時々このような手動でやるには辛いテストが必要になることがあります。 サービスの各ページにアクセスし、契約期間内ならアクセスできるが契約期間外ならアクセスできないことを確認したい。ページのリストは無く事前調査が必要で、おそらく数十ページを繰り返しアクセスして比較する 特定のキーワードを検出し、警告するようなチェックシステムをテストしたい。外部LLMによるチェックなのでキーワードリストはないが、精度を見ておきたいのでさまざまなキーワードを試して動作確認したい。 このように、同じような操作を何回も繰り返したり、ページの調査やキーワードリストの準備といった事前調査などを含む地道な作業が必要になるテストを自動生成・自動実行できないか試したところ、PlaywrightとClaude Codeを使い手軽にテストコード生成〜実施〜レポート出力までが実現できたので、その実践例をご紹介します。 はじめに Claude CodeとPlaywrightについて 前準備 テスト計画を書く 例:アクセス制御テスト 実装 → 実行 → レポート 良かった点 時間が大幅に短縮され、手を離せた 気軽に使えた 使いやすい わかりやすいレポートが出せる 今後の展開 おわりに We're Hiring! Claude CodeとPlaywrightについて 弊社ではClaude Codeが無制限で利用可能となっていて、普段の業務からテストまたはコーディング業務などに活用しています。Claude Codeに自然言語で依頼すると、人間が指定しなくてもタスク実行のためにClaude Codeが適切なツールを選定して使用してくれます。また、plan modeを利用すると、実行前に対話したり、向こうから質問してもらい精度を上げてから実行できます。 PlaywrightはWebアプリケーション向けのE2Eテストフレームワークで、テストランナー、アサーション、分離機能、並列処理、自動待機(Auto-wait)機能など豊富なツール群が備わっており、実現したいテストをシンプルに書くことができます。 エムスリーでは多くのプロジェクトでE2EテストがPlaywrightで書かれています。また、Playwright MCPをインストールしておくことで必要に応じてブラウザを起動し操作したり内容を確認してくれます。 今回の例では必須ではありませんが、PlaywrightにはPlaywright Test Agentが付属しており、導入しておくとこのようなタスクを効率よく実行してくれます。 Planner : アプリケーションを探索し、マークダウン形式のテスト計画を自動作成 Generator : マークダウン計画から実行可能なPlaywright Testコードを生成 Healer : 失敗したテストを自動検査・修復し、パスするまで調整 計画も含めAIに任せられる仕組みがありますが、今回のケースでは計画を人間が作り、設計、実装、実行、レポート出力をAIに任せる使い方をしました。 前準備 Playwright MCP、Claude Codeをインストールし、Playwright(+ Playwright Test Agent)を入れたリポジトリを用意します。 テスト計画を書く まずは自然言語でやりたいことを簡潔に書きます。 例:アクセス制御テスト # やりたいこと アクセス可能/不可能の比較テストをしたい。 契約期間内の組織は専用LPにアクセスできるが 契約期間外の組織は専用LPのどのページにもアクセスできないことを確認したい。 契約期間内の組織のLP:https://example.com/active 契約期間切れした組織のLP:https://example.com/expired 先に契約期間内の組織LPを探索し、存在する画面のリストを作成した後に URL末尾のIDを契約期間切れした組織のIDに書き換え、それぞれにアクセスできないことを確認してほしい。 また、テスト結果レビューのため、確認したURLのリストと結果を一覧で残してほしい。 実装 → 実行 → レポート このようなテスト計画をざっくり書いたら、Claude Codeに計画を読み込んでもらいます。 plan modeで実行するとテスト計画に盛り込めていなかった詳細をAIが質問してくれるので、対話形式でより細かく作り込むことができます。 今回の例では、このような事項が対話を通じて追加されました。 クローリングを行うが、何階層まで行うか 「アクセスできない」の基準(HTTPステータスコードが4xxエラー、特定のエラーが表示されるなど) 除外すべきURLの種類 テスト実施結果レポートに含めたい情報 テストの失敗条件(契約切れ1つでも失敗したら失敗とみなすなど) 記事コンテンツなど同じ種類のページを全てテストするか テスト実行中の進捗表示の必要有無 etc... これらをインプットすると実装前に実装計画が出ます。確認し、疑問や修正点があれば指摘し、問題なければ実装を進めてもらいます。 AIはテスト計画を読んで ページクローリング機能 URL書き換えロジック ステータスチェック 結果レポート生成 といった機能を持ったテストコードを生成してくれました。 実装が終わったら、生成されたテストコードを実行します。コンソールからコマンドを入力して自分で実行してもいいですし、Claude Codeに依頼して実行してもらうこともできます。 実行後、このように結果をレポートしてくれます。 # アクセス制御テストレポート **実行日時:** 2026/4/19 18:53:04 ## 組織情報 - **契約期間内組織:** active - **契約期間切れ組織:** expired ## サマリー | 指標 | 値 | |------|----| | 発見ページ数 | 42 | | 契約期間内アクセス成功 | 41 | | 契約期間切れアクセス拒否 | 42 | | テスト成功 | 41 | | テスト失敗 | 1 | ## 詳細結果 | ページパス | 契約期間内 | 契約期間切れ | テスト結果 | |-----------|----------|------------|----------| | /active | ✓ (200) | ✗ (404) | ✅ PASS | | | | *理由: 404 Not Found* | | | /active/about | ✓ (200) | ✗ (404) | ✅ PASS | | | | *理由: 404 Not Found* | | (以下略) 内容を確認し、更に確認したいことがあればインプットして追加で実装・実行することも可能です。 また、レポートの形式変更や内容の整理のリクエストなどもできるので、レビューする上で助かりました。 良かった点 時間が大幅に短縮され、手を離せた もし手動で実行していたら繰り返しの操作と確認の作業が数十回必要となっていましたが、これらを自動実行に任せ、また、実行・出力のタスクも10〜20分程度で完了しました。 短時間で済んだほか、タスク終了までの待機時間中に他の作業を進めることができました。 気軽に使えた ラフな計画から対話を通じて精度を高め、集中力を他の仕事に振り分けながら実行できたのは嬉しいことでした。私は複数PJを兼任していて、仕様やタスク内容を思い出すなどの他の仕事からのスイッチングのコストが負担になる場合がありますが、AIのアシスタントでこれを軽減できるのでありがたいです。 曖昧な甘い部分をAIが補完してくれるので、作り込んでから渡さなくてもタスクが発生した時点でまず軽い相談から最初の一歩を踏み出せます。 使いやすい 生成や実行が早く、気軽に生成して気軽に使い捨てできるので、自動化できそうだなと思ったらまずはやってみるといった使い方ができます。 今回の例だとクローリング、アクセスしてステータスコードをチェック、とシンプルな手順でしたが、テスト内容によってはいざコードは生成されたが実行してみるとロケーターが不安定、操作内容やUIが複雑でPlaywrightの操作がうまくいかないなどのトラブルもありえます。 そういう時は実装を切り上げ、一部の実施のみ手動で実行するなどの方針転換も容易です。どこまでを自動化するかを相談しながら進めることもできます。 わかりやすいレポートが出せる テストの内容と実施結果がレポートとして出力されます。 最初に出されたものを元に失敗したものについて質問して切り分けを進めてもいいですし、フォーマットが気に入らなければ再生成の指示を出せば指示通りのフォーマットにしてくれます。 また、レポートのレビュー中に実施結果の不足や追加確認の必要が分かれば追加で指示を出し、更なるテスト実装・実行も可能です。 今後の展開 現在の使い方としては、単発のテストのタスクを簡易に自動化するという使い方をしています。 このため使い回す仕組みがなく、タスクごとにテストに必要なテストコードを都度生成していますが、同じプロダクトでタスク実行の機会を重ねれば、ログインやメイン機能などよく使うテスト手順を共通化し、テストコードを育ててより複雑なテスト手順に対応することも考えられます。 または、同プロダクトにはすでにPlaywrightで書かれたE2Eテストがあるので、こちらからコードを借用する使い方もできるかもしれません。 ただし、複雑な手順はコードも複雑になるほか、手動で実施すべき操作とかけ離れた手順で実装されるリスクもあり、意味のあるテストであるかはしっかりレビューする必要があります。このレビューの精緻化、効率化も課題となるでしょう。 おわりに PlaywrightとClaude Codeを使うことで、テストを手軽に自動化できました。 手軽に試せてすぐに成果物が出るので、「まずはやってみるか!」の気持ちで軽く相談することから始めてみても良さそうです。自動化をやってみたいけどハードルの高さを感じていた方も、まずは試してみてはいかがでしょうか。 We're Hiring! エムスリーQAチームでは一緒に働く仲間を募集しています。ご興味のある方は、ぜひ採用ページをご覧ください! jobs.m3.com
【QAチーム ブログリレー5日目】の記事です。 こんにちは。エンジニアリンググループ QAチームの須賀です。 最近エムスリーに復帰しました。 私は2月1日に入社してからQAエンジニアも使い放題のClaude Codeを用いてmabl(ノーコードのE2Eテストツール)の自動テストをPlaywrightにリプレースしています。 AIエージェントを用いた開発は未経験だったため、最初は期待するコードをなかなか生成できず試行錯誤の連続でした。しかし、最近では人の介入なしで期待するコードが生成できることも増えてきました。 試行錯誤の結果、AIエージェントの活用において重要だったのは、実は『人間同士が円滑に仕事をするためのマネジメント方法』を適用することだと気づきました。 本記事では、次の参考書籍の知見をベースにしつつ、実際のリプレース業務において試行錯誤して私が構築した、6つのサブエージェントを活用したワークフロー設計の実例を紹介します。 参考書籍 gihyo.jp 設計の全体像 — .claudeディレクトリとCLAUDE.md commands:自律的なワークフローの定義 ワークフローの流れ ワークフローに沿ったタスクリストの作成・更新 テスト仕様書の作成と人によるレビュー AIによる実装、テスト実行、コードレビューのループ 作業中に発生した課題の振り返り agents:役割の分離と責任の明確化 各工程でエージェントを分ける エージェントの作業の流れを具体的に書く やらないことを定義しておく skills:再利用可能な知見の集約 エラー分析フローについて おまけ:Claude Codeに私のリポジトリを採点してもらった 総合スコア: 92点 / 100点 おわりに We are Hiring! 設計の全体像 — .claudeディレクトリとCLAUDE.md Claude Codeでは、リポジトリのルートに置くCLAUDE.mdファイルと.claude/ディレクトリでAIエージェントの振る舞いをカスタマイズできます。 AIエージェントに期待する成果物を生成させるには、AIエージェントの行動を細かく定義したり、成果物を別のツールやエージェントでチェックしたり、コンテキスト量を減らしたりする必要があります。 そこで、私は、.claudeディレクトリとCLAUDE.mdの設計を次のようにしました。 要素 役割・定義 詳細 CLAUDE.md インデックス(目次) AIエージェントが目的の情報に辿り着くためのガイド。詳細情報は別ドキュメントに委ねる。 commands 抽象的なワークフロー 作業の種類に応じた工程と利用エージェントを定義。現在はE2Eテストのリプレース作業のみ。 agents サブエージェントの定義 エージェントの役割・目的、および特定の工程における具体的なワークフローを管理。 skills 具体的な手順・ナレッジ ルール、Tips、実作業レベルの手順など、各エージェントが参照すべき情報を集約。 hook(settings.json) 自動チェック(静的解析) コーディング規約等の即座なチェック。サブエージェントの負担軽減と手戻り防止。 1つのスラッシュコマンドを実行する親エージェントが6つのサブエージェントをオーケストレーションし、各エージェントが必要に応じてスキルを参照する形になっています。 なお、各ファイルはClaude Codeに設計方針などを指示して作成してもらいました。 commands:自律的なワークフローの定義 E2Eテストのリプレース作業について、計画から実装完了までのワークフローを定義しました。 いつ誰が何をするかといった「作業の進め方やルール」「AIエージェントに作業を任せる範囲」を決めることで、AIエージェントが迷わずに安定して作業できるようになります。 ワークフローの流れとcommandsに関する意図や工夫点などをいくつかご紹介します。 ワークフローの流れ ワークフローの大まかな流れは次の通りです。 ワークフローに沿ったタスクリストを作成 mabl CLIを用いてmablのテストケースをPlaywrightのファイルにエクスポート このファイルをテスト仕様書作成やテストコード実装フェーズで参照する テスト仕様書(テストコードのスケルトン)を作成 spec-writer-agentによるスケルトンの作成(または、修正) spec-reviewer-agentによるレビュー → spec-writer-agentによる修正のループ 指摘がなくなるか、5回実施するまで繰り返す 人によるスケルトンのレビュー 指摘がある場合は3-aに戻る テストコードの実装 test-writer-agentによるテストコードの実装(または、修正) コード編集のたびにhooksでESLint自動実行 → Lint違反はAIが即座に自己修正 test-runner-agentによるテスト実行 → test-writer-agentによる修正のループ テスト実行が成功するか、AIが自力で解決できない問題が発生するか、10回実施するまで繰り返す code-reviewer-agentによるコードレビュー → test-writer-agentによる修正 → test-runner-agentによるテスト実行のループ 指摘がなくなるか、5回実施するまで繰り返す 人によるコードレビュー 指摘がある場合、4-aに戻る 作業中に発生した課題の振り返り 図で表すと、次のようになります。 E2Eテストリプレースのワークフロー ワークフローに沿ったタスクリストの作成・更新 最初に上記ワークフローに沿ったタスクリストを作成し、各工程が終わった後にAIエージェントにタスクリストを更新させます。 タスクリストを更新する際は、作業の開始・終了時刻だけでなく、作業時に発生した問題点も記載します。 タスクリストは次のような効果が期待できます。 コンテキスト量の上限を超えてコンテキストの圧縮が行われてもタスクの進捗を追跡できるようにする 最後に課題を振り返りやすくなる タスクリストを残さない場合、人が修正を依頼した内容しか振り返り対象にできません。一方、タスクリストを残すと、AIだけで作業は完結したが時間がかかってしまった課題なども振り返り対象にできることが多いです テスト仕様書の作成と人によるレビュー テスト手順をAIエージェントが実装する前に、人がレビューして認識を合わせます 理由は、現状ではアプリケーションの仕様や画面遷移に関する情報が不足しており、AIエージェントがテスト手順の正しさを判断するのが難しいからです。そのため、仮に誤ったテスト手順で実装してしまうと、AIエージェントが実行エラーの原因を自力で解消できず、実装に時間がかかってしまいます。 ただし、既存のテストコードをリプレースするプロジェクトという特性もあり、最近はこの工程で人から指摘が入ることは多くありません。 将来的には、リプレースプロジェクトにおいてこの工程は不要になると考えています。 AIによる実装、テスト実行、コードレビューのループ AIエージェントによる実装、テスト実行、レビューのループを繰り返すことで、人がコードレビューを実施する前のコードの完成度を向上させます。 ただし、テスト実行時のエラー原因はテスト手順やテストデータの不備などAIエージェントが自力で解決できない原因の場合もあります。 そのため、無限ループにならないようループを抜ける条件を定義しておきます。 作業中に発生した課題の振り返り .claudeの各ファイルを継続的に改善するために、作業中に発生した課題を振り返ります。 ただし、AIエージェントに.claudeの各ファイルを自動で修正させていません。 なぜなら、AIエージェントによる課題分析がまだ浅く、本質的ではない改善案を提案されることが多いからです。例えば、ループを8回繰り返してしまった場合に「ループの上限を増やす」という提案をされますが、本来はループ回数を減らすための根本的な改善策を提案すべきです。 最終的には、振り返り用のスキルを改善するなどして、自動で.claudeの各ファイルが改善される仕組みを作りたいと考えています。 agents:役割の分離と責任の明確化 私が作成したエージェントは次の通りです。 エージェント名 役割・担当業務 mabl-export-agent mablテストをPlaywright形式にエクスポートする。 spec-writer-agent エクスポート結果を基に、テスト仕様書(スケルトン)を作成する。 spec-reviewer-agent テスト仕様書がmablテストの内容を忠実に再現できているかレビューする。 test-writer-agent テスト仕様書に基づき、Page Objectパターンでテストコードを実装する。 test-runner-agent テストの実行および、発生したエラーを分析する。 code-reviewer-agent 設計の妥当性や保守性の観点から、実装されたコードをレビューする。 agentsに関する意図や工夫点などをいくつかご紹介します。 各工程でエージェントを分ける 各工程でエージェントを分ける理由は、「情報を整理」してAIエージェントに必要なコンテキスト量を減らすこと、「役割を分けて視点の違いを活かす」ことにあります。 1つのエージェントで全ての作業をするとすぐにコンテキスト量の上限を超えてしまい、指示内容を忘れるといったことが発生します。 そのため、各工程でエージェントを分けて作業に必要なコンテキスト量を減らします。 また、成果物を生成するエージェントと成果物をレビューするエージェントを分けることで、成果物の品質を保ちやすくなります。 例えば、test-writer-agentはテストコードの実行成功を重視し、コーディング規約への配慮を怠ることがあります(例えば、ハードコードされた待機時間を入れる)。 一方、code-reviewer-agentはコードの品質を守ることを重視するため、このような品質の課題を適切にチェックできます。 エージェントの作業の流れを具体的に書く 「作業の進め方」をより具体的に示すことで、エージェントの振る舞いを安定させることができます。 例えば、test-writer-agent.mdファイルに「既存のページオブジェクトのファイルがあれば再利用して」とだけ書いても、既存のものを使わず新規作成することがあります。 Claude Codeに原因を質問すると、「既存のページオブジェクトのファイルを探す手順を示していないので、探し方がセッションによって異なるからではないか」という趣旨の回答が得られました。 そのため、既存のページオブジェクトのファイルを探す手順を次のように具体的に定義する必要があります(agentsのファイルに記載するか、このような記載のあるskillを参照させます)。 ページオブジェクトの一覧と説明が記載されたファイルを参照する 今回実装するテストに関連するキーワードでgrep検索する 類似の画面・機能をテストしているテストコードのファイルを参照し、利用しているページオブジェクトのファイルを確認する やらないことを定義しておく AIエージェントに任せる作業の範囲を具体化するため、やらないことも定義しておきます。 やらないことを定義しない場合、テスト実行とエラー原因の分析をするだけのエージェントなのにコードを修正してしまう、テスト仕様書のレビューで指摘しなくても良い観点を指摘してしまう、といったことが発生する場合があります。 そのため、エージェントの振る舞いを安定させるためには、やらないことも定義しておく必要があります。 前者(test-runner-agent)については、エージェント定義でwrite権限自体を外しておきます。Claude Codeのサブエージェント定義では、利用可能なツール(Read、Write、Bashなど)をエージェントごとに制限でき、「やらないこと」をルールだけでなく権限レベルで強制できます。 後者(spec-reviewer-agent)については、リプレースプロジェクトのためテスト観点の網羅性はレビュー対象外とする、というように記載します。 skills:再利用可能な知見の集約 複数エージェントで利用する知見や、情報量の多い知見などはスキルとして切り出しました。
【QAチーム ブログリレー4日目】 はじめに こんにちは、QAチームの草場です。 レーモン・クノーの『文体練習』という本をご存知でしょうか? 1947年に出版されたこの本は、とある短い1ストーリーを99通りの文体で書きわけるもので、語られるのは同じストーリーなのに文体を変えるだけで得られる情報や印象の変化を感じられる味わい深い本です。 今回は「文体練習」を参考に、システムの仕様を表す文体として最適なものは何か? もしくは文体による差は無いのか?をカジュアルに実験してみました。 仕様は、書き手や場面によって様々な文体で書かれることがあります。決められた書式で厳密に書かれた物、広く知られた記法では無いが構造的に整理された物、Slackに貼られたメモのような物、ユーザーからの口伝を文字起こしした物など、多種多様です。どのテキストでも同じ機能の話をしているとして、そこから読み取れる情報量やテスト観点の数は、同じなのでしょうか? 異なるのでしょうか? このように、実験してみました。架空のフードデリバリーアプリの注文フローを11の文体で書き分け、それぞれをAIに読ませてテストケースを生成させ、事前に人間が作成したテストケースに対するカバレッジを比較するというものです。 結論を先に言うと、文体によるカバレッジの差はありました。ただし、それ以上にAIの実行ごとのブレや評価方法の影響が大きく、「文体にこだわる必要は薄い」という結果になりました。以下、実験の詳細と結果です。 はじめに 実験の概要 対象 11の文体 評価方法 同じシーンを11文体で書き分けると Gherkin 散文調 要件定義書風 極端に簡潔(メモ書き) 結果: 確かに差は出た Top Tierの5文体はほぼ同じ結果 文体の差より効いたこと 1. 実行ごとのブレが大きい 2. 生成と評価を分離する 3. 文体で拾えない観点を別添えする まとめ: AI時代の文体選びの考え方 余談 We're hiring! 実験の概要 対象 架空のフードデリバリーアプリ「QuickEats」の注文フロー。レストラン検索からカート追加、決済、配達追跡、評価までを含みます。誰でも使ったことがあるサービスで、AIにも十分ドメイン知識があることを想定しています。 11の文体 現場で実際に見かける記述スタイルを11個用意しました。 Gherkin / 箇条書き / 散文調 / ユーザーストーリー / フローチャート / 会話形式 / 表形式 / 要件定義書風 / メモ書き / 契約による設計風(Design by Contract) / スクリーンショット指示 各文体の特性に従って「自然に」書くというルールにしました。あえて、その文体で書けば自然に抜け落ちる情報はそのままにします。「文体が情報の取捨選択に影響する」現象そのものを観測したいからです。 評価方法 各文体をClaude(Sonnet 4.6)のサブエージェントに入力し、同一プロンプトでテストケースを生成させます。生成されたテストケースを、事前に人間で用意した26個のテスト観点と照合し、カバレッジを測ります。 AIの確率性を見るため 各文体で5回ずつ実行。合計55回の実行です。また、同じAIに生成と評価の両方をさせると客観性に欠けるため、生成するAIエージェントと評価するAIエージェントを別にしました。 なお、本実験ではコンテキストエンジニアリングやプロンプトチューニングをいっさい行っていない「素のClaude Code」を使いました。システムプロンプトの仕込みも、few-shot例の追加も無しです。 同じシーンを11文体で書き分けると 結果を出す前に、文体の違いを体感してもらうために、1つ見てもらう実験があります。 「商品をカートに追加する」というシーンだけを取り出して、11文体がそれぞれどう書いているかを並べてみます。同じ出来事なのに、11通りに書き分けると、読む印象がこれほど違います。長くなりますが、クノーの『文体練習』を追体験する意味でもスクロールしながら眺めてください。 Gherkin Scenario: 商品をカートに追加する Given レストラン「麺屋たろう」のメニュー画面を開いている When 「味噌ラーメン」を選択する And サイズ「大盛り」を選択する And トッピング「味玉」を追加する And 「カートに追加」ボタンを押す Then カートに「味噌ラーメン 大盛り 味玉」が追加される And カートアイコンに個数「1」が表示される 厳格で、テスト自動化にはそのまま使えそうです。一方で「売り切れだったらどうなる?」「価格はいくら?」には触れていません。 散文調 メニューをスクロールしていく。味噌ラーメン、醤油ラーメン、つけ麺。味噌ラーメンにしよう。タップすると、オプション画面が出てきた。サイズは大盛りにして、味玉もつけよう。合計980円。「カートに追加」を押す。 ユーザーの気持ちの流れは伝わります。でも操作手順の境界が曖昧で、テスト設計には使いにくそうです。 要件定義書風 3.3 メニュー閲覧及び商品選択(REQ-ORD-003) (2)利用者が商品を選択した場合、本アプリは当該商品に関するオプション(サイズ、トッピング、調理方法の指定等。以下、「商品オプション」という。)の選択画面を表示するものとする。商品オプションの内容及び価格は、各レストランパートナーが設定するものとする。 (3)利用者は、商品及び商品オプションの選択を完了した後、当該商品をカートに追加することができるものとする。 (4)商品が売り切れ(在庫切れ)の状態にある場合、本アプリは当該商品が現在利用できない旨を表示するとともに、可能な範囲において代替商品の提案を行うものとする。 厳密なようで厳密ではない文体ですが、売り切れ時の代替商品提案といった観点が定義されています。他の文体では触れられていない観点です。 極端に簡潔(メモ書き) 商品選ぶ → オプション(サイズ、トッピング等)→ カート追加 時間がない時にSlackに書き殴ったような3行メモ。 残り7文体 箇条書きステップ 5. 商品を選ぶ 6. オプションを指定する(サイズ、トッピング、辛さ 等) 7. 「カートに追加」を押す(複数商品を追加する場合は手順5〜7を繰り返す) ユーザーストーリー形式 US-004: 商品オプション指定とカート追加 ユーザーとして、商品のサイズやトッピングを選んでカートに追加したい。なぜなら、自分好みにカスタマイズした商品を注文したいから。 受け入れ条件: - サイズを選択できる(例: 並盛り/大盛り) - トッピングを追加できる(複数選択可) - オプション選択後の合計金額がリアルタイムで表示される - 「カートに追加」で商品がカートに入る - カートアイコンに商品数が表示される フローチャート SelectItem[商品を選択] ↓ SetOptions[オプション指定\nサイズ/トッピング等] ↓ AddToCart[カートに追加] ↓ Continue{買い物を続ける?} Yes → BrowseMenu に戻る No → ReviewCart[カート確認] 会話形式 ユーザー: 「マルゲリータ」を選択 システム: 商品詳細を表示。サイズ(S/M/L)とトッピング(チーズ追加、バジル増量 etc.)のオプションを提示。 ユーザー: Mサイズ、チーズ追加を選んで「カートに追加」をタップ システム: カートに追加完了。画面下部にカートアイコン(1点、合計1,580円)を表示。メニュー画面に戻る。 表形式 # ステップ 操作 入力データ 期待結果 4 商品選択 メニューから商品をタップ - 商品詳細画面が表示される。オプション選択UIが表示される 5 オプション指定 サイズ・トッピング等を選択 サイズ: M, トッピング: チーズ追加 選択内容に応じて価格が更新される 6 カート追加 「カートに追加」ボタンをタップ 数量: 1 カートに商品が追加される。カートアイコンに件数と合計金額が表示される 契約による設計風(Design by Contract) Operation: AddToCart Pre-condition: - selectedRestaurant ≠ null - item ∈ selectedRestaurant.menu - item.isAvailable = true Trigger: - ユーザーが商品を選択し、オプション(サイズ、トッピング等)を指定してカートに追加する Post-condition: - cart.items.contains(item, selectedOptions) - cart.totalAmount が再計算されている Exception: - item.isAvailable = false → 売り切れ通知を表示する スクリーンショット指示 1. 注文したい商品をタップしてください 2. 商品詳細のモーダルが画面下部からスライドアップします 3. サイズ選択がある場合、ラジオボタンで「S / M / L」等が表示されます。希望のサイズをタップしてください 4. トッピング選択がある場合、チェックボックスのリストが表示されます。追加したいトッピングをタップしてチェックを入れてください 5. モーダル下部の「-」「+」ボタンで数量を調整してください 6. 画面最下部の緑色のボタン「カートに追加 ¥XXX」をタップしてください 文体を比較してみると、「拾えている観点」に違いが見えてきます。 売り切れの扱い: 要件定義書風と契約による設計風だけが明示。散文やメモ書きは触れていない 具体的な数値: 散文は「980円」、会話形式は「1,580円」と自然に書かれるが、価格に触れていない文体が多い UIの見た目: スクリーンショット指示だけが「緑色のボタン」「モーダル」に言及 ユーザーの動機: ユーザーストーリーの「なぜなら〜」だけが「なぜ」を説明している 同じことを書こうとしているが、拾える観点が違う。これは確かに文体でテスト観点のカバレッジに差が出そうです。実際、AIに読ませてテストケースを生成させたら、どれだけの差が出るでしょうか。 結果: 確かに差は出た 55回の実行結果を人間が準備したテストケースと照合したところ、カバレッジに差が出ました。2層に整理したのが以下です。 11文体のカバレッジ比較。赤がTop Tier、灰色がそれ以外 Top Tier(76〜80%): 契約による設計風・表形式・ユーザーストーリー・箇条書き・要件定義書風 それ以外(63〜72%): 散文・Gherkin・フローチャート・会話形式・メモ書き・スクリーンショット指示 上位と下位の差は最大16ポイント(契約による設計風 79.24% vs スクリーンショット指示 63.08%)。文体がテスト観点に影響を与えているように見えます。 ただし、ここで注意が必要です。今回の実験はAIにテストケースを生成させたものです。AIは確率的に動作するため、同じ入力でも毎回結果が変わります。もしルールベースのツール(たとえばGherkinパーサーでGiven-When-Thenを機械的にテストケースに変換するもの)を使えば、文体の差は決定的に効くでしょう。そもそもGherkin以外の文体はパースできません。以降の分析と結論は、あくまで「AIにテストケース生成を任せる場合」に限定した話です。 その前提で、結果をもう少し詳しく見てみます。 Top Tierの5文体はほぼ同じ結果 順位 文体 平均 SD 1 契約による設計風 79.24% 11.6 2 表形式 79.22% 6.7 3 ユーザーストーリー 78.46% 5.2 4 箇条書き 77.70% 13.6 5 要件定義書風 76.14% 9.3 6 散文 71.54% 11.6 7 Gherkin 67.68% 10.0 7 フローチャート 67.68% 3.9 9 会話形式 66.92% 6.3 10 メモ書き 66.14% 8.2 11 スクリーンショット指示 63.08% 4.6 1位(契約による設計風)と2位(表形式)の差は0.02ポイント。1位と5位の差も約3ポイントです。一方で、同じ文体でも実行ごとのバラつき(SD)が5〜13ポイントあります。文体間の差よりも、同じ文体内のブレの方が大きいため、5回の実行ではこの順位に意味があるかどうかを判断できません。 また、同じ文体・同じプロンプト・同じ入力でも実行ごとに結果がブレます。たとえば箇条書きは61.5%〜100%の間を行き来しました。5回の実行で毎回上位5文体の順位が入れ替わるため、この実験からは「Top Tier内のどれが優れているか」は言えません。 一方で、Top Tier(76〜80%)とそれ以外(63〜72%)の間には傾向としての差が見えます。ただしこれもn=5での観察であり、サンプルを増やせば変わる可能性があります。 ここから言えるのは、AIにテストケース生成を任せる場合、Top Tierの5文体の中ではどれを選んでも大きな差はなさそうだということです。だとすれば、文体の微妙な優劣を追求するよりも、AIの運用の仕方に時間を使う方が実質的な改善につながるのではないか。実験の中で、文体の差以上に結果を左右する要因が3つ見えてきました。 文体の差より効いたこと<
TL;DR 背景と課題 なぜClaude Codeを選んだのか 課題1: テスト実行時間の長さ 何が問題だったのか 解決アプローチ 1. 待機処理の最適化 2. Page Objectパターンの徹底 3. 並列実行の自由度向上 結果 課題2: ワークフロー自動化の余地 何が問題だったのか 解決アプローチ MCP(Model Context Protocol)による外部ツール連携 カスタムスキルによるワークフロー定義 結果 課題3: テストの保守性と安定性 何が問題だったのか 解決アプローチ 1. CI/CD認証パターン 2. 環境依存テストの「警告扱い」パターン 結果 実績サマリー 半年間のコード貢献 学んだこと・Tips 意外だった発見 うまくいったこと 課題と対策 これから始める方へのTips 今後の展望 まとめ 参考リンク 【QAチーム ブログリレー3日目】Claude Code、MCP、Playwrightを活用したE2Eテスト自動化の半年間の実践から得た気づきを共有します。技術的なアプローチだけでなく、「AI活用に不安があったけど、実はこれまでのドキュメント整備が土台になっていた」という発見についてもお伝えします。 テストプレイをサボりすぎたRPG こんにちは、エムスリー QAチームの今井です。最近話題の「テストプレイをサボりすぎたRPG」をご存知でしょうか? テストをサボりすぎてバグだらけになったRPGを、プレイヤーが「ミス」を探しながら進めるというフリーゲームです。私たちQAはそうならないよう、いかに効率よくテストを回すかが課題でした。 TL;DR エムスリーではClaude Code使い放題: QAエンジニアも最新AIツールをフル活用 テスト実行時間74%短縮(既存ツール 105分 → Playwright 27分)、年間約68時間分のCI/CD待ち時間を削減 67件のMR/PRをマージ(E2Eテスト、リリース作業自動化、Agent開発など) MCP(Model Context Protocol)で外部ツールと連携し、テスト〜ドキュメント更新を一気通貫で自動化 背景と課題 私たちのチームでは、製薬企業向けWebサービスのQAを担当しています。 なぜClaude Codeを選んだのか エージェント型の特徴(「テストを実行して、結果をConfluenceに書いて」と指示すれば自律実行)、MCP(Model Context Protocol)による既存ツールとの連携、Co-Authored-ByでのGit履歴への明示的な記録といった点が、QA業務の効率化に適していると判断しました。 Pragmatic Engineerの調査(2026年2月)によると、Claude Codeは2025年5月のローンチからわずか8ヶ月で開発者人気1位(46%)を獲得しており、テスト自動化での50-80%の時間短縮が業界で報告されています。 以下、3つの主要課題とその解決アプローチを紹介します。 課題1: テスト実行時間の長さ 何が問題だったのか 週次で実行する定期リリースチェックは約30種類、実行に毎回1時間45分以上。既にかなり自動化・システム化されており、テスト内容から考えると効率的に運用できていました。しかし、AIで開発チームのリリーススピードが加速する中、さらなる効率化の余地があると考えました。 主な原因: - 既存ローコードツールでは操作ごとに固定のwait時間(例:3秒)が設定され、それが積み重なって実行時間が長期化 - SaaS型ツールのプラン制約で並列実行数に制限 - より柔軟なPlaywrightへの移行が必要だったが、手作業での移行は現実的でなかった 解決アプローチ Claude Codeと協力してPlaywrightへの移行を進めました。 1. 待機処理の最適化 固定wait時間から、条件ベースの待機に変更: // 動的な待機処理 await this.page.waitForResponse(resp => resp.url().includes('/api/items') && resp.status() === 200 ); waitForResponseやwaitForLoadStateで「完了したら次へ進む」という条件ベースの待機ができるため、無駄な待ち時間がなくなりました。 2. Page Objectパターンの徹底 保守性の高いテストコードを設計: // pages/SamplePage.ts export class SamplePage { constructor(private page: Page) {} async createItem(options: ItemOptions) { // role-basedセレクターで安定性向上 await this.page.getByRole('button', { name: '新規作成' }).click(); await this.page.getByRole('textbox', { name: 'タイトル' }).fill(options.title); // 動的な待機処理 await this.page.waitForResponse(resp => resp.url().includes('/api/items') && resp.status() === 200 ); } async verifyCreated(expectedTitle: string) { await expect(this.page.getByText(expectedTitle)).toBeVisible(); } } ポイント: - getByRoleなどのrole-basedセレクターでUI変更に強いテストに - Page Objectにビジネスロジックをカプセル化し、テストコードはシンプルに 3. 並列実行の自由度向上 SaaS型ツールはプランによって同時実行数に制限がありましたが、Playwrightではworker数を自由に設定できます。 結果 指標 Before After(Playwright) 効果 1回あたりの実行時間 約105分 約27分 74%短縮 週次実行(月4回) 420分/月 108分/月 312分/月短縮 年間 約91時間 約23時間 約68時間短縮 課題2: ワークフロー自動化の余地 何が問題だったのか テスト実行後、結果をConfluenceに転記し、失敗があればJIRAでチケット起票という一連の作業フローが確立されていました。このルーチンワークをさらに効率化できないかと考えました。 既存の運用フロー: 1. テスト実行 2. 結果を確認 3. Confluenceページを開いて結果を更新 4. 失敗があればJIRAチケット作成 5. Slackでチームに共有 解決アプローチ MCP(Model Context Protocol)による外部ツール連携 MCPはAIエージェントと外部ツールを接続するためのオープンな標準プロトコルです。Claude Code、Cursor、VS Code Copilotなど多くのツールで利用できます。私たちは次のMCPサーバーを活用しました: プロジェクト管理: atlassian-mcp(Confluence/JIRA連携) ソースコード管理: github-mcp, mcp-gitlab(PR/MR連携) ブラウザテスト: playwright(E2Eテスト実行) 実際のワークフロー例: ユーザー: 「定期リリースチェックを実行して、結果をConfluenceに更新して」 Claude Code: 1. Playwright でテスト実行 2. 結果を解析 3. atlassian-mcp でConfluenceページを更新 4. 失敗があればatlassian-mcp でJIRAチケット作成 これにより、「テスト実行→結果確認→ドキュメント更新→チケット起票」という一連の作業が1コマンドで完了します。 カスタムスキルによるワークフロー定義 繰り返し行うワークフローは「スキル」として定義しました: # SKILL.md の例(ticket-investigation) ## トリガー 「チケット調査」「○○について調べて」 ## 実行フロー 1. Redmine/JIRAからチケット情報を取得 2. 関連するConfluenceページを検索 3. GitLabのMR履歴を確認 4. 結果を統合してレポート生成 これにより、「チケット#12345について調べて」と言うだけで、複数システムを横断した調査が自動で行われます。 結果 確立された5ステップの作業フローを1コマンドに集約できました。 課題3: テストの保守性と安定性 何が問題だったのか
こんにちは。エンジニアリンググループ QA (Quality Assurance) チームの津向です。 2月に入り、暖かくなってきたのでBBQをしたのですが、ピンポイントで降雪になり、雪の中で肉を焼くという稀な経験をしてきました。 後日、元プロテニス(現スポーツキャスター)の方が国内不在と知りました。 そんなわけでQAチームブログリレー2日目になります。 お肉は焼くことで美味しくなると言われています。 はじめに 1. エージェントの2層構造 専門エージェントの役割分担 逐次処理による「AIのコンテキスト過負荷」防止 2. 標準化を担う生成エージェント ナレッジベースによる標準化 共通パーツの再利用 3. デバッグエージェントの独立化 生成エージェントと分けた理由 Playwright MCPによるライブ診断 「堂々巡りの修正」をさせない 4. 2種類のレビューエージェント 役割を分けた理由 5. AI+ツールでレビュー おわりに We're Hiring! はじめに 以前からPlaywrightへの移行は検討されていましたが、工数や学習コストを考えるとなかなか進められずにいました。 そんな中、QA EngもClaude Codeが使い放題になったことを機に、mablからの移行を開始しました。 開始当初は専用のClaude Agentは作成せず、そのまま移行作業を行なっていました。 次々に生成されるコードを眺めて「このClaudeすごいよ、さすがClaude Chatのお兄さん!」と生温かく見守っていたのですが、次々に課題が出てきました。 コード規約が途中からスルーされていく 既存資産を再利用せずに1から作る 移行元のアサーションがスキップされる 大量に作成されるのでレビューが大変 AIにレビュー任せても、本当にレビューできているのか不確実 人によって、手順が異なるので成果物も微妙に異なる など様々でした。 そこでPlaywright移行に特化したClaude Agentを作成することで解決を図りました。 今回はこのClaude Agent(以降はエージェントと記載)についての記事になります。 上記の様々な課題は下記方針に基づくエージェントの作成により、課題を解決できました。 2層構造:全体の流れを制御する「オーケストレーター」と、各工程担当である「専門エージェント」の分離 ダブルチェック検証:技術品質と仕様完全性を別々のエージェントで検証 AI+ツール検証:AIの文脈判断とツールの機械的チェックを組み合わせた検証 1. エージェントの2層構造 単一の巨大なエージェントですべてをこなそうとすると、ルールが徹底されない、見落とし発生する、メンテナンス性が悪い、などデメリットが多く発生します。そのため、工程で役割を分けることで、デメリットを解消し精度を高めています。 オーケストレーター層:全体の進捗管理と、次にどの方針で進むべきかの判断に特化。 専門エージェント層:コード生成、デバッグ、品質レビュー、仕様レビューといった特定のタスクの実行に特化。 専門エージェントの役割分担 オーケストレーターの指揮下で働く、4つの専門エージェントの役割は次の通りです。 エージェント名 担当フェーズ 主な役割 playwright-code-generator 生成 mablの原本や仕様書を読み込み、Page Objectモデルに基づいたコードを作成。 playwright-debug-fix-engine デバッグ テスト失敗時に起動。ブラウザのライブ操作と証拠収集を行い、論理的に修正。 playwright-code-quality-reviewer 品質検証 コードがプロジェクト規約や技術的なベストプラクティスに沿っているかを査読。 playwright-spec-reviewer 仕様検証 生成されたコードが、原本の要求事項を100%満たしているか「実装漏れ」を検証。 表で説明した各エージェントは、実際には次のようなディレクトリ構造で管理しています。 . ├── orchestrators/ # 【全体制御】全体管理用オーケストレーター │ └── mabl-migration-orchestrator.md ├── code-generation/ # 【生成・修正】コード生成とデバッグ │ ├── playwright-code-generator.md │ └── playwright-debug-fix-engine.md ├── code-review/ # 【レビュー】2種類のレビュー │ ├── playwright-code-quality-reviewer.md │ └── playwright-spec-reviewer.md ├── knowledge/ # 【共通辞書】共通で使用するナレッジベース │ └── playwright_knowledge_base.md └── tool/ # 【補助ツール】静的解析用の自作スクリプト └── playwright-reviewer-v3.js 逐次処理による「AIのコンテキスト過負荷」防止 本システムにおいて、複数のテストケースを扱う際に「逐次処理」を徹底しています。 全ファイルのコードを一度に生成して最後にまとめて実行するのではなく、1ファイルごとに作業を完結させ、成功してから次のファイルに移るというサイクルを回します。これにより、AIのコンテキスト過負荷を防ぎ、精度の高い移行を実現しています。 2. 標準化を担う生成エージェント 役割分担の中でも、生成を担うplaywright-code-generatorは、全体の標準化と効率化の役割を担っています。 ナレッジベースによる標準化 生成の詳細なルールはエージェント内に記載せず、外部のナレッジベースplaywright_knowledge_base.mdに分離し、エージェントの容量を圧縮しています。また、このナレッジベースは、レビューエージェントplaywright-code-quality-reviewerでも共通して参照しています。生成側と検証側が同じ基準を持つことで、手戻りの少ない効率的な開発サイクルを実現しました。 共通パーツの再利用 ログイン処理やヘッダー操作など、各テストで使い回す共通パーツを自動で認識し、Page Objectモデルとして適切に再利用します。これにより、類似コードの乱立を防ぎ、長期的にメンテナンスしやすいテストの作成が可能になりました。 生成エージェントによる標準化によって、品質とスピードを両立できるだけでなく、誰が実行しても一貫性のある自動テストを作成できるようになりました。 3. デバッグエージェントの独立化 テスト失敗時の原因究明する playwright-debug-fix-engine は、AIの推測に頼った「なんとなくの修正」を排除し、客観的な証拠を最優先する設計です。 生成エージェントと分けた理由 生成するエージェントと同じセッションでデバッグすると、AIが自分で書いたコードや過去のやり取りに引きずられるバイアスが生じます。 独立したエージェントとして呼び出すことで、新しいセッションで調査を開始でき、事前の状況に左右されず、現在のコードと実行結果だけを客観的に診断することが可能になります。 Playwright MCPによるライブ診断 AIがブラウザを直接操作する MCP (Model Context Protocol)を活用し、エラー時の状態をリアルタイムで診断します。 構造解析: アクセシビリティツリーから、壊れにくいセレクタを再選定します。 視覚的確認: スクリーンショットやHTMLで、要素の被りや配置のズレを捉えます。 切り分け: ネットワークログ等から、プログラムのミスか通信エラーかを区別します。 「堂々巡りの修正」をさせない AIが3回試行しても解決できない課題に対し、それ以上の修正をストップしています。 代わりに、それまでの試行過程と収集した証拠をまとめ、Gemini等の外部LLMに相談しやすい形にし、人間にバトンタッチします。 エージェントで無理な修正をしないことで、堂々巡りの修正を防いでいます。 4. 2種類のレビューエージェント レビュー工程では、1つのエージェントに全てを任せるのではなく、観点を「技術」と「仕様」に完全に分離しました。 レビュー種類 担当エージェント 主な検証ポイント 技術品質レビュー playwright-code-quality-reviewer POM設計、セレクタ戦略、認証管理、CI/CD統合、規約準拠。 仕様完全性レビュー playwright-spec-reviewer 原本との突き合わせ、実装漏れ検出、定量的評価。 役割を分けた理由 エージェントを分けた理由は、AIに役割を絞らせて見落としを防ぐためです。 情報の整理: 1つのAIに大量の規約と複雑な仕様を同時に読み込ませると、情報が混ざり、未実装の機能を実装済みと思い込むようなミスが起きやすくなります。 チェックの精度向上: コードの綺麗さと仕様の網羅性を別々の視点でチェックすることで、細かな実装漏れも見つけ出せるようになります。 責務を分離することで、各工程のチェック精度を高めています。 5. AI+ツールでレビュー AIエージェントは文脈判断に優れますが、一方で単純な形式チェックを見落とすことがあります。 そこで、AIレビューに加えて、独自の静的解析ツール playwright-reviewer-v3.js を実行するハイブリッド体制にしました。 AI:POMの責務分離や、アサーションの論理的な配置をレビュー。 ツール:23項目のアンチパターン(waitForTimeout の使用、await 忘れ等)を機械的に検出。 この2段構えにより、AIの見落としを防ぎ、品質を担保しています。 おわりに 本システムの活用により、Playwrightの経験有無に関わらず、チーム全員が移行タスクの即戦力になれました。 今は移行メインですが、今後は新規テスト作成や継続的な品質を担保する仕組みとしても活用していく予定です。 We're Hiring! そんなわけで自動化や効率化が大好きな方、AIが大好きな方、なぜか不具合に好かれる方など様々なQAエンジニアを募集しています。一緒にQA活動しましょう! jobs.m3.com
【QAチーム ブログリレー1日目】 こんにちは。マルチデバイスチームQAエンジニアの前川です。 新国立のテート展のダミアン・ハースト、久しぶりにホルマリン漬け来るか!の期待に対しての無難なオフィス机の展示にちょっぴり落胆した春先です。 最近LAでクラブよりも午前中のコーヒーパーティが流行っているらしいです。夜&酒の脳コンディションよりもシラフで冴えた頭への社交シフトがビジネスやクリエイティブ層で強まっている。密度と精度、スピードが重視されるAIの普及が、コミュニケーション指向へも影響していると取れなくもないと思えます。 画像はAI (Gemini)により生成したイメージです はじめに 消えることのない課題 Firebase MCPを用いたワークフロー実装 1.Firebase側(書き込み): 2.GitLab CI側: Cloud Functions /Firestoreを用いない実装 Prompt run.py MCP設定ファイル生成のコンテンキストマネージャー Claude呼び出し 通知サンプル フロー改善の感触 解析精度 ポジティブな点 不足点:修正提案の精度 まとめ We are hiring! はじめに テスト設計&実行のAI Agent活用がQAチームでも加速度的に進んでいます。 ・テスト設計&実行の自動化 -> QA工程の短縮化 -> リリース数の増加 テスト生産性が上がり、リリースのインターバルは短くなる、とその先の運用品質、運用監視がQAとしてより一層留意される流れになってきます。 テストプロセスから少し目線を変えて、運用プロセスでのAI有用性の観点でマルチデバイスチームの模索を紹介できればと思います。 消えることのない課題 モバイルアプリにつきもののCrash。 クライアント側起因に加えてOSライブラリ依存、ユーザー環境、予想外のユースケースなどでエラーはコンスタントに積み重なっていきます。 サービス横断のモバイルアプリはバックエンド、BFF、フロントとエラー因数も多いため、切り分けもなかなかに手こずります。 レポート注視:Firebase Crashlytics、クライアント側&ライブラリ起因でPriorityたかひく混成で日々発生 気づき:warnレベルを含めた単純なレポートの自動通知は誰かが気づいたときに対応する属人的な運用になりがち 解析:スタックトレースを追う、エラー解析の因数は多い このCrash監視&修正の一連の運用でClaude Codeの活用は2軸になろうかと思います。 レポート注視から解析までのワークフローの自動化 解析精度自体の向上 Firebase MCPを用いたワークフロー実装 まずはCrashlyticsのレポートを拾い、解析し、重症度のPriorityをつけて、Slack通知する仕組み化、です。 以下、マルチデバイスチームのエンジニア小林さんに実装いただいた経緯です。 Firebase MCP はご存知のようにAI AgentがFirebaseの諸機能にアクセスできる公式サーバです。 firebase-tools(Firebase CLI)はFirebaseの各種機能のツールセットとして今回のケースではカスタムログやキー情報など解析&修正提案に必要なコンテキストを取得します。 Claude Code <---> Firebase MCP <---> firebase-tools <---> Crashlyticsサーバー 実装方法ですが、当初はGeminiの勧めもあってまず検索性や分析精度を踏まえてCloud Functionsの利用を検討しました。 Cloud Functionsでcrashイベントを受け取ってcrash情報をFirestoreに保存 Gitlab CIの定期実行でjobを起動し、Firestoreにある未解析なクラッシュ情報を探して処理 以下、Firebase側とci側の実装案です。 1.Firebase側(書き込み): クラッシュ発生時、Cloud FunctionsがFirestoreの crash_queue コレクションに「未解析(pending)」として情報を書き込む。 Firebaseのアラートを検知して情報をキュー(待ち行列)として保存。 2.GitLab CI側: 定期実行ジョブでGitLab CIがFirestoreをチェック。 pending なデータがあればそのIDを使ってClaude解析を実行し、終わったら completed に更新する。 オンプレのGitLab Runnerは内側から外側(Google Cloud)へ通信するため、特別なネットワーク設定なしでFirestoreのデータを取得できる firebase_gitlab_process しかし、 functionsのデプロイには従量課金のblazeプランへの移行が必要な点 テスト運用段階ではシンプルな実行が望ましい 最終的にまずはGitLab CIのみで完結させる方法で実装でいくことにしました。 Cloud Functions /Firestoreを用いない実装 テスト運用はエムスリーのサービスの軸であるポータルアプリ”m3.comアプリ”のiOS版で実装してもらいました。 (m3.comアプリの設計について気になる方はこちらをご覧ください。) Prompt は以下です。まずは3時間の定期実行にしました。 重症度は以下4レベル判定になっています。CriticalとHighを拾って深掘りする運用の前提です。 Critical: アプリ起動不能・データ損失など致命的 High: 主要機能が使えない・多数ユーザーに影響 Medium: 一部機能に影響・回避策あり Low: 軽微・再現頻度低 あなたはiOSアプリ「m3comapp-ios」のCrashlytics監視エージェントです。 Firebase MCPツールを使用して以下の手順でクラッシュを調査し、結果をJSONで出力してください。 ## 調査手順 1. Firebase MCPツールを使用して、以下のプロジェクトのCrashlyticsから条件を満たすクラッシュ情報を取得する - プロジェクトID: $FIREBASE_PROJECT_ID - アプリID: $FIREBASE_APP_ID - 条件: firstSeenが過去3時間以内 2. 該当Issueが0件の場合は `[]` のみ出力して終了する 3. 該当Issueが1件以上ある場合、各Issueについて以下を行う: a. スタックトレースを取得する b. スタックトレースのファイルパスを元に、このリポジトリ内の関連Swiftファイルを読み込む c. 原因の分析結果と修正方針を日本語で推定・提案する d. 重篤度を評価する (Critical / High / Medium / Low) - Critical: アプリ起動不能・データ損失など致命的 - High: 主要機能が使えない・多数ユーザーに影響 - Medium: 一部機能に影響・回避策あり - Low: 軽微・再現頻度低 ## 出力形式 必ずJSONのみ出力すること。前後に説明文・マークダウンのコードブロック (``` など) を含めないこと。 出力をJSONとしてパースするので、失敗してしまいます。 出力するJSONの構造 (このテキスト自体は出力しない): [ { "issue_id": "CrashlyticsのIssue ID", "title": "クラッシュのタイトル", "severity": "High", "first_occurred_at": "ISO8601形式の日時", "occurrences": 0, "affected_users": 0, "analysis_result": "原因の分析結果と修正方針(日本語、マークダウン記法、JSONのエスケープルールに従うこと)", "firebase_console_url": "FirebaseコンソールのIssue URL" } ] run.py 実行ファイルは以下です。 MCP設定ファイル生成のコンテンキストマネージャー @contextlib.contextmanager def _mcp_config_file(): """MCP 設定ファイルを生成するコンテキストマネージャー。 iOS プロジェクトは Crashlytics 依存関係が自動検出されないため --only crashlytics が必須。 """ mcp_server = { "command": "npx", "args": ["-y", "firebase-tools@latest", "mcp", "--only", "crashlytics"], } config = {"mcpServers": {"firebase": mcp_server}} config_path = None try: with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: json.dump(config, f) config_path = f.name yield config_path finally: if config_path: Path(config_path).unlink(missing_ok=True) Claude呼び出し subprocess.run()でclaudeを呼び出し、必要な情報を渡します。 許可ツールはread-onlyのもののみに制限して安全に実行できるよう設定。 プロンプト (-p prompt): prompt.md から読み込んだ内容(Firebase プロジェクトIDとアプリIDを埋め込み) MCP設定 (--mcp-config): Firebase Crashlytics MCPサーバーの設定 許可ツール (--allowedTools): Read, Grep, Glob + Firebase Crashlytics関連のMCPツール def run_claude() -> str: """claude CLI を非インタラクティブ実行してクラッシュ分析結果を返す。""" allowed_tools = [ "Read", "Grep", "Glob", "mcp__firebase__firebase_get_environment", "mcp__firebase__crashlytics_batch_get_events", "mcp__firebase__crashlytics_get_issue", "mcp__firebase__crashlytics_list_events", "mcp__firebase__crashlytics_get_report", "mcp__firebase__crashlytics_list_notes", ] prompt = build_prompt() with _mcp_config_file() as mcp_config_path: <span c
AI・機械学習チームブログリレー15日目の記事を三浦 (@mamo3gr) がお送りします。前日は須藤さんによるClaude Codeと安全に付き合うためのサンドボックス機能の検証でした。 www.m3tech.blog 私は先月まで半年間の育児休業を取得していたのですが、復帰してからというもの、AIエージェントの進化とそれに伴う開発プロセスの様変わりにびっくりしています。日進月歩の変化にキャッチアップしなくては…、と危機感を募らせつつ、今日は20年以上も続く老舗ソフトウェアへのコントリビュートに挑戦したエピソードを通して、ちょっとだけ世界を良くするために小さなことでも始めようよ、という話をします。 厳島神社の大鳥居。この神社は台風や高潮など自然災害のたびに大小の改修を経て、1400年以上の歴史を持つとされています Emacsとorg-modeによるタスク管理 見積もり時間が空になるバグ 修正パッチの提出と挫折 フォロワー現る 世界をちょっとだけ良くする We are hiring !! エンジニア採用ページはこちら カジュアル面談もお気軽にどうぞ インターンも常時募集しています Emacsとorg-modeによるタスク管理 Emacsは多くのハッカーに愛用されているテキストエディタですが("Famous Emacs Users"などのワードでWeb検索すると名だたるメンバーのリストが見られます)、その代表的なプラグイン*1の1つに org-mode があります。org-modeは、メモ取りから論文まで至る各種文書の作成、タスクリストやプロジェクトの管理、文芸的プログラミングなど、幅広く使えるプレーンテキストのファイルフォーマットと、それを扱う機能群を指します。ちなみに、この使い勝手の良さのせいか、Vim*2やNeovim*3, Visual Studio Code*4といった他のテキストエディタやIDEにも移植されています(ただし機能面や安定性はやはり本家が充実しているように見えます)。 私は専ら、タスク管理にこのorg-modeを重用してきました(正確なタイミングは記録していないのですが、遅くとも2012年には使い始めていたようです)。Emacsの軽快な操作性をベースにしているので、思いついたら即座にタスクを書き出して頭をクリアにできますし、重要なタスクを選び出して翌日のタスクリストを作成するのに1分もかかりません。また、プレーンテキストなのでgrep検索やGitでの管理が容易なのも扱いやすいです。 見積もり時間が空になるバグ このようにタスク管理に便利なorg-modeですが、ある日、意図した挙動をしなくなりました。org-modeで管理できるアイテムは大きく分けて、会議のように開始と終了の時間が決まった「予定」と、特に時間の決まっていない「タスク」があります。タスクはそれぞれ見積もり*5 (Effort) を入力でき、開催時間帯の決まっている予定は、その長さ(時間)に合わせて自動的にEffortが算出・表示されます*6。この自動算出が機能しなくなり、本来時間が表示されるべき列がポッカリと空欄になってしまったのです。翌日のタスクリストを作成するときには、タスクの見積もり時間と会議の所要時間を合計して業務時間(8時間)を超えないことを確認するので、Effortが自動算出されないのは困ります。 今日のタスク一覧。時間帯の記載があるアイテムが会議(予定)で、下段にまとまっているのがタスク いわゆるカラムビュー。見積もり (Effort) と実績 (Clock) が見られますが、Effortはポッカリ空 ああでもないこうでもないと調査していると、Web検索の結果、なんと同じように困っている人を見つけました。 emacs.stackexchange.com 「どうやらバグらしい」という見立てとともにワークアラウンドまで回答されています。私は自分の設定が悪いのではないかと疑っていたので、このように疑問をフォーラムに投げ込んだり、それに回答されたりしているこの一連のやり取りに救われました。しかも回答者の方は、公式メーリングリストへバグ報告もされています*7(素晴らしい!)。 さて、ワークアラウンドも分かったので、あとは自分の設定ファイルにそれをコピペすればいいだけなのですが、ここではたと手が止まります。バグを回避するだけなのに数十行に及ぶ複雑なLispコードを設定ファイルに追加することになりますし、これが将来のバージョンでも継続して通用するかはわかりません。また、自分以外にも存在するであろう数多くのユーザーが今後も、同じように調査したり、ワークアラウンドの設定を追加したりすることになりそうです*8。じゃあ、いっそバグを直してしまったほうが、自分も含めてみんなハッピーじゃん、ということで、深いEmacs Lisp*9の知識もなかったものの、挑戦することにしたのです。 修正パッチの提出と挫折 git bisect で壊れたコミットを見つけた後、printデバッグするという地道な作業の結果、何とか問題点を突き止め、それを修正するためのパッチを提出できました。カラムビューを作成する際に、関数 org-columns--collect-values がタスク一覧における各行の duration(つまりEffort)を取ってくるはずなのですが、どういうわけかこの属性が消えてしまっているようでした。そこで、取得のタイミングで既存の関数を流用して再計算させてしまおう、というのがアプローチでした。 ちなみに、このプロジェクトは伝統的にメーリングリストを使っており、GitHubなどのホスティングサービスにPR (Pull Request) を出すのではなく、差分 (diff) をパッチでメール送信します。なお余談かつ手前味噌ですが、環境(バージョン)、バグを再現する最低限の設定と手順、バグ調査の結果で分かったこと・分からなかったこと(時間や知識の都合で追えなかった範囲)、修正の方針など、分かりやすく書かれています。 lists.gnu.org 実は、このパッチが取り込まれることはありませんでした。ケアできていないケースがあり、可能であればリファクタリングすべきというフィードバックをいただいたのですが、これ以上時間が取れませんでした。20年以上も開発されている*10コードベースに対して影響範囲を調査したり、過不足のない修正をしたりする難しさを体感しました。このパッチを作成したのは2022年のことですが、2026年の今なら、Claude CodeのようなAIエージェントのサポートを受けながら、当時よりもずっとラクに修正できたのかもしれません。 フォロワー現る ふとある日、「そういえばあのバグはどうなったのだろう?また修正に再トライしてみようかな」と思い立って調べてみると、何と修正がコミットされているではありませんか! cgit.git.savannah.gnu.org コミットでは私のパッチへのリンクがあったり、修正方針の違いを論じたりしており、私の調査やパッチが多少なりとも寄与できたのではないかと考えています。属性 duration が消えてしまったのは関数 org-columns--collect-values がそれを回収する位置(行)を見失ってしまったのが原因のようで、同コミットではその位置を追加の引数として与えることで duration を取得できるようにしています。 ちなみに、このバグが発生したのはバージョン8.3(2015-08-05リリース)で、修正は2024-10-12にコミットされていることから、何と9年もの間壊れていたことになります(!)。 バグが修正されたカラムビュー。会議のEffortが自動算出されています 世界をちょっとだけ良くする 今回の経験を通じて、OSSへの貢献は必ずしも「完璧な修正パッチをマージさせる」だけではないと実感しました。9年越しのバグが解消されるまでの流れを振り返ると、そこにはいくつもの「小さな貢献」が連鎖していました。 声を上げる:「これ、おかしいよね?」とフォーラムやメーリングリストに投稿する。この投稿だけでも、誰かの時間や手間が救われます。 後に続く:先人の報告に対し「自分の環境でも再現した」「この辺が問題のようだ」と情報を足す。特定の個々人の問題ではない、という表明とともに、少しずつ解決に近づくための手がかりを増やせます。 バトンを託す:たたき台でも世に出すこと。私のパッチはマージされませんでしたが、その過程で共有された調査結果やアプローチは、別の誰かの役に立ったはずです。 もしみなさんの手元に、長年「仕方ない」と諦めている不具合や、自作のワークアラウンドで凌いでいるバグがあれば、それをどこかに報告したり、書き留めたりしてみてください。その一歩が、数年後の世界をちょっとだけ良くするかもしれません*11。 そういえば、実はこの修正コミットは2026年4月現在、正規のリリースにはまだ取り込まれていません(git tag --contains e2823be9d の結果が空であることから)。取り込まれたmainをビルドして使う必要があるのですが、その旨を冒頭のQ&Aサイト (StackExchange) にコメントしておきました。たぶんリリースブランチに取り込み忘れていると思うので、これから公式メーリングリストにも共有しないといけませんね。 <h2 id="We-are-hi
こんにちは、AI・機械学習チームの須藤です。 この記事はAI・機械学習チームブログリレー14日目の記事です。 13日目は田中さんによる「スタートアップCTOが、M3のAIチームに転職して3か月。感じた不安と、その答え。」でした。 www.m3tech.blog 突然ですが、私は今年に入ってからランニングを始めました。1月頃はキロ8〜9分ペースで2〜3km走るのがやっとでしたが、毎日続けているうちに最近はキロ4分台で走れるようになり、20km程度であれば走れるようになってきました。今年の目標はマラソン大会に出場することです。継続は力ですね。 最近買ったadizero evo sl woven。AIエージェントが自走するということは人間も走らないといけない(?)。 はじめに Claude Codeのサンドボックス機能 基本的な設定と使い方 はじめ方 設定のカスタマイズ Managed設定 サンドボックスの動作検証 ファイルシステムの制限 ネットワークの制限 まとめ We are hiring! はじめに さて、最近はAIエージェントが急速に普及し、開発の仕方が大きく変わってきているのを感じています。私自身もここ1年で働き方がだいぶ変わってしまいました。コードを書くのはClaude Codeに任せて、自分は設計の壁打ちやレビューに集中するような形になっています。 最近私が考えているのは、いかに人間が運用のボトルネックにならないようにするか、という点です。品質と安全性を保ちながら、AIが自律的に動ける範囲を広げられないかを模索しています。 ただ、AIに自律的にコマンドを実行させる以上、安全性への不安もつきまといます。Claude Codeは実行前に承認を求めてくれますが、公式ドキュメントにも「承認疲れ」*1という言葉があるように、承認を繰り返しているうちに、人はどうしても内容を確認しなくなってしまいがちです。また、その承認が本当に正しい操作なのかを判断するのは、慣れたエンジニアでも難しいことがあります。Claude Codeはエンジニア以外でも使う機会が増えており、普段開発をしていない人にとってはなおさらです。 さらに、ユーザーが意図しなくても被害を受けるケースもあります。間接的プロンプトインジェクションと呼ばれる攻撃があり、悪意ある指示をAIに読み込ませることで意図しない操作をさせるものです。たとえば、画像やPDFに人の目には見えない形でプロンプトを埋め込む手法や、GitHubのPRレビュー画面には表示されないがソースファイルには悪意ある指示が埋め込まれているケースなどがあります。 Claude Codeにはこうしたリスクを軽減する「サンドボックス機能」が用意されています。本記事ではこの機能を実際に試してみた内容を紹介します。 Claude Codeのサンドボックス機能 Claude Codeのサンドボックス機能は、Bashコマンドの実行環境をOSレベルで分離することで、より安全なエージェント実行を実現します。あらかじめ境界を定義しておくことで、その範囲内ではClaude Codeが自由に動作できる仕組みになっています。 具体的には、ファイルシステムアクセスを特定のディレクトリに制限し、ネットワークアクセスはサンドボックス外で動作するプロキシサーバーを通じて制御します。これらの制限はOSレベルの分離機能を用いて実現されており、macOSではSeatbelt、Linux / WSL2ではBubblewrapが使われるそうです。*2 基本的な設定と使い方 はじめ方 Claude Codeを起動し、/sandboxコマンドを実行することでサンドボックスを有効化できます。 /sandbox 実行するとモード選択メニューが開くので、モードを選択します。 Configure Mode: 1. Sandbox BashTool, with auto-allow 2. Sandbox BashTool, with regular permissions 3. No Sandbox Auto-allow mode: Commands will try to run in the sandbox automatically, and attempts to run outside of the sandbox fallback to regular permissions. Explicit ask/deny rules are always respected. 各モードは次の通りです。 Sandbox BashTool, with auto-allow:サンドボックス内で実行されるコマンドが自動承認されるモードです。サンドボックス外へのアクセスが必要な場合は通常の許可フローにフォールバックします。 Sandbox BashTool, with regular permissions:サンドボックス化されている場合でも、すべてのコマンドで通常の許可フローが走るモードです。 後ほど説明しますが、サンドボックスではアクセス可能なファイルやネットワークのドメインを制限でき、Claude Code自体のAllow/Denyルールと組み合わせることで、やっていいこと・やってはいけないことをあらかじめ定義できます。auto-allowモードでは、こうした境界の範囲内でClaude Codeが自律実行することが想定されています。 両モードともサンドボックスによるファイルシステムとネットワークの制限は同様に適用されます。違いは、サンドボックス化されたコマンドが自動承認されるか明示的な許可が必要かどうかのみだそうです。 なお、Linux / WSL2 の場合は bubblewrap や socat などの追加パッケージが必要です。不足している場合は /sandbox 実行時にインストール手順が表示されるので、それに従ってインストールしてください。 設定のカスタマイズ settings.json に次のように書くことでサンドボックスの動作をカスタマイズできます。 { "sandbox": { "enabled": true, "failIfUnavailable": true, "autoAllowBashIfSandboxed": false, "allowUnsandboxedCommands": false, "filesystem": { "allowWrite": ["~/.kube", "/tmp/build"], "denyRead": ["~/.ssh"] }, "network": { "allowedDomains": ["*.npmjs.org"] } } } 設定項目 説明 enabled サンドボックスの有効・無効。/sandbox コマンドでも有効化できるが、こちらに書いておくとClaude Code起動時から有効になる failIfUnavailable サンドボックスが起動できない場合にエラーで終了する(false の場合は警告を出してサンドボックスなしで続行) autoAllowBashIfSandboxed サンドボックス内のコマンドを自動承認するか(前述のauto-allowモードに相当) allowUnsandboxedCommands サンドボックス外でのコマンド実行を許可するか(false にするとエスケープハッチを無効化) filesystem.allowWrite 書き込みを許可するパスの追加 filesystem.denyRead 読み取りを拒否するパス network.allowedDomains アクセスを許可するドメイン ファイルシステム、ネットワークのデフォルトの動作は次の通りです。 ファイルシステム 読み取り:コンピュータ全体への読み取りが可能 書き込み:カレントディレクトリとそのサブディレクトリのみ ネットワーク allowedDomains に登録済みのドメインにはユーザーの承認なしにClaude Codeがアクセスできます 未登録ドメインへアクセスしようとするとユーザーに承認プロンプトが表示されます allowManagedDomainsOnly を設定すると、allowedDomains 以外のドメインへのアクセスは即時エラーになります(Managed設定でのみ設定可能) 詳細な設定項目については公式ドキュメントを参照してください。 Managed設定 Managed設定はIT管理者が組織内の全ユーザーに適用できる設定で、ユーザー側でオーバーライドできません。*3*4 配信方法はいくつかありますが、たとえば次のようなシステムディレクトリへのファイル配置で実現可能です。 OS パス macOS /Library/Application Support/ClaudeCode/managed-settings.json Linux / WSL /etc/claude-code/managed-settings.json Windows C:\Program Files\ClaudeCode\managed-settings.json サンドボックスの文脈では、次の設定が特に有効です。 サンドボックスの強制 sandbox.enabled: true を設定すると、ユーザーがサンドボックスを無効化できなくなります。 許可ドメイン以外の即時ブロック sandbox.network.allowManagedDomainsOnly: true を設定すると、allowedDomains に登録されていないドメインへのアクセスが承認プロンプトなしに即時エラーになります。前述の通りこの設定はManaged設定でのみ有効です。 MCPの制限 後述しますが、サンドボックスのネットワーク制限はBashサブプロセスにのみ適用されます。MCPのネットワークアクセスを制御したい場合は、allowedMcpServers と allowManagedMcpServersOnly: true を組み合わせることで、使用できるMCPサーバーを管理者が制御できます。 サンドボックスの動作検証 ファイルシステムの制限 まずはファイルシステムの制限を試してみます。Claude Code外のターミナルで検証用ファイルを作成しておきます。 echo "This is a test file for sandbox verification." > read_test.txt settings.json に denyRead を設定し、read_test.txt への読み取りを拒否してみました。 { "sandbox": { "filesystem": { "denyRead": ["./read_test.txt"]