Nx活用術!モノレポ内のStorybookのパス設定自動化

ファインディ株式会社でフロントエンドのリードをしている新福(@puku0x)です。

この記事はFindy Advent Calendar 2024 4日目の記事です。

adventar.org

Nxはモノレポ管理の便利なユーティリティとして @nx/devkit を提供しています。

今回は @nx/devkit を利用したStorybookの設定の自動化についてご紹介します。

Nxについては以前の記事で紹介しておりますので、気になる方は是非ご覧ください。

tech.findy.co.jp

モノレポでStorybookをどのように管理するか?

皆さんはモノレポでStorybookを運用されたことはありますでしょうか?

概ねどちらかの方法を採用することになるかと思います。

前者はStorybookのデプロイ設定をシンプルにできますが、全プロジェクトのStorybookへのパスを記述する必要があります。後者は設定がやや複雑であり、プロジェクト毎にStorybookのデプロイ設定も必要です。

Findyのフロントエンドはモノレポで管理されており、フィーチャ単位に細分化された多数のプロジェクトを持つという性質と運用の難易度を考慮し、前者の手法を選びました。

どうやってパス設定を自動化するか?

単一のStorybookで集中管理する際に課題となるのは↓の部分でしょう。

// .storybook/main.js
const config = {
  addons: ['@storybook/addon-essentials'],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  stories:[ // 👈 ここ!👇
    '../apps/app1/src',
    '../libs/components/src',
    '../libs/app1/feature-a/src',
    '../libs/app1/feature-b/src',
    '../libs/app1/feature-c/src',
    // 以下、全プロジェクトのパスが続きます
    // :
  ]
};

export default config;

数個程度であれば十分に運用できそうですが、モノレポ上となると話が違ってきます。

ちなみに、Findyのフロントエンドではプロジェクト数は 70 個でした。
※他プロダクトでは100個近くになる場合もあります

「え?これ全部手動で管理するんですか!?」

さすがに手動での管理には限界がありますので、ここはNxの力を借りましょう。

使用するのは createProjectGraphAsync というユーティリティです。

createProjectGraphAsync | Nx

Nxは各プロジェクトの依存関係を保持しており、そこからStorybookのパスを算出できます。

最終的に次のようなものを目指します。

// .storybook/main.js
const config = {
  addons: [...],
  framework: {...},
  stories: getStories(),  // https://storybook.js.org/docs/configure#with-a-custom-implementation
  // 👇 こんな感じの配列を返して欲しい
  // [
  //   { titlePrefix: 'app1',  directory: '../apps/app1/feature-a/src' },
  //   { titlePrefix: 'app1-feature-a',  directory: '../libs/app1/feature-a/src' },
  //   { titlePrefix: 'app1-feature-b',  directory: '../libs/app1/feature-b/src' },
  //   { titlePrefix: 'app1-feature-c',  directory: '../libs/app1/feature-c/src' },
  //   ...
  // ]
};

export default config;

実装してみよう!

方針が決まったところで実装していきましょう。

事前の準備として、Storybookのパスを取得したいプロジェクトに tags を設定しておきましょう。Nxの @nx/enforce-module-boundaries ルールによる依存の制御を導入している場合は自然と設定されてあると思います。

// apps/app1/project.json
{
  "name": "app1",
  "tags": ["scope:app1"],
  ...
}
// libs/app1/feature-a/project.json
{
  "name": "app1-feature-a",
  "tags": ["scope:app1", "type:feature"],
  ...
}

nx.dev

@nx/devkitcreateProjectGraphAsync を利用して、各プロジェクトの name および sourceRoot を取得します。

// apps/app1/.storybook/main.js
import { createProjectGraphAsync } from '@nx/devkit';

const getStories = async () => {
  const graph = await createProjectGraphAsync();

  // Storybookが不要なプロジェクトは無視
  const ignoredProjects = ['app1-e2e', 'app1-utils'];

  return Object.keys(graph.nodes)
    .filter((key) => graph.nodes[key].data.tags?.includes('scope:app1'))  // 関連するStorybookの絞り込み
    .filter((key) => !ignoredProjects.includes(key))
    .map((key) => {
      const project = graph.nodes[key].data;
      return {
        titlePrefix: project.name,
        directory: `../../../${project.sourceRoot}`,
      };
    });
};

あとはこれを stories に渡せば完成です。

// apps/app1/.storybook/main.js
const config = {
  addons: ['@storybook/addon-essentials'],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  stories: getStories(),  // 👈 プロジェクトの追加・削除に応じて自動的に設定されます
};

export default config;

Storybookが表示されました!

Nxによる自動化のもう1つのメリットは、↓のように titlePrefix にプロジェクト名を関連付けることで、フィーチャ毎の分類がより明確になる点だと思います。

const project = graph.nodes[key].data;
return {
  titlePrefix: project.name,
  directory: `../../../${project.sourceRoot}`,
};

開発メンバーからは、

「モノレポ構造とStoryの構造がリンクすることで画面の使い勝手が非常に良い」
「検索したときにStory名が同じでもどの階層にいるか判断して目的にたどり着ける」

といったフィードバックを受けることができました👏 検索性の向上に一役買えましたね!

今回のサンプルは次のリポジトリから動作を確認できます。是非お試しください。

github.com

まとめ

この記事では @nx/devkit を利用したStorybookの設定の自動化についてご紹介しました。

Nxの機能を活用すれば「モノレポにプロジェクトを追加した後のStorybookの設定が漏れていた!」といった事とは無縁になるでしょう。

検証した時点では、@storybook/test-runnerstories をAsync Functionで渡すパターンとの相性がまだ悪いようでした。今後の更新に期待したいですね。

今回はStorybookとの組み合わせでしたが、同じ仕組みを使ってGraphQL Codegenの設定自動化も可能であると確認しています。また別の機会にご紹介できればと思います。

それではまた次回!


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers

【エンジニアの日常】エンジニア達の人生を変えた一冊 Part3

【エンジニアの日常】エンジニア達の人生を変えた一冊 Part2に続き、エンジニア達の人生を変えた一冊をご紹介いたします。

今回はPart3としまして、Findy Freelanceの開発チームメンバーから紹介します。

人生を変えた一冊

マスタリングTCP/IP―入門編

主にバックエンド開発と開発チームのリーダーを担当している中坪です。

私が紹介する「マスタリングTCP/IP―入門編」は通信プロトコルのTCP/IPの基礎について解説している書籍です。

私が最初にこの本を読んだのは、新卒入社した会社で、システムエンジニアとして働き始めた頃です。 当時、Webやスマホなどのアプリケーション開発部署への配属を希望していました。 しかし、実際にはネットワーク機器の設定、導入を業務とする部署に配属となりました。

最初はネットワークという分野に興味を持てず、仕事をする上での必要な知識も足りず、苦労しました。 そんなときに、先輩に勧められて読んだのがこの本です。当時、私が読んだのは第3版です。

本を読み進めながら、業務でPCとルータやファイアウォールを接続し、疎通確認をしたり、Wiresharkを使ってパケットの中身を確認する作業をしました。 本に記載されているプロトコルの仕様と、実際に目の前で行われている通信の挙動を結びつけることができました。

いくつか具体的な例を出すと次のようなことです。

  • IPアドレスとMACアドレスの役割
  • パケットのカプセル化の仕組み
  • 代表的なプロトコルがTCP/UDPのどちらをベースにしており、何番ポートを使っているか
  • デフォルトゲートウェイの役目やルーティングの挙動

そこから、徐々にネットワークに興味を持つようになりました。 目論見どおりに通信を制御できたときは、達成感を感じることができました。

今振り返ると、最初興味がなかったのは知識がないからであって、理解することで後から興味が湧いてくることを体験しました。 また、基礎を学ぶ大切さや、本を読むことと実践を組み合わせることで、学習が加速することも学びました。 エンジニアとしての原体験をもたらしてくれた一冊です。

新しい技術や役割に挑戦する際の姿勢に影響を与えてくれていると感じています。 自分がなにか新しいことを任せる立場になったときも、このときの経験を活かしていきたいと思っています。

今はネットワーク関連の仕事をしていませんが、 本ブログを書くにあたり、久々に最新の第6版を購入して読んでみました。 現在はWeb開発をしているので、その立場から見たときのおすすめの章は次の通りです。

  • 1章 ネットワーク基礎知識
  • 4章 IPプロトコル
  • 5章 IPに関連する技術
  • 8章 アプリケーションプロトコル
  • 9章 セキュリティ

ネットワークの分野でもIPv6やHTTP/3などの変化がありますが、TCP/IPはこの先も当分は通信の基盤として使われると考えています。 そのため、本書にある基礎知識は長い期間有効であり、 ネットワークを専門としないエンジニアであっても1度は読んでおいて損はないと思います。

ハッカーと画家 コンピュータ時代の創造者たち

インフラ・バックエンドエンジニア兼、Embedded SREの久木田です。

著者のPaul Grahamによって書かれたエッセイ集です。コンピュータ時代の革新を担うハッカーたちのものの考え方について書かれています。

各章は独立して書かれているので、どの章から読むことができます。

私がこの本を読んだのは大学1年のときです。もう10年以上前になります。

情報系の学科に入学してはじめてプログラミングにふれた自分としては、今後プログラマとして食べていけるのか、やっていけるのかを非常に悩んでいた時期でした。2011年当時は、プログラマ35年定年説やIT業界はブラックな環境が多いというネガティブな情報が目立っていたように感じ、その影響を受けていました。

そんなときに出会ったのがこの本でした。この本に書かれている「ハッカー」にすごく憧れて、勉強を続けていくこともできて、いまの職につけていると思っています。

私が一番好きな章は第16章の「素晴らしきハッカー」です。

良いハッカーとはどのようなことを好んでいるのか、何を大切にしているのかについて書かれています。良いハッカーとはプログラミングを本当に愛していて、コードを書くことを楽しんでいると書かれています。他にもどういった要素がハッカー足らしめているかを書かれているので興味を持って詳しく知りたいと思った方はぜひこの章を読んでいただきたいと思います。

また、良いハッカーを見分けるには同じプロジェクトで一緒に仕事をすることで初めてわかると書かれていて、当時はそういうものなのかと思って読んでいましたが、今は確かにそうかも知れないと思っています。エンジニアを採用する立場であったこともあるのですが、面接時にわからなかった特定の分野に関する知見の深さを同じチームで一緒に働くことで気づき、その人の凄さを初めて知ることが有りました。

この章のすべてが好きなのですが、特に好きなのは次の一節です。

何かをうまくやるためには、それを愛していなければならない。ハッキングがあなたがやりたくてたまらないことである限りは、それがうまくできるようになる可能性が高いだろう。14歳のときに感じた、プログラミングに対するセンス・オブ・ワンダー1を忘れないようにしよう。今の仕事で脳みそが腐っていってるんじゃないかと心配しているとしたら、たぶん腐っているよ。

大学での勉強や研究室配属後の取り組みを通して感じた、プログラミングの面白さやWebサーバ・ネットワークの仕組みを知ったときの感動が原体験となって私を形作っています。

初版が2005年2007年2と古い本なので、エピソードはコンピュータ黎明期の話が多かったりしますが、ハッカーのマインドに関する説明などは今でも通じる部分は多いかと思います。ハッカーの考え方を理解したい人やコンピュータを扱う世界にいる人、飛び込もうとしている人には特におすすめしたい一冊です。

UNIXという考え方

バックエンド開発を担当している金丸です。

この本はUNIXというOSの背後にある基本的な考え方を知ることができる一冊です。 UNIX自体の利用方法やコマンドについての説明はほとんどなく、UNIXがどのような思想に基づいて作られたかが説明されています。

本書と出会ったのは、新卒入社した会社で、情シスとしてFreeBSDを利用したサーバー管理の業務に従事しているタイミングでした。 当時の私は初めてのCLIに四苦八苦しており、ファイルをコピーするシェルスクリプトを作成するのにも苦戦していました。 「なんでコピー完了したことを教えてくれないんだろう」と同僚に話していたところ、この本を薦められました。

当初は疑問の答えを求めて読み進めていましたが、疑問の答えだけでなく、システムをどのように設計すべきかの指針も学ぶことができました。 プログラミング経験がなかった当時の私にとって、UNIXの考え方は初めて自分が理解できる内容で納得感のあるものでした。

この本は「定理」という形でUNIXの思想を説明しています。

9つの定理を紹介していますが、私の中で特に印象に残ったのは次の3つです。

  • 定理1: スモール・イズ・ビューティフル
  • 定理2: 一つのプログラムには一つのことをうまくやらせる
  • 定理3: できるだけ早く試作を作成する

定理1と2ではUNIXというソフトウェアの大前提となる部分で、互いを補完している関係にあります。 この定理により、シンプルなコマンドを自由に組み合わせて処理を行うことができます。

例: ls, awk, sort コマンドを組み合わせて、ディレクトリ内のファイルを名前順に並べて表示するシェルスクリプト

$ ls -l | awk '{print $9}' | sort

スクリプトで利用されている ls コマンドはディレクトリが空の場合、何も表示せずプロンプトに戻ります。 これにより、次に組み合わせるコマンドに必要な情報だけを渡すことができ、コマンド同士がスムーズに連携できるようになっています。

$ ls
$

この設計思想を知ったとき「なるほど!」と、非常に納得感がありました。 不要なメッセージを出さないことで、コマンドの組み合わせが直感的で柔軟にできるという点にUNIXの考え方への感銘を受けました。

上記の考え方を通じ、疑問と思っていた cp コマンドの役割はコピーすることであり、その機能のみをもつことが、定理2の「一つのプログラムには一つのことをうまくやらせる」に即していると理解しました。 合わせて、完了メッセージが必要であれば、コマンドを組み合わせて出力するのがUNIXらしい考え方だと解釈しました。

定理3では、プロトタイプを活用した開発の重要性を説いています。

この定理で紹介された次の一節が特に印象に残っています。

製品の完成後に百万のユーザーから背を向けられるより、少数から批判を受けるほうがはるかにいい。

当時の私は自分の仕事が批判されたように捉えてしまっていたため、プロトタイプを社内レビューで見せることに抵抗がありました。 ですが、この定理を読んで、自己本位の開発になっていることに気が付き、これではダメだと衝撃を受けました。 それ以来、プロダクトを誰のために作っているのかという意識を持つようになりました。

現在でも、機能の根幹となる部分から優先的に開発し、早い段階からPdMに都度確認してもらいながら実装を進めるスタイルを取っています。 実際の画面を確認していただくことで必要な情報が欠けていたことに気づくこともあり、細かく試作することでより良いプロダクトを作ることができると感じています。

紹介される定理はいずれもシステム設計の指針を示しており「設計の思想とは何か」を本書を通じて学ぶことができます。

設計を担当される方はもちろん、設計の指針となる考え方を学びたい方にもおすすめの一冊です。

まとめ

いかがでしたでしょうか?

偶然ですが、今回はどれもメンバーの初期キャリアに影響を与えた本の紹介となりました。 このブログを読んでくださった方で、そのような本がある方も多いのではないでしょうか。 久々に読み返してみると、原点に立ち返ったり、新たな気づきを得ることができるかもしれません。

ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers


  1. ここでいうセンス・オブ・ワンダーとはプログラミングに触れたときに感じた感覚や感動を意味していると私は解釈しています。
  2. 正しくは2005年でした。はてぶのコメントでご指摘があり気が付きました。ありがとうございます!

Nx活用術!Larger runnerの動的設定でGitHub Actionsのコスパ改善!

ファインディ株式会社でフロントエンドのリードをしている新福(@puku0x)です。

皆さん、GitHub ActionsのLarger runnerはご存知でしょうか?

高性能なマシンを使ってCIを実行できる一方、変更の少ない場合や計算負荷の低いCIではコストパフォーマンスが悪くなってしまいがちですよね?🤷‍♂️

この記事では、Nxの機能を利用してLarger runnerを動的に切り替える方法をご紹介します。

Nxについては以前の記事で紹介しておりますので、気になる方は是非ご覧ください。

tech.findy.co.jp

Larger runner(より大きなランナー)

Larger runnerは、「GitHub Teamプラン」または「GitHub Enterprise Cloudプラン」の場合に利用可能です。

docs.github.com

プライベートリポジトリ用の通常のランナー(GitHub-hosted runner)は、Linuxマシンの場合、CPUは2コアとなりますが、Larger runnerでは4コアや8コア、16コアなどより高いスペックのマシンを選択できます。

Larger runnerの例

最近はArmベースのCPUも利用できるようになり、x64ベースのCPUを使う場合よりもコストを抑えられるようになりました。積極的に使っていきたいですね。

課題

Larger runnerは強力ですが、その分コストがかかります。

コードの変更が少ない場合や、CI全体が数分で終わってしまうような場合では、せっかくの高いスペックも宝の持ち腐れとなってしまうでしょう。

負荷が高い時だけLarger runnerのスペックを上げるにはどうすれば良いか? というのが今回の課題となります。

現在のGitHub Actionsには、変更の規模や負荷に応じて動的にランナーを切り替える機能が標準で備わっていないため、自分で組まなくてはいけません。

解決策

単純に変更されたファイル数や行数をカウントしても実際の影響範囲とは乖離があるため、より高度な制御が必要となります。

✨そこで、Nxの登場です。✨

Nxはモノレポ内のプロジェクトの依存関係を Project Graph として保持しており、コードの変更から影響範囲を割り出すことが可能です。

次のコマンドを実行すると、影響されるプロジェクトをJSON形式で取得できます。

npx nx show projects --affected --json

show - CLI command | Nx

あとは実行結果をパースしてLarger runner切り替えの条件を組めば実現できそうです。

ワークフローの例を示します。

on:
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      runs_on: ${{ steps.output.outputs.runs_on }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - uses: nrwl/nx-set-shas@v4
        with:
          main-branch-name: ${{ github.base_ref }}
      - run: npm ci
      - name: Get affected projects
        id: get_affected_projects
        # 1. 影響されるプロジェクト数を算出
        run: |
          length=$(npx nx show projects --affected --json | jq '. | length')
          echo "length=$length" >> "$GITHUB_OUTPUT"
      - name: Output
        id: output
        # 2. 影響されるプロジェクト数に応じたLarger runner名をセット
        run: |
          if [ ${{ steps.get_affected_projects.outputs.length }} -gt 20 ]; then
            echo "runs_on=arm64-4-core-ubuntu-22.04" >> $GITHUB_OUTPUT
          else
            echo "runs_on=arm64-2-core-ubuntu-22.04" >> $GITHUB_OUTPUT
          fi

  build:
    needs: check
    # 3. 指定されたLarger runnerで実行
    runs-on: ${{ needs.check.outputs.runs_on }}
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - uses: nrwl/nx-set-shas@v4
        with:
          main-branch-name: ${{ github.base_ref }}
      - run: npm ci
      - run: npx nx affected --target=build

actions/checkoutactions/cache は適宜最適化しましょう(1分ほど速くなる余地あり)

ワークフローを実行すると、まず影響されるプロジェクト数が算出されます。

後続のジョブでは、その結果を元に runs-on: ${{ needs.check.outputs.runs_on }} で利用するLarger runnerを設定します。

やりたかったことが実現できていますね!🎉

負荷の高い時は高スペックのLarger runnerが動くため、CI時間の短縮が見込めます。負荷が低い時は、Larger runnerのスペックを落としてコストを節約できます。

結果

直近100回のCI結果を元に、4コアマシン固定の場合と2〜4コア可変の場合でコストを計算してみました。

Before(4コア固定) After(2〜4コア可変)
$6.53 $4.79

※ArmベースのCPUを使用する想定でコストを算出しています
※直近100回中、約1割が高負荷なCI(約12分)、残りが低負荷なCI(約5分)でした

Larger runnerを動的に切り替える方法を採用することにより、コストを3割ほど削減できました。

以前の状態と比較してコストパフォーマンスが改善されたと思います。

まとめ

この記事では、Nxの機能を活用してLarger runnerを動的に設定することで、コストパフォーマンスを改善する方法を紹介しました。

今回は runs-on の切り替えのみ紹介しましたが、他にもNx CLIの --parallel オプションの動的設定など応用は様々です。

皆さんの参考となりましたら幸いです。


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers

Findyの爆速開発を支える、価値提供を最優先にするための開発手法

こんにちは。

Findy で Tech Lead をやらせてもらってる戸田です。

このテックブログでは開発生産性を向上させるための取り組みや、開発テクニックを紹介してきました。

意外に思われるかもしれませんが、弊社では全てのことを100%でやってるわけではなく、ユーザーへの価値提供を最優先するために後回しにしている部分もあります。

しかし、その影響で障害が多発したり、困ったことになることは滅多にありません。

そこで今回は、ユーザーへの価値提供を最優先するために弊社で実践していることを紹介していこうと思います。

それでは見ていきましょう!

綺麗なコードは後。アプリケーションの振る舞いが先。

Pull requestをレビューする中で、「もっと良い書き方がありそうで、議論が長引いて中々マージされない」といったケースがあるかと思います。

そのような場合、弊社ではテストが網羅されていればマージしてOKというスタンスを取っています。

もちろん、パフォーマンスやセキュリティ等に悪影響が出るようなコードはマージしませんが、基本的にはアプリケーションの振る舞いに影響がなければマージします。キレイなコードを書くことは非常に重要ですが、ユーザーが求めているのはアプリケーションの振る舞いであるからです。

マージ後に良い書き方を思いついたら、そのタイミングでリファクタのみの修正を行ったPull requestを出します。

後でリファクタするとしても、アプリケーションの振る舞いをテストコードで守っているので、強気でリファクタできます。

弊社のとあるリポジトリでは、リファクタのPull requestが1ヶ月で50個程度作成されていました。思いついたり気づいたりしたら日常的にリファクタを行う文化が根付いている証拠です。

このように、最初の段階で綺麗なコードを突き詰めることよりも、ユーザーに早く価値提供をすることを優先しています。テストコードの存在が、このような開発手法を許しているのです。

コミットの粒度は不問。Pull requestの粒度は維持。

Pull requestの粒度については以前、別の記事 で触れましたが、コミットの粒度に関してはレビュー対象にはしていません。

コミットの粒度まで指摘してしまうと気軽に修正できなくなり、開発そのものを楽しむことが難しくなってしまうと考えているからです。

もちろん、試行錯誤のコミットが多すぎてgitのログに悪影響を及ぼすと判断したらrebaseなどで纏めることはありますが、基本的にローカル環境では自由に色々と試して欲しいと考えています。

その代わり、コミットメッセージには一定のルールを設けています。コミットメッセージのルールやprefixには Conventional CommitSemantic Versioning などがあります。

例えば既存のAPIに対する機能追加があった場合のコミットメッセージは、

minor-feat: add hoge feature

のようになります。

コミットメッセージのprefixに悩むということは、1つのコミットの中でたくさんのことをやりすぎているということに気づくことができます。

このようにコミットメッセージにルールを設けることによって、自然に一定内のコミットの粒度を維持できるようになっています。そのため、コミットの治安が悪くなることはほとんどありません。

実装途中のコードでもマージOK

実装途中のコードであっても、次の条件の全てを満たす場合はマージを許容しています。

  • テストコードがあり、CIが通っている
  • 本番環境の振る舞いに影響がない
  • 実装途中であることが、コードやPull requestのコメントなどで明確になっている

対応スコープ内の全てが完了してからマージするのではなく、Pull requestを細かく作成し、少しずつ作成、修正をしていくような流れです。

マージしてしまうとどうしても本番環境の振る舞いに影響が出てしまう場合は、Feature Flagを使ったりtopic branch運用を行うこともあります。

Feature Flagやtopic branch運用に関しては、↓の記事を参照してください。

tech.findy.co.jp tech.findy.co.jp

対応予定の全てが完了するまでマージを止めてしまうと、base branchとの差分が大きくなりやすく、conflictや不具合が発生しやすくなり、ユーザーへの価値提供までのスピードが遅くなってしまいます。

本番環境で利用されないコードをマージしてしまうのは抵抗感があるかもしれませんが、本番環境の振る舞いに影響がないということは悪影響もないはずであり、ユーザーへの価値提供を最優先するためにこのような開発手法になっています。

本番環境への影響がないということについては、既存コードに対するテストコードによって守られているため、強気でマージ出来るようになっています。

まとめ

いかがでしたでしょうか?

開発組織によっては「そんなことやっていいの?」と思われるかもしれませんが、弊社ではユーザーへの価値提供を最優先するために、このような開発手法になっています。

もちろん、しっかりとしたテストコード、CI/CD環境が整っているからこそ、このような開発手法が可能となっています。

他にも、このような開発手法を支えている開発テクニックを別記事にて紹介していますので、興味がある方は是非読んでみてください。

tech.findy.co.jp tech.findy.co.jp

現在、ファインディでは一緒に働くメンバーを募集中です。

興味がある方はこちらから ↓ herp.careers

IT人材不足79万人の真因:生産性向上を阻む『人月の神話型請負』からの脱却

はじめに

こんにちは。ソフトウェアプロセス改善コーチでFindy Tech Blog編集長の高橋(@Taka_bow)です。

経済産業省の2019年発表によると、日本のIT人材不足が2030年には79万人に達する可能性があると予測され、しばしばメディアにも引用されてきました。

この調査レポート発表から5年以上が経過しましたが、果たして79万人という人材不足は現実となるのでしょうか?

今回は最新のデータからこの予測を検証してみたいと思います。

  • 2023年11月2日のNHKニュース www3.nhk.or.jp

  • 2024年7月9日 5:00 (2024年7月13日 17:40更新) 日経新聞 [会員限定記事] www.nikkei.com

「IT人材需給に関する調査」とは

このレポートは、みずほ情報総研株式会社が2015年に経済産業省からの受託調査研究として実施した調査レポートです。

2019年3月に経済産業省から発表されました。当時話題となったのは、このデータです。

出典:「IT 人材需給に関する調査」(経済産業省)(https://www.meti.go.jp/policy/it_policy/jinzai/houkokusyo.pdf

これは「今後、IT需要が高位に推移した場合約79万人の人材不足になる可能性がある」という試算でした。なお、需要が中位シナリオで約45万人、低位シナリオでも約16万人の不足する可能性があります。

この背景には2つの要因があります。

  • 日本の労働人口(特に若年人口)が減少(新卒IT人材の入職率は一定右肩上がり)

  • 日本の労働生産性が低い(2022年、OECD 加盟 38 カ国中 30 位)

日本の労働人口減少に関しては、IT業界に限らず日本全体の問題です。

冒頭のNHKニュース「日本のIT人材79万人が不足? インドで始まった人材獲得戦略」は、まさしくこの問題に対応するための動きのひとつと言えます。

労働生産性の低さ

さて、もうひとつの要因「労働生産性の低さ」ですが、次の表をごらんください。これは上記のグラフの一部を表にしたものです。

出典:「IT 人材需給に関する調査」(経済産業省)(https://www.meti.go.jp/policy/it_policy/jinzai/houkokusyo.pdf)を加工して作成

IT需要が高位と仮定したとき、

  • 赤で囲んだ部分:生産性上昇率が0.7%だった場合、78.7万人の人材不足
  • 青で囲んだ部分:生産性上昇率が5.23%だった場合、人材不足はゼロ

を表しています。生産性上昇率が5.23%以上アップしていれば人材不足は起きない、という予測です。

では、最新の労働生産性はどうなっているでしょう。

最新のデータを読む

現時点で最新の日本生産性本部が公表している「労働生産性の国際比較2023」から、情報通信業の労働生産性の推移分析を見てみました。

出典:日本生産性本部「労働生産性の国際比較2023」から情報通信業の労働生産性の推移 https://www.jpc-net.jp/research/detail/006714.html

日本の2000年から2021年にかけての生産性上昇率(年率平均)は「-0.1%」にとどまっています。これは、経済産業省が予測した平均成長率0.7%を大きく下回る結果です。

そして、調査レポートが予測する「78.7万人の人材不足」というシナリオよりも深刻な状況である可能性があります。

日本の特徴は、

  • 付加価値額は拡大傾向にある
  • 就業者数も同等ペースで増加している

ことから、付加価値が就業者数の増加に見合った成長をしておらず、生産性の上昇を抑えてしまっていると考えられます。

「人月の神話型請負」が生産性向上を阻む

なぜ、比較的安定推移すると言われる情報通信業において、日本の生産性上昇率は低いのでしょうか。

ひとつの要因として、日本におけるソフトウェア産業の構造的な問題が挙げられます。

まず、多くの企業はソフトウェア開発を外部の受託企業に委託する傾向があります。その際、事前に詳細な仕様を確定し、その通りに開発を進める「ウォーターフォール型」の手法が一般的です。*1

この手法では変更に柔軟に対応することが難しく、生産性向上に課題があると指摘されています。

さらに、多くの受託開発は「人月」に基づく見積もりと請負契約に依存しており、多重下請け構造が蔓延しています。この構造は、柔軟な対応や効率的な開発を妨げる要因となっています。

私はこれを「人月の神話型請負」と呼んでいます。

  • 定義:「人月による見積もりを前提とし、事前合意の仕様とプロセスを厳守するため柔軟な変更対応が難しい請負契約」

「ブルックスの法則」や「銀の弾などない」で有名なフレデリック・ブルックスは、著書「人月の神話(The Mythical Man-Month)」の中で、このように述べています。

私たちが使っている見積もり手法は、コスト計算を中心に作られたものであり、労力と進捗を混同している。人月は、人を惑わす危険な神話である。なぜなら、人月は、人と月が置き換え可能であることを暗示しているからである。(第二章「人月の神話」)

この本が書かれたのは1975年、私でさえ幼少期であり、読者の多くは生まれる前の話ではないかと想像します。

しかし、現代日本のIT業界では「人月で見積もる」が未だに健在である事実に着目せざるをえません。

なぜなら「人月の神話型請負」には、次のような負の側面があると考えられるからです。

負の側面 内容
要求仕様の硬直化 事前合意した仕様に縛られ、 状況に応じた柔軟な改善が難しい
形式的なプロセス管理 決められた手順の遵守が優先され、効率化や創意工夫の余地が少ない
人月ベースの評価 投入工数で報酬が決まるため、生産性向上への意欲が生まれにくい
人員増加による解決 課題への対応を人員増員で行うため、チームの効率が低下しやすい
エンジニアの裁量不足または制限 顧客からの指示が中心となり、技術的な改善提案を行いにくい
技術力の蓄積不足 技術資産が顧客側のものであり、自組織内にナレッジが貯まらない

これらの負の側面は、開発チームの「自立」を損ない、エンジニア個人の「自律」も抑え込みます。

結果として、組織全体の生産性が低下し、改善や変革が難しくなる要因となります。

また、「人月の神話型請負」の問題は、すぐには目に見えにくい「遅効性の毒」のように、ゆっくりと組織全体を蝕むのが特徴です。

この構造的な問題に対処するためには、柔軟で価値を重視した新たな開発体制への転換が不可欠です。少なくとも、次の2点を変えることが必要だと思います。

  • 事前確定型から状況適応型の開発プロセスへの移行

  • 形式的な遵守よりもビジネス価値を重視した評価の導入

つまり、「人月の神話型請負」から脱却し生産性向上を図るには、アジャイル開発を基盤に据え、委託側と受託側双方で「価値を共に創り出す体制」を築くことが重要です。

受託開発でもアジャイル開発はできる

しかし、「人月の神話型請負」をビジネスモデルの柱としてきた多くのソフトウェア受託企業にとっては、どこから手をつけるべきかが大きな悩みだと思います。

そんな難題に光を当てるヒントが、2024年10月30日に開催された「プロジェクト成功への挑戦」というイベントで共有されています。

イベントでは、株式会社永和システムマネジメント Agile Studio プロデューサー/アジャイルコーチの木下史彦さん(@fkino)が、「アジャイル開発と契約」のテーマで、主に「モデル契約」についてお話しくださいました。

木下さんは、従来型の契約が「決めたことを守る」ことを重視するのに対し、アジャイル開発における契約では「変化に対応する」柔軟な仕組みが重要であると説明。

さらに、IPAのモデル契約を例に、準委任契約の採用や、スプリントごとの計画調整が可能な体制づくりをどう進められているかについても詳しく解説されました。

speakerdeck.com

続いて、株式会社永和システムマネジメントのエンジニア、藤田みゆきさんからは「受託開発でのアジャイル奮闘記」と題して、実際の受託開発におけるアジャイル導入の取り組みをご紹介いただきました。

speakerdeck.com

永和システムマネジメントの皆様が示してくださった変化に対応する具体的なアイデアや実践方法は、『人月の神話型請負』から脱却するための大きな一歩となると感じました。

以下に、イベントの録画ビデオもリンクしていますので、ぜひご覧ください。

youtu.be

お知らせ!

現在、私と一緒にイネイブリングチームの立ち上げを行うメンバーを探しています!

イネイブリングチームは、エンジニア組織だけではなくファインディ社全体を支援するチームとしていく予定です。

  • 組織全体のエンジニアリング力を向上
  • 開発スキル向上のためのトレーニングやワークショップを実施
  • プロセス改善の提案とコーチングを行い、開発生産性とDevExを向上
  • 社内外のエンジニアを対象とした活動を展開

このチームで、ファインディの成長エンジンとなりませんか?興味がある方は、ぜひこちらをクリックしてみてください↓

herp.careers

エンジニアのポジションは他にも色々あります。ファインディでは一緒に働くメンバーを募集中です!

herp.careers

では、本日はこのへんで。

See you!

*1:ウォーターフォール型の元となったと言われるウィンストン・W・ロイスによる論文”Managing the Development of Large Software Systems(1970)”では「手戻り」が推奨されています

Findyの爆速開発を支えるFeature Flagの使い方

こんにちは。

ファインディでソフトウェアエンジニアをしている栁沢です。

ファインディの各プロダクトでは、1日に複数回デプロイしています。

例えば、私が所属するFindy転職のプロダクトでは、1日に6回ほど本番環境にデプロイしています。

高いデプロイ頻度でもデプロイ起因による障害や不具合がほぼ発生しておらず、開発スピードと品質の両立を実現できています。

今回はファインディ社内でのFeature Flagの使い方について詳しく解説します!

Feature Flagを使うことのメリット

Feature Flag(フィーチャーフラグ)は、コードを書き換えることなく特定の機能を有効化や無効化できる開発テクニックです。

Feature Flagで機能を無効化しておくことで、ユーザーに影響がでないように本番環境にコードをガンガン反映させることができるようになります。

これにより次のようなメリットがあります。

  • 開発途中でもどんどんメインブランチにマージできる。そのため、複数人の開発でもコンフリクトがほぼ起こらない。
  • 本番環境で特定のユーザーやセグメントのみに機能を限定公開できるので、リスクを抑えながら機能を公開できる。
  • 万が一本番環境で問題があった場合、機能を無効化させることですぐに切り戻しができる。

Feature Flagの実現方法

Feature Flagの実現方法は、ライブラリやSaaSを使うなど様々な方法がありますが、現在のFindyでは「環境変数」と「ライブラリ」の2パターンが採用されています。

Findy転職プロダクトでは、導入や運用がシンプルな「環境変数」を使ってFeature Flagを実現しています。

  • 環境設定ファイルで環境変数を追加することで機能を有効化させる
  • 環境変数の値で条件分岐を実装して機能をだし分ける

実際のコード例を見てみるとシンプルなことがわかると思います。

envファイルに環境変数を追加することで機能を有効化させます。逆に、環境変数を削除することで機能を無効化できます。

# .env

FEATURE_NEW_LABEL=true

実装コードは、環境変数の値によって条件分岐を実装することで振る舞いを変えています。

export const SampleComponent = () => {
  // 特定の環境変数の値が'true'なら"NEW"というラベルを表示する
  const isEnabledNewLabel = process.env.FEATURE_NEW_LABEL === 'true';

  return (
    <div>
      {isEnabledNewLabel && <span>NEW</span>}
      <span>Sample</span>
    </div>
  );
};

補足:この実現方法の伸びしろとしては、「環境設定ファイルの修正が手間」、「フロントエンドとAPIでFeature Flagが分散している」、「一部のユーザーに部分的に公開がしづらい」という課題があります。そのため別チームでは、ライブラリを使ってAPIから機能のON/OFF情報を取得する仕組みでFeature Flagを実現しているようです。

Feature Flagを使った開発の流れ

ここからは、より具体的にFeature Flagを使った開発の流れを説明していきます。

  1. Feature Flagを追加する
  2. Feature Flagを使って新機能を実装・テストコードを書く
  3. 検証用の環境で動作確認を実施する
  4. 動作確認が完了したら、本番環境で機能を有効化させる
  5. 一定期間の安定稼働を確認できたら、Feature Flagと条件分岐を削除する

1. Feature Flagを追加する

環境変数を追加し、ローカル環境や検証用環境で機能を有効化させます。

# .env.local や .env.qa

FEATURE_NEW_LABEL=true

Tipsとして、早めに検証用環境で機能を有効にしておくことでバグを早く発見しやすくなり、バグ対応の工数を減らせます。(シフトレフトを進められる)

2. Feature Flagを使って新機能を実装・テストコードを書く

環境変数の値によって条件分岐をすることで機能のON/OFFをできるようにします。

export const SampleComponent = () => {
  // 特定の環境変数の値が'true'なら"NEW"というラベルを表示する
  const isEnabledNewLabel = process.env.FEATURE_NEW_LABEL === 'true';

  return (
    <div>
      {isEnabledNewLabel && <span>NEW</span>}
      <span>Sample</span>
    </div>
  );
};

また、機能をON/OFFしたときに、それぞれのケースで振る舞いが壊れないように自動テストを書いておきます。

describe('SampleComponent', () => {
  it('should render "NEW" label when FEATURE_NEW_LABEL is true', async () => {
    process.env.FEATURE_NEW_LABEL = 'true';

    render(<SampleComponent />);

    expect(screen.getByText('NEW')).toBeInTheDocument();
  });

  it('should not render "NEW" label when FEATURE_NEW_LABEL is false', async () => {
    process.env.FEATURE_NEW_LABEL = 'false';

    render(<SampleComponent />);

    expect(screen.queryByText('NEW')).not.toBeInTheDocument();
  });
});

自動テストを書いておくことで、開発途中でもどんどんメインブランチにマージして、自信をもって本番環境にデプロイできます。

3. 検証用の環境で動作確認を実施する

機能開発が完了したら、検証用の環境で動作確認を実施します。

本番環境では機能が無効化されているため、ユーザーに影響ない形で検証を進めることができます。

4. 動作確認が完了したら、本番環境で機能を有効化させる

検証環境で動作確認が完了したら、本番環境で機能を有効化させます。

# .env.production

FEATURE_NEW_LABEL=true

本番環境で深刻な不具合が発生し、機能を緊急で無効にする必要がある場合は、環境変数を削除することで簡単に切り戻しができます。

# .env.production

# 削除
# FEATURE_NEW_LABEL=true

切り戻しを素早くできるのもFeature Flagの強みです。

5. 一定期間の安定稼働を確認できたら、Feature Flagの処理を削除する

最後に、機能が実際に使われて安定稼働していること確認できたら、環境変数と分岐の処理を削除していきます。

export const SampleComponent = () => {
  return (
    <div>
      <span>NEW</span>
      <span>Sample</span>
    </div>
  );
};

Feature Flagの削除時に間違って必要な処理を削除してしまいデグレをおこしてしまうミスはやりがちです。しかし、自動テストがあることで自信をもって削除作業を進められることができます。

まとめ

今回は、Findyの爆速開発を支えるFeature Flagの使い方を紹介しました。

Feature Flagを使うことで、ユーザーに影響ない形で、開発途中でもどんどんメインブランチにマージできます。これにより、本番環境へのデプロイも1日に複数回実施することが可能になります!

個人的に爆速開発を支える1つの要素として、Feature Flagの活用は必須だと感じています!

もしまだ導入していない場合は、この記事をきっかけにぜひトライしてみてください :)

他にもファインディではいろいろなテクニックを使っているので、興味がある方は次の記事も合わせて読んでみてください。

現在、ファインディでは一緒に働くエンジニアを募集中です。

興味がある方はこちらから ↓

herp.careers

AWS上のNext.js App RouterとCDNキャッシュ利用の課題と解決策

こんにちは。 Findy Toolsの開発をしている林です。

私たちのプロジェクトではフロントエンドのフレームワークにNext.js App Routerを使っており、AWSのECSへデプロイして運用しています。 そして、一部のレンダリングの処理が重いページのキャッシュを実装する際に、直面した課題と解決策を紹介します。

Next.jsのキャッシュ機構について

まず、Next.jsのキャッシュ機構について簡単に説明します。

Next.jsではサーバサイドで使えるキャッシュ機構が次の3種類あります。

  • Request Memoization
    • 同一リクエストの中で外部APIリクエストをメモ化する
  • Data Cache
    • 外部APIのレスポンスなどを一定時間キャッシュする
  • Full Route Cache
    • HTMLおよびRSC Payloadをユーザセッションまたは共通で一定時間キャッシュする

キャッシュの細かい仕様に関しては詳しくは公式ドキュメントを参照してください。

Building Your Application: Caching | Next.js

また、Full Route Cacheでは revalidatePath APIで任意のタイミングでキャッシュを削除できます。

Functions: revalidatePath | Next.js

今回実現したいこと

今回の要件は、一部の処理の重いページのレンダリング結果を丸ごとキャッシュしてレスポンスを高速化したいというものです。 これには、Findy Toolsはメディアサイトという側面もあり、多くのページは更新頻度がそれほど高くなく、フルページでのキャッシュがやりやすいという背景もあります。

さらに、ログインが必要なユーザ固有の情報はすべてクライアントフェッチをしているため、ユーザごとのキャッシュを考慮する必要もありませんでした。

また、エンドユーザーが利用するフロントエンドアプリケーションの他に、コンテンツ管理用のアプリケーションがあります。そして、この管理用アプリケーションからコンテンツが更新された際に、フロントエンド側にすぐに反映させたいという要件もありました。

補足:

現状のFindy Toolsのインフラ構成図は次の図のようになっています。 すべてAWS上に構築されており、CloudFrontを経由してNext.jsへリクエストを送っています。

また、コンテンツ管理用のアプリケーション(Rails)もECSへデプロイしています。

Findy Tools インフラ構成図

2ヶ月でリリースしたFindy Toolsの技術選定の裏側 - Findy Tools より

課題と解決策

今回の要件を実現するにあたり、直面した2つの課題と解決策をそれぞれ紹介します。

課題1: Next.jsの機能では要件に合わない

まず、ページのレンダリング結果を丸ごとキャッシュしたかったので、Full Route Cacheを検討しました。

しかし、セルフホスティングのNext.jsではキャッシュをメモリと.next/cache ディレクトリに保存しているため、ECSのコンテナを水平スケールして複数台でリクエストを受ける構成にすると、キャッシュの一貫性がなくなります。

負荷軽減目的のキャッシュで、一貫性が問題にならない場合はこれでも良いですが、キャッシュの削除をする revalidatePath が全てのコンテナで実行されず、一部で古いキャッシュが残るのは今回の要件を満たせません。

このため、ドキュメントにはコンテナオーケストレーションプラットフォームではRedisやS3のような外部のストレージにキャッシュを保存するようcache-handlerを実装する必要があると紹介されています。

Building Your Application: Deploying | Next.js

Data CacheやFull Route Cacheをフルに活用したい場合には、カスタムのcache-handlerを用意しても良いと思いますが、今回の要件にはオーバースペックすぎると判断しこれは見送りました。

また、Next.jsのFull Route Cacheを使った場合にもレスポンスヘッダーに Cache-Control: stale-while-revalidate が付与されるため、 削除の際にcache-handlerのデータと後術するCDNのキャッシュの2つを削除する必要もあり、複雑になるということもFull Route Cacheを見送った理由の1つです。

解決策1: CloudFrontのみでキャッシュ

私たちの構成ではNext.jsへのリクエストは静的ファイルを配信するため、CDNであるCloudFrontを経由させています。

そこで、Next.jsのアプリケーションレベルではキャッシュをせず、CDNでレンダリングされたHTMLやRSC Payloadをキャッシュすることにしました。

当初、Next.js App Routerでは特定のページにカスタムのCache-Controlヘッダーを付与するにはmiddlewareの処理で対処する必要がありましたが、v14.2.10 からPagesRouterと同様に next.config.jsheaders() で指定できるようになりました。

Add ability to customize Cache-Control by ijjk · Pull Request #69802 · vercel/next.js

next.config.js で特定のパスにCache-Controlヘッダーを設定するコードの例を以下に示します。

module.exports = {
  async headers() {
    return [
      {
        source: '/path/:slug',
        headers: [
          {
            key: 'Cache-Control',
            value: 's-maxage=86400, stale-while-revalidate',
          },
        ],
      }
    ]
  },
}

これで、CloudFrontの設定でオリジンのCache-Controlヘッダーを元にキャッシュするよう設定すると、CDNでレスポンスをキャッシュができます。

コンテンツが更新された際のキャッシュの削除は、CloudFrontのAPIで特定のパスのキャッシュを削除するようにコンテンツ管理用アプリケーションから呼び出しています。

課題2: エラーページがキャッシュされる

CloudFrontではHTTPレスポンスコードが404や一部の5xxの場合も、Cache-Controlヘッダーに基づいてキャッシュされる仕様になっています。

CloudFront がオリジンからの HTTP 4xx および 5xx ステータスコードを処理する方法 - Amazon CloudFront

特に私たちのプロジェクトでは、公開前のコンテンツのURLにアクセスされることがあり、404ページがキャッシュされてしまうという課題がありました。

公開前にキャッシュの削除をすれば良いのですが、時間を指定して公開するケースではキャッシュの削除が複雑になってしまいます。

一見するとNext.jsでバックエンドのAPIから情報を取得して、404やエラーの時はCache-Controlヘッダーをno-cacheに指定すれば解決しそうなのですが、Next.jsではレスポンスをストリーミングしており、リクエストの最初に処理されるmiddlewareやnext.config.jsのheadersの設定を条件に基づいて変更できません。

これに関してはページごとにカスタムヘッダーを設定できるような generateHeaders() というAPIが必要か、という議論がNext.jsのGitHub Discussionsで行われています。

App Router Custom Header Use Cases · vercel/next.js · Discussion #58110

解決策2: Lambda@Edgeを用いたCache-Controlヘッダー制御

Next.jsではCache-Controlヘッダーを条件に基づいて変更することが出来ないため、別のアプローチとしてLamda@EdgeでステータスコードによってCache-Controlヘッダーを書き換えることにしました。

Lamda@EdgeはオリジンとCloudFrontのキャッシュ(Regional Edge Cache)の間で動作し、リクエストやレスポンスを操作することが出来ます。

次のコードはオリジンのNext.jsのレスポンスを受け取り、ステータスコードが4xx, 5xx系の時にCache-Contorolヘッダーをno-cacheに上書きしてCloudFrontへレスポンスを返すような例です。

'use strict';

export const handler = async (event, context, callback) => {
  const response = event.Records[0].cf.response;
  const headers = response.headers;

  if(response.status >= 400 && response.status <= 599) {
    headers['cache-control'] = [{
        key:   'Cache-Control', 
        value: "private, no-cache, no-store, max-age=0, must-revalidate"
      }];
  }
  
  callback(null, response)
};

これにより、Next.jsのステータスコードによってCache-Controlヘッダー書き換え、CloudFrontにキャッシュしないということが実現可能になりました。

まとめ

この記事では、App Routerを用いた場合の「コンテンツ更新の際のキャッシュ削除」という課題に対して次の工夫をして、エラーレスポンスまで含めた柔軟なキャッシュ制御を実現しました。

  • AWS CloudFrontを用いてApp Routerのレスポンスをキャッシュ
  • Lambda@Edgeを用いてCache-Controlヘッダーを書き換え

また、レンダリングの重いページや更新頻度の低いページをCloudFrontから返すことで、レスポンスタイムが改善され、ユーザ体験の向上やサーバ負荷の軽減も達成できました。

Next.js App Routerは一部の機能においてまだ発展途上ですが、将来的に、セルフホスティングやCDNでのキャッシュが扱いやすくなることに期待ですね。

ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers