TECH PLAY

エス・エム・エス

エス・エム・エス の技術ブログ

255

はじめに こんにちは!カイポケのリニューアルプロジェクトを担当しているエンジニアのNobです。 普段はWebのフロントエンドが中心ですが、最近はモブプロをとおしてバックエンドのタスクにもチャレンジさせてもらっています。 プライベートでは2歳になったばかりの息子の子育てに奮闘中です。最近は話せる言葉が増えて少し会話が成り立つようになってきたり、遠くへの外出でも泣かなくなってきました。子育ては大変ですが、それ以上に子供からたくさんの笑顔と幸福感をもらっています。 さて、今回はESLintに追加されたMultithread Lintingについて、私が携わっているプロジェクトのCIへの導入を検討したので共有します! Multithread Linting 機能の概要 ESLint v9.34.0で追加されたこの機能は、その名のとおりESLintによるチェック処理を並行して実行することによって高速化するものです。リリースとともに 公開された Blog によると、1.3倍から3倍ほど高速化した例があるようです。大規模なコードベースで開発している我々としても高速化が期待できそうなので試してみることにしました。 concurrency オプションの設定値 concurrencyに指定可能な値は以下のいずれかです。 正の整数: 最大のスレッド数。任意の数を指定。 auto : CPUのコア数と対象ファイル数に基づいてESLintに並列数を決定させる。 off : Multithread Linting機能を無効にする。デフォルト。 auto がうまく機能しそうであれば、実行環境に合わせて値の調整をしなくて済むので手間が省けそうです。なので、まずは auto から試してみることにしましょう。 初回ベンチマーク: off vs auto 現在の私の開発端末のスペックは以下です。 MacBook Pro: 14インチ、 Nov 2024 CPU: Apple M4 Max メモリ: 64GB この端末で現在私が開発に携わっているプロジェクトを対象に、どのくらい処理速度に変化があるのか見てみましょう。まずはESLintの処理速度の変化を検証したいので、 ESLintのキャッシュを利用しない状態で比較してみます。 ベンチマークには hyperfine というコマンドラインベンチマークツールを使用します。 hyperfine --warmup 1 --runs 3 -L concurrency off,auto " pnpm exec eslint --concurrency {concurrency} './**/*.{ts,tsx,graphql}' " Benchmark 1: pnpm exec eslint --concurrency off ' ./**/*.{ts,tsx,graphql} ' Time ( mean ± σ ) : 62 . 363 s ± 0 . 257 s [ User: 74 . 000 s, System: 10 . 615 s ] Range ( min … max ) : 62 . 110 s … 62 . 624 s 3 runs Benchmark 2: pnpm exec eslint --concurrency auto ' ./**/*.{ts,tsx,graphql} ' Time ( mean ± σ ) : 76 . 222 s ± 1 . 714 s [ User: 262 . 973 s, System: 138 . 288 s ] Range ( min … max ) : 74 . 245 s … 77 . 297 s 3 runs Summary pnpm exec eslint --concurrency off ' ./**/*.{ts,tsx,graphql} ' ran 1 . 22 ± 0 . 03 times faster than pnpm exec eslint --concurrency auto ' ./**/*.{ts,tsx,graphql} ' 試した結果、期待とは違って --concurrency auto を指定した場合の方が1.22倍時間がかかるようになってしまいました。なぜこのような結果になってしまうのか少し調べてみましょう。 auto モードの並列数決定ロジック まずは --concurrency auto を指定した場合、並列数がどのような処理で決定されるのか調べてみます。 concurrency というキーワードでgrepしてESLintのコードを眺めてみると、どうやら このあたりの処理 で判断しているようです。 case "auto" : { workerCount = Math . min ( availableParallelism() >> 1 , Math . ceil (fileCount / AUTO_FILES_PER_WORKER), ); break ; 周辺コードも含めて少し噛み砕いて見ていくと availableParallelism() >> 1 この availableParallelism() はNode.jsの標準ライブラリの os.availableParallelism() であり、その実体は libuv の uv_available_parallelism() 。OSやCPUによる細かな違いはあるものの、シンプルに言えばプロセスが利用可能なCPUのコア数を返す。 availableParallelism() の結果を1ビット右にシフト(2で割って小数を切り捨て)した値となる。 Math.ceil(fileCount / AUTO_FILES_PER_WORKER) fileCount は対象ファイル数。 AUTO_FILES_PER_WORKER は 35 という定数 。 この値はヒューリスティックな値であり、将来的により適切な値や算出処理に改善される可能性がある。 のように読みとることができます。最終的にはそれぞれの値のより小さい方が並列数として使用されることになります。 これを思い切って単純にすると Math.min(CPU のコア数の半数, ファイル数 / 35) となります。 ではここで ファイル数 / 35 が実際にどのくらいの値になるのか考えてみましょう。 100 / 35 => 2.8… 300 / 35 => 8.5… 500 / 35 => 14.2… のようになり、この値の比較対象がCPUのコア数の半数であることを考慮すると、対象ファイルが500以上あるような比較的大規模なプロジェクトではCPUのコア数の半数が並列数になると考えることができます。 検証端末での並列数計算結果 前述のとおり、並列数の算出にはCPUのコア数と対象のファイル数が関係してくるのでした。今回検証に使っているプロジェクトには4,000ファイル以上が存在しています。対象ファイル数が十分に多いため、並列数はCPUのコア数で決まると考えることができそうです。 検証に使っている開発端末のCPUはApple M4 Maxであり、この環境で availableParallelism() が返す値は16です。 node -p " require('node:os').availableParallelism() " 16 この値を2で割った値、つまり8が並列数として利用されることになります。実際に並列数として使用された値はESLintの実行時に --debug オプションを指定することで出力されるログからも確認することができます。 eslint:eslint Linting using 8 worker thread ( s ) . ここでApple M4 Maxのコアの内訳を見てみましょう。 12個がPerformanceコアで4個がEfficiencyコアです。 Performanceコアは高いクロック周波数で動作するCPU集約的なタスクが向いているコアで、 Efficiencyコアは低消費電力で動作する電力消費効率を重視したコアです。 一方Node.jsが利用しているlibuvの uv_available_parallelism() ではこれらのコアの違いを知ることはできません。 16個のうちの4個が他のコアよりも性能が低いことを考慮すると、 8という並列数は実際のスペックよりも高い値なのかもしれない、という仮説をたてることができます。 最適な並列数の探索 それでは実際に8以下の並列数で改めて検証してみましょう。 hyperfine --runs 1 -L concurrency off, 2 , 3 , 4 , 5 , 6 , 7 ,auto " pnpm exec eslint --concurrency {concurrency} './**/*.{ts,tsx,graphql}' " Benchmark 1: pnpm exec eslint --concurrency off ' ./**/*.{ts,tsx,graphql} ' Time ( abs ≡ ) : 64 . 260 s [ User: 75 . 702 s, System: 11 . 435 s ] Benchmark 2: pnpm exec eslint --concurrency 2 ' ./**/*.{ts,tsx,graphql} ' Time ( abs ≡ ) : 51 . 809 s [ User: 108 . 378 s, System: 22 . 637 s ] Benchmark 3: pnpm exec eslint --concurrency 3 ' ./**/*.{ts,tsx,graphql} ' Time ( abs ≡ ) : 48 . 950 s [ User: 135 . 421 s, System: 35 . 065 s ] Benchmark 4: pnpm exec eslint --concurrency 4 ' ./**/*.{ts,tsx,graphql} ' Time ( abs ≡ ) : 49 . 328 s [ User: 163 . 220 s, System: 50 . 156 s ] Benchmark 5: pnpm exec eslint --concurrency 5 ' ./**/*.{ts,tsx,graphql} ' Time ( abs ≡ ) : 52 . 281 s [ User: 187 . 664 s, System: 67 . 241 s ] Benchmark 6: pnpm exec eslint --concurrency 6 ' ./**/*.{ts,tsx,graphql} ' Time ( abs ≡ ) : 59 . 303 s [ User: 216 . 758 s, System: 91 . 124 s ] Benchmark 7: pnpm exec eslint --concurrency 7 ' ./**/*.{ts,tsx,graphql} ' Time ( abs ≡ ) : 67 . 345 s [ User: 236 . 109 s, System: 113 . 020 s ] Benchmark 8: pnpm exec eslint --concurrency auto ' ./**/*.{ts,tsx,graphql} ' Time ( abs ≡ ) : 72 . 838 s [ User: 263 . 605 s, System: 133 . 785 s ] Summary pnpm exec eslint --concurrency 3 ' ./**/*.{ts,tsx,graphql} ' ran 1 . 01 times faster than pnpm exec eslint --concurrency 4 ' ./**/*.{ts,tsx,graphql} ' 1 . 06 times faster than pnpm exec eslint --concurrency 2 ' ./**/*.{ts,tsx,graphql} ' 1 . 07 times faster than pnpm exec eslint --concurrency 5 ' ./**/*.{ts,tsx,graphql} ' 1 . 21 times faster than pnpm exec eslint --concurrency 6 ' ./**/*.{ts,tsx,graphql} ' 1 . 31 times faster than pnpm exec eslint --concurrency off ' ./**/*.{ts,tsx,graphql} ' 1 . 38 times faster than pnpm exec eslint --concurrency 7 ' ./**/*.{ts,tsx,graphql} ' 1 . 49 times faster than pnpm exec eslint --concurrency auto ' ./**/*.{ts,tsx,graphql} ' どうやら私の端末では明示的に --concurrency 3 を指定することで off の場合よりも1.31倍、 auto の場合よりも1.49倍高速なようです。 concurrency auto を指定すると遅くなってしまうものの、適切な並列数を明示的に指定することで良いパフォーマンスを得ることができそうです。 キャッシュ有効時の性能比較 これまではESLintのキャッシュが無効な状態で検証をしてきました。次はESLintのキャッシュが有効な状態はどのような結果になるか試してみます。 hyperfine --warmup 1 --runs 3 -L concurrency off, 3 " pnpm exec eslint --cache --concurrency {concurrency} './**/*.{ts,tsx,graphql}' " Benchmark 1: pnpm exec eslint --cache --concurrency off ' ./**/*.{ts,tsx,graphql} ' Time ( mean ± σ ) : 2 . 516 s ± 0 . 044 s [ User: 2 . 006 s, System: 0 . 578 s ] Range ( min … max ) : 2 . 470 s … 2 . 557 s 3 runs Benchmark 2: pnpm exec eslint --cache --concurrency 3 ' ./**/*.{ts,tsx,graphql} ' Time ( mean ± σ ) : 4 . 574 s ± 0 . 037 s [ User: 6 . 710 s, System: 2 . 061 s ] Range ( min … max ) : 4 . 540 s … 4 . 614 s 3 runs Summary pnpm exec eslint --cache --concurrency off ' ./**/*.{ts,tsx,graphql} ' ran 1 . 82 ± 0 . 04 times faster than pnpm exec eslint --cache --concurrency 3 ' ./**/*.{ts,tsx,graphql} ' 今回の検証ではすべてのファイルに対するキャッシュが有効な状態で試しているので極端な例ではありますが、この場合は直列で実行したほうが良い結果を得ることができました。 ここまでの整理のために、同じオプションに加えてキャッシュが無効な場合も含めて比較してみましょう。 hyperfine --warmup 1 --runs 3 -L cache --cache,--no-cache -L concurrency off, 3 " pnpm exec eslint {cache} --concurrency {concurrency} './**/*.{ts,tsx,graphql}' " Benchmark 1: pnpm exec eslint --cache --concurrency off ' ./**/*.{ts,tsx,graphql} ' Time ( mean ± σ ) : 2 . 417 s ± 0 . 026 s [ User: 1 . 886 s, System: 0 . 534 s ] Range ( min … max ) : 2 . 391 s … 2 . 442 s 3 runs Benchmark 2: pnpm exec eslint --cache --concurrency 3 ' ./**/*.{ts,tsx,graphql} ' Time ( mean ± σ ) : 4 . 508 s ± 0 . 031 s [ User: 6 . 682 s, System: 2 . 051 s ] Range ( min … max ) : 4 . 473 s … 4 . 530 s 3 runs Benchmark 3: pnpm exec eslint --no-cache --concurrency off ' ./**/*.{ts,tsx,graphql} ' Time ( mean ± σ ) : 59 . 652 s ± 0 . 536 s [ User: 73 . 527 s, System: 10 . 505 s ] Range ( min … max ) : 59 . 221 s … 60 . 253 s 3 runs Benchmark 4: pnpm exec eslint --no-cache --concurrency 3 ' ./**/*.{ts,tsx,graphql} ' Time ( mean ± σ ) : 49 . 708 s ± 0 . 678 s [ User: 134 . 778 s, System: 34 . 085 s ] Range ( min … max ) : 48 . 982 s … 50 . 323 s 3 runs Summary pnpm exec eslint --cache --concurrency off ' ./**/*.{ts,tsx,graphql} ' ran 1 . 86 ± 0 . 02 times faster than pnpm exec eslint --cache --concurrency 3 ' ./**/*.{ts,tsx,graphql} ' 20 . 56 ± 0 . 35 times faster than pnpm exec eslint --no-cache --concurrency 3 ' ./**/*.{ts,tsx,graphql} ' 24 . 68 ± 0 . 34 times faster than pnpm exec eslint --no-cache --concurrency off ' ./**/*.{ts,tsx,graphql} ' この結果から、今回の環境では キャッシュが有効なら直列のほうが速い キャッシュが無効なら並列のほうが速い ということが言えそうです。 CI 環境への適用判断 このプロジェクトではCIでESLintを実行しているので、最後にCIでESLintの concurrency オプションを追加するべきか検討してみます。 前提として、このプロジェクトはTurborepoが導入されています。Turborepoは複数のパッケージからなるモノレポにおいて、ビルドやテストなどのタスクを効率的に実行するツールです。修正したファイルの範囲によりますが、このプロジェクトは最大で9つのパッケージに対してESLintが実行されることになります。Turborepoでそれぞれのパッケージへのコマンドを並列実行することになるため、 ESLintの concurrency オプションを使うと過剰な並列数になってしまい、良いパフォーマンスを得られない可能性がありそうです。 実際に試してみましょう。以下ではturboコマンドを経由してfmtコマンドを実行していますが、これは概ね eslint --cache --fix './**/*.{ts,tsx,graphql}' であると思っていただいて構いません。 hyperfine --warmup 1 --runs 3 -L concurrency off, 2 , 3 " pnpm exec turbo run --cache local:w,remote:w fmt -- --concurrency {concurrency} " Benchmark 1: pnpm exec turbo run --cache local:w,remote:w fmt -- --concurrency off Time ( mean ± σ ) : 4 . 561 s ± 0 . 290 s [ User: 2 . 431 s, System: 0 . 870 s ] Range ( min … max ) : 4 . 376 s … 4 . 895 s 3 runs Benchmark 2: pnpm exec turbo run --cache local:w,remote:w fmt -- --concurrency 2 Time ( mean ± σ ) : 6 . 291 s ± 0 . 075 s [ User: 5 . 689 s, System: 1 . 793 s ] Range ( min … max ) : 6 . 208 s … 6 . 353 s 3 runs Benchmark 3: pnpm exec turbo run --cache local:w,remote:w fmt -- --concurrency 3 Time ( mean ± σ ) : 6 . 322 s ± 0 . 012 s [ User: 7 . 296 s, System: 2 . 287 s ] Range ( min … max ) : 6 . 315 s … 6 . 335 s 3 runs Summary pnpm exec turbo run --cache local:w,remote:w fmt -- --concurrency off ran 1 . 38 ± 0 . 09 times faster than pnpm exec turbo run --cache local:w,remote:w fmt -- --concurrency 2 1 . 39 ± 0 . 09 times faster than pnpm exec turbo run --cache local:w,remote:w fmt -- --concurrency 3 やはり直列で実行したほうがより良いパフォーマンスを得ることができそうです。 これらの処理はGitHub ActionsでGitHubのPull-Requestに対して実行されるようにしていますが、ほとんどのPull-Requestでは一度に大量のファイルを修正することはありません。つまり多くのケースではESLintのキャッシュは大多数のファイルで有効な状態であると考えることができます。 またこのワークフローはUbuntuの4コアで実行するようにしていますが、コア数が少ないためさらなる並列化によってより良いパフォーマンスが得られる見込みは薄そうです。 実際にこのワークフローでベンチマークした結果が以下です。 hyperfine --warmup 1 --runs 3 -L concurrency off, 2 , 3 , 4 ,auto " pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency {concurrency} " Benchmark 1: pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency off Time ( mean ± σ ) : 13 . 241 s ± 0 . 038 s [ User: 41 . 594 s, System: 5 . 830 s ] Range ( min … max ) : 13 . 204 s … 13 . 280 s 3 runs Benchmark 2: pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency 2 Time ( mean ± σ ) : 31 . 019 s ± 0 . 075 s [ User: 104 . 797 s, System: 13 . 536 s ] Range ( min … max ) : 30 . 954 s … 31 . 102 s 3 runs Benchmark 3: pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency 3 Time ( mean ± σ ) : 39 . 606 s ± 0 . 037 s [ User: 136 . 718 s, System: 17 . 224 s ] Range ( min … max ) : 39 . 568 s … 39 . 643 s 3 runs Benchmark 4: pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency 4 Time ( mean ± σ ) : 48 . 413 s ± 0 . 007 s [ User: 169 . 105 s, System: 21 . 288 s ] Range ( min … max ) : 48 . 406 s … 48 . 420 s 3 runs Benchmark 5: pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency auto Time ( mean ± σ ) : 27 . 108 s ± 0 . 056 s [ User: 91 . 045 s, System: 11 . 620 s ] Range ( min … max ) : 27 . 063 s … 27 . 170 s 3 runs Summary pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency off ran 2 . 05 ± 0 . 01 times faster than pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency auto 2 . 34 ± 0 . 01 times faster than pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency 2 2 . 99 ± 0 . 01 times faster than pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency 3 3 . 66 ± 0 . 01 times faster than pnpm exec turbo run --cache local:w,remote:w fmt -- --cache-strategy content --concurrency 4 どうやらこのプロジェクトのESLintには concurrency オプションは追加せず、Turborepoによる並列化のみに留めたほうがよさそうです。 まとめ 以上、 ESLintのMultithread Lintingの導入を検討する際に検証したことでした! 今回検証したプロジェクトのCIには concurrency オプションの追加は見送りましたが、一定の条件下であれば concurrency オプションを使うことでESLintの処理を高速化することができます。しかしその効果は、対象のファイル数、実行環境、キャッシュの有無などによって大きく異なります。導入する前に対象の環境や用途に合うか検証することをオススメします。 他にもチーム内ではESLintの一部のルールを oxlint に置き換えて高速化を試みる動きもあります。良い結果が出てより高速なCI環境が手に入るといいですね! それでは!
アバター
はじめに こんにちは、カイポケのリニューアルプロジェクトを担当しているエンジニアの菅原です。2023年12月に入社し、現在はフロントエンドエンジニアとして機能開発を行っています。 最近、Claude CodeやGitHub CopilotなどのAIエージェントが注目されており、弊社でも活発にAIエージェントを活用した開発が行われております。 私が所属しているチームでも、AI活用の取り組みの一環として、 フロントエンドの実装自動化 に挑戦しました。具体的には、二週間のスプリント期間内で実施するすべてのフロントエンドタスクを、AIエージェントによる自動実装で完成させることを目標とした実験を行いました。 結論から申し上げると、完全な自動化には至らなかったものの、適切な仕組みを整備することで実装プロセスの大幅な効率化を実現することができました。 本記事では、この取り組みを通じて得られた知見と課題、そしてAIエージェントによる実装自動化の現実的な可能性について紹介させていただきます。 取り組みのモチベーション カイポケのリニューアルプロジェクトでは、開発手法としてLeSS(Large Scale Scrum:大規模スクラム)を取り入れており、隔週でスプリントを回しています。LeSSのイベントである、リファインメントを通じて、複数チームに跨ったPBI(ブロダクトバックログアイテム)の分割や曖昧な要求を詳細化することで、スプリント活動中にチームが担当するPBIの不確実要素が下がった状態を実現できています。 また、スプリント期間中もチーム内でモブプログラミングや実例マッピングといった手法を活用して、チームが担当するPBIの要求をより詳細化していき、SBI(スプリントバックログアイテム)というPBIを完了させるために取り組む必要があるタスクのリストに分割しています。スクラムのプラクティスの1つとして、SBIを1日以内に終わる単位で分割することが推奨されており、開発者が実装するタイミングでは仕様が明確になっているため、スムーズに開発できるケースが多くなっています。 このような状況の中で、私はAIエージェントを活用しつつ日々の開発に取り組んでいましたが、要求が明確なため実装後に期待するアウトプットのイメージが持ちやすく、ある程度はAIエージェントに実装を任せても十分な品質のコードを出力できていると感じていました。 そのため、弊プロジェクトで採用しているLeSSによるPBIの不確実性を下げる取り組みと、AIエージェントによる自動実装には強い親和性があるという仮説を持ちました。そこで思い切って、私が担当する一スプリント内のすべてのタスクをAIエージェントにより実装させることで、 実装をどこまで自動化できるか という挑戦を行うことにしました。 LeSS導入時の経緯は過去記事をご参照ください。 自動化の概念図 今回の取り組みでは、スプリント期間中のすべての開発プロセスを自動化するのではなく、初期の実装とPRレビューの指摘事項への修正を対象に自動化することとしました。 自動化対象のイメージは以下の概念図を参照してください。 取り組み内容 1. 設計ドキュメントからAIエージェントがdraft PRを自動作成 まず、AIエージェントによる実装自動化の第一歩として、FigmaのコンポーネントURLと「このコンポーネントを実装して」のような簡潔な指示を出して、AIエージェントに実装を任せてみることから始めました。 しかし、このアプローチでは以下のような問題が発生し、生成されたコードは一見動作するものの、期待値を満たしておらずに大幅な修正が必要になるという結果になりました。 デザインシステムとの不整合 : デザインシステムのコンポーネントやカラー、フォントサイズなどの定義を無視した、独自の実装になってしまう インタフェース定義の不備 : 他のコンポーネントとの連携に必要なProps定義やGraphQLスキーマの型定義が不適切になってしまう 振る舞いの実装ミス : UIの細かな状態変化やユーザー操作に対する振る舞いが期待値と異なる実装になってしまう これらの問題を解決するため、スプリント活動でアウトプットした設計ドキュメントを基に、実装時に必要な情報を構造化して提供する方針にしました。 MCPを活用してデザインシステムに準拠 デザインシステムとの不整合を解消するため、弊社の内製Figma MCPとデザインシステムMCPを活用したアプローチを採用しました。これにより、AIエージェントは以下のような情報を参照しながら実装を行えるようになりました。 デザイントークン : カラー、スペーシング、フォントサイズなどの基本的なデザイン要素を参照 コンポーネント定義 : 各UIコンポーネントで定義されたPropsを参照 この仕組みにより、AIエージェントが生成するコンポーネントは、デザイナーが意図した見た目と動作に近い状態で実装できるようになりました。 弊社内製のMCPの実装については以下記事で詳しく解説しているので、併せてご参照ください。 インタフェース定義の標準化 実装するコンポーネントのインタフェース定義やGraphQLスキーマの定義が曖昧な状態では、AIエージェントは柔軟に実装してくれます。しかし、この「柔軟性」が問題となり、毎回異なる結果を出力してしまうことから、実装予定の他のコンポーネントとの連携が取れない実装になってしまうという問題がありました。 この問題を解決するため、スプリント活動の設計フェーズで明確になった仕様を基に、事前にインタフェース定義を固定し、AIエージェントへのインプット情報として明示的に提供するようにしました。 以下はインプットとして提供する情報の例です。 コンポーネントを実装するディレクトリの例 src/services/careReceivers/tableRow/TableRow.tsx にコンポーネントを実装 コンポーネントのPropsの例 type Props = { id : string ; name : string ; } ; 利用するGraphQLスキーマの例 fragment CareReceivers on User { id name } UIの振る舞いの構造化 AIエージェントが生成したコンポーネントは、見た目は正しく実装されているものの、ユーザーの操作に対する反応や状態変化が仕様と異なることがありました。 この課題に対しては、スプリント活動の実例マッピングで整理したUIの振る舞いを、Given When Then形式で構造化し、AIエージェントにテスト駆動開発で実装させる方針を採用しました。 具体的には、以下のような流れで実装を行います。 振る舞いの構造化 : 実例マッピングの結果をGiven When Then形式で記述 テストの実装 : AIエージェントがその仕様に基づいてテストコードを実装 コンポーネントの実装 : テストがパスするまでコンポーネントを繰り返し修正 UIの振る舞いの例(Given When Then形式で記述) Given : 利用者一覧画面が表示されている状態で When : 利用者のチェックボックスをチェックすると Then : ヘッダーにチェックした利用者件数が表示されること このアプローチにより、AIエージェントが生成するコンポーネントは、見た目だけでなく動作も仕様通り実装されるようになりました。 プロジェクト固有のコンテキストの提供 上記の主要な課題への対策に加えて、AIエージェントがプロジェクトの既存実装パターンを理解し、一貫したコードスタイルで実装できるよう、以下も補完情報として加えました。 リファレンス実装のディレクトリパス コーディングガイドラインのファイルパス これらの構造化されたインプット情報を整備した結果、AIエージェントによる実装品質が安定するようになりました。 2. インラインレビューによるAIエージェントの自動修正 上記のアプローチにより、実装品質が安定したものの、インプットの内容の不備やコンテキストの欠落により、AIエージェントが生成したdraft PRをそのまま商用環境に適用することは厳しいことがわかりました。 そこで、draft PRで不十分な箇所については、人手でのPRレビューにより修正する方針としました。 最初のアプローチとして、AIエージェントが GitHub CLI を活用してPR reviewの内容をチェックして修正するようにしていましたが、以下の課題がありました。 すでに解決済みのレビューコメントも取得してしまい、フィードバックの回数が増えると適切な修正が行われなくなる レビューコメントに対して、AIエージェントが修正した内容をチェックするのに手間取り、指摘内容が適切に修正されているか確認しづらい そこで、効率的なPRレビューを実現できるように GitHub GraphQL API を活用して、以下のツールを持つ簡易的な自作GitHub MCPサーバーを作成し、レビューコメントの修正を行うようにしました。 get_pull_request_review_comments : 未解決かつ参照元のコードが最新のレビューコメントのみを取得するツール reply_to_fixed_commit_in_pull_request_review_thread : レビューコメントに対して修正内容の完了と修正した対象のコミットハッシュを通知するツール これらのツールを活用することで、AIエージェントが以下のプロセスでインラインレビューの指摘事項を修正することができたため、コードを期待する品質まで改善させることができました。 レビューコメントの取得 : get_pull_request_review_comments でPRの未解決コメントを一括取得 修正内容の特定 : AIエージェントがコメントの内容を解析し、必要な修正を特定 コードの修正 : 指摘事項に基づいてAIエージェントが自動でコードの修正を実行 修正完了通知 : reply_to_fixed_commit_in_pull_request_review_thread で修正コミットと修正内容を報告 具体的なMCPツールの実装イメージは以下を参考にしてください。 MCPツールの実装イメージ 1. get_pull_request_review_comments PRのレビューコメントを構造化されたデータとして取得するツールです。 以下のコードブロックでは、現在の作業ブランチ(draft PRを作成したブランチ)を入力値として渡すと、未解決のレビューコメントのファイルパスとスレッドを返す仕組みを表現しています。 server.registerTool( "get_pull_request_review_comments" , { description : "Get comments from pull request review threads" , inputSchema : { branchName : z.string().describe( "Branch name of the pull request" ), } , outputSchema : { filePaths : z.array( z.object( { filePath : z.string().describe( "File path of the review thread" ), reviews : z .array( z.object( { threadId : z.string().describe( "ID of the review thread" ), startLine : z .number() .nullable() .describe( "Start line of the review thread" ), endLine : z .number() .nullable() .describe( "End line of the review thread" ), comments : z .array(z.string()) .describe( "Comments in the review thread" ), } ), ) .describe( "Reviews in filePath" ), } ), ), } , } , async ( params ) => { // 未解決のPRレビューコメントを構造化されたデータとして返却する処理 } , ); リクエストの例 branchName: 現在の作業ブランチ(draft PRを作成したブランチ) { " branchName ": " feature/current-branch " } レスポンスの例 filePath: レビュー対象のファイルパス reviews: レビューのスレッドID、コメントの範囲、コメントの内容 { " filePaths ": [ { " filePath ": " src/components/UserList.tsx ", " reviews ": [ { " threadId ": " threadId1 ", " startLine ": 15 , " endLine ": 20 , " comments ": [ " 型定義が不十分です。Propsの型を明確に定義してください。 ", " ユーザビリティの観点から、エラーハンドリングを追加したほうが良いです。 " ] } ] } ] } 2. reply_to_fixed_commit_in_pull_request_review_thread レビューコメントに対して修正完了の返信を自動で行うツールです。 以下のコードブロックでは、 get_pull_request_review_comments で取得したレビューのスレッドIDと修正したコミットハッシュ、修正内容を入力値として、対象のレビュースレッドに対して修正内容を通知する仕組みを表現しています。 server.registerTool( "reply_to_fixed_commit_in_pull_request_review_thread" , { description : "Reply to a fixed commit in a pull request review thread" , inputSchema : { threadId : z.string().describe( "ID of the comment to reply to" ), commitHashes : z .array(z.string()) .min( 1 ) .describe( "Array of commit hashes of the pull request" ), message : z.string().describe( "Custom message to include in the reply" ), } , outputSchema : { result : z.object( { success : z.boolean().describe( "Whether the reply was successful" ), body : z.string().describe( "Content of the reply" ), createdAt : z .string() .describe( "Timestamp of when the reply was created" ), } ), } , } , async ( params ) => { // レビューのスレッド単位で修正内容を返信する } , ); リクエストの例 threadId: レビューのスレッドID commitHashes: レビューコメントに対して修正したコミットハッシュ message: 修正内容 { " threadId ": " threadId1 ", " commitHashes ": [ " ea9f557c44b545b93d7f86fcc7cb796c77022367 " ] , " message ": " ご指摘いただいた点を修正しました。型定義を追加し、エラーハンドリングも実装しています。 " } レスポンスの例 threadIdで指定したスレッドに対して、レビューコメントを返す まとめ 本記事では、LeSSによってPBIの不確実性が低減された状況とAIエージェントによる自動実装に親和性があるという仮説のもと、スプリント期間中のフロントエンド実装自動化に挑戦した取り組みについて紹介させていただきました! 完全な自動化には至らなかったものの、この取り組みを通じて、以下の仕組みによりAIエージェントによる実装の大幅な効率化を実現できました。 スプリント活動でアウトプットした設計情報を、AIが理解しやすい形式に整理し、設計ドキュメントを構造化 独自のGitHub MCPを活用して、PRレビューコメントの自動取得と修正完了通知の仕組みを実装し、自動修正フローを構築 さらに、弊社内製のFigma MCPやデザインシステムMCPとの連携により、デザインシステムに準拠した実装を自動生成する体験も実現しました。 一方で、完全な自動化を実現するには、まだまだ以下のような課題があることもわかりました。 AIエージェントが安定した出力を行うための設計ドキュメントの構造化が必要 複雑なビジネスロジックや例外処理では、人手による詳細な指示やレビューが必要 AIが解釈しやすい形式でのコーディングガイドライン整備が必要 現状では「完全自動化」よりも「効率的な協働」が現実的であることがわかりました。今後はこれらの課題を解消しつつ、人とAIがより良く協働できる開発体験の実現を目指していきたいと考えています。
アバター
こんにちは。介護・医療・障害福祉・保育の求人サイト「ウェルミージョブ」のQAを担当している林です。 ウェルミージョブは、2025年7月にカイゴジョブからリブランディングしてサービス提供を開始しました。 私はアジャイルな開発チームの中で、テストをこなすだけでなく、開発チーム全体でプロダクト・サービス品質を向上すべく日々挑戦しています。 今回の記事では、QA担当の私が開発チームにポストモーテムを導入し、チームでの実践に至るまでの経緯と、その具体的な進め方についてお伝えします。 0. はじめに エス・エム・エスのQA組織では、Value(行動指針)として「チームで品質保証」を掲げています。 これは、介護/障害福祉事業者向け経営支援「カイポケ」のQAチームにて策定されたもので、ウェルミージョブのQAチームでも同じマインドを共有しています。 QA組織の行動指針を言語化した取り組みについては、以下の記事をご覧ください。 tech.bm-sms.co.jp 「チームで品質保証」の範囲はQAに留まらず、開発・デザイナー・PdM・事業メンバー・運用メンバーなどと幅広く協同することを想定しています。 横断的に「チームで品質保証」を実践している事例については、以下の記事をご覧ください。 tech.bm-sms.co.jp 1. QAの私がポストモーテムにチャレンジした経緯 チームでの品質保証活動における「3つの課題」 私はかねてより「チームで品質保証」のもと、開発チーム全体で質の高い原因分析をして再発防止に取り組みたいと思っていました。 しかしながら、以下のような課題感がありました。 QAが実施する不具合分析やインシデントの振り返りが、開発メンバーを巻き込んでの活動に繋げづらい 過去に開発メンバーでインシデント振り返りやポストモーテムを行った実績もあるが、経験値や関心度はメンバーによって差がある 同じ方向をむいて原因分析・再発防止を検討できる基盤がない 私をポストモーテムへの挑戦に導いた「3つの決め手」 課題を抱えていた私に、ポストモーテムについて触れる機会が次々とやってきました。 そして、以下の決め手により、ポストモーテムへ挑戦したい気持ちが固まっていきました。 チームで同じ方向を向ける「指南書」の存在 他チームの成功事例による「道しるべ」 「QAの業務」ではなく「チームの活動」にできる可能性 以下に、それぞれの決め手についてお話しします。 1. チームで同じ方向を向ける「指南書」の存在 同僚のQAエンジニアが作成したドキュメント中に、ポストモーテムの指南書ともいえるものを見つけました。 その中には「直接的原因・間接的原因・動機的原因」を切り分けた分析アプローチの例示と、フィッシュボーンチャートがありました。 私は「これを使えばチームで同じ方向を向いて活動できそう!」と直感しました。 「直接的原因・間接的原因・動機的原因」を切り分けた分析アプローチ 「原因」という言葉は意味が広く、人によってイメージするものが異なりがちです。「直接的原因・間接的原因・動機的原因」を分けて考えることで、チームメンバー間のイメージをすり合わせが容易になり、チームで同じ方向を向いて分析を進めることができます。指南書では、「直接的原因・間接的原因・動機的原因」について以下のように身近な例でわかりやすく説明されていました。 例:カロリーの摂りすぎで肥満になり、⚪︎⚪︎病(インシデント)になった 起きたこと -> 〇〇病 直接的原因 -> 肥満(hogehoge数値の増加) 間接的原因 -> カロリーの摂りすぎ 動機的原因 -> 日々の仕事でストレスがたまっており、過食の傾向があった フィッシュボーンチャート フィッシュボーンチャートは、ある問題(結果)とその原因の関係を、魚の骨のような形で整理・可視化するための図です。ある問題(魚の頭)は、どのような原因(骨)から起きているのか?をひと目で理解することができます。 2. 他チームの成功事例による「道しるべ」 他開発チームにおいて「ポストモーテムを重ねた結果、検討の観点・深さがよくなってきて、品質向上のプロセスが磨かれている」との情報をキャッチしました。これは、まさに私の目指したい姿です。 また、他チームにてポストモーテムのフォーマットが確立していることも確認できたため、フォーマットをそのまま流用して省コストでチャレンジできそうでした。 3. 「QAの業務」ではなく「チームの活動」にできる可能性 私は、チーム全体で活動をするにあたり「QA業務を開発メンバーに協力してもらう」ではないやり方を模索中でした。 ポストモーテムは、当時のウェルミージョブ開発チーム内では「QA(または開発)がやるもの」という概念がない状態だったため、「これならQAの業務ではなくチームの活動にできそう!」と感じました。 そして、ついにポストモーテムへ挑戦する日が訪れました。 2. ポストモーテム実施 初回チャレンジ ステークホルダへ原因や再発防止について報告が必要な状況となったため、私から「今回はポストモーテムにチャレンジしてみませんか?」と開発チームへ提案しました。 ポストモーテムについて、社内における他チームでの実績や参考にできる情報が多くあることを伝え、開発チームの賛同を得ました。 誰がファシリテートするかについてはもちろん、ポストモーテムへの挑戦にワクワクしている私が引き受けました。 以下、初回チャレンジのサマリです。 活動のステップ 資料たたき(ステータス・サマリ・タイムライン・影響・原因・対応・アクションアイテム)作成:QA(私) 読合せ会1:開発・QA・PdM・事業メンバー 読合せ会2:開発・QA・事業メンバー 改善アクション実行:開発・QA 大事にしたこと チーム全体で同じ方向を向いて原因分析に取り組むこと 効果的かつ実現可能な再発防止策を導き出すこと 次回以降、私ではない他のメンバー(特に、QAではなく開発メンバー)がチャレンジできるように敷居を下げること チャレンジ結果 【◎】「直接的原因・間接的原因を切り分けた分析アプローチ」によりチーム全体で方向性を合わせ、解像度を高めて効果的な分析ができた 【△】読合せ会が初動の話で盛り上がり、原因分析の話が十分にできなかったため、別日に読合せ会2を追加開催することになった 【△】後から資料を整理する時間がなく、資料が読みづらいままになってしまった 【◎】改善アクションを、QAタスクではなく開発チームのタスクとして進められている このように、ポストモーテム初回チャレンジは概ね成功といってよい形で実施することができました。 さて、このポストモーテムの活動を開発メンバーへ展開していきたい…と思っていた矢先、予想外に早く、その時は訪れてしまいました。 2回目チャレンジ 初回チャレンジから間を空けず、ポストモーテムの成功体験が記憶に新しいタイミングで、開発チームへ2回目のポストモーテム実施を提案したところ賛同が得られました。 「ぜひ開発メンバーにチャレンジしてほしい、私が伴走する」とファシリテーターについて持ち掛けたところ、ポストモーテム未経験の開発メンバーに立候補してもらえました。 以下、2回目チャレンジのサマリです。 活動のステップ 資料たたき(ステータス・サマリ・タイムライン・影響・原因・対応・アクションアイテム)作成:開発(伴走:QA) 原因分析の分科会:開発・QA(主要メンバーのみ) 読合せ:開発・QA・事業メンバー・運用メンバー 改善アクション実行:開発・QA 大事にしたこと 私はできるだけ裏方に徹すること チーム全体で同じ方向を向いて原因分析に取り組むこと 効果的かつ実現可能な再発防止策を導き出すこと チャレンジ結果 【◎】QAが行った品質活動に、開発メンバー主導でチャレンジした実績ができた 【◎】原因分析を分科会で事前に実施し、課題が一定クリアになった状態で読合せができた 【◎】ポストモーテム前提でインシデント対応中に時系列を記録できていたことで、情報収集の負荷が軽減できた 【△】同じ方向をむいて原因分析するために重要なフィッシュボーンチャート等がカットされてしまったため、私にて再掲 【△】ポストモーテムの形式にとらわれすぎて、読合せ会の前半が資料の読み上げになってしまった 【△】後から資料を整理する時間がなく、資料が読みづらいまま 【◎】改善アクションを、QAタスクではなく開発チームのタスクとして進められている このように、2回目のポストモーテムも概ね成功といってよい形で実施できたのではと思います。 特に原因分析について、分科会としてメンバーを絞って事前に実施したことで、必要十分な工数をかけて余計な圧などがない環境で議論ができ、より確度の高い分析ができたと感じています。 また、改善アクションについて、開発メンバーにて迅速に対応がなされて一部改善効果が出ており、効果的かつ実現可能な再発防止が進められています。 3. おわりに その後、報告書が必要となる大規模のインシデントは発生していませんが、小規模のインシデント対応においても開発チームのメンバーから「ポストモーテムしましょうか」と声が上がり、ポストモーテムの実施が定着しつつあります。 また、ポストモーテムを意識した情報整理も意識されるようになり、別のインシデント対応において他システムの開発チームとの連携にも役立ちました。 今回のポストモーテムへのチャレンジは、今後の更なる「チームで品質保証」への取り組みの礎となると大いに期待しています。 今回のチャレンジができたのは何よりも、日ごろから開発・QA・デザイナー・事業メンバーが一丸となってアジャイルなチーム開発を進めていることにあります。 チームメンバーである塩井さんの記事『社会課題に取り組みたいRuby大好きエンジニアがセカンドキャリアにエス・エム・エスを選んだ理由』の中で「開発に責任感を持って真摯に向き合い、ごく自然にお互いに助け合うチームメンバー」とあるように、ウェルミージョブの開発チームでは、チーム全体へ働きかける形での品質向上へのチャレンジが歓迎されています。 tech.bm-sms.co.jp これからも私は「チームで品質保証」のもと、異なる専門性を持つメンバーと協力してチーム全体で品質の向上を目指すべく、チャレンジを続けます。 告知! ウェルミージョブの開発をリードする @moro が、この度「Kaigi on Rails 2025」にて初日のKeynote Speakerを務めます。 一昨年のKaigi on Rails 2023、そして去年のKaigi on Rails 2024でも大好評を博した@moroの基調講演にどうぞご期待ください! Kaigi on Rails 2025 Keynote: dynamic! 昨年までの発表 Simplicity on Rails - RDB, REST and Ruby / MOROHASHI Kyosuke - Kaigi on Rails 2023 Identifying User Identity / MOROHASHI Kyosuke - Kaigi on Rails 2024
アバター
皆さん、こんにちは! エス・エム・エスの人材紹介開発グループでマネージャーをしている @kenjiszk です。私は2023年4月に入社し、気づけば3年目に突入しました。今回は、私たちのグループが新たに始めたオフラインイベントについてご紹介します。 なぜオフラインイベント? エス・エム・エスの開発組織はフルリモートで業務を行っているため、実はまだ一度も顔を合わせたことのないメンバーが多くいました。特に九州や関西地方など遠方に住んでいるメンバーもいるため、気軽にランチをするということすら難しい状況です。 このような状況下で、私たちのグループには「チーム間のつながりが希薄である」という課題がありました。各チームは、それぞれのサービスごとに少人数のエンジニアで構成されており、日常的な業務や開発についてはこのチームの中で解決することが多いです。各チーム内での会話や議論は非常に活発ですが、チームを跨いだ議論や交流はあまり多くありませんでした。 なぜ横のつながりを大事にしたいのか? 私たちのサービスは、看護師・介護職・保育士など対象とする従事者によってサービスとチームが分かれていますが、それぞれに求められる技術的な要件やアーキテクチャは似ている部分があります。チームに閉じずに、横断的なコミュニケーションが活発になることで、自分たちが困っていることは実は他のチームが解決してくれていた、とか、自分たちが行っていることが他のチームの助けになった、ということは往々にして起こり得ます。 オンラインで機会を作ってもなかなか難しい問題 オンラインでもチームを横断したような雑談の機会や、他チームのメンバーの人となりがわかるようなLT(ライトニングトーク)を企画していましたが、偶発的にチームを横断した雑談が生まれるような雰囲気の醸成はなかなか難しいと感じていました。 物理的な距離を越えて、心の距離を縮める試み この課題を解決するため、私たちはまず「物理的に会ったことのないメンバーをなくす」ことに焦点を当てました。そして、お互いの精神的な壁を低くすることを目指し、初のオフラインイベントを企画・開催しました! イベントでは、「マシュマロチャレンジ」というチームビルディングゲームを行いました。このゲームは、パスタ、テープ、ひも、マシュマロを使い、自立可能なタワーを作るというシンプルなルールながら、チームの創造性、問題解決能力、そしてコミュニケーション能力が試されます。最も高いタワーを作ったチームが勝利となります。 普段話す機会のないメンバー同士が自然に交流できるよう、チームはランダムに編成しました。初対面のメンバーも多い編成でしたがどのチームもお互いに協力しあいタワーを作りました。 今回一番成績の良かったチームは、71cmのタワーを完成させることができました(ちなみに世界記録は99cm)。 マシュマロ・チャレンジの概要は以下の動画で確認できます。 Build a tower, build a team | Tom Wujec チームリーダーによるパネルディスカッション マシュマロゲームで会場が温まった後には、各チームのリーダー4名によるパネルディスカッションを実施しました。ここでは、現在取り組んでいる課題や、今後挑戦していきたいことなどについてざっくばらんに語ってもらいました。 普段は聞くことのできない他のチームの取り組みやリーダーたちの熱い想いに触れることができ、参加者からは「とても刺激になった」「視野が広がった」といった声が上がりました。リーダー陣のリアルな声は、今後の業務へのモチベーション向上にも繋がったことと思います。 半年に一回の開催で継続していきます イベント後のアンケート結果は、概ね好評でした!「普段話さないメンバーと交流できて楽しかった」「他チームのことが知れてよかった」といったポジティブな意見が多く寄せられ、このイベントがチームの絆を深める上で非常に有効であったことを実感しました。 リモートワークが主流の今、オフラインでの交流はより一層貴重な機会となります。次回以降もメンバー間の交流を促進し、より強固なチームを築いていけるようなイベントを継続的に開催していく予定です。 今回はリモートワークのデメリットについてスポットライトを当てましたが、リモートワークにはメリットがたくさんあり今後も良い形で続けていきたいと考えています。この取り組みを通じて、私たちの会社がどんな会社なのか、少しでも皆さんに興味を持っていただけたら嬉しいです。
アバター
こんにちは!ブログ編集チームの @_kimuson です。 我々はエス・エム・エス テックブログをはてなブログで運用しており、従来はGoogle Documentやesa *1 で下書きを書いて入稿をしていました。 今回、はてなさんが公開している Hatena-Blog-Workflows-Boilerplate を一部利用しつつ、我々のワークフローに合わせてカスタマイズすることでブログの入稿やブログの公開に伴う様々な作業を自動化することで記事管理がかなり楽になったので事例を紹介させていただきます! これまでのフローのペイン 入稿フローの複雑さ 入稿は執筆者の好みでGoogle Documentあるいはesa(markdown)に書いてもらったものをレビューしていましたが、それぞれ入稿やレビュープロセスにペインがありました。 Google Documentの場合: 入稿時のセマンティックを正しく反映したMarkdownへ変換したり、見た目の調整をしたりが大変 esaの場合: エンジニアが慣れ親しんだmarkdownで書きやすいが、行レベルコメントが利用できないのでレビュープロセスが大変 markdownも方言が異なるのでそのまま入稿できることはほとんどない 執筆者が実際のデザインで記事を見られない 執筆者が記事を書いてから下書きとしてはてなブログに入稿されるまでにラグがあり 執筆者が自分のブログデザインの崩れに気づけない(気づくのが遅くなる) 実際に当てはめてみて読んでみることができない というペインもありました。 品質チェックの負荷 記事の公開前には常に広報ガイドラインに則ったチェックとフォーマットのチェックを手動で行っており、これも負担になっていました。 広報のガイドライン準拠チェック 英数字の前後空白など、スタイルルールの確認 チャットベースのLLMを活用した一部効率化は行っていたものの、コピペや修正の手間は依然として残っており大変でした。 アイキャッチ作成の手間 記事ごとにアイキャッチ画像(OGP画像)を作成して設定していましたが、これもKeynoteで調整して書き出しており労力がかかっていました。 適切な改行位置の決定 SNS投稿時の見切れを防ぐ文字サイズ調整 記事をリポジトリで管理することで、こういった面倒な作業を自動化しやすくなります。これらのペインを解消すべく取り組みました。 Hatena-Blog-Workflows-Boilerplate はてなブログ用の記事管理をリポジトリでやるなら、はてなさんが提供する hatena/Hatena-Blog-Workflows-Boilerplate を利用すると便利です。 このリポジトリをテンプレートにして記事管理リポジトリを作成することで、ボイラープレートに用意されているGitHub Actionsワークフローを利用できます。 create-draft: 手動実行でドラフト記事を作成 pull-draft: ドラフト記事をpushした際に、はてなブログ側へ変更を反映する pull: はてなブログから公開済みの記事のみを取得 push-draft: はてなブログから特定のタイトルの下書き記事を取得 push-when-publishing-from-draft: ドラフト記事を公開ステータスでpushすると公開 push: 公開済みの記事を更新し、 はてなブログに反映 すでに公開されている記事の同期から、新しいmarkdown記事の作成・入稿・公開まで一通りの機能がオールインワンで提供されており、基本的にはこれをそのまま使用すれば記事管理を行うことができます。 予約投稿機能が使えない 非常に便利なHatena-Blog-Workflows-Boilerplateですが、予約投稿機能を利用できないという点で困りました。 我々は記事の公開は基本的に予約投稿機能を利用しています。 しかしながら 予約投稿がサポートされていない frontmatterのdraftプロパティで公開/非公開が制御される 公開済み記事は draft_entries から entries ディレクトリへの移動される という仕様になっていました。 Hatena-Blog-Workflows-Boilerplateを利用した上で予約投稿も併用すると、予約投稿によって公開された記事が draft_entries に残り、他記事のリポジトリ操作ではてなブログ側へ意図しない状態変更が起きるリスクもありそうです。 解決アプローチ 考え方を少し変え、「記事の執筆から入稿・公開・公開後の修正まですべて管理する」のではなく、 「下書き記事の作成から入稿までを管理する」 という方針に変更しました。 我々のペインは入校後の記事管理にはほとんと存在せず、下書き記事の執筆から入稿までの間に集約されていました。 公開済みの記事管理までスコープを広げて変に複雑にするより、下書き記事の入稿までに振り切る方がシンプルで運用しやすいと考えたため、この方針を採用しました。公開した記事の内容を変更したり調整することもないわけではありませんが、頻度が多いわけではないので割り切っています。 具体的には以下のように実現します: draft_entries ディレクトリのみを使用し、公開済み記事は管理しない(entriesディレクトリ不使用) entriesに関連するワークフローは削除し、一部のワークフローのみ利用(create-draft, push-draftのみ) 記事PRマージ後にファイルを自動で削除する 公開済みの記事について編集する場合はリポジトリを介さずに調整する この方針により、下書きから入稿までの諸々の手間はリポジトリに寄せて自動化しつつ、下書き→公開のプロセスにはリポジトリは関与させないことで、責務を明確に分離できました。 機能が不足している予約投稿やカテゴリ設定といった公開に関するオペレーションは従来の方針を維持して運用できています。 親子アカウント非対応問題 Hatena-Blog-Workflows-Boilerplateのセットアップは基本的には公式READMEに従って簡単に設定できましたが、一部問題が発生しました。 はてなブログでは親子アカウント機能を利用でき、我々は強すぎる権限を持たせないようにしないため子アカウントを普段使いしています。 しかし、Hatena-Blog-Workflows-Boilerplateでは親子アカウントに対応しておらず、親アカウントのトークンが必要でした。 このため、初期設定時のみ親アカウントを使用してトークンを発行する必要がありました。 編集 URL の修正 Hatena-Blog-Workflows-Boilerplateでは create-draft ワークフローを利用することで記事の下書きファイルとPRを作成し、対応するはてなブログ上のエントリの作成・PR Descriptionにプレビューや編集のURLを添付するところまでやってくれます。 ただし、記事の編集URLがAtomPubのURL形式( https://blog.hatena.ne.jp/bm-sms/sms-tech.hatenablog.com/atom/entry/<id> )で生成されるのですが、これを参照するには親アカウントでないと閲覧できないという問題がありました。 これを解決するため、create-draftのワークフローを拡張し、後続のJobで通常の編集URL形式( https://blog.hatena.ne.jp/bm-sms/sms-tech.hatenablog.com/edit?entry=<id> )に変換するワークアラウンドを実装しました。 name : create draft on : workflow_dispatch : inputs : title : description : "Title" required : true jobs : create-draft : uses : hatena/hatenablog-workflows/.github/workflows/create-draft.yaml@ce4c0e01255ad9348842e5ce09809c3ec499e43d # v2.0.5 with : title : ${{ github.event.inputs.title }} draft : true BLOG_DOMAIN : ${{ vars.BLOG_DOMAIN }} secrets : OWNER_API_KEY : ${{ secrets.OWNER_API_KEY }} fix-edit-url : # 編集ページ URL が AtomPub ベースの URL になっていて編集チームでアクセスできないので、普段使っている URL 形式に変換する # before: https://blog.hatena.ne.jp/bm-sms/tech-bm-sms.hatenablog.com/atom/entry/<ID> # after : https://blog.hatena.ne.jp/bm-sms/tech-bm-sms.hatenablog.com/edit?entry=<ID> needs : create-draft runs-on : ubuntu-latest steps : - name : Checkout repository uses : actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name : Get PR number from create-draft job id : get-pr run : | # 記事タイトルをもとに該当するPRを検索し、最新のものを取得 PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --author github-actions[bot] --search "in:title \" ${{ github.event.inputs.title }} \" " --limit 1 --json number --jq '.[0].number' ) echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT env : GH_TOKEN : ${{ github.token }} - name : Update PR description with correct edit URL and issue link run : | # 現在のPRのdescriptionを取得 CURRENT_DESCRIPTION=$(gh pr view ${{ steps.get-pr.outputs.pr_number }} --repo ${{ github.repository }} --json body --jq '.body' ) # URLを置換(atom/entry/ → edit?entry=) UPDATED_DESCRIPTION=$(echo "$CURRENT_DESCRIPTION" | sed 's|/atom/entry/|/edit?entry=|g' ) # PRのdescriptionを更新 gh pr edit ${{ steps.get-pr.outputs.pr_number }} --repo ${{ github.repository }} --body "$UPDATED_DESCRIPTION" env : GH_TOKEN : ${{ github.token }} これにより、子アカウントを利用していても編集URLへアクセスできるようになりました。 不要なワークフローの削除とお掃除機能の実装 下書きの入稿までをスコープとするため、PRをマージした時点で下書き記事は削除を行います。 これはHatena-Blog-Workflows-Boilerplateではサポートされない独自のワークフローなので自前でGitHub Actionsを実装しました。 name : cleanup draft entries after merge on : pull_request : types : [ closed ] branches : - main jobs : cleanup-draft-entries : # PRがマージされた場合のみ実行 if : github.event.pull_request.merged == true runs-on : ubuntu-latest steps : - name : Checkout repository uses : actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with : # PRがマージされた後の最新のmainブランチをチェックアウト ref : main token : ${{ secrets.GITHUB_TOKEN }} - name : Get changed draft files id : get-changed-files run : | # マージされたPRで変更されたdraft_entriesディレクトリ内のファイルを取得 CHANGED_FILES=$(gh api /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files \ --jq '.[] | select(.filename | startswith("draft_entries/")) | .filename' \ | tr '\n' ' ' ) echo "changed_files=$CHANGED_FILES" >> $GITHUB_OUTPUT echo "Changed draft files: $CHANGED_FILES" env : GH_TOKEN : ${{ secrets.GITHUB_TOKEN }} - name : Delete draft files if : steps.get-changed-files.outputs.changed_files != '' run : | # 変更されたdraft_entriesディレクトリのファイルを削除 for file in ${{ steps.get-changed-files.outputs.changed_files }}; do if [ -f "$file" ] ; then echo "Deleting $file" rm "$file" else echo "File $file not found, skipping" fi done - name : Commit and push changes if : steps.get-changed-files.outputs.changed_files != '' run : | # Gitの設定 git config --local user.email "action@github.com" git config --local user.name "GitHub Action" # 変更をコミット git add -A # 削除されたファイルがある場合のみコミット if ! git diff --cached --exit-code > /dev/ null ; then git commit -m "chore: cleanup draft entries after merge of PR #${{ github.event.pull_request.number }}" git push origin main else echo "No changes to commit" fi 執筆・入稿作業の効率化 ここまでの対応でリポジトリを用いた記事管理ができるようになりました! 本来やりたかったのは「記事をリポジトリで管理すること」で、ローカル向けのツールやGitHub Actionsを用いた品質管理やレビュープロセスの効率化なので、実際に運用している効率化の仕組みも紹介させていただきます。 textlint を使った文章校正 人間によるレビューで発見される内容を極力減らすため、また「英数字の前後には空白を置く/置かない」と言った機械的なルールを適用するため textlint を導入しています。 日本語関係のルールや、フォーマットに関するルールを追加し、日々運用しながらルールを調整しています。 文法上の細かい指摘などはVSCodeで書くと同時にtextlintの拡張機能によりフィードバックされるので、一部の不適切な記述は執筆者が自分で修正できるようになりました。 GitHub Actions + actions/ai-inference を活用した広報ガイドラインチェック 我々はテックブログを公開するための広報ガイドラインを持っており、公開前に必ずガイドライン違反する記述や内容がないかを編集チームがチェックしています。 このプロセスの負荷を軽減するため、LLMを用いたガイドラインチェックを追加しました。 actions/ai-inference を利用することで、GitHub Actions上で手軽にGitHub ModelsのLLMを利用できます。PRがReady For Reviewになったらワークフローが起動し、LLMがガイドラインと記事の内容を照らし合わせて問題のある箇所をPR上にコメントしてくれるようになっています。 OGP 画像生成の効率化 記事のOGP画像の生成もコマンドラインツール化しました。 完全な自動化は適切な改行位置の設定をすることが難しいため、一部手動で変更するオプションを残した上で自動化をしています。 デフォルトの改行位置の決定には google/budoux を利用しています。budouxに記事ファイルのfrontmatterから取得したタイトルを食わせることで、タイトルを適切な改行可能な位置で分割してくれます。そして予め決めておいた文字数や行数の上限を超えないようにまとめます。 import { loadDefaultJapaneseParser } from "budoux" ; const MAX_CHARS_PER_LINE = 30 ; const MAX_LINES = 6 ; export const separateTitle = ( title : string ): string [] => { // BudouXを使って日本語文章を適切な位置で分割 const parser = loadDefaultJapaneseParser(); const segments = parser.parse(title); const lines: string [] = [] ; let currentLine = "" ; for ( const segment of segments) { // 現在の行に新しいセグメントを追加しても30文字以内の場合 if ((currentLine + segment). length <= MAX_CHARS_PER_LINE) { currentLine += segment; } else { // 30文字を超える場合、現在の行を確定して新しい行を開始 if (currentLine) { lines. push (currentLine); currentLine = segment; } else { // currentLineが空の場合(segmentが30文字を超える場合) currentLine = segment; } } } // 最後の行を追加 if (currentLine) { lines. push (currentLine); } // 行数制限の処理 if (lines. length <= MAX_LINES) { return lines; } // 6行を超える場合、最初の5行を取り、残りを最後の行にまとめる const result = lines. slice ( 0 , MAX_LINES - 1 ); const remainingLines = lines. slice (MAX_LINES - 1 ); const lastLine = remainingLines. join ( "" ); result. push (lastLine); return result; } ; 適切な改行位置での分解ができれば、あとはOGP画像を生成するだけです。これは前例となる記事がたくさん世に出ているので割愛しますが、 node-canvas を用いて、文字が見切れない横幅に収まる範囲で最大のフォントサイズが適用されるように実装しました。 例えば今回の記事タイトルを渡した場合、初回では以下のようになります。 今回に関してはやや文字が小さいので手動で調整します。 分割の情報はtemporaryなjsonファイルへ書き出すようにしており、これを変更して再実行することで区切り位置を変更します。 { "lines": [ - "Hatena-Blog-Workflows-Boilerplate を", + "Hatena-Blog-Workflows-Boilerplate", - "使って記事をリポジトリ管理し、レビュープロセスや入稿作業を", + "を使って記事をリポジトリ管理し、", - "効率化した話" + "レビュープロセスや入稿作業を効率化した話" ] } 再実行することで、再度生成されます。 良い感じになりました。 こういった形でOGPの画像も手間なく作成できるようにしました。 まとめ Hatena-Blog-Workflows-Boilerplateをベースに、我々の運用に合わせたカスタマイズを行うことで、テックブログの執筆・レビュープロセスを効率化した事例を紹介させていただきました! 予約投稿やカテゴリ・タグ設定といった公開に関する機能との兼ね合いで、あくまで下書きの入稿までを行う仕組みとして導入を行いましたが、かなり上手くワークしていると感じています。編集チーム内からの評判も良く、テックブログ運営が楽になりました。 ブログ編集チームは各々が開発チームに所属しながら有志として活動しているようなチームなので、こういった効率化を行って運用負荷を下げていくことは今後もやっていきたいなと考えています。 ブログ編集チームの取り組みについて紹介した記事も出ているので良ければ併せて御覧ください! *1 : 社内で利用しているMarkdownベースのナレッジSaaS
アバター
みなさんこんにちは! プロダクト推進本部の人事をしているまゆゆ( @mayuyu_desuyo ) です。 7/3 -7/4に丸の内で開催されたファインディ社主催の「 開発生産性Conference 2025 」にブース出展および登壇してきましたので今日はそのレポートブログです! まゆゆ 今回登壇したプロダクト推進本部カイポケ開発部エンジニアリングマネージャーのsoranakkさんに、登壇の振り返りをまずはしてもらおうと思います! soranakkさんどうぞ!!! 空中 はい、代わりました。 カイポケ開発部エンジニアリングマネージャーをしている空中清高 ( @soranakk ) です。 「開発生産性Conference 2025」での登壇の振り返りをしたいと思います。 登壇のテーマと背景 まずなぜ「失敗から再構築した開発推進チームの立ち上げ」というテーマで登壇することにしたのかについてお話しします。 登壇することが決まった時、テーマについてチーム内で相談したところ以下のような意見が出ました。 あえて失敗した話や現場の生の声を共有することは価値があるのではないか 失敗から学ぶことの重要性を伝えたい エス・エム・エスの文化として、失敗を恐れずに挑戦していることを伝えたい こういった意見があって「失敗から再構築した開発推進チームの立ち上げ」というテーマで登壇することにしました。 登壇の振り返り 実際に登壇してみた感触ですが、鄧皓亢(でんはおかん)さんが話した前半のパートで、エス・エム・エスで開発生産性のために組織として取り組んでいることをお話しできました。 特にドメインやプロダクト戦略に合わせた開発生産性向上のための専任チーム立ち上げという取り組みについて、組織として開発生産性に取り組みたいときの参考になると良いなと思います。 また私が話した後半のパートの開発推進チームの活動内容について具体的に紹介できたのも良かったと思います。 開発生産性を上げるという抽象的な課題に対して、具体的な取り組みを紹介できたことで、参加者の方々にとって何かのヒントになれば嬉しいです。 登壇の様子や資料については、スライドを公開していますので、ぜひご覧ください。 speakerdeck.com 登壇で話しきれなかったエピソード 登壇の時間が限られており、開発推進チームの全ての活動内容を話しきることができませんでした。 スライドではたくさんの活動内容を紹介しましたが、具体的に内容を話せたのはフロントエンドの開発生産性向上の取り組みの2つの例だけでした。バックエンドの開発生産性向上のための取り組みや、リリースとデプロイの改善活動など、他にもたくさんの取り組みがありますが、時間の都合で話しきれませんでした。 また開発生産性向上のためには品質保証もセットで必要だと考えているので、QAの一部自動化や効率化、本番環境のモニタリングの強化についての取り組みもあるのですが話しきれませんでした。 この辺りについて、興味のある方はぜひカジュアル面談等でお話できればと思います。この記事の末尾にカジュアル面談についてリンクを貼りますので、ぜひ気軽にお声がけください。弊社のカジュアル面談は本当に選考と関係ないカジュアル面談なので、気軽にお話しできると思います。 空中 では、登壇の振り返りについてはこの辺りにして、ブースの状況などについてまゆゆさんと交代したいと思います。まゆゆさん、よろしくお願いします。 まゆゆ soranakkさんありがとうございました!私からはブースの様子をお伝えします。 ブースの様子 エス・エム・エスはsoranakkさんとデンさんの登壇と共に7/4の1dayでブース出展をしました。 こちらのブログ記事 でもお伝えしたとおり、ブースは受付から入ってすぐのところでした! ブースでは、エス・エム・エスの事業や「カイポケ」のプロダクトについてご説明させていただきました。 今回のイベントテーマ「開発生産性」にちなみ、技術や開発をテーマにした「おみくじ」をコンテンツとしてご用意。 「カイポケ」の事業ドメインである「介護」について少しでも立ち止まって考えていただくきっかけになればと思い、ブース内にこんな問いかけを設置しました。 「将来あなたに介護が必要になった時、どんな社会になっていて欲しいですか?」 以下の3つの選択肢から、ご自身の考えに近いものや共感できるものを選んでいただき、そこにおみくじを結んでいただくという企画です。 A:AIやロボットなど介護のIT化が進む社会 B:高齢者の社会参加が重視され生き生きと過ごせる社会  C:在宅医療や介護がより充実する社会  結果としてはAが最も多く、エンジニアが多く参加するイベントということもあり、テクノロジーによる課題解決への関心の高さがうかがえました! 選択肢を眺めながらご自身の体験談などをお話しくださった方などもいらっしゃり、正解が1つではなく、多様な未来の可能性があることについて、来場者の皆様と対話ができた大変貴重な機会でした! また、ブースでは終日デモも行なっていて、デザインシステムMCP化のデモをご用意しました。 デモは終日行なっていましたが、soranakkさんとデンさんの登壇の中でデモをやっていることをお伝えしたところ、たくさんの方にブースへお越しいただき、おかげさまで大盛況でした! 中には一度ブースでデモを体験してくださった方が、同じチームの方と一緒に再度ブースへ来てくださるという大変嬉しい一幕も。 デザインシステムMCP化についてのアウトプットはこちらですので是非こちらもご覧ください。 zenn.dev speakerdeck.com 当日は予期せぬハプニングなどもありましたが、参加メンバーで協力し合いながら無事にカンファレンスを終えることができました!今後の活動にも活かしていきたいと思います!
アバター
Who I am みなさまはじめまして。2025年6月よりAnalytics&Innovation推進部(通称A&I)に入りました井手と申します。 肩書としてはデータサイエンティストというくくりで仕事をしてきております。統計分析や自然言語処理にかかわる学問領域を修めて社会に出たあと、データを集めるところから、それを加工し、分析を行いモデルにするまで幅広くデータ周りに関するお仕事に関わってきました。過去にはECサイトの分析基盤構築と分析業務、直近前職では社内インフラデータの分析基盤構築やそれを用いたデータ利活用推進業務、機械学習を用いたエンジニアHR業の業務マッチングシステムの構築などに携わってきておりました。 When I decided to change 前職での仕事はやりがいもあり特に大きな不満もなかったのですが、関わっていたプロジェクトが落ち着いたタイミングで先のことをぼんやり考えるようになりました。私は大学にいた期間が長かった関係で、社会人経験で言うと同年代同世代の人たちと比べるとそこまで多くないんです。ぺーぺーです。ぺーぺーなのですが、社会人経験の長さにかかわらず皆平等に年をとっていきます。私の残りの社会人人生はそこまで長くない。環境を変えるならそろそろ最後かなっていう思いが、私の背中を押しました。 Why I’m here Motivation 環境を変えるとして何がしたい? これについては私はひとつ、いつか医療や介護に関わる人達のための仕事に自分の能力を役立ててみたいという方向性を持っていました。人間歳を取ってくると、自分や家族が医療介護のお世話になる機会が増えてきます。私もその例に漏れず家族が随分とお世話になる経験がありました。そして私たちは大変幸運なことに素晴らしい方が担当をしてくださり、頼もしい想いや安堵を感じたことをよく覚えています。これは、担当してくださった方の才能はもちろんのこと、その職掌が現場においてまさに適材適所だったからこそなのだと私は思っています。適材適所っていうのは、難しい問題です。それでも、適材適所になる確率を少しでも高めることはできないか、そして私たちのような想いをする人を一人でも多くできるように自分の能力を活かせないかという気持ちを長らく抱いておりました。 看護介護の人材マッチングを柱の一つとするエス・エム・エスは、私のチャレンジの舞台としてはまさに理想的でした。 The Determinants もちろん看護介護のマッチングを提供する会社は他にもあります。その中で、私がエス・エム・エスを選んだ要因。今一生懸命思い出してみるとたくさんあります。たくさんありますが、常にぱっと思い浮かぶのは以下の2点。どちらも全然ロジカルではなく、すごい感覚的なのですが、まあ、意思決定なんてそんなものですよね。 ① 選んでもらえて誇らしかった 転職をするにあたって、もちろん候補となる企業はいろいろ調べます。企業HPをみたり、転職サイトの口コミ見たり社員のみなさんがやっているブログを読んだり。エス・エム・エスも、私が目指したいゴールは十全に共感できるものの、そこに向かう、一緒に働く方々はどういう人達なんだろうっていうことも気になってだいぶ調べました。エス・エム・エスの皆さん、エンジニアはもちろんキャリアパートナーとして働く人もみな志が高く実力者。ああ、皆さんレベルが高いんだな私の力が通用するかなって少し不安になるほど。選考も緊張の連続。冷や汗だらだら。それでも無事に内定をいただき、その後再び内部のみなさんとお話をする機会にて改めて志の高さを確認。そのような方々に選んでいただけたことを大変に誇らしく思い、入社の決め手の一つとなりました。 だから今も私、すごく誇らしい気持ちで働いています。 ② 田辺さん 本部長の田辺さん。一次面接を担当してくれました。私、もう、めちゃくちゃ緊張していたんですね。オンラインだけどちゃんとスーツ着て、ちゃんと3分前くらいには入室して言おうと思っていることをつっかえずに言えるかなって頭の中で何度もリハーサルして。 そしてついに登場した田辺さん。私が事前に想像していた「本部長の田辺さん」とは随分異なり、なんでしょう。カジュアルというか。登場するなり私の緊張を察してか、2、3言ことばをかけていただいて。私も緊張していたのであまり覚えていないのですが、その言葉で肩の力が抜けた気がして。 なんでしょうね。面接には関係のないほんのささいな言葉だったし、いま思い出してもこの会話にどんな情報量が詰まっているか未だにわからないんですよ。でも、この瞬間に、あ、ここでぜったい働きたい。田辺さんといっしょに働きたいって思うんだから人間って不思議ですよね。本質的にはロジカルな生物ではないんでしょうね。人間。 結局①も②も根底は似ているのかな。いいなって思った方々に選んでいただけた。それがすごく誇らしいです。今も。 What I’m doing now いくつかの業務を担当させていただいておりますが、メインはキャリアパートナー向けのサービス改善です。キャリアパートナーのみなさんが現在利用している、求職者とのトランザクションを管理するシステムを、より機能的にスケーラブルで使いやすいものへと刷新する中で、私は特に、求職者と事業者のマッチング確率を計算してリコメンドを生成する部分で関わらせてもらっています。 一般的なマッチングあるいはリコメンドの手法は言うに及ばず、HR領域においても、国内海外を見渡してみると盛んに手法が提案されてきています。ただ、データの持ち方や取得できるデータの性質はビジネスのスタイルによって大きく変わります。今一番効果的だと謳った手法を採用し、手元のデータに適用しようとしてもさっぱりということはざらです。とりわけ私達の場合、マッチングに際して間にキャリアパートナーという人間が介在し、キャリアパートナーの情報も利用する必要があります。そうすると問題構造が、研究が盛んに行われている単純なResume-Jobマッチングと大きく異なってきます。既存の技術だけではどうにもならないぞ、と少し不安はありますがそれでもこのチャレンジングな課題に大きなやりがいを見出しており、なんとかクリアしようと奮闘中です。 Where I want to go 「高齢社会に適した情報インフラを構築することで人々の生活の質を向上し、社会に貢献し続ける」が、エス・エム・エスのミッションです。 私はこれに共感して入社しています。だから、私の大きな目標はただ一つ。自分が定年になって退職し、そして高齢社会の一員になったとき、そして社会を見渡したときに「あ、エス・エム・エスで頑張ってよかったな」って思えるようにするということです。世間の報じられ方からすると、特に若年層の人から見ると、高齢社会というのは一種のディストピアのように見えてしまっているかもしれません。大きな重荷に見えているかもしれません。確かにやりたいこともできず、他人の世話をしなくてはいけない世界というのは明るいとは言えません。ただ、高齢者がいて、その高齢者の周りには個々の力が適材適所で発揮できる場所が存在するのだとしたら、その社会は重荷でしょうか。やりがいのある社会とは言えないでしょうか。私はそういう社会になるよう貢献したいと思っています。 とはいえちょっと漠然としすぎた目標なので、もうちょい自分の能力に引き寄せた目標を立てると、まずは上述したマッチング計算のアルゴリズム。未踏な部分が多いですが、やり遂げたいと思っています。そして過程で得られる様々な知見については、できる限り発信していきたいと思っています。少子高齢化は、遠くはイタリア、近くはお隣韓国など、日本以外の様々な国でも問題になってきており世界の喫緊の課題です。日本のみならず世界の人々と知識をシェアし、よりよい解決策を皆で検討できるようになれたらと思っております。 How I’ll get there なんかえらそうなことを延々書いてきてしまいましたが、私の力はまだまだ足りません。全然足りません。ともすると定年退職の当日まで成長しなくてはいけないくらい足りないかもしれません。 だいぶ長く生きてきて、そうすると自分の能力についてもだいぶ把握できてくるのですが、自分は小さな頃に信じていたほどのスーパーマンにはなれませんでした。いや、ちょっとのスーパーマンにもなれていません。誰かが1回でできることは、私は3回かかります。いま書いていて思いましたが、スーパーマンとか持ち出しましたが本当はどんくさいのかもしれません。ごめんなさい盛りました。 ただ少なくともスーパーマンではない私、3回繰り返すことはできるんです。3回でできなくても5回繰り返すことはできるんです。この才能は心から親に感謝しています。私の座右の銘は本当に月次で陳腐だと思うのですが「継続は力」です。これは本当。そして私の唯一の武器です。 高く掲げた目標、1回挑戦してだめなら10回やります。いつかきっとできます。いつかきっとできるんだけど、もしかしたらそれは定年の日を超えちゃうかもしれない。間に合わないかもしれない。まあでも、やらないって選択肢はないですよね。誇りをもって、進んでいきます!
アバター
こんにちは、カイポケの開発組織責任者の酒井 ( @_atsushisakai )です。 事業会社で働くソフトウェアエンジニア、特にプロダクト開発に関わる人にとって、個人の目標設定に関する悩みはよく話題に上がるテーマです。「どう立てればいいのかわからない」「立てたはいいものの形骸化してしまう」「目標を達成しても思ったように評価されない」といった悩みを聞くことが多くあります。 以前、私自身の目標設定の考え方を社内でまとめたところ反応がよかったこともあり、改めて「目標」と「評価」の関係性を整理し、形骸化しにくく、成果と成長につながる目標設定の考え方を紹介したいと思います。 目標設定に関するよくある悩み これまで複数社で主にエンジニアのマネジメントをやってきて、目標設定が難しいと感じる理由にはいくつかの共通点があるように感じています。 形だけ立てて終わる とりあえず何かを書かないといけないから、無難な内容にしてしまう。 達成しても評価が思い通りにならない 「やったことはやったのに、なぜ評価が上がらないんだろう」とモヤモヤする。 そもそも目標に何を書けばいいかわからない 「なりたい自分像」や「理想のキャリア」がはっきりしないと、目標が浮かばない。 こうした悩みを抱える人は少なくないと思います。また、目標を一緒に立てていくマネージャーにとっても、成熟した考え方が身についていないと、このようなメンバーの問題を解決するのは結構難しいことだと思います。 目標と評価の関係性 これまでに何度もメンバーから「目標設定」に関する相談を受けてきました。その度に、前提として目標と評価の関係性をしっかりと認識してもらうためのコミュニケーションを取っています。 目標と評価について、私の考え方を整理すると次のようになります。 目標と評価は直接紐づくものではない エス・エム・エスのエンジニア評価は「成果評価ではなく能力評価」という手法をとっています (詳しくは、 エンジニア採用ページ の記載をご覧ください)。この評価手法において、目標は「これを達成したから等級UPする」「達成できなかったから評価が上がらない/下がる」というものではなく、あくまで自分がどの課題にチャレンジするのかを表明するものと位置付けています。 目標の本質は成長のための仮説立て 目標は評価のためではなく、達成することを通じて自己成長を実現するためのガイドツールです。評価されるのは「どのような意味のある成果を生み、その過程でどういう成長を遂げたのか」という結果に対してであり、目標を達成した/しなかったというそれ自体ではありません。 評価は組織ごとのマネジメントポリシーに依存する どれだけ目標を達成し成長できていると言っても、それが会社から期待されている役割や等級の範囲を超えていなければ評価に反映されにくいこともあります。それぞれの組織にはその組織のマネジメントポリシーに沿った評価軸が存在し、その評価軸に照らし合わせて下されるものです。 つまり、目標は「成果を生むためにどの課題に取り組むかを明確にするためのツール」として活用し、評価については評価の意思決定を行うマネージャーとその評価軸をすり合わせておくことが大切だと考えています。 私自身の体験と学び 私自身も目標設定にはかなり苦労してきました。自分がどんなエンジニアやマネージャーを目指したいのか、明確なロールモデルを持っていたわけではなく、最初は「何を書けばいいかわからない」という感じでしたし、それゆえ目標設定という作業はとても苦手でした(今も得意ではありません)。 そんな中で気づいたのは、「なりたい自分像」から逆算して目標を立てるのは必ずしも必要ではないということです。むしろ、自分が今いる環境やプロジェクトの中で、 どんなゴールが組織やプロダクトにとって意味があるのか そこに向かう中で何が課題になるのか その課題を解決するために、自分はどんな貢献ができそうか その貢献は自分にとって十分チャレンジングなものか を考える方が、現実にフィットするし、プロダクト開発を主軸とするエンジニアである自分は目標を作りやすいと感じました。 ここ最近、私は目標を立てる際はいつも、 まずは少し先の「ゴール(組織やプロダクトについて、こうなっていたら嬉しい状態)」を考える そこに向かう上での課題を多面的に洗い出す 特に重要度が高い課題を選ぶ その課題をどう解くか、課題を解くためにはどんなステップがありそうかを考える というステップを踏んでいます。 これを繰り返すうちに、「目標を立てる」こと自体は苦手ではあるがやるべきことだとも感じられるようになり、周囲にも説明しやすく、評価にもつながりやすい形に自然となっていったと思います。 「目標設定テンプレート」の紹介 こうした体験を踏まえて「もっとシンプルに、形骸化しにくい目標を誰でも作れる形にしたい」と思い、今回あらためて整理したのが、ここから紹介する「目標設定テンプレート」です。 このテンプレートの大きな特徴は、「まずは事業やプロダクトのゴールを解釈し、そのゴールから逆算して課題を見つけ、そこに挑む中で成長を得る」という順序にあります。「なりたい自分像」ありきではなく、現実の課題から出発してチャレンジを設計するという考え方です。ただし、このテンプレートはプロダクト開発に関わるエンジニア向けに作っており、職種や役割によっては当然合わない部分もあるかもしれません。必要に応じて、自分の役割に合わせて調整して使ってください。 テンプレートの構成 テンプレートは大きく次の5ステップで構成しています。 ゴール 現状と課題 テーマ ステップとチャレンジ 目標 (Objectives) テンプレートの中身と意図 テンプレートの各項目の意図と、どういう考え方で埋めると良いかを簡単に説明します。 1. ゴール まずは、ビジネスやプロダクトの戦略、長期的な目標を踏まえて目指したい状態・達成した成果を書きます。 2. 現状と課題 このゴールに向かう上で、現在立ちはだかっている問題について言語化してください。チーム、組織、プロダクト、プロセス、自分のスキルやマインドなど多面的に洗い出しましょう。最終的に、この期間に解決すべき特に重要・緊急な課題は何かを明示してください(フォーカスすることを重視したいので最大3個程度に絞りましょう)。 3. テーマ 選んだ課題に対して、自分が取り組む際のテーマを明確化します。なぜこの課題解決を自分が担うことに意味があるのか(貢献・成長の視点)、その課題が解決された状態はどのようなものなのかについて言語化してください。 4. ステップとチャレンジ 課題を解くために、どのようなステップが必要だと考えていますか?ステップを踏んでいく中で、あなたにとって予想されるハードルやチャレンジはどんなものありますか? 5. 目標(Objectives) フォーカスすることを重視するので、2〜3個に絞って設定してください。定性的でもOKで、ゴールや成果を示すだけでなく、ありたい姿や取り組み方に焦点を当ててもよいです。また、自身の現在の等級や役割における期待を踏まえた上で、「その目標は水準として妥当か」を見立てる意識を持つことが重要です。 この順序で整理すると、「最終的な組織全体が目指す成果を生むために今期自分自身はどの課題にチャレンジするのか」が自然と言語化できると考えています。 テンプレート活用の工夫 テンプレートを最大限活かしつつ、目標を形骸化させないために、いくつか工夫を加えると良いかもしれません。 例えば以下のような工夫を交えながら目標設定という作業に向き合うと、単なる儀式的な作業にならず、現実の課題と向き合うツールとして長く活かせるものになるはずです。 目標設定作業を1人で抱え込まない 目標は自分だけで完璧に作るものではありません。マネージャーやチームメンバーともオープンな会話ですり合わせながら、課題の優先順位やゴールの解像度を段階的に一緒に確認すると、より現実に即した内容になります。 定量化に縛られすぎない 目標を全部数字で表そうとすると、逆に本質からズレてしまうこともあります。「どういう状態を目指すのか」「どんな姿勢で取り組むのか」といった定性的な要素も、成長を示す大事なポイントです。 達成できなくてもOK 目標は「やることの宣言」ではなく「こういう仮説をもってチャレンジする」という表明です。仮に目標が達成できなくても、その過程で何を学び、未達成の原因を内省・追求し、次にどうつなげたかを言語化することができれば十分に意味のある作業です。 まとめ プロダクト開発を担うソフトウェアエンジニアが事業成果を起点に考える目標設定をどのようなステップで行うか、その背景にある評価の考え方について紹介しました。目標は「評価のためのコミットメント」ではなく、「今どの課題にあえて挑戦するのか」を言語化し、成長と成果を結びつけるツールだと私は考えています。 もし、目標が形骸化しがちであったり、何を書けばいいかわからないなどの悩みがある場合には、ぜひ一度このテンプレートを参考にしてみてください。
アバター
はじめに こんにちは、介護/障害福祉事業者向け経営支援サービス「カイポケ」のリニューアルプロジェクトでモニタリングやオブザーバビリティ周りを担当している加我 ( @TAKA_0411 ) です。 私が携わっているカイポケのリニューアルプロジェクトではオブザーバビリティのツールとしてDatadogを活用しています。そしてこの度Findy様からのお声がけで、DatadogのレビューをFindy Toolsに寄稿しました。 findy-tools.io Findy Toolsについて 「Findy Tools」は、開発ツールに特化したレビューサイトです。第三者の視点で実際にツールの選定を行った企業の生の声を集めることで、ツール選定に関する不安を解消し、導入検討に必要な情報を提供します。 「Findy Tools」を開発ツールの導入検討をしているユーザーが利用すると、実際にツール選定を行った大手企業やメガベンチャー企業の技術責任者やエンジニアによるレビューを集めることができ、導入検討がスムーズになります。また、開発ツールを掲載するベンダーには、実際の利用企業の声を活かしたコミュニティマーケティングによる新規顧客の獲得や、認知向上をご期待いただけます。 findy.co.jp レビューの寄稿に至った経緯 以前、弊社のエンジニアが別のツールに関するレビューをFindy Toolsに寄稿したことがありました。その時のご担当者様が 私のブログ記事 を見て「Datadogの活用ノウハウについてのレビューを寄稿してみませんか」とお声がけいただいたのがきっかけでした。 実はお声がけいただいたタイミングが非常に良く、社内でも「オブザーバビリティのツールの技術選定についての意思決定をADR (Architecture Decision Record) として残しておいた方がよいのでは」という議論がありました。これにより、関連するADRを整備し、それに沿ってレビュー記事を執筆することで無事に寄稿できました。 レビューの注目箇所 オブザーバビリティのツールは多く存在しますが、利用する組織の規模やプロダクトの特性によって最適解が異なります。ツールの導入にあたり「実運用をしっかりイメージすること」「特定の人やチームに属人化させないこと」「より多くの開発者に利用してもらえること」が重要だと私は考えています。特に「より多くの開発者に利用してもらえること」は私たちのリニューアルプロジェクトでも重要な判断軸です。 それを踏まえて以下の内容をご覧いただくと、理解が深まると思います。 私たちがツールを導入する前にどのような課題があったのか ツールを導入することでどのような状態を目指していたか 比較検討したツールと比較の軸 導入の成果 特に最後の導入の成果は私のイチオシです。オブザーバビリティのツールを導入することでどのような問題が解決できるようになったのかという話は、現場のエンジニアだけではなく導入を判断する立場のマネージャーにとっても気になるポイントだと思います。私たちのサービスは開発途中のため、現時点での成果が気になる方も多いでしょう。ぜひみなさまの目で確かめてみてください。 最後に Findy ToolsへDatadogに関するレビューを寄稿した話でした。今後オブザーバビリティのツールを導入しようと考えている読者のみなさんはぜひ参考にしてみてください。 今回のレビュー寄稿にあたり、当時からドキュメントを残しつつ丁寧にDatadog導入をリードしてくれたSREチームの @okazu_dm さんと小笠原さんには感謝が尽きません。特に小笠原さんは今回のレビューの寄稿を行うにあたり、過去のやり取りの取りまとめやADRの整備を率先して進めていただき非常に助かりました。 もしレビューの内容について詳しく聞きたいという方がいましたら下記のイベントやTwitter (X) 等で気軽にお声がけください。 datadog-jp.connpass.com
アバター
はじめに みなさん、こんにちは!! 株式会社エス・エム・エスの川名と申します。 私は2024年7月にエス・エム・エスに入社し、人材紹介事業の業務基盤を開発する「BPR推進部 キャリア横断開発グループ」でテックリードを務めています。 この記事では、BPR推進部におけるテックリードのミッション、そして日々の課題にどのように向き合いながら事業貢献に紐づけているのかを、具体的な事例を交えながらご紹介したいと思います。 この記事を通してエス・エム・エスで働くことのやりがいや現在抱えている課題なども共有することで、皆さんが当社で活躍いただくイメージを持つ一助となれば幸いです。 我々のエンジニア組織については、弊社及川の記事『 Team Topologies で社内ユーザ向け業務システムを開発する組織を再設計してみた 』をご覧いただくことで、よりイメージが具体的になるかと思いますので是非ご覧ください。 自己紹介 簡単に私の経歴についてお伝えさせてください。 私はエンジニアになる前、工業製品の生産部門で1工程を担当していたのですが、製造にはある程度の型があり、その基準通りに生産する事だけが成果となっている事に個人的に違和感がありました。 生産数や品質をキープしながら、継続的にアウトプットし続ける仕事の大変さや重要性を理解する一方で、どのようにすればより良いアウトプットになるのか?そのための仕組み作りや設計に楽しさを見出し、それが実現できそうなソフトウェアエンジニアの道に進みました。 ソフトウェアエンジニアとしては、SIerのWebエンジニア、事業会社(小売)の情報システム担当、SaaS企業のコーポレートエンジニアとキャリアを積んできました。 コーポレートエンジニアになってからは、経営や事業部からの要求を直接ヒアリングし、要求を技術でどう解決するかに注力してきました。具体的には、Salesforceを中心としたCRM/SFAのカスタマイズや外部システム連携による業務プロセスの自動化、GCPやAWSを活用した連携基盤の構築、各種SaaSの導入・運用サポートによる生産性向上などが主なミッションでした。 単にシステムを導入/開発するだけでなく、それがビジネス価値にどう繋がるかを常に意識し、最適な技術選定やアーキテクチャ設計、コミュニケーション、DevOpsなどが得意な領域です。 私が感じるコーポレートエンジニアの魅力 職歴の中でも特にエンジニアとしての楽しさがより増したのが、コーポレートエンジニアとして活動し始めた頃です。SIer時代は開発するものが概ね決まっている事が多く、本質的には「どのような型や基準を作るべきなのかを考える事」がほとんどハンドリング出来なかったように思います。 コーポレートエンジニアというロールの魅力はエンジニアリング面からビジネスのOps改善を事業部と一緒に進められたり、より良いアーキテクチャを自分たちで考えたりしながらビジネスに貢献していける点です。 コーポレートエンジニアはステークホルダーと近い距離でコミュニケーションをとりながら、技術的な解とビジネス要求のバランスを取る難しさと面白さがあると感じています。 エス・エム・エスに入社した理由 上記のようにやりがいのある日々を過ごしていましたが、そんな中でも転職しようと思ったきっかけは、自分自身がまだまだ成長出来ると感じた事でした。 よりチャレンジングな環境で自分を成長させながら、それを事業成長に繋げられる事が実感できればより有意義なキャリアになると考えたのです。 転職活動を開始し、幸いなことにいくつかお声がけをいただく中で、最終的にエス・エム・エスに入社を決めた理由は以下です。 社会貢献度の高い事業且つ業界におけるシェアが大きい 規模感が大きい+エンジニア組織へのコミットメントから、「自身が最も成長できそうな環境」だと感じた オファー面談のメッセージに感銘を受けた 事業規模についてですが、エス・エム・エスは医療介護領域の人材紹介事業において大きく社会に貢献しているプレーヤーだと感じました。これほどの規模の事業を支えるエンジニア組織の成長に興味があったというのが最初のきっかけでした。 その後、Team Topologiesを利用した組織構造の改変や組織ミッションの見直し、アジャイルな組織への変革を行なっていることを伺い、この規模でありつつモダンな取り組みを行なっている事がとても魅力的だと感じ、結果的に自分の次の成長を実現するフィールドとして最適だと感じたのです。 所属する組織とミッションについて 当社エス・エム・エスには、全社の業務改善を推進する「BPR推進部」があります。その部内に、医療・介護業界の人材不足を解決する社内業務システムを開発する「キャリア横断開発グループ」が設置されています。私はその一員として、従事者様と事業者様をつなぐことを価値とした機能開発を担当する、「マッチングチーム」のテックリードを務めています。 キャリア横断開発におけるテックリードの役割 事業部やBA(ビジネスアーキテクト)といったロールのメンバーと日々コミュニケーションを取りながら、バックログアイテムの整理、最適なアーキテクチャ設計、リアーキテクティング、レビュー、リリース調整、運用、場面によって自ら実装を行うなど、開発ライフサイクルにおける全体を見ながら推進する役割になります。BAやチームリーダーがWhat/When/Who/Whyなどに重きをおき、テックリードとしてはHowを主軸にWhat/Whyも深掘りしていきます。 参考までに弊社のビジネスアーキテクトについては西田の記事を、チームリーダーに関しては井上の記事をご紹介させていただきます。 ビジネスアーキテクト × Salesforce:改善だけで終わらない、戦略推進と戦術実行を追求する BPR推進部の開発リーダーとしての取り組み テックリードの具体的な活動 ここからは私がチームの中で日々どのような観点で仕事をしているのか、さらに課題感や今後の展望などについてシェアさせていただければと思います。 私たちのチームではスクラムを用いてプロダクト開発を行っているので、今回はスクラムのイベント(プランニング、デイリースクラム、スプリントレビュー、レトロスペクティブ)を軸に活動をシェアできればと思います。 スクラムイベントへの関わり方 スプリントプランニング プランニングでは新たなスプリントでのゴール設定、取り組むべきバックログアイテムをチームで決定していきます。優先順位についてはチームリーダーやBAと会話をしながら調整し、開発視点で実現可能なラインを決定していきます。タスクの規模感についてはリファインメントという設計の時間を設けていて、そこでアーキテクチャなどをチームで共有しながら事前に算出する作業を行っています。アーキテクチャ検討時には既存のコードベース、技術的負債やスケジュールなど、トレードオフを考慮しながら、最終的にどこまでやるべきかを考えながら決定しています。ここがとても難しいところではありますが、醍醐味でもあると感じています。技術的にあるべき姿を考えながら具体化していく活動は、エンジニアにとってとてもやりがいのあるミッションだと思っています。 デイリースクラム 毎朝行っているデイリーでは主に開発メンバーから出たブロッカーについて確認するようにしています。テックリードという立場としては技術的観点で方向性について改めて議論が必要になった場合にスプリントゴールの達成に向けて各種調整を行なっていきます。私はデイリーだけでなくタスクを進める上で困ったことがないか?などをMTGの最後などに軽く聞くなどして発信しやすい環境づくりを心がけています。そうすることで1人だけでタスクをこなしているという感覚から、チームミッションとして捉え、全員でゴールに向かって動けるとメンバーに感じて欲しいからです。 スプリントレビュー スプリントレビューではチーム内で「受け入れ要件」と「完了の定義 / Definition of Done (DoD)」の確認を行い(プロダクトレビュー)、ステークホルダーのレビューはスクラムイベント外で実施しています。この時、当初の見積もりと異なっていた点や改善出来そうな点や疑問点などをチームでディスカッションしながら次のプランニングに向けて改善内容を確認していきます。またリリース後については事業部メンバーに利用状況などをヒアリングする場面もあります。 レトロスペクティブ スプリント終了時の振り返りはKPTを利用してディスカッションしています。また今期からFour Keysを軸とした開発生産性指標のメトリクスをFindy Team+で確認する取り組みを開始しました。コミットからオープンまでのリードタイムやオープンからレビューまでのリードタイムなど各種KPIをスプリント間で比較しながら、要因の深掘りをすることでより良い開発体験を実現するための情報収集やファシリテーションを行なっています。 上流での関わり方 テックリードという立場は基本的に「How」に責務を持っています。「こういうものが欲しい」という要求があって、どう実装すべきかという「How」を考えて具体に落とし込む作業です。 しかしながらエス・エム・エスでは何を作るべきなのか?という事業部やBAとのディスカッションにも関わることができる楽しさがあると思います。 時間的な制約もあるので全ての会話に入ることは難しいのが現実ですが、大規模なテーマの改善においては開発者として参加することができ、「エンジニアだから事業の事は分からないだろう」といった空気感もなく、分からない事も丁寧に教えていただく事が出来る点がとても助かっています。これは各自が弊社の 理念 を大切に思い、「情熱」「誠実」をもって「プロフェッショナル」であることを追求する という考え方を基本としていることから生まれている文化だと思います。 開発生産性向上に向けた取り組みとその意義 今期、私たちのチームおよび組織全体では「開発生産性の向上」を重要テーマとして掲げ、さまざまな取り組みを進めています。 私たちが担当するモノレポは、10年以上にわたり機能追加と改修を重ねてきた結果、現在では認知負荷の高さや技術的負債の蓄積が課題となっています。こうした背景の中で、いかにして継続的に価値を届け続けるか、どのように開発生産性を高めて変化に強い開発体制を築くかが、私たちにとって喫緊のチャレンジです。 このような取り組みを通じて、 「高齢社会に適した情報インフラを構築することで人々の生活の質を向上し、社会に貢献し続ける」 というエス・エム・エスのミッションを、より高い次元で実現していけると確信しています。 なお、開発生産性向上の取り組みについては、エス・エム・エス BPR推進部にて導入している、 経営と開発現場をつなぐ戦略支援SaaS「Findy Team+」 をぜひご参照ください。 We are hiring!!! 私が考えるエス・エム・エスで働く魅力は、社会が抱える大きな課題の一つである高齢化社会の領域で貢献していけることにあると思っています。 同時にとても難易度が高い領域であるとも感じていまして、まだまだエンジニアの手が足りていない状況です。 今回の記事を読んで共感いただけた方、共に成長しながら社会に貢献していきたいと思ったエンジニアの方、ぜひ一度カジュアル面談などでお話しましょう! open.talentio.com
アバター
みなさんこんにちは! プロダクト推進本部の人事をしている まゆゆ です。 エス・エム・エスはファインディ社主催の「 開発生産性Conference 2025 」にブース出展および登壇することになりました! 開催日程は7/3 - 7/4の二日間で開催方式はハイブリット形式、オフライン会場は東京丸の内のJPタワーホール&カンファレンスとなっています。 エス・エム・エスはシルバースポンサーとしてブース出展をするとともに、7/4に弊社開発部のメンバーが登壇します。 今日はその内容について少しお伝えさせてください。 登壇について 介護/障害福祉事業者向け経営支援サービス「カイポケ」のリニューアルプロジェクトを行なっている中で、開発者の生産性を向上させる取り組みとして技術戦略チームを発足し運営しております。 今回はその取り組みの成功事例・・・ではなく敢えて「失敗談」をお伝えさせてもらえたらと思っています! タイトル:「失敗から再構築した開発推進チームの立ち上げ」 登壇日時:7/4 (金) 15:25〜 オフライン登壇会場:ホールB 登壇者紹介:2名 タイムテーブルはこちら 👉 https://dev-productivity-con.findy-code.io/2025/detail/aDlc4Phu ※ 大変ありがたいことにオフライン会場は満席となり、現在はオンライン参加のみのお申し込みを受け付けているとのことです!お申し込みをお待ちしています! 登壇者1 : 鄧 皓亢(でん はおかん) 株式会社エス・エム・エス プロダクト推進本部 カイポケ開発部 アーキテクト/PdM データサイエンティスト、データエンジニア、フルスタックエンジニアを経てCTOに就任し、技術戦略と開発文化を推進し、海外・新規事業の立ち上げやレガシーシステムのリファクタリングなどもやってきました。 エス・エム・エスではカイポケリニューアルプロジェクトのアーキテクト兼開発推進チームのPdMに就任して分析を前提としてシステム設計と開発組織の効率化を推進しています。 飼っている猫がとっても可愛いです!ジーパンを育てること、木工家具のメンテが趣味です! 登壇者2 : 空中 清高(そらなか きよたか) 株式会社エス・エム・エス プロダクト推進本部 カイポケ開発部 エンジニアリングマネージャー Webフロントエンドエンジニア→Webサーバーサイドエンジニア→Androidアプリケーションエンジニアという経緯でソフトウェアエンジニアをやってきて今に至ります。 DroidKaigi と iOSDC Japan、PHPerKaigi のコアスタッフをしています! PCゲームやボードゲームが好きで、日本酒やウイスキーを嗜みます。 ブース出展について エス・エム・エスのブースでは、当社の事業内容やエンジニアリングに関する取り組みをご紹介します。 ブースではみなさんにデモ画面を触っていただき、日頃の取り組みについて少しでもお伝えできたらと思っています! またささやかではありますが、ちょっとしたコンテンツや、ノベルティをご用意してお待ちしております。 季節柄、アイスクリームスプーンをご用意する予定です。 尚、数に限りがございますためなくなり次第終了となります。 またエス・エム・エス登壇の後はブースでAsk The Speakerの時間があります。 おしゃべり大好きなメンバーばかりなので、どんな些細なことでも気になることやご質問などがあったら是非お立ち寄りください:) ブースの場所は受付入ってすぐです! (オレンジの丸あたり) フロアマップ 👉 https://dev-productivity-con.findy-code.io/2025#floormap それでは当日ブースでたくさんの方とお話しできるのを楽しみにしています!
アバター
はじめに @emfurupon777 です。エス・エム・エスに入社したのが2022年1月なので、3年ちょっとたちました。ようやくエス・エム・エスで働いているのだという実感も得始めているところで、これまでに経験してきた環境とは実感の仕方が異なることに面白みを感じ始めています。 この感覚をもって、Slackに戦略コンサル出身の経営陣が作り上げた前々職での実感を『変態的に優秀な人たちに引っ張り上げられつつ搾り取られ続けているような感覚』(※これは最大限の賛辞のつもりです)と表現したところ、プロダクト組織の責任者である @sunaot からも『奇遇ですね。私も以前の職場(私の前々職とは同じ戦略コンサル出身の別人による経営)のときはそういう感覚でしたw』と返ってきたので、妙な納得感がありました。一方で、「なるほど @sunaotも異なる実感がありそうなので、やはりエス・エム・エスで求められることはこれまでの経験と異なるのだなー」と再認識し、マネージャーとして考えていることをポエムとして書いていきます。(あくまで一社員としての私見ですので、その点はご容赦ください) 視座をあげるということ VUCAなどという小難しい表現を用いるまでもなく、変化の激しい現代において。目の前にある今イシューだと見えているものに正面からぶつかって、砕けて、を繰り返していくだけでは、なかなか前に進むことができなくなる状況はまま発生し、『視座を上げて見つめ直してみて』(意訳ですが)と、1on1などの場で伝えたり、伝えられたりした経験を持つ方は多いのではないでしょうか。 マネージャーとしてはチームに求めるべきことだと理解はできるものの、視座という言葉の必要性をうまく言語化し、説明することに難しさを感じていました。受け手側も、『それはマネージャーであるあなたに必要なものであって、自分には関係ないのでは?』と咀嚼しきれないのではないでしょうか。(私自身、かつてそう感じた一人です) チームへの役割期待 少し話は変わりますが、ここで前提を2点おさえておきます。 1点目は、社内の”チーム”定義です。 エス・エム・エスにおけるチームは短期と中長期、主戦場と周辺、時間軸に対する成長へどう取り組むかを考えていくのが役割になっていて、担うことが期待されているのは主にこのような事です。 固有の文脈の解釈・評価 今期、半期の目標とKPI、そこへのプラン おや、、一般的に言われるマネージャーの役割と認識されている事の相応の内容が、エス・エム・エスでは、一般的にマネージャーの役割とされる内容の多くが、チームへの期待として明文化されていますね・・・ アウフヘーベン(止揚) つづいて2点目は、上位グレードに対する期待値です。 弊社プロダクト責任者の @sunaotが書いた社内ドキュメントの中にこんな記載があります。 答えがなく選択肢複数の中でアウフヘーベンするのが上位グレードのイメージ あれ?おやつの話ですか?あ、違った。 Google日本語辞書より引用 ある命題(テーゼ)と対立関係にある命題(アンチテーゼ)を統合し、より高い次元の命題(ジンテーゼ)を導き出す止揚(アウフヘーベン)の考え方を土台とした思考法があり、これを弁証法というそうです。 重要なのは、対立する2つの命題(テーゼとアンチテーゼ)に優劣はなく、片方がもう片方を打ち消すのではなく、統合され進化していくという点です。なるほど。 一見、矛盾や対立する要素を統合し、より高次の価値を創出するための重要な手法で、この考え方は、利益追求と社会貢献、短期的な目標と長期的なビジョンなど、経営における多くの課題に適用されるとのこと。 (一般的に)マネージャーとはなんなのか マネージメントとは「なんとかすること」というのはよく言われることで、その任にあたっている人は感覚的にもこの表現はしっくりくる気がしています。 ところが、「うちの会社のEM / PdM / PjMって何をする人なんですか?」という、特定のアクションによってマネージャーの役割が定義されているかのような質問を投げかけられることが多いように感じています。 なぜそう感じるのか。試しにAIにマネージャーの果たす役割を尋ねてみると・・・ 業績管理と目標設定 人材管理と育成 コミュニケーションと連携 モチベーション管理 etc. なるほど、確かにこれをやる人がマネージャーという感じの答えが返ってきて、確かに表層でわかりやすく観測できるのはこういうことかもしれないという気はします。 エス・エム・エスにおけるマネージャーとは エス・エム・エスのプロダクト組織でもEMという表現をすることや、プロダクトマネージメントグループが存在するため、それを担う人という意味で共通認識を持つためにPdMといった表現をすることはそれなりにあるのですが、各人が担う期待は均一ではなく、事業遂行に必要な能力と、それぞれの得意・不得意などによってグラデーションがついています。(あたりまえのことを言っていますね) 普段の業務では、『EMだからこのアクションをする』『プロダクトマネージャーだからこのアクションをする』といった職種に縛られた考え方はしていません。マーケットに向き合う上で必要なことを自らのスタンスで取捨選択し、優先順位をつけながら推進していくことが求められています。(もちろん、全てのタスクを自分一人で実行するのではなく、仲間の力を借りて実現していく必要があります)。 この役割は一般的な企業であれば『経営層』や『ボードメンバー』、場合によっては『役員』といった、会社上の機関として位置づけられる立場の者が担う割合が多いと感じます。しかし、エス・エム・エスは役員が少ない経営体制であることからも推測できるように、それぞれの社員に明確な権限移譲が行われている組織です。 この移譲を受けて事業を推進する主体がマネージャーなのだと私は解釈しています。エス・エム・エスでは組織階層の中でのポジショニングを表す表現は必要としていない実情があります。たとえば、最近多く使われるようになってきた印象の表現に”VPoE”がありますが、社内で議論をしていく中で、「なんか誰もVPoEを名乗る必要性を感じてないね・・」となる瞬間があったりします。 どういう取り組みが必要なのか 私個人の経験では『社長意識』、@sunaotの経験では『2ランクアップ』、そしてエス・エム・エスでは経営層との縦横の連携など、優れた経営を行っている組織では、何らかの形で視座を高める取り組みが推奨されていると感じます。 表現の違いはあれど、行き詰まったり迷ったりした際は、意識的に自身の視座を上げるか、あるいは(無理にでも)視座を上げてくれる人に積極的に関わっていく(例えば1on1を申し込むなど)ことで、アウフヘーベンを体現するための新たな視点が得られるのではないでしょうか。。 もちろん良書を読んでインプットするといったことも重要だとは思いますが、組織として事業を推進していくにあたって、対話を重ねていくのは必要不可欠です。この対話を疎かにせずに行動し続けることができるのであれば、それぞれ得意領域は違えどマネージメント適正はあり、経営的思考を歩んでいくことにも繋がっていくと考えています。 組織が成長する中で、時には矛盾や対立構造が生じることがあります。そんな時こそアウフヘーベンを意識し、それぞれのチームが発揮している価値を尊重した上で統合していく、そうした戦略的な役割を担う意識を持つことが最重要であると考えます。。 おまけ 最後に @sunaotの記載を紹介しておきます。 ここでいうマネジメントというのが、EMなのかアーキテクトなのかPdMなのかデザインマネージャーなのか、実情でいくとこれはどれでもいいなあというのが今の状況で、職種的適切さよりも個人能力依存の限界のほうが早く来ている印象がある 問題に取り組み、前へ進めることができているのならば、それは良いマネジメントでありバックグラウンドの選別をしている余裕はない いや、マジで余裕はないですw すべての事業領域で仲間を求めていて、求人票の表現としては例えばEMといった一般的な表現はとっていますが、細かい表現にこだわる必要はありません。実体として今回記載させていただいたように、”事業を推進するマネージメント”に興味がある方はぜひカジュアル面談で意見交換したいです!というくらい、私たちは仲間を求めています。
アバター
はじめに はじめまして。 人材紹介開発グループでweb履歴書作成サービス『履歴書できるくん』の開発を担当している松尾と申します。 2020年に新卒入社し、昨年度よりスタートしたweb履歴書プロジェクトでwebの開発を担当させていただいております。今回はそんな新規サービスの立ち上げからリリースまでの経緯と振り返りについてお話しできればと思います。 プロジェクト発足 転職活動において、履歴書の作成は転職活動の内定を左右する大変重要なプロセスです。それ故に多くの労力を必要とします。たくさんの文字を書かなければいけなかったり、少し難しい文章を考えないといけなかったり。さらに、弊社の人材紹介サービスを利用いただく場合は、転職活動をサポートするキャリアパートナーに作成した履歴書の画像を送付し、その画像に対してキャリアパートナーが添削を行うというフローで履歴書作成が行われておりました。 これにより、質の高い履歴書の作成は実現できていた一方で、システムがない中での運用は多くの時間と手間を必要としていたため、よりスムーズな転職活動のご支援に向けた改善が求められていました。 今回のプロダクトでは、上記の課題を解消し、求職者とキャリアパートナーの双方にとってより良い体験を提供することを開発の軸に据えました。 プロダクトでの解決 本プロジェクトでは履歴書作成から添削までを一貫して行えるWebサービスを開発しました。このプロダクトは、求職者がスムーズに履歴書を作成・提出できるだけでなく、キャリアパートナーとのやりとりも簡潔かつ効率的に進められるよう設計されています。 具体的な機能は以下の通りです。 自動入力機能と豊富な例文 住所検索APIを用いた住所の自動入力や、生年月日から学歴の入学・卒業年を自動算出する機能、弊社人材紹介サービスで取り扱う職種に最適化された資格一覧や例文一覧を取り揃え、履歴書作成にかかる様々な手間を削減しています。また、添削にかかるリソースも大幅に削減できる見込みです。 データの自動保存 履歴書は自動的に保存されるため、途中で編集を中断しても安心です。プライベートとの両立の中で、忙しい日常の隙間時間に履歴書の作成を進めることができます。 キャリアパートナーとのスムーズな連携 作成された履歴書は、キャリアパートナーに自動連携されます。通知を受け取ったキャリアパートナーはWeb上で履歴書を閲覧でき、リアルタイムで添削ができます。 PDFダウンロード 完成した履歴書はPDFとしてダウンロード可能です。電子書類としての提出のほか、コンビニでプリントアウトすることもできるため急な提出にも即座に対応できます。 これにより、求職者の作業負担を大幅に軽減しつつ、サービス全体としてのサポート品質を向上させることができました。 大変だったこと プロジェクトを進めるにあたって、いくつかの困難にも直面しました。特に印象的だったのは、不確実性の高い課題への向き合い方とプロジェクト状況に応じた意思決定です。 不確実な課題にどう取り組むか 今回の開発チームは私含め比較的キャリアの浅いメンバーで構成されています。そのため、ゼロからプロダクトを立ち上げるという経験が不足しており、「何から手をつけるべきか」「どこまで決めれば開発に進めるのか」といった基本的な判断に迷う場面が多くありました。 そのような中で意識していたことは、「不確実性の高い領域から優先して取り組む」でした。最も見通しが立っていない部分から調査・検証を行うことで、その後の判断や設計の前提となる材料を早い段階で得るように心掛けていました。 また、判断の精度を高めるため、シニアエンジニアとの壁打ちやステークホルダーとのコミュニケーションを積極的に行い、プロジェクトのフェーズに応じて「今私たちがやるべきことは何か」をチーム内で意識的にすり合わせるようにしていました。 このような試行錯誤を繰り返す中で、チーム全体が「完璧な見通しがなくても、小さく進めて調整すればよい」と考えられるようになり、不確実性を前提に前進できる自走力が徐々に育っていったと感じています。 スコープ調整と意思決定 こうした不確実性の高い課題に対して優先的に向き合いながらも、プロジェクト全体を前に進めるには、状況に応じた柔軟な意思決定が求められました。 実際にプロジェクトを進める中で、当初の計画にはなかったタスクや新たな課題が判明し、当初想定していたスケジュールでのリリースは現実的ではないという判断に至りました。そこで私たちは、現時点で実現できる最も価値のあるプロダクトとは何かを考え、リリースブロッカーを洗い出すことで、機能の優先順位を見直しながらスコープの調整を行いました。 結果的に、限られた時間とリソースの中で価値のあるリリースを実現できたということが、チームの大きな自信につながりました。 最後に リリースから間もない段階ではありますが、早速次のような成功事例が寄せられています。 履歴書の作成が心理的なハードルになっており、なかなか転職活動に踏み出せなかった求職者の方が、このツールを使って履歴書を提出してくださった 他社の人材紹介サービスと併用していた求職者が、履歴書作成までサポートする一貫したサービス提供を理由に、最終的に弊社を通じて転職を決めてくださった またキャリアパートナーからは、当初予定していた完璧なリリースができなかったことに対して 「自分たちが活用して意見を出していける、自分たちによってよりよいサービスになる」 機能追加をきっかけに、中長期的なご支援や定期的なフォローアップが可能となり、より一人ひとりに寄り添ったサポートができるようになった といった前向きなフィードバックをいただいています。 当初の計画からスコープを絞り込んでのリリースでしたが、限られたリソースと不確実性の中で「何を作り、何を作らないか」を判断し、プロダクトの価値を最大化するという経験ができたことは、今回のプロジェクトを通じて得られた最大の学びでした。 今回得た学びを活かしながら、求職者、キャリアパートナー双方の課題に向き合い、価値あるプロダクトを届けられるように引き続き取り組んでいきたいと思います。
アバター
エス・エム・エスのテックブログ編集チームの髙木です。 弊社のテックブログは2021年3月3日に1本目の記事が公開されており、今年で5年目になります。ちなみに1本目の記事はこちらです。 僕がブログ編集チームに参加したのは2023年4月からなので、いつの間にか2年が経ちました。記事自体はある程度コンスタントに出ているブログではありますが、うまくいった取り組みもあればうまくいかなかった取り組みもあります。このあたりをシェアできればと思います。 ブログ編集チームについて 弊社のブログは編集チームによって運営されています。各々が開発チームに所属しながら有志として活動しているようなチームです。したがってプロジェクトが忙しいタイミングでは参加できず、他のブログ編集メンバーにお願いするような状態で運営しております。現在は数人で運営しています。 役割としては、以下の4つです。 記事のテーマの提案 原稿の添削 はてなブログへの入稿やサムネイルの作成 社内への宣伝 うまくいっていないこと まずうまくいっていないことから共有します。うまくいっていないことがある前提でうまくいっていることを共有できたほうが現在の状態が把握しやすいと考えたためです。 自発的に書きたい人が出てくる仕組みづくり それなりにアクティブに投稿できている弊社のブログですが、これらのほとんどはブログ編集チームや採用担当から話を持ちかけることが多いです。比率的には 編集から持ちかけ:執筆者の持ちこみ=5:1 ぐらいの感覚です。 編集担当者からの執筆依頼がベースになっているため、スケールしにくい問題があったり記事がなくて投稿できない週が発生したりしています。わりとゆるい運営でやっているので大きな課題にはなっていませんが、投稿を安定させるためには自律的に投稿したいと思える仕組みづくりが必要かなと思っています。 また編集担当者からの持ちかけだと、相手の都合やモチベーションがないなど断られることもそれなりにあります。依頼をして断られるというのは当たり前です。当たり前なのですがやっぱり最初は精神がすり減ります。僕自身は2年ほど担当する中で慣れたのですが、慣れるまではしんどい気がします。そのため自律的に投稿者を募れる仕組みがあるとブログを運営するという意味で安定すると思います。難しいことだとわかりつつ今後も模索していきたいです。 技術的な記事が出にくい 弊社のブログは技術そのものにフォーカスした記事というよりもプロジェクトやプロダクトにフォーカスした記事が出やすい傾向にあります。このブログを愛読してくださっている方はご存知かもしれませんが、技術にフォーカスした記事は1年で数本しかでない傾向にありました。 ただ、昨年弊社のZenn Publicationが始動したことで状況が少し変わりました。 結果として技術そのものにフォーカスした記事はZenn Publicationで以前に比べると活発に投稿されるようになりました。 このことから考えるに、弊社の特性として技術的な記事が投稿されなかったのではなく、技術的な記事を投稿しにくい雰囲気があったり投稿までのステップが重かったのだと思います。Zenn Publicationは入稿までのステップが個人で完結するようになっていることで、気軽に投稿するというアクションが取りやすくなったのだと思います。 厳密に決まっているわけではありませんが、プロダクトや会社の情報を扱うものはテックブログ、技術単体にフォーカスした記事はZennという棲み分けができたことで、ジャンルによらずアウトプットがしやすい環境になったと思います。テックブログとしては技術的な記事が出にくいという課題は抱えたままですが、社としては技術記事がアウトプットされるようになったので良かったと思います。 うまくいっていること 週次ミーティングの運用 ブログ編集チームでは、週次ミーティングを行い以下を実施しています。 記事の進捗確認 記事の添削 記事の候補探し 進捗確認や添削などがうまくいっているというところもありますが、ミーティングをスキップせずに毎週取り組めていること自体がうまくいっていることだと思います。メンバー都合で参加できない週はありますが、誰かは必ず参加できるようなメンバーで構成できていることに意味があると思っています。毎週必ず実施し、スケジュールが把握できていることで結果としてブログ記事が定期的に出せているように思います。 異なるチームのメンバーで構成されているため情報が多い チームはそれぞれ状況が異なるため、持っている情報や見えている環境も異なります。そのため複数の所属でチームが構成されていることで、社内の情報が見えやすくなるように思います。現在は執筆依頼を前提にしたオペレーションとなっていることもあり、いろいろな人の状況が見えるようになっていることでアイディアを見つけやすくなっているように思います。 採用担当が絡むことで幅の広い情報が出る 採用担当と定期的にコミュニケーションを取るようにしております。採用担当は横断でいろいろなプロジェクトの現状を把握しているため、プロジェクトの状態に応じたアイディアをもらえることが多いです。 テックブログに採用の色が強く出すぎてしまうと、読み手にとって得られるものがなく意味のない記事になってしまうと思っています。それはこのブログを運営する我々にとっても広いインターネットの海で偶然にもこの記事にたどり着いてくださった方にとっても不幸なことです。そのため採用担当に関わってもらってはいますが、記事の内容については基本的に編集チームと執筆者で決めて、採用目的になりすぎないよう気をつけています。やっていることの工夫や反省が読む人にとって価値になると思うので、俯瞰的な記事アイディアをもらえる採用担当には継続的に関わってほしいと感じています。 編集担当者がばらついている 記事を執筆するに当たり、ブログ編集チームから1人が担当し執筆者をサポートしています。人数に余裕があるということもありますが、編集担当者が特定の個人に依存していないというのは良いことだと思っています。編集担当者がつくことによって記事の質は上がりますが、その仕組み自体をつくるにはどうしても人が必要ですし、特定の個人に依存していると同時に制作できる記事の数に限界があります。ブログ編集チームをアクティブに動ける人たちで形成できたので、記事を定期的に出せているのだと思います。 一方で編集担当者がつかないと記事を出せないということでもあるため、自分たちがボトルネックにならないように活動はしていければと思っています。 社内ドキュメントを利用したLLMによるネタ探し(試験運用中) ネタに困ったときには弊社のドキュメントをLLMにインプットとして与え、テックブログの記事として使えそうなアイディアを探させています。弊社は esa に技術的な取り組みや課題や考え方などさまざまな文章が保存されているため、その内容からアウトプットを得ようとしています。 記事にできなさそうなものがほとんどですが、たまによさそうなものが見つかったりしています。社内の取り組みを知ることもできており、役に立つかどうかはおいておいて今後も続けていきたい施策です。 この取り組みは、 @_kimuson の貢献が大きくいつも助けてもらっています。_kimusonが執筆した記事もいくつかでているのでよろしければ読んでみてください。 やっていないこと 記事の評価 現在は記事のPVや伸びたかという評価は行っていません。行っていないというか難しいからできていないというのが正しい表現だと思います。 インターネット上に投稿された記事の貢献とは、プレビュー数のようにわかりやすく数値で見えるものもあれば読者への貢献度など数値で見えないものもあると思います。1つの記事によって価値を最大化しようとすると検討のフェーズが挟まってしまい記事を出すことのハードルが高くなりすぎてしまいます。もし毎日100本の記事が生まれるのであれば実施したほうが良いと思いますが、今のフェーズでは月に数本しか記事は出ないので考えなくても良いと思います。 最後に 今回はテックブログ自体の運営やうまくいっていることなどを整理してみました。おそらく会社によってテックブログの運営や役割など多岐にわたると思います。今後も皆さんに役立つ記事を公開できればと思います。
アバター
井上大輔(いのうえ だいすけ)と申します。BPR推進部 キャリア横断開発グループメンバーとして、2023年にエス・エム・エスに入社いたしました。 もともとは保育士を目指し、保育士資格・幼稚園教諭二種を取得。その後、家庭の事情とIT分野への興味からIT業界へ転身し、営業職を経て、現在はエンジニア職でチームリーダーを務めています。 異業種からIT分野へ 私が育った地域では共働きの家庭が多く、年齢に関わらず子どもたちが集まって遊ぶ環境でした。その中で子どもの成長を支援したい考えを持つようになり、学童保育ボランティアの経験を経て短大に進学し、保育士・幼稚園教諭資格を取得しました。 しかし、家庭の事情により保育士を続けることが難しく転職を検討していたところ、親戚の薦めからIT分野へ興味を持つようになり、プログラマ系の専門学校へ入り直してからIT業界へ転身をしました。 専門学校卒業後に入社した企業で営業職の能力適正を見出され、営業に配属されました。 営業職では、お客様の課題を深くヒアリングし、その解決策を提案することにやりがいを感じ、顧客視点の重要性を学びました。この営業経験は、表面的な問題解決に留まらず、事業部と連携して課題の根本原因を掘り下げ、共通認識を持った上でシステム開発を進める現在の業務にも活きています。 転職活動とエス・エム・エス入社 一方で開発をしたい思いを諦めきれず、エンジニアを目指し転職。開発実績がない中、OA事務として入社し、業務改善ツールの開発を通じて社内経験を積み、念願のエンジニアへと転属しました。エンジニア転属後は、Salesforce顧客管理システムの開発を一通り経験。更なる成長を求めベンチャーへ転職し、Salesforceに加えAWSインフラなど幅広い技術を習得しました。 前職ではSalesforceの契約終了に伴い転職活動を開始。Salesforceの経験を活かしつつ、開発体制が整った環境で自身の成長を追求したいと考えていたところ、転職エージェントを通じてエス・エム・エスをご紹介いただきました。熊谷さんとの面接が、エス・エム・エスへの入社の決め手となりました(私が面接をしてもらった熊谷さんも入社エントリーを書いていますので是非ご参照ください!: 二つ返事で書いた「私のキャリア」と「転職決断話」 ) 熊谷さんからエス・エム・エスの人材紹介事業における業務基盤システムにSalesforceが採用されており、事業成長に伴う要求の複雑化・高度化に対応するため、共に課題解決に取り組む仲間を求めているというお話を伺いました。自身の営業と開発双方の経験が活かせると感じたこと、そしてアジャイル開発導入に向けた組織体制づくりが進められていることを知り、自身の成長に繋がる環境だと確信しました。 また、熊谷さんのエス・エム・エスでの取組みからエンジニア自身が主体となって課題解決に取り組んでいる話からもエス・エム・エスに魅力を感じ入社を決めました。 キャリア事業領域における私のチームの役割 キャリア事業領域は医療・介護/障害福祉業界の人手不足という課題解決を使命とし、人材紹介サービスを通じて課題解決に取り組んでいます。私が所属する開発チームは、医療・介護事業者の働き手が欲しいニーズに応えるべく、人材紹介サービスの求人情報を充実させるための機能を提供しています。 チームリーダーとしての役割 一般的な開発チームリーダーのイメージとして、責任範囲の広さから業務が集中してしまい、ボトルネックになりやすかったり、会議体が多くなるイメージがあるかと思います。 そこで、これらの課題についてはスクラムを導入し責任範囲をチームに分散することで解決しました。 予算管理や契約面など中央集権型組織としての必要最低限のマネジメント業務はリーダーに委ねられますが、ステークホルダーとの会議にメンバーも参加することで、課題への理解を深め実装時のリスクを事前に回避できたり、要件定義の時点からチームを巻き込んで調整窓口としての機能もメンバー自身が担うことで、リーダーがボトルネックになることなく、複数の開発案件を同時に実行できるようにしています。 (スクラムのイベントに関する具体的な取り組みはこちらをご覧ください: https://tech.bm-sms.co.jp/entry/2024/12/17/100000_1 ) 取り組んでいるチーム課題 私たちのチームは、スクラムで開発案件を進める中でこれまで見えてこなかったいくつかの課題に直面しました。 その中でも特に大きかったのが、チーム発足前に各メンバーが専任に近い状態で担当していた機能があり、ナレッジやスキルに偏りが生じていたことです。 この課題を解決するため、私たちは積極的にチーム内で話し合いの場を持ちました。 課題解決への第一歩:チーム改善MTGの活性化 まず取り組んだのは、チーム改善MTGをより活発にすることでした。「もっとこうすればチームがうまく回るのではないか」というアイデアが出ても、個人の発散で終わってしまうことがあります。それを防ぐため、メンバーが意見を出しやすい雰囲気を作るためのMTGを新たに開催することを提案しました。 このMTGでは、各メンバーが付箋を使って自由にアイデアを出し合い、それらを共有しながら議論を進めました。その結果、「ナレッジを蓄積する場所」や「ナレッジに記入するべき内容」について、チーム全員で合意形成を図ることができました。 ナレッジ共有の基盤:情報共有サービスesaの活用 チームで蓄積したナレッジは、情報共有サービスのesaを活用して一元管理しています。esaは、機能仕様だけでなく、利用者、そして利用者の運用方法についても記録するようにしています。 機能仕様: システムの設計や実装に関する基本的な情報はもちろんのこと。 利用者: どのような目的で機能を利用しているのか、どのような課題を感じているのかといった、ユーザー視点の情報。 利用者の運用方法: 実際にどのような手順で機能が使われているのかといった、具体的な利用状況。 これらの情報を共有することで、開発者側の視点だけでなく、ユーザーの視点や実際の利用状況を踏まえた改善活動に繋げることができます。 スキルの偏り解消への取り組み スキルの偏りを解消するための具体的なアクションとして、以下の2つの取り組みを始めました。 技術分野についての勉強会の開催: 各メンバーの得意な分野や興味のある技術について勉強会を開催し、チーム全体の技術力を底上げを目指します。 機能を全員で理解するためのグループワーク: 特定の機能についてチーム全員で集まり、仕様を読み解いたり、実際に触ってみたりしながら、相互理解を深めます。 これらの取り組みを通じて、チームのナレッジ・スキルの偏りの解消を目指しています。 チームリーダーとしてのやりがい エス・エム・エスの【高齢社会に適した情報インフラを構築することで人々の生活の質を向上し、社会に貢献し続ける】というミッションに対して、チームとして何をすることで貢献できるかを考え、事業部とコミュニケーションをとりながら課題を発見し、解決方法を見出していくことにチームリーダーとしてやりがいを感じます。 個人的なやりがい、楽しみ エス・エム・エスで仕事をする上で大きなやりがいとなっているのが、かつて目指していた保育士としての経験から、間接的ではありますが保育業界の労働人口増加や定着支援に貢献できていると感じられることです。自身はもう保育士ではない為、現場に直接かかわることはできませんが、保育業界がより良くなっていく為のシステム提供が出来ればと思い仕事に取り組んでいます。 おわりに 私たちのチームが所属するキャリア事業領域は、今後もサービス領域を拡大していく方針です。そのため、将来の事業拡大を見据えたシステム全体の最適化と、変化に素早く対応できるチーム作りが急務となっています。 チーム体制は発足したばかりで、まだ多くの課題があります。しかし、私自身もメンバーと共に課題を発見し、解決していく過程に大きなやりがいを感じています。 事業の目標や課題を深く理解し、システムを通じて社会に貢献していくことは決して簡単な挑戦ではありません。しかし、私たちと共にこの重要なミッションに情熱を持って取り組んでいただける方をお待ちしています(チームリーダー募集の詳細はこちら: https://open.talentio.com/r/1/c/smsc/pages/104978 )。
アバター
はじめまして。人材紹介開発グループで看護師向け人材紹介「 ナース専科 転職 」のWEB開発を担当している藤井と申します! 2024年4月に当社に入社してからあっという間に1年が経過しました。本稿では主にスクラムに関して入社当初に私が感じた課題と、課題に対して行ってきたアクションをご紹介できればと思います。 ※私を含めた開発を行うエンジニアチームと事業部のマーケター(主には集客に責任を持ちつつ、事業成長をミッションとするマーケティング組織)が度々登場するため、それぞれ「開発チーム」「マーケター」と呼ばせて頂きます。 スクラムの体制について 「ナース専科 転職」では以下のスクラムイベントを1週間スプリントで実施しています。 デイリースクラム スプリントプランニング スプリントレトロスペクティブ リファインメント(適宜) リリースが適宜行われる運用体制のため、スプリントレビューとしてまとまった時間をとって行うのではなく、タスクごとに担当者がマーケターと成果物の確認を実施しています。 入社時に感じた課題 私はチームがスクラムを取り入れて半年程度の頃に参画したため、当時はチームが徐々にスクラムに慣れてきている段階でした。その状況で私は特に「開発チーム内の透明性」と「開発チームとマーケターの距離」に改善の余地があると感じていました。 開発チーム内の透明性 デイリースクラムで対応内容の共有は行っていたものの基本的に個で動いていた 何がチームで課題となっているかの把握が難しかった 日中困った時に相談するのに障壁があった 開発チームとマーケターの距離 週1回の定例でタスク依頼の共有が行われる以外は基本担当者同士でやりとり マーケターはスクラムには未参加 開発チームもマーケターもお互いが何をしているのか把握しづらい 上記のような事象が発生していることでスクラムの三本柱である「透明性・検査・適応」が十分に機能し切れていなかったため、これらを改善することでチームとしてさらに成長することができると考えていました。 実践アクション 上述した課題を解決するために、「チームビルディング」「スクラムイベントの改善」「コミュニケーションの改善」の大きく分けて3つの観点から改善のアクションを実施しました。 チームビルディング スクラムガイドの読み合わせ スキルマップの作成 ドラッカー風エクササイズ ワーキングアグリーメント スクラムイベントの改善 デイリースクラム レトロスペクティブ リファインメント・プランニング(マーケターの参加) コミュニケーションの改善 開発チームとマーケターで1on1ミーティングの実施 オフラインイベントの実施 それぞれについて詳しく紹介していきます。 チームビルディング まず始めに、開発チーム内でメンバー間の距離を縮めると同時にスクラムに対する認識を合わせようと思いチームビルディングを実施しました。 ここでご紹介するチームビルディングは主に『アジャイルサムライ(Jonathan Rasmusson著 20011.7.16)』に記載の内容を参考にして行いました。 活動 内容 成果 スクラムガイドの読み合わせ スクラムガイドを元に現状のチーム体制になぞらえた資料を作成して読み合わせ 現状の課題とどうあるべきかについての擦り合わせを行うことができた スキルマップの作成 開発に求められるスキルを列挙し、そのスキルに対する習熟度への自信を回答 チームの強み・弱みの把握や困った時の拠り所の発見に繋がった ドラッカー風エクササイズ 自分は何が得意なのか・大切に思う価値は何か・何を期待されていると思うか・他のメンバーに何を期待するかを発表し合う メンバー間の認識を合わせると同時にモチベーションの向上にも繋がった ワーキングアグリーメント マインドとアクションの両側面で意見を出し合い、チームの約束事と大事にすることを決める より働きやすいチームにするためにお互い意識すべきことを話し合う良い機会になった スクラムイベントの改善 デイリースクラム デイリースクラムは状況報告の場になっていたため、スプリントの検査を適切に行うための場にすることを意識してアクションを実践しました。 改善前:デイリースクラムまでにまとめておいた昨日やったこと・今日やること・困っていることを共有 メンバーによって情報の粒度が違い状況が把握しにくかった スプリントの全体像がわからず検査がしにくかった 改善後:スプリントに積まれたバックログアイテムをベースにして各自昨日やったこと・今日やること・困っていることの共有 スプリント全体のタスクを見渡せるので臨機応変に動きやすくなった バックログ上に開発時のメモやマーケターとのやりとりなどが記載されているので内容の把握がしやすくなった レトロスペクティブ レトロスペクティブはチームでのふりかえり自体はできていたので、そこからさらに具体的なアクションへの落とし込みができればもっと良いものになると感じていました。 改善前:毎週のスプリントでの出来事をKPTA法でふりかえり その場その場でのふりかえりはできていたが、スクラムが改善していっている実感は薄かった アクションアイテムの決定にあまり注力できていなかった 改善後:毎回テーマを決め様々な手法を使ってふりかえりを行い、毎回アクションアイテムを議論する時間を設けた テーマに応じて異なる手法(5つのなぜ、YWT、小さなカイゼンアイデア、etc)を使って効果的なふりかえりを実施(マンネリ化の防止も) miro を使って自分が気になった・共感した付箋に対してスタンプをつけ、関心が集まった内容について議論 2,3個に絞ったほうが次スプリントのアクションとして実践しやすく毎週続けやすかった 多くのメンバーの関心がある効果的なアクションが決められるため、チームとして改善を進めていると実感できた レトロスペクティブ自体のふりかえりも定期的に行い、より有意義な時間となるよう意見を出し合っている リファインメント・プランニング(マーケターの参加) リファインメント・プランニングはこれまで開発チームだけで行なっていましたが、マーケターとも話し合い、スクラムに本格的に参加してもらうよう体制を変えていきました。 改善前:マーケターはスクラムに未参加 マーケターが開発チームの状況を把握できる場がなかった スプリントに積むタスクの意思決定を開発が行っており認識相違が発生していた 改善後:毎週のリファインメントとプランニングに入ってもらう 透明性が格段に上がったことで認識相違が減った リファインメントの際に確認作業もその場で効率的に済ませられるようになった 環境改善タスクの必要性も理解してもらえて負債の解消タスクもスプリントに積みやすくなった コミュニケーションの改善 開発チームとマーケターで1on1ミーティングの実施 こちらは人材紹介開発グループのグループ長である @kenjiszk さんの提案で、担当領域ごとに開発チームのメンバーとマーケターのリーダーで主に情報共有を目的とした定期的な1on1ミーティングを実施するようになりました。 改善前:タスク依頼の共有を行う定例でしか話す場がなかった マーケターの業務に関する情報を知る場がほとんどなかった マーケターが小さな相談をしたい時にコミュニケーションする場がなかった 改善後:週次で1on1ミーティングを実施することでコミュニケーション活性化 お互いの状況を把握できて相談しやすくなった この1on1ミーティングの実施が開発チーム・マーケター間の関係性を強めるきっかけになった オフラインイベントの実施 こちらは改善のアクションという名目で行ったわけではないのですが、様々な改善を行ってきた結果、開発チームとマーケターで本社でのオフラインイベントを実施することができました! 【イベントの詳細】 チーム対抗NASAゲーム(チームで合意形成を行うコンセンサスゲームの一種) この後のふりかえりをチームで行うため、先にチームの雰囲気を良くする意図 話したことがない人とも話すためお互いの自己紹介も兼ねていた 開発チーム・マーケターの体制についてふりかえり チームごとにふりかえりを行い、アクションアイテムを出し合う それぞれのチームから出たアクションアイテムを共有し、特に関心が集まった内容から開発チーム・マーケター合同のネクストアクションを決定 「アジャイルの各イベントをマーケターにもっと浸透させる」「開発チームに対して事業状況の共有と施策のふりかえりを行う」等のお互いに理解を進めるための良いアクションアイテムが決定 最後にお世話になった人たちにmiroで感謝を伝えあう 【イベントを終えて】 「すごく楽しかったです!」「とても良い取り組みでした!」と色々な人に言っていただけて嬉しかったです。 企画から当日の進行まで私が責任を持って任せて頂いたため、本番前の一週間はうまくいくか心配で寝つきが悪かったですが頑張ってよかったです。 個人的にはこのイベントを実施したことでマーケターのリーダー以外のメンバーの人たちとも話しやすくなったと感じたので何よりでした。 最後に 最後までご覧いただきありがとうございました。入社時に主にスクラムに関して私が感じた課題とそれに対するアクションをご紹介させていただきました。 以前より開発チームとマーケターの距離が縮まったものの、タスクの管理方法やより効率的なミーティングの設計など課題は多く残っています。 それらの課題を改善していくと同時に、今後サービスをより成長させるためにマーケターと協力しながら何ができるのかを考え実践していこうと思います。
アバター
こんにちは!カイゴジョブの開発をしております @katorie です。普段はRuby on Railsを使ってプロダクトの改善や新機能開発に取り組んでいます。 すでにこのブログでは弊社の学生支援によってRubyKaigi 2025に参加した学生の皆さんのレポートが公開されておりますが、ご覧いただけましたでしょうか? 私からは、子連れで参加したRubyKaigi 2025についてお伝えしたい思います。あくまで私個人の感想とともに一例としてのご紹介になりますが、RubyKaigi に参加するスタイルのひとつとしてどなたかの参考になることを願っています。 なぜ子どもを連れてRubyKaigiに参加するか 今回RubyKaigiに子どもを連れて参加したのには、いくつか理由があります。 まず単純に、「行きたい」という気持ちがありました。RubyKaigiは、Rubyに関わる開発者にとって年に一度のお祭りのような存在。毎年楽しみにしているイベントのひとつです。とはいえ、家庭の事情や育児の状況によって「今年は無理かな…」と参加を諦めてしまいそうになる瞬間もあります。 でも、「子どもがいるから行けない」のではなく、「子どもと一緒に行く」という選択肢があってもいいのではないでしょうか。 幸い、RubyKaigiでは託児所の提供など子連れ参加へのサポートが整っています。それを活用すれば、セッションを楽しみながらも、子どもに無理のない形で参加できるかもしれない。そう思って、思い切って申し込むことにしました。 ちなみに、弊社ではRubyKaigiへの参加は出張の扱いになります。私の出張に子どもを帯同するということになり、私の旅費と宿泊費は会社負担になります。会社からは子どもを帯同することで勤務時間の確保が可能かという点の確認がありましたので、託児所の存在が決め手になりました。 もうひとつの理由は、自分の尊敬する人たち、仕事仲間を子どもに少しでも見せてみたいという気持ちです。私は普段、「カイゴジョブ」の開発に携わっています。日々PCに向かっている姿を見ている子どもたちに、「お母さんはこんな仲間と一緒に働いてるんだよ」と伝える機会があったら素敵だなと思いました。 子どもを連れてRubyKaigiに参加するのは、控えめに言って最高である まず、子どもを連れてRubyKaigiに参加することは、控えめにいって最高です!いきなり個人的な感想全開ですが、具体的に最高と感じた瞬間を整理してみたいと思います。 まずありがたいことに、RubyKaigi 2025ではNursery SponsorとしてSTORES社が運営する託児所が存在します。事前登録すれば、会場内の専用スペースで子どもを預かってもらえるため、安心してセッションに集中することができます。もちろん、日々の子どもの体調や様子には常に気を配りながらにはなりますが、プロの保育士さんや親切なSTORES社のスタッフの皆さんがあたたかく迎えてくださったので、安心して預けることができました。親としても、エンジニアとしても、どちらも大切にできる環境が整っていると感じました。 また、私が仕事で関わっている人たちや尊敬するプログラマーの皆さんと直接会い、子どもに紹介できたことも貴重な経験でした。たとえば、2日目のLightning Talks(LT)で「Road to RubyKaigi: Making Tinny Chiptunes with Ruby」というタイトルで発表されていた @makicamel さんには、LTで披露されていたゲームを子どもに体験させてくださいました。目の前にあるものを作っている人と触れ合うという体験は、子どもにとってもとても印象深かったようです。 後日、また思い出して「あのゲームかわいかったな、あれ作ったのすごいな」と思い出していました。 私自身が楽しんでいる姿を子どもに見せられるというのも、思った以上に意味があることでした。子どもは大人の背中をよく見ています。いつもと違う場所で、刺激を受けながら楽しむ親の姿は、きっと何かしらポジティブな印象として残るのではないかと思います。 さらに、託児所で子どもたちが普段の生活圏とは違う友達と出会い、新しい環境で過ごすという体験も大きなメリットでした。親子ともに「非日常」を共有できるこの時間は、ちょっとした旅行以上に心に残るものでした。 3日目の最後に来年の会場が函館であることがわかってからは、初めて聞く地名を地図で探したり、託児所で一緒だったお友達とまた会えるかなと期待したり、すっかり来年のRubyKaigiを心待ちにしています。 子連れ参加に向けた準備 もう少し具体的なTipsもご紹介したいと思います。子連れでカンファレンスに参加するとなると、やはり事前の準備は必要です。私も「どうなるかな?」と不安を抱えながらの参加でしたが、いくつか工夫をすることで、当日は比較的スムーズに過ごすことができました。 まず、託児所の申し込みは最優先事項です。RubyKaigiにおける託児所の設置は初回が2016年となっており、コロナ禍を除いてはほぼ毎回用意されています。チケットの購入時点でその存在を知ることができるようになっています。 宿泊先については、子どもと過ごしやすいかどうかが重要なポイントになります。例えば大浴場付きだったり、部屋が広めだったり、近くにコンビニやコインランドリーがあると何かと助かります。子どもが小さい場合は、ベビーベッドや子ども用の備品が借りられるかどうかも要チェックです。これは過去の私のエピソードですが、託児所に預けるためにはお弁当持参だったので、宿泊先にキッチンがついていることが条件になったこともありました(今ではコンビニで買ったものが食べられるようになったのでキッチンは必要条件ではなくなりました)。今回、私は道後温泉エリアのホテルに宿泊しましたが、歩いて外湯を巡ることができる環境だったので、毎晩子どもたちと温泉を楽しんでいました。 荷物については、移動中の荷物を少なくするために事前にホテルに3泊分の着替えを送っていました。移動中はお気に入りのおやつやタブレット、ヘッドフォンなど、子どもが落ち着いて過ごせるアイテムはマストです。子どもの月齢にもよりますが、子どもが座ることができるキャリーケースは大活躍しています。 もし可能であれば、家族のサポートも得られるとよいでしょう。今回私はひとりで子ども2人を連れて行きましたが、現地では弟家族と合流して過ごしました。他のご家庭の様子をうかがうと、パートナーやご両親も一緒に滞在されている方が多いようでした。 最後に、心構えとして「全部のセッションに参加できなくてもOK」という気持ちを持っておくと、当日の心理的負荷がだいぶ下がります。見たいセッションを事前にピックアップしておきつつ、場合によっては録画やスライドで後から追うことも良しとして、無理せず楽しむことを優先しました。 実際の参加レポート 会場に到着するまでは「本当に子どもと一緒で大丈夫かな……」という不安も正直ありました。でも、いざ始まってみると、そんな心配はすぐに吹き飛びました。 託児スペースはセッション会場とは別の階に設けられていて、落ち着いた雰囲気の中で子どもたちが過ごせるようになっていました。スタッフの方々も慣れていて、子どもが安心して過ごせる空気を作ってくれていたのが印象的です。専用のSlackチャンネルを用意していただいていて、楽しそうに遊んでいる様子が写真で共有されていました。 イベント中は、子どもを連れて会場内を移動する場面もありましたが、周囲の参加者やスタッフの方々がとてもフレンドリーに声をかけてくださり、肩の力が抜けました。RubyKaigiらしい、オープンで温かい雰囲気に何度も助けられました。 2日目の夜には、STORES社が主催するSTORES Cafeにも参加しました。ノンアルコールのカジュアルな雰囲気で、子連れの参加者も多く、とてもありがたい存在でした。子どもも少し遅い時間ではありましたが、楽しく過ごすことができ、ゆるやかに交流の輪が広がる時間を楽しめました。 参考までに、3日目の我が家のタイムラインを書き出してみます。 託児所では公園での外遊びに連れて行ってくださったり、紙コップアートのワークショップがあったりと子どもたちが楽しめる工夫がたくさんありました。 まとめとアドバイス RubyKaigi 2025に子どもと一緒に参加してみて、本当に良かったと思っています。 準備はそれなりに必要でしたし、当日もイレギュラーなことが起きないわけではありません。でも、「カンファレンスに行くか、子どもと過ごすか」の二択ではなく、「子どもと一緒に行く」という選択肢があることを、あらためて実感しました。 もちろん、すべての家庭やタイミングで実現できるわけではないと思いますが、もし少しでも「行ってみたいな」「子どもを連れても大丈夫かな」と考えている方がいたら、ぜひチャレンジしてみてほしいです。 ここに、私なりの子連れ参加Tipsをいくつかまとめておきます。 託児所の事前申し込みは最優先 ホテル選びは立地と子ども向け設備を重視 荷物は事前に送る、手荷物にはお気に入りグッズを セッションは無理せず、気になるものを数本ピックアップ 子どものペースを最優先に、余白あるスケジュールを 子育て中でも技術コミュニティとつながり続けたい、そんな思いを叶えてくれる場として、RubyKaigiはとてもやさしい場所でした。 子ども帯同での出張として快く送り出してくださったチームの皆さんにもとても感謝しています。 来年も、また家族で参加できたらいいなと思っています。 株式会社エス・エム・エスでは、子育てしながらお仕事する環境も整っています!普段どんなふうに過ごしているか等、具体的なお話が気になった方はカジュアル面談でカジュアルに聞いてくださいね。
アバター
エス・エム・エスで全社 SRE というロールで活動している Security Hub 芸人の山口( @yamaguchi_tk )です。 おすすめのAWSサービスは営業です(いつもお世話になっています)。 1. はじめに 1.1 背景 株式会社エス・エム・エスでは、全社横断の SRE チームが AWS Organizations 配下で 130 以上の AWS アカウントと、200 名を超える開発者の認証と認可を管理しています。AWS IAM Identity Center の導入と Terraform による IaC 化、CI/CD による自動デプロイは整っていますが、ユーザー追加や権限変更のリクエストが積み重なり、運用負荷は増加する一方でした。 1.2 目指す姿 私たちが狙ったゴールはシンプルです。 運用負荷の軽減 設定ファイルベースで権限管理し CI/CD で自動化 開発チームへの安全な委譲 直感的に編集できるテキストファイルで「自分が担当しているアカウントの権限付与は自分たちで管理」 以下では、これを実現するために採ったアプローチと具体的なコードをご紹介します。 TL;DR: 「ヒトに権限を割り当てる」変更はテキストファイルを直すだけ。あとは CI/CD が全部やってくれる、そんな世界を Terraform とAWS IAM Identity Center で実現しました。 2. AWS IAM Identity Center の基本 2.1 機能概要 シングルサインオン (SSO) 一度の認証で複数アカウントにアクセス Azure AD や Okta など外部 IdP との連携 マルチアカウント管理 全てアカウントへのアクセス制御を統合 許可セット IAMポリシーをテンプレート化して再利用 ユーザー/グループ単位で割り当て 2.2 主要な概念 ユーザー Identity Center で管理される個々の利用者 メールアドレスを一意の識別子として使用 グループ ユーザーの集合を管理する単位 許可セット ポリシーとロールのテンプレート アカウント割り当て アクセス制御の基本単位 ユーザー/グループとアカウントの紐付け 許可セットの適用 3. Terraform での実装 3.1 標準的な実装例 標準的な Terraform による実装では、以下のようなコードを記述します。 # ユーザーの定義 resource "aws_identitystore_user" "example" { identity_store_id = aws_ssoadmin_instances.example[0].identity_store_id display_name = "Example User" user_name = "user@example.com" emails { value = "user@example.com" } } # グループの定義 resource "aws_identitystore_group" "example" { identity_store_id = aws_ssoadmin_instances.example[0].identity_store_id display_name = "Example Group" description = "Example group for testing" } # ユーザーをグループに割り当て resource "aws_identitystore_group_membership" "example" { identity_store_id = aws_ssoadmin_instances.example[0].identity_store_id group_id = aws_identitystore_group.example.group_id member_id = aws_identitystore_user.example.user_id } # 許可セットの定義 resource "aws_ssoadmin_permission_set" "example" { name = "ExamplePermissionSet" description = "Example permission set" instance_arn = aws_ssoadmin_instances.example[0].arn session_duration = "PT1H" } # 許可セットへポリシーのアタッチ resource "aws_ssoadmin_managed_policy_attachment" "example" { instance_arn = aws_ssoadmin_instances.example[0].arn permission_set_arn = aws_ssoadmin_permission_set.example.arn managed_policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" } # ユーザーをアカウントに割り当て resource "aws_ssoadmin_account_assignment" "example" { instance_arn = aws_ssoadmin_instances.example[0].arn permission_set_arn = aws_ssoadmin_permission_set.example.arn target_id = "123456789012" target_type = "AWS_ACCOUNT" principal_id = aws_identitystore_user.example.user_id principal_type = "USER" } 標準的な実装では、各リソースを個別に定義し、リソース間の依存関係を明示的に記述します。 例えば、グループメンバーシップの定義では、ユーザーとグループの ID を直接参照(リソース指定することも可能)する必要があります。 3.2 標準的な実装例での Terraform Plan 例 標準的な実装例での Terraform Plan 例を以下に示します。 # aws_identitystore_user.example will be created + resource "aws_identitystore_user" "example" { + display_name = "Example User" + identity_store_id = "d-1234567890" + user_id = (known after apply) + user_name = "user@example.com" + emails { + value = "user@example.com" } } # aws_identitystore_group_membership.example will be created + resource "aws_identitystore_group_membership" "example" { + group_id = "1234567890abcdef" + identity_store_id = "d-1234567890" + member_id = (known after apply) } # aws_ssoadmin_account_assignment.example will be created + resource "aws_ssoadmin_account_assignment" "example" { + instance_arn = "arn:aws:sso:::instance/ssoins-1234567890abcdef" + permission_set_arn = "arn:aws:sso:::permissionSet/ssoins-1234567890abcdef/ps-1234567890abcdef" + principal_id = (known after apply) + principal_type = "USER" + target_id = "123456789012" + target_type = "AWS_ACCOUNT" } 許可セットを付与する対象のユーザーが内部 ID( ssoins-1234567890abcdef など)で表示されています。 3.3 標準的な実装における課題 標準的な実装では、以下のような課題があります。 コードが複雑になる 多数のリソース定義が必要となり、コード量が膨大になる リソース間の依存関係の管理が複雑 変更の影響範囲の把握が困難 レビューの困難さ 内部 ID による表示のため、Terraform Plan で変更内容を把握することが難しい 3.4 課題を解決するために採用したアプローチ これらの課題に対処するために、私たちは三つの方針に沿って改善を進めました。 コード量の削減 Assignment 等の実装をテキストファイル化する レビューを楽に Terraform Plan にメールアドレス、グループ名、許可セット名を表示する 開発チームに安全に委譲できるように 安全に変更できるように AWS アカウント単位で委譲できる仕組みにする ディレクトリ、ファイル名だけで目的が伝わる構造にする 4. 具体的な実装 4.1 ディレクトリ構造 terraform/ ├── user/ # ユーザー管理 │ ├── user.txt # ユーザーリスト(メールアドレス) │ └── dummy.tf ├── membership/ # グループメンバーシップ │ ├── XXX_Developers.txt # 開発者グループ │ ├── XXX_SREs.txt # SREグループ │ ├── YYY_Developers.txt # 開発者グループ │ ├── YYY_SREs.txt # SREグループ │ └── dummy.tf ├── assignment/ # アクセス権限の割り当て │ ├── 12345678/ # AWSアカウントID │ │ ├── AdministratorAccess_GROUP.txt │ │ ├── PowerUserAccess_GROUP.txt │ │ ├── AdministratorAccess_USER.txt │ │ ├── PowerUserAccess_USER.txt │ │ └── dummy.tf │ ├── 23456789/ │ └── ... ├── su/ # symlinkによる組織構造 │ ├── 医療キャリア/ # 開発者グループ │ │ ├── prd -> ../../assignment/12345678 │ │ ├── stg -> ../../assignment/23456789 │ │ └── dev └── others/ # Terraformの実装コード ├── variables.tf # 変数定義 ├── assignmeents.tf # assignmentの実装コード ├── assignmeents_dummy.tf # dummy.tfを読み込むための実装コード ├── memberships.tf # group membershipの実装コード ├── permissionsets.tf # permission setの実装コード ├── users.tf # userの定義コード └── groups.tf # Groupの実装コード 4.2 設定ファイルの実装例 ユーザー管理(user/user.txt) Identiy Center に登録するユーザーのメールアドレスを記載します。 user1@example.com user2@example.com グループ管理(membership/XXX_Developers.txt, membership/XXX_SREs.txt) {グループ名}.txt という形式でファイルを作成します。 Group に所属させるユーザーのメールアドレスのメールアカウント部分(@より左側)を記載します。 user1 user2 アクセス権限・グループ(assignment/12345678/AdministratorAccess_GROUP.txt) {許可セット名}_GROUP.txt という形式でファイルを作成します。 ディレクトリ名の AWS アカウント ID で、テキストファイル名のアンダースコア `_` の左側に記載の許可セットを付与する Group 名を記載します。 XXX_SREs アクセス権限・ユーザー(assignment/12345678/AdministratorAccess_USER.txt) {許可セット名}_USER.txt という形式でファイルを作成します。 ディレクトリ名の AWS アカウント ID で、テキストファイル名のアンダースコア `_` の左側に記載の許可セットを付与する ユーザーのメールアドレスのメールアカウント部分(@より左側)を記載します。 user1 user2 ※ 弊社の環境では、Identity Center への認証は外部 IdP による SSO からのアクセスのみに絞っているので、@より右側のドメイン名は全ユーザー共通です。 4.4 実装コード user 定義(others/users.tf) ################################################################################ # User ################################################################################ resource "aws_identitystore_user" "this" { for_each = local.users display_name = each.value identity_store_id = "d-abc1234567" user_name = each.value emails { primary = true type = "work" value = each.value } name { family_name = " " given_name = " " } } ################################################################################ # Users ################################################################################ locals { user_file = "../user/user.txt" # ファイルが存在すれば読み込む、なければ空リスト emails = (can(fileexists(local.user_file)) && fileexists(local.user_file) ? split("\n", trimspace(file(local.user_file))) : []) users = { for email in local.emails : regex("(^[^@]+)", email)[0] => email } } ################################################################################ # dummy module ################################################################################ module "dummy_user" { source = "../user" } Group 定義(others/groups.tf) ################################################################################ # Groups ################################################################################ locals { groups = [ "XXX_SREs", "XXX_Developers", "YYY_SREs", "YYY_Developers", ] group_map = { for group in local.groups : group => { group_name = group } } } resource "aws_identitystore_group" "this" { for_each = local.group_map display_name = each.value.group_name identity_store_id = "d-abc123456" } assignment 定義(others/assignments.tf) locals { merged_targets = concat(local.assignment_target_users, local.assignment_target_groups) # assignments_targets の各要素に対して処理 # { # file_name = "PowerUserAccess_USER.txt" # permission_set_arn = "arn:aws:sso:::permissionSet/..." # principal_type = "USER" # # file_exists = true/false # raw_user_names = [...] # user_ids_map = { "alice" = "uuid1", "bob" = "uuid2" } # } # のような構造を作る flatten_targets = flatten([ for account in local.assignment_target_aws_accounts : [ for t in local.merged_targets : { file_path = "${local.assignment_file_path}/${account}/${t.file_name}" file_name = t.file_name account = account permission_set_arn = t.permission_set_arn principal_type = t.principal_type } ] ]) assignment_targets_expanded = [ for t in local.flatten_targets : { file_name = t.file_name account = t.account resourcename_prefix = trimsuffix(t.file_name, ".txt") permission_set_arn = t.permission_set_arn principal_type = t.principal_type file_path = t.file_path file_exists = can(fileexists(t.file_path)) && fileexists(t.file_path) # ファイルが存在すれば読み込む、なければ空リスト raw_user_names = (can(fileexists(t.file_path)) && fileexists(t.file_path) ? split("\n", trimspace(file(t.file_path))) : []) # TXTファイルが空の場合を考慮 # 空文字列""の場合はLISTから外す user_names = [ for name in can(fileexists(t.file_path)) && fileexists(t.file_path) ? split("\n", trimspace(file(t.file_path))) : [] : name if name != "" ] } ] } ################################### # assignment_targets_expanded をフラットに合体 ################################### # 例: { # "PowerUserAccess_USER.txt-alice" = { user_id="xxx", permission_set_arn="yyy", principal_type="USER" } # "PowerUserAccess_USER.txt-bob" = { user_id="xxx", permission_set_arn="yyy", principal_type="USER" } # "AdministratorAccess_USER.txt-charlie" = { ... } # } locals { combined_assignment_users = merge([ # assignment_targets_expanded の各要素 t に対して… for t in local.assignment_targets_expanded : { # さらに t.user_ids_map の各 entry (user_name, user_id) について… for user_name in t.user_names : # キーを "<awsaccount_id>_<file_name>_<user_name>"、値をオブジェクトにする "${t.account}_${t.resourcename_prefix}_${user_name}" => { user_name = user_name account = t.account permission_set_arn = t.permission_set_arn principal_type = t.principal_type } } ]...) } ################################### # for_each で一括作成 ################################### # combined_assignments には「(ファイル名)-(ユーザー名)」がキーになっている resource "aws_ssoadmin_account_assignment" "this" { for_each = local.combined_assignment_users instance_arn = local.instance_arn target_id = each.value.account target_type = "AWS_ACCOUNT" principal_id = each.value.principal_type == "USER" ? aws_identitystore_user.this[each.value.user_name].user_id : aws_identitystore_group.this[each.value.user_name].group_id principal_type = each.value.principal_type permission_set_arn = each.value.permission_set_arn } membership 定義(others/memberships.tf) locals { # memberships_targets の各要素に対して処理 # { # file_name = "SREs.txt" # group_id = "..." # # file_exists = true/false # raw_user_names = [...] # user_ids_map = { "alice" = "uuid1", "bob" = "uuid2" } # } # のような構造を作る memberships_targets_expanded = [ for t in local.memberships_targets : { file_name = t.file_name resourcename_prefix = trimsuffix(t.file_name, ".txt") group_name = trimsuffix(t.file_name, ".txt") file_path = "${local.membership_file_path}/${t.file_name}" file_exists = can(fileexists("${local.membership_file_path}/${t.file_name}")) && fileexists("${local.membership_file_path}/${t.file_name}") # ファイルが存在すれば読み込む、なければ空リスト raw_user_names = (can(fileexists("${local.membership_file_path}/${t.file_name}")) && fileexists("${local.membership_file_path}/${t.file_name}") ? split("\n", trimspace(file("${local.membership_file_path}/${t.file_name}"))) : []) # TXTファイルが空の場合を考慮 # 空文字列""の場合はLISTから外す user_names = [ for name in can(fileexists("${local.membership_file_path}/${t.file_name}")) && fileexists("${local.membership_file_path}/${t.file_name}") ? split("\n", trimspace(file("${local.membership_file_path}/${t.file_name}"))) : [] : name if name != "" ] } ] } ################################### # membership_targets_expanded をフラットに合体 ################################### # 例: { # "SREs-alice" = { group_id="xxx", user_id="xxx" } # "SREs-bob" = { group_id="xxx",user_id="xxx" } # "SREs-charlie" = { ... } # } locals { combined_memberships = merge([ # assignment_targets_expanded の各要素 t に対して… for t in local.memberships_targets_expanded : { # さらに t.user_ids_map の各 entry (user_name, user_id) について… for user_name in t.user_names : # キーを "<file_name>_<user_name>"、値をオブジェクトにする "${t.resourcename_prefix}_${user_name}" => { group_name = t.group_name user_name = user_name } } ]...) } ################################### # for_each で一括作成 ################################### # combined_memberships には「(ファイル名)_(ユーザー名)」がキーになっている resource "aws_identitystore_group_membership" "this" { for_each = local.combined_memberships identity_store_id = "d-95671a55c2" group_id = aws_identitystore_group.this[each.value.group_name].group_id member_id = aws_identitystore_user.this[each.value.user_name].user_id } ################################################################################ # dummy module ################################################################################ module "dummy_menbership" { source = "../membership" } variables 定義(others/variables.tf) locals { instance_arn = tolist(data.aws_ssoadmin_instances.instance.arns)[0] assignment_file_path = "../assignment" # assignment以下のフォルダを列挙 assignment_target_aws_accounts = distinct([ for file in fileset(local.assignment_file_path, "*/*.txt") : dirname(file) ]) membership_file_path = "../membership" } locals { # assignments_targets の定義 assignment_target_groups = [ { file_name = "AdministratorAccess_GROUP.txt" permission_set_arn = aws_ssoadmin_permission_set.AdministratorAccess.arn principal_type = "GROUP" }, { file_name = "DeveloperAccess_GROUP.txt" permission_set_arn = aws_ssoadmin_permission_set.DeveloperAccess.arn principal_type = "GROUP" }, ] assignment_target_users = [ { file_name = "AdministratorAccess_USER.txt" permission_set_arn = aws_ssoadmin_permission_set.AdministratorAccess.arn principal_type = "USER" }, { file_name = "Developer_USER.txt" permission_set_arn = aws_ssoadmin_permission_set.Developer.arn principal_type = "USER" }, ] } locals { memberships_targets = [ { file_name = "XXX_SREs.txt" }, { file_name = "XXX_Developers.txt" }, { file_name = "YYY_SREs.txt" }, { file_name = "YYY_Developers.txt" }, ] } dummy.tf 読み込み(others/assignments_dummy.tf) module "dummy_12345678" { source = "../assignment/12345678" } module "dummy_23456789" { source = "../assignment/23456789" } ... 4.5 実装のポイント 設定ファイルの読み込みは、Terraform の file() 関数を使用して実装しています。ファイルの内容を行ごとに分割し、 trimspace() で空白を除去することで、クリーンなデータを取得します。空行は if user != "" の条件で除外し、有効なデータのみを処理対象としています。 リソースの生成は、 for_each を使用して動的に行います。ネストされたリストは flatten() で平坦化し、一意のキーを生成することで重複を防止しています。これにより、設定ファイルの変更が効率的にリソースの更新に反映されます。 エラーハンドリングは、ファイルの存在確認から始まります。存在しないファイルに対しては file() 関数がエラーを返し、不正な形式のデータは split() や trimspace() で適切に処理されます。また、存在しないユーザーやグループへの参照は、Terraform の実行時にエラーとして検出されます。 今回は tfaction を利用することを前提としているため、設定ファイルを置いているディレクトリに dummy.tf を配置し、ディレクトリ自体を module として扱っています。 tfaction-root.yaml に以下の設定を追加することで module 側で変更があった場合(=設定ファイルに変更があった場合)に Terraform Plan が実行されるようになります。 update_local_path_module_caller: enabled: true 4.6 実装したコードでの Terraform Plan 表示 # aws_identitystore_group_membership.this["XXX-SREs_user1"] will be created + resource "aws_identitystore_group_membership" "this" { + group_id = "abc12345-1234-1234-1234-abc123456789" + id = (known after apply) + identity_store_id = "d-1234567890" + member_id = (known after apply) + membership_id = (known after apply) } # aws_ssoadmin_account_assignment.this["123456789012_PowerUserAccess_USER_user1"] will be created + resource "aws_ssoadmin_account_assignment" "this" { + id = (known after apply) + instance_arn = "arn:aws:sso:::instance/ssoins-1234567890abcdef" + permission_set_arn = "arn:aws:sso:::permissionSet/ssoins-1234567890abcdef/ps-1234567890abcdef" + principal_id = (known after apply) + principal_type = "USER" + target_id = "123456789012" + target_type = "AWS_ACCOUNT" } # aws_identitystore_user.this["user1"] will be created + resource "aws_identitystore_user" "this" { + display_name = "user1@example.com" + external_ids = (known after apply) + id = (known after apply) + identity_store_id = "d-1234567890" + user_id = (known after apply) + user_name = "user1@example.com" + emails { + primary = true + type = "work" + value = "user1@example.com" } + name { + family_name = " " + given_name = " " } } 4.7 開発チームへの委譲 ここまで実装できると、AWS アカウント単位に作成したディレクトリ単位や、XXX_GROUP.txt 単位で CODEOWNER を開発チームに設定することで、開発チームが管理しているグループへのメンバー追加や AWS アカウントへの権限付与について、開発チームが自律的な対応が可能になります。 4.8 課題の解決 コードが複雑になる 日常的に変更する必要のあるコードをシンプルにすることで解決しました。 具体的には、ユーザーの追加、グループへのユーザー追加、ユーザー・グループへの権限付与について、プレーンなテキストファイルによる設定ファイル方式を採用することで、コードをシンプルに保つことができるようになりました。 レビューの困難さ 内部 ID ではなく、人間が見て変更内容がわかるような Plan 表示を行うことで解決しました。 具体的には、「誰に」「何の権限を」「どの AWS アカウントに」付与するのかを人間が見てわかるような Terraform Plan 表示を実現することで、Plan 表示を確認するだけで変更内容が把握できるようになりました。 4.9 今回の実装が実現できた要因 今回の実装が実現できた要因として、現状の運用規模と運用スタイルがあると思います。 弊社の規模として、前述の通りAWS アカウント数が 130 以上、開発者数が 200 名以上です。それ以外の Identity Center での管理単位としては、グループ数が数十程度、許可セット数が10個程度で運用しています。 許可セットはセキュリティの最小権限の原則を考慮すると、AWS アカウント、アプリケーションのアーキテクチャ、開発者の属性、運用方法に応じた細かい粒度での許可設定が求められますが、弊社では、全社 SRE チームが全社横断で管理するため、管理負担とのバランスを取りつつ最小権限ではなく広めに権限を付与するような許可セットで運用しています。 この運用方法と規模感だから、プレーンテキストファイルによる設定、モノレポ管理、CI/CDでの自動化の実装と運用ができていると言えます。 5. まとめ 運用の効率化 1行1エントリのプレーンなテキストファイルでのシンプルな設定により、簡単に権限付与・削除が出来るようになりました。 また、Git の履歴を追うだけで権限付与の履歴も一目瞭然になりました。 for_each / flatten() による 動的リソース生成 を実装することでコード量を大幅削減できました。 レビュー体験の向上 Plan 出力にメールアドレス、ユーザー名、グループ名、許可セット名がそのまま表示されることで、「誰に」「何の権限を」「どの AWS アカウントに」付与するかが、Terraform Plan を確認するだけで可能になりました。 目的を持ったディレクトリ構造のため、ファイル差分で変更の意図が伝わるようになりました。 開発チームへの安全な委譲 プレーンなテキストファイルの編集だけで権限の付与が完了します。 AWS アカウント ID 毎のディレクトリ構造で、環境と権限の関係が直感的になりました。 また、symlink で日本語名称を付けたディレクトリ構造を作成することで、対象とするAWS アカウント IDを直感的に探すことが可能になりました。 これからのチャレンジ 生成 AI を活用した Issue から自動で PR を作成する等による AI OPS にチャレンジしていきます。(注釈:過去に Amazon Q Developer を利用したチャレンジは失敗しています→ https://speakerdeck.com/yamaguchitk333/codecatalyst-in-action-automating-pr-creation-for-route-53-and-iam-identity-center-management ) 許可セットの単位がまだ荒いので、その粒度を細かくするか ABAC にすることでセキュリティの強化を行います。
アバター
こんにちは、 Retasusan です。 普段は大学に通いながら、京都のスタートアップでRuby on Railsを使ったアプリケーションを書いていますが、「Rubyエンジニア」と胸を張って名乗れるほどRubyについて詳しいかと言われると正直まだまだです…。 そんな自分が、日本最大級のRubyカンファレンス RubyKaigi 2025 に初参加してきたので、現地の空気感や交流の様子を中心にまとめてみました! RubyKaigiとの出会い 最初にRubyKaigiを知ったのは、 Kyoto Tech Talk というイベントにお邪魔した時のことでした。学生支援があるという話を聞き、紆余曲折ありつつエス・エム・エスに応募させていただき、書類選考とリモートでの面接の後に採択していただきました! ありがたいことに松山への交通手段とRubyKaigiのチケットを確保できました。本当にありがとうございます! 事前準備 正直なところ、「RubyKaigiって何をするの?」というレベルからのスタートでした。 せめて登壇セクションの内容くらい理解しておかないといけないと思い、全く時間が足りませんでしたが、インターネットの内海に潜っては、時に溺れながらRubyについて調べていきました。 そして、今回の学生支援にはOfficial Partyなどのイベントは含まれていなかったため、各イベントをRubyKaigiのeventページと睨めっこしながらconnpass経由で予約していきました。 事前の予定は以下の通りです。ご覧の通り具体的な予定は全くないのですが、まぁなんとかなるだろうという気持ちで当日を迎えました。また、Day0の飛行機をうっかり早朝に予約してしまったため、前日をカラオケで過ごすことで搭乗に成功しました。 日付 4/14(月) Day-1 深夜のカラオケにて夜を明かす 4/15(火) Day0 松山観光(松山に前乗り) 4/16(水) Day1 4/17(木) Day2 4/18(金) Day3 最終日 4/19(土) Day4 松山観光(京都に帰還) Day0 この日は、ほとんど観光しかしていません。松山空港に着くとRubyKaigiのバナーがあったのでテンションが上がりました。 来ました #rubykaigi pic.twitter.com/gmvSGMoGcm — Retasusan (@retasusan_510) 2025年4月15日 友人とPre-Checkinのために会場に行きましたが、時間を間違えていたため出直しました。 Pre-checkin失敗(時間を読み間違えていた…) — Retasusan (@retasusan_510) 2025年4月15日 まだスタッフさん以外いない会場 その後は、松山城や道後温泉に行った後で、 SmartHRさんのDrinkup に参加しました。 まだRubyKaigiの雰囲気に馴染めておらず、あまり交流できませんでした。 Day1 この日聞いたセッション Ruby Taught Me About Encoding Under the Hood Introducing Type Guard to Steep State of Namespace TRICK 2025: Episode I ima1zumiさんの文字コードの話は印象深かったです。途中からDevanagari文字の話が出てきていました。 本来「クタァ」と繋がらないといけない部分が「ク」と「タァ」に分割されているというお話がありました。それを聞いて、Unicode対応にはDevanagari文字に精通しないといけないのかと戦々恐々としました。 ima1zumiさんのKeynoteのスライド資料は公開されているのでよければそちらをご覧ください。 This is my RubyKaigi keynote presentation. Thank you! https://t.co/Ld7X6QNyMf — ima1zumi (@ima1zumi) 2025年4月16日 また、Takeshi KOMIYAさんのSteepでの型ガードの話はかなり興味深かったです。正直、RBSを使用したことがなかったのですが、元々動的型付け言語であるRubyに型をつけることは、かなり難しさがあるのだろうなと思いました。 この時点では未来の話になるのですが、実は3日目の さくらインターネットさんでの打ち上げ でお話をする機会がございました。 楽しかったです! #rubyfriends pic.twitter.com/kMUqrAtGzo — Retasusan (@retasusan_510) 2025年4月18日 お昼はPixivさんの学生支援の方とエス・エム・エスの支援学生でお昼の交流会がありました。実は遅刻するほどCookpadさんの方との話が盛り上がってしまいました…!振り返ると、RubyKaigiでは意識して時計を見ないと時間を忘れて話してしまいがちでした。次回以降のRubyKaigiではこの反省を活かしたいと思います! 鯛めし。おいしかったです。 午後からは、tagomorisさんのNamespaceについてのセッションをお聞きしました。 この日最後のセッションは実は一番楽しみにしていた TRICK でした。審査員の方も応募できることに驚き、審査員の方がたくさん入賞していたことでさらに驚きました。僕が一番好きだったのは、花の模様が動く様がとても綺麗だった beta_chelseaさんの作品 でした。正直どの作品も何がどうなってるのかわからないようなものでしたが、僕のRubyへの価値観を広げてくれました。 この日は夕方から城山公園にてOfficial Partyを楽しみました。色々な方がいらっしゃった中で、せっかくRubyKaigiに来たからには僕自身も何か一歩を踏み出してみようと思い海外の方に積極的に話しかけてみることにしました。海外の方に話しかけたことで、僕の中のRubyやgemへのコミット欲求がかなり高まりました。RubyKaigiに来るような海外の方は皆さん何かしらのgemやRuby自体にコミットしてらっしゃる方ばかりだったので話題が基本的にそのコミット内容についてでした。そのような方々の会話を聞いていると、僕の中で会話についていきたい欲求が高まりました。また、Matzとツーショットを撮ることにも成功しました!感無量です…。 Official Partyの後は、友人と合流後Yusuke Wadaさんが幹事をしてくださった二次会に参加していました。ここでも海外の方がたくさんいらっしゃる卓につくことができて貴重なお話を伺うことができました。 Ruby API の作者の Colbyさん ともお話をすることができて本当にすごかったです。(語彙力の欠如) 俺が幹事してるんでこい #rubykaigi pic.twitter.com/Xb31dGlbvH — Yusuke Wada (@yusukebe) 2025年4月16日 #rubyfriends 楽しかったです! pic.twitter.com/j45hyUf8Ka — Retasusan (@retasusan_510) 2025年4月16日 流石に体力の限界を感じたため、二次会の後にホテルに戻りDay1は終了です。(日付は超えていたのでもうDay2に突入していた可能性もあります) Day2 この日は前日の二次会ではしゃぎすぎたこともあり、正直あまりセッションを聞くことができませんでした、反省。 迷い込んだ先でKengo Hamasakiさんの Running JavaScript within Ruby を聞くことができました。このセッションは、JavaScriptをRubyで動かすセッションでQuickJSなどをRubyで実装し直すというような内容だった気がします。体調が悪かったことに加え、内容も難しく部分的にしか理解できなかったので、スタンプラリーをまわりながらブースの方でお話を伺いました。 なんとか休憩を挟みつつDay2のセッションを聴きに行き最後にLTを聴き、気づけばDay2が終わっていました。 夕方からは、エス・エム・エスの社員の方と学生支援の方で交流することになっていたので微妙に回復した体を引き摺りつつお話をしていました。エス・エム・エスの社員でRubyコミッターでもある 塩井さん ともお話をさせていただき、これまた貴重なお話を聞くことができました。(RubyKaigiで貴重なお話を聞くこと以外の方が少なかった) 社員の方との食事後も、大街道で出会ったRubyistと突発的な2次会が始まり、様々な会社の方と交流することができました。 #rubyfriends pic.twitter.com/onJWBwRomE — ありたそ (@alitaso346) 2025年4月17日 流石にカラオケに行くほどの元気はなかったので、その日はホテルに帰ってから大学の課題を二時間ほどこなした後に寝ました。 Day3 この日は、 Ruby Committers and the World から始まりました。コミッターの方々の議論を聴き、「Static Barrierなんてものが検討されているのか」と驚いたり、「Namespaceはまだまだ調整が必要なんだなぁ」と思ったりしてました。中でも互換性を無視して無くしたいものなどは面白い議論でした。 MatzのKeynoteでは、Ruby4.0のリリース予定が最後に全ての話題を持っていきました。あの瞬間の会場の盛り上がりは忘れられません。 クロージングトークでは、次回のRubyKaigiが函館で開催されることが発表されました。その時には、来年も参加することが僕の中で確定事項になっていました。 そんなこんなで、RubyKaigi 2025は幕を閉じましたが、そんなことで僕たちのRubyKaigiは終わりません。(!?) 友人と少し談笑したのちに、 さくらインターネットさんの打ち上げ に向かいます。 色々な方と交流したのち、Pixivさんの RubyMusicMixin に向かいました。 Mixinでは、これまた海外の方に積極的に話しかけました。ドイツやオーストラリア、カナダの方などとお話することができました。ここまでくればかなりRubyKaigiに染まっていました。 楽しい!fun!!! #rubykaigi pic.twitter.com/MW1bPvG4mj — Retasusan (@retasusan_510) 2025年4月18日 #rubyfriends pic.twitter.com/YLNKo6OuTw — Retasusan (@retasusan_510) 2025年4月18日 #rubyfriends pic.twitter.com/rBbKliSc72 — Retasusan (@retasusan_510) 2025年4月18日 ドイツの方とはかなり話が盛り上がり、ハリボーが好きな話をしたら来年のRubyKaigiではハリボーを持ってきてくださるとおっしゃってくださいました。グミ好きとしては来年のRubyKaigiには必ず参加したいところ。 この日は流石に疲れたので、深夜1時にはお暇させていただきました。 Day4 この日は、朝から友人とクロスフィットに参加していました。運動不足の自分にはかなりキツイ運動量で6名いた中で自分だけ途中でギブアップしてしまいました。日頃からちゃんと運動しようと決意しました。 Wellness up! Morning CrossFit at RubyKaigi Day4 終了しました! 皆さん本当にナイスガッツでした👍 @yfractal @bash0C7 @hetare70914 @h2z @ymneet 今回はクロスフィット松山様に全面協力いただきました。お近くにお住まいの方は、体験にお越しください! #rubykaigi #rubykaigi_hacomono pic.twitter.com/HWlhWZuTfh — hacomono Developers / hacomono プロダクトチーム公式 (@hacomono_Dev) 2025年4月19日 珍しく運動をした後はJR松山駅に荷物を預け、松山の街を彷徨っていたところ、Ruby Committerのシャツを着ている人物を発見し友人と突撃しました。後ほど判明したのですが、この方は 2024年のKeynote を担当なさった Samuelさん でした! 時に真面目な話を挟みながら道後温泉や神社、謎の寺、ジェラート店、DD4Dなどを一緒に観光してもらいました。本当にありがとうございましたSamuelさん! #rubyfriends #rubykaigi @ioquatix Dogo onsen!!! pic.twitter.com/n8xwSpvheL — Retasusan (@retasusan_510) 2025年4月19日 @retasusan_510 @rokuosan_dev @ioquatix #rubykaigi #rubyfriends pic.twitter.com/RCT64Jc9a0 — joker1007 (アルフォートおじさん) (@joker1007) 2025年4月19日 Rubyist増えた! #rubyfriends #rubykaigi pic.twitter.com/cNOuNWkpoT — joker1007 (アルフォートおじさん) (@joker1007) 2025年4月19日 その後、松山空港に向かいそこでも案の定Rubyistと交流しながら京都へと帰還し、僕のRubyKaigi2025は幕を閉じました。 最後に 僕は今回エス・エム・エスの 学生支援 を利用してRubyKaigiに参加させていただきました。 支援してくださったエス・エム・エスには本当に頭が上がらないです! 今回スポンサーブースでお話をしてくださった方々やRubyKaigiにてお話したたくさんの方々、勿論今回支援していただいたエス・エム・エスの皆様も本当にありがとうございました!来年のRubyKaigiでお会いした際にはまたよろしくお願いします! 今回のRubyKaigi期間中にお会いしたエス・エム・エスの社員の皆様の温かさや、イベントへの熱い想いにとても刺激を受けました! 今回ご支援いただいたご縁を大切に、もしご興味があれば、ぜひエス・エム・エスの採用情報をご覧いただき、応募も検討してみてください。 open.talentio.com
アバター
ご機嫌麗しゅうございます。 @kimukei です。 これは 【前編】Visual Regression Testing の内製化への道 🚀 〜Chromaticから代替ツールへ〜 の後編にあたります。 ではさっそく、どうやって Chromatic から代替ツールへ移行していったかを紹介していきます。 コンテキスト カイポケリニューアルでは 577 個の Story, kaipoke-ui 1 は 63 個ほどの Story を有している VRT しているスナップショット数はカイポケリニューアルで 969 枚、kaipoke-ui で 139 枚程度 カイポケリニューアルでは比較的動きの多い Modal や yagisan-reports 2 を使用した PDF を表示する Story が多く存在する これは、そんなカイポケリニューアルの pull request に実行する CI で Storybook の配信から VRT の実行とテストレポートの発行と配信までを 5 分程度に収めた物語。 また、それまで VRT の仕組みは Chromatic を使っていたため、なるべく VRT の体験は Chromatic のそれを損なわないように工夫しました。 ここで行う VRT としての CI と Storybook の配信と VRT の結果レポートの配信としての CD についてワークフローで行う処理の流れは以下のようになります。 Storybook を build Baseline 3 の取得 Storybook の撮影 撮影した画像と Baseline との比較(VRT) VRT レポートを作成 Storybook と VRT レポートをデプロイ & 配信 VRT で差分が検出されたときはマージをブロックする CI/CD の実行環境は GitHub Actions です。 VRT は reg-viz/storycap と reg-viz/reg-cli を用いて実現しています。 そこで、実現する中でいくつか知見が溜まったので紹介していきます。 Storybook, VRT について CI/CD 最適化の知見 GitHub Actions ランナーとコスト最適化 GitHub Actions hosted runner と AWS CodeBuild hosted runner のコスト比較 large runner についてホストが GitHub Actions hosted runner と AWS CodeBuild hosted runner とでコスト比較すると、CodeBuild の方が安いことがわかります。 GitHub Actions hosted runner pricing: https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates-for-x64-powered-larger-runners AWS CodeBuild hosted runner pricing: https://aws.amazon.com/codebuild/pricing/ ※ GitHub Actions hosted runner は arm64 アーキテクチャだと安いのですが、今回 chrome や chromium と puppeteer を動かしつつ継続的にメンテナンスしていく都合上 arm64 アーキテクチャの runner の採用は見送っています 今回私が選定した runner は CPU のコア数が欲しくて CodeBuild(ap-northeast-1)の general1.xlarge ($0.1002/min) なのですが、近しい性能を出そうとすると GitHub Actions self hosted runner の Linux 32-core ($0.128/min)を選ぶことになります。 また、CodeBuild の runner はネットワーク上、S3 との通信が速いため Baseline の取得が早くなる利点などもありました。 ジョブ分割の落とし穴 VRT の時間的なネックはだいたい撮影にあります。枚数が多いのです。そこでよく sharding をして job を分割する手段が取られますが、この場合 job 分割のオーバーヘッドがなかなか無視できません。 具体的には以下のようなオーバーヘッドが乗ってきます。 runner の割り当て待ち 前の job で作られた生成物のダウンロード(前の job では生成物をアップロードするコストも発生します) runner の割り当て待ちについてですが、GitHub Actions では標準 runner ではないものを使うとしばしば pick されるまで遅い現象が発生しました。 CodeBuild では provisioning なども発生するため、だいたい job の最初の step が実行されるまで 30 秒程度かかってきました。 reg-viz/storycap には parallel オプションが、 reg-viz/reg-cli には concurrency オプションが提供されています。 そのためここではあえて job 分割を避けて large runner を用いてその中で大量に実行することでオーバーヘッドを極力なくしました。(そのために CPU コア数が欲しかった!) ちょうどこれくらい大きな Storybook だと、build した生成物も 50MB 強くらいの大きさで、GitHub Actions の標準 runner を使っていると動作が安定しないことなども起こっていましたが、CPU コア数が潤沢な runner を選ぶとだいたいそこそこ潤沢なメモリも付いてきます。これが撮影動作の安定化なども副産物的にもたらしてくれました。 ちなみに CodeBuild hosted runner を用いているとマシンリソースについてのメトリクスも同時に取れるので、最適な runner 探しも捗りました。GitHub Actions hosted runner でもメトリクスは見ようと思えば見れますが、見るためのスクリプトを入れたりなどが必要で「そういえば」とふと思った時に見られる準備をしていないことがほとんどです。 この辺はマシンスペックのチューニングと共に job と step のチューニングも同時に行うと良いでしょう。 ワークフロー全体のネック部分や pick 部分の可視化を行うのに Kesin11/actions-timeline がとても便利でした。設定や基盤が不要でワークフローに差し込むとすぐにこのような図が見られるようになります。 actions-timeline の図 まとめると、一見効率的に思えるジョブの分割ですが実際には 生成物のアップロード・ダウンロードに時間がかかる ジョブ間のリレーで待機時間が発生 ランナーの割り当て待ちが複数回発生 むしろ 1 つの大きなジョブで実行する方が、総合的なパイプライン実行時間が短くなることがあるということでした。 並列実行できる step をバシバシ並列実行させていく コマンドはこんな感じで並列実行できます。 jobs: parallel-commands: runs-on: ubuntu-latest steps: - name: Run commands in parallel run: | command1 & command2 & command3 & wait Baseline のダウンロードと Storybook の撮影は並列化可能でどちらも時間のかかる処理のため並列化したことで 1 分程度の短縮につながっています。 これも large runner のスペックがあるが故ですね。 配信基盤の選択とサイズ管理 Storybook と VRT レポートの配信について「どこ」に配信するかは重要な問題です。 よく使われるのは Amazon S3 でしょうか。今回は Private GitHub Pages を選択しています。 Private GitHub Pages の利点 AWS で認証付き配信基盤を構築するよりも、Private GitHub Pages を使用する方がセットアップが容易で管理コストも低くなります。 前提として、弊社ではエンジニアはほぼフルリモートで開発しています。 そのため何かを限定公開する際にはアクセスできるのは弊社の人間のみにする何かしらの仕組みが必要になります。 S3 では VPN の IP のみアクセス可能にするのがお手軽ですが、VPN 接続がめんどくさいという体験上のネックポイントが発生します。 組織の Google アカウントと連携した認証を用意するには工夫が必要です。 弊社の GitHub リポジトリは Google アカウントでの SAML 認証を必須化しているため、Private GitHub Pages で配信することで VPN 接続の煩わしさを解決しつつ同時にセキュアな限定配信を可能にしました。 ただし、GitHub Pages には制限があります。 アーティファクトのサイズ制限がシビア 同時デプロイリクエストに弱い(順次処理が基本) それぞれ次のセクションでどうやって対応したのか紹介します。 アーティファクトサイズ管理の重要性 GitHub Pages のデプロイは 特定のブランチ(ここでは gh-pages ブランチ)の内容を artifact 化し、アップロード アップロードした artifact を Pages にデプロイ という流れで行われます。 この artifact は 1GB 程度に収めておかないと deploy の失敗確率が上がったり、deploy に時間がかかるようになります。 GitHub Pages のパフォーマンスを維持するために、 VRT レポートから不要な画像(差分がないもの)を除去 変更がない場合はレポートを作成しない判断ロジックを導入 デプロイは可能な限りまとめて実行(同時リクエストの頻度を減らす) Pull Request の merge/close タイミングでの gh-pages ブランチの掃除 定期的な古いファイルの gh-pages ブランチの掃除 を行いました。 VRT レポートから不要な画像を除去できたり、変更がない場合はレポートを作成しないようにできたのは、フロントエンド部分を実装しているエンジニアにヒアリングしたところ PASSED の画像はほぼ見ていないとこがわかったためです。 PASSED の画像をレポートから除外することでフルだと 50MB くらいの容量のレポートが大抵 2~3MB になり 90%以上のサイズ削減が見込めます。 同時デプロイリクエストの制御 GitHub の Deployments はリクエストを排他的に処理します。つまり、実行中の Deployments があれば他の Deployments のリクエストは失敗します。 また、GitHub Pages の Build and deployment には「GitHub Actions」と「Deploy from a branch」の 2 種類のやり方があります。 前者は完全にこっちが deploy の面倒を見る設定で、後者がブランチが更新されたら自動で GitHub の方でデプロイしてくれるやり方です。 「Deploy from a branch」の設定は自動でやってくれるのは嬉しいのですが、ブランチの変更頻度が高いと同時実行性の点で難があります。 重複実行が回避されるように、実行中に新しい実行リクエストが来たら現在の実行をキャンセルし、新しい実行を始めるように設計されていて、この設定はいじれません。 そのため、ブランチの変更頻度が高い場合に実行リクエストが続々と来てしまうと常に最新のリクエストを処理しようとして最初のリクエストの変更が延々と Pages に反映されません。 これを避けるために前者の「GitHub Actions」で Pages デプロイする方法を取りました。 この方法ですと、ワークフロー内の concurrency 設定で同時実行された際の行動を制御できるようになります。 ここではデプロイのワークフローは他の実行によりキャンセルされないようにして、それぞれデプロイをやり切るようにしました。 その際、Deployments のリクエスト自体は排他的ですので、後続の Deployments リクエストは失敗する恐れがありますが、前のデプロイが終われば成功します。 そのため自動的に rerun できる仕組みを構築 4 し、artifact name はみな同一にしておいて last write win の状況を作り、upload artifact を行う job と deploy artifact を実行する job とを分け、deploy artifact を実行する job のみ rerun が行われるように組むことで順次デプロイは解消しつつ最終的には最新の artifact が Pages にデプロイされている状況を作り出しました。 VRT の最適化 これまで Chromatic を活用していたため、開発者のメンタルモデルには Chromatic の体験が根付いています。 今回 VRT の手法を Chromatic から移行させるにあたり、いかに Chromatic の体験に近づけていったか、それぞれポイントを紹介します。 storycap により生成される画像ファイル名と対象の Story の連携改善 Chromatic の体験として大きいのが、VRT で検知されたスナップショットで該当する Story のページへ即座に飛べることがあります。 この体験を移行するのに、 reg-viz/storycap で生成される画像ファイル名から Storybook の URL に変換できる関数を実装し、VRT レポートのサマリーを Pull Request にコメントすると共に変更を検知したスナップショットについてそれぞれ該当する Storybook のリンクをコメントと一緒に添えるようにしました。 例) GitHub Actions へのレポート reg-viz/reg-cli で生成した VRT レポート内に Storybook のリンクを配置するのは難しいですが、これならばそこまで体験を損ねずに VRT の結果と該当の Storybook への遷移がしやすくなります。 アニメーション対策とテスト安定性向上 VRT は flaky さとの戦いです。 VRT の安定性を高めるための施策をいくつか実施しました。 CSS アニメーションを無効化 JavaScript アニメーションを一時停止 タイミングに依存する要素の制御 最初の 2 つは DOM のテスティングでもよくやる推奨されるようなやり方ですし、紹介は割愛します。 タイミングに依存する要素の制御については、 parameters.screenshot.waitFor で頑張るのが良いのですが、構造上やむを得ないものは parameters.screenshot.delay でお茶を濁す感じになりました。 さらに、カイポケリニューアルでは複数のフォントを扱っているのですが、たまにフォント読み込みが終わっていないのに撮影されたために誤検出するケースもありました。 その問題については、すべてのフォント読み込みまで待つ util を実装しました 5 。 他にもスナップショットを安定させるためのあらゆる待ち条件を util 化し、各 Story で管理しやすくしました。 さらに、テストの不安定性(flaky)を検出するための仕組みも構築しました。具体的には、Baseline となる画像群(すでに S3 にアップロード済み)と、デフォルトブランチの最新コードから新たに生成した画像群を比較するワークフローを実装し、定期的(デイリー)かつ複数回実行することで、環境や実行タイミングによる差異を検出しています。 約 1000 枚のスナップショットを扱う規模では、単純な数の問題からも不安定性が生じやすくなります。このため、検出 → 対応 → 再確認のサイクルを地道に繰り返すことが信頼性向上のカギとなりました。特に flaky テストは確率的に発生するため、複数回の実行による検証は非常に重要です。 PDF のビジュアルテスト PDF 生成は yagisan-reports を使用しているのですが、PDF に対しても VRT を実施したいとなると何かしらの viewer が必要になります。 wojtekmaj/react-pdf を viewer に活用し、PDF の VRT を実現しています。 PDF は他のコンポーネントに比べるとレンダリングにやや時間がかかるため、撮影タイミングの制御は必須です。レンダリングを検知するための data-testid などを設定し、その要素の表示を待ってからスクリーンショットを撮影するようにしています。 VRT 判定基準の最適化 VRT における変更検知の閾値設定は、いわば「匠の塩加減」のようなものです。例えば Chromatic では diffThreshold のデフォルト値として .063 (6.3%) を採用しており、この閾値の決定には経験と試行錯誤が不可欠です。 https://www.chromatic.com/docs/threshold/ しかし、Chromatic の 6.3% をそのまま reg-viz/reg-cli に適用すると、検知感度に違いが生じ、偽陰性(変更があるのに検知されない)が増加する傾向がありました。 また、 reg-viz/reg-cli では、より詳細な閾値設定が可能になっています: -M, --matchingThreshold Matching threshold, ranges from 0 to 1. Smaller values make the comparison more sensitive. 0 by default. Specifically, you can set how much of a difference in the YIQ difference metric should be considered a different pixel. If there is a difference between pixels, it will be treated as "same pixel" if it is within this threshold. -T, --thresholdRate Rate threshold for detecting change. When the difference ratio of the image is larger than the set rate detects the change. Applied after matchingThreshold. 0 by default. -S, --thresholdPixel Pixel threshold for detecting change. When the difference pixel of the image is larger than the set pixel detects the change. This value takes precedence over thresholdRate. Applied after matchingThreshold. 0 by default. https://github.com/reg-viz/reg-cli?tab=readme-ov-file#options 様々なパラメータを試した結果、最終的に --matchingThreshold 0.011 --thresholdPixel 100 という値に落ち着きました。閾値調整の基本姿勢としては、まず 0 に近い値(高感度)から始め、偽陽性(変更と検出されたくないのに検知される)が目立つようであれば徐々に値を上げていく方法が効果的です。これは偽陰性の方が発見・対応が難しいためです。 また、開発者のメンタルモデルとして、画像差分を画面全体の割合で捉えるよりも、変更部分のピクセル数で判断することが多いと観察されたため、割合ベース(thresholdRate)ではなくピクセル数ベース(thresholdPixel)での検知手法を採用しました。大きな画面であっても、開発者は画面全体ではなく構成コンポーネント単位で変更を認識しているためです。 VRT の承認プロセスとマージブロックの連動 プルリクエストのマージ制御には、GitHub の Branch protection rule にある「Require status checks to pass before merging」機能が効果的です。これにより、ステータスチェックとマージ条件を連動できます。 ただし、VRT を実施するワークフローでマージブロックを直接制御しようとすると問題が生じます。承認プロセスという強制的に成功とみなす「裏口的な手法」がワークフロー自体に混入してしまい、全体の見通しが悪くなりメンテナンス性を損なう恐れがあります。そこで私たちは、マージブロック制御を別の仕組みとして実装しました。 具体的なアプローチは以下の通りです: VRT で差分検出時、プルリクエストに専用のラベルを付与 このラベルが存在する場合、ステータスを pending に設定 ラベルが外れた場合、ステータスを success に更新 このマージブロック制御には on.pull_request.types: [labeled, unlabeled] イベントを活用し、ラベルの付け外しという単純な操作で VRT の承認管理を実現しています。 また、開発者からのフィードバックを受け、VRT 承認待ち状態ではコミットステータスを「failure」ではなく「pending」に設定するよう改善しました。pending 状態はブラウザタブで以下のように表示され、対応状況が視覚的に把握しやすくなっています。 ステータスが失敗の見え方 ステータスがペンディングの見え方 これは最新のコミットに対する status を制御することで実現できます。GitHub の REST API( Create a commit status )を使って、VRT の承認状態に基づいて最新コミットのステータスを操作し、「Require status checks to pass before merging」で設定している job 名のコンテキストで付与することでブロック制御と両立させました。 この一連の処理フローは以下のとおりです: VRT で差分検出時、GitHub App のトークンを使用 6 してプルリクエストにマージブロック用ラベルを付与 このラベルが付いている場合、最新コミットのステータスを特定の job 名のコンテキストで pending に設定 VRT の変更を承認する場合、VRT 結果を案内するコメント内のチェックボックスにチェックを入れる(issue_comment.types: edited イベントが発火) コメント編集イベントをトリガーに、承認コメントであることを確認してマージブロック用ラベルを除去 ラベル削除により、最新コミットのステータスを job 名のコンテキストで success に更新 これでマージが可能になる この仕組みにより、意図しないビジュアル変更のマージを防止しつつ、意図的な変更は簡単な承認プロセスで対応できるワークフローを実現しました。 まとめ カイポケリニューアルのプルリクエストに対する CI 処理で、Storybook の配信から VRT の実行、テストレポートの配信までを 5 分程度に収めるために実施した主な最適化ポイントは以下の通りです: ランナー選定の最適化 : AWS CodeBuild hosted runner の採用によりコスト効率を改善 適切なマシンスペックの選定により VRT のパフォーマンスを大幅に向上 ジョブ構成の最適化 : 多数の小さなジョブへの分割ではなく、単一の大きなジョブ内での並列処理を選択 並列実行可能なステップを特定し、効率的な実行パイプラインを構築 配信基盤の効率化 : Private GitHub Pages を活用した認証付き配信の簡素化 アーティファクトサイズの厳格な管理による安定したデプロイ デプロイメントリクエストの効率的な制御 VRT の開発者体験向上 : Chromatic に近い使用感を再現し、移行コストを最小化 アニメーションなどの不安定要素に対する堅牢な対策 効果的な判定基準の設定による誤検出の最小化 直感的な承認プロセスとコミットステータスの連動 これらの最適化により、577 個の Story と 969 枚のスナップショットを扱う大規模 VRT プロセスを約 5 分で完了できるようになりました。この改善は開発サイクル全体の効率化に貢献し、開発者体験の向上にもつながっています。 また、この移行を通して VRT や Storybook を配信するのに支払っていた月々の支払いを 90% 近く削減できました。 内製している UI コンポーネント群。Chakra UI をベースに作っている。 ↩ PDF の生成に利用している帳票発行エンジン。 https://denkiyagi.jp/yagisan-reports/ ↩ VRT をする際の比較元となる main branch のスナップショットを事前に S3 にアップロードしています。 ↩ https://github.com/orgs/community/discussions/67654#discussioncomment-8038649 が参考になると思います ↩ FontFace API の loaded プロパティを活用し、プロジェクトで使用するすべてのフォントを列挙して、それぞれの loaded() の Promise が解決するまで待機する実装としました。参考: https://developer.mozilla.org/en-US/docs/Web/API/FontFace/loaded ↩ GitHub App のトークンを使用する理由はデフォルトトークンだとイベントが後続のワークフローにフックされない GitHub Actions の仕様のためです。 ↩
アバター