# プロジェクトアーキテクチャ

この章では、フロントエンドのプロジェクトのアーキテクチャについて見ていきたいと思います。一般的にプロジェクトのアーキテクチャは、モノリス、マイクロサービス、モジュラーモノリスなどがあります。バックエンドではマイクロサービスが一般的になりましたが、フロントエンドでもマイクロフロントエンドというアーキテクチャが採用されつつあります。アーキテクチャでは、フロントエンドで一般的なモノリスの構成とマイクロフロントエンドについて見ていきたいと思います。 また、リポジトリの構成では、ポリレポとモノレポの違い、モノレポの実装方法、CIの実装などを検証したいと思います。

# アーキテクチャ

# モノリス

モノリス(モノリシックアーキテクチャ)は、単一の独立したアーキテクチャを指します。1つのアプリケーションに対して単一のコードベースで管理し、チーム全体で同じリポジトリを開発します。また、リリースも単一のデプロイメントで管理します。APIへの接続は、複数または、BFFのように単一のパターンがありますが、フロントエンドの実装部分は単体のコードベースで管理します。

プロジェクトアーキテクチャ-モノリス-1.jpg

# メリット

モノリスは、単一のコードベースで管理するため、認知コストを下げることができます。一つのリポジトリにコンポーネントやAPI接続の実装が全て収まっているため、開発の初期段階では、設計しやすいというメリットがあります。また、デプロイも単一になるため、CI/CDの構成がシンプルになります。チーム開発においても、同じコードを共有するので、追加機能やリファクタリングのレビューがしやすいというメリットがあります。そのため、小規模から中規模なアプリケーションで、スピード重視の開発の場合は、モノリスが有効なアーキテクチャになるでしょう。

# デメリット

モノリスのデメリットは、コードベースが肥大化してしまうことです。開発メンバーが増え、機能が増えていくことでコードベースが段々と大きくなっていきます。コードが肥大することで、シンプルだった構成が複雑になり、様々な問題が発生する可能性があります。同じようなコンポーネントや処理が増えたり、コードが複雑化して不具合の特定が難しくなったり、テストが遅い、依存関係が原因でライブラリのバージョンアップができなかったり、などのケースが発生します。

また、少しの修正でもアプリケーション全体のデプロイをする必要があるため、リリースまでのリードタイムが増加する可能性があります。 モノリスは、小規模から中規模なアプリケーションで有効ですが、大規模なアプリケーションではデメリットの方が大きくなる傾向があります。チームのガイドラインを定めることである程度回避をすることはできますが、大規模になってくると難しい場合もあるでしょう。

# モノリスの実装

では、フロントエンドのモノリスの例を見てみましょう。

次のようなNext.jsで構成されたDashboardアプリケーションを想定してみます。

プロジェクトアーキテクチャ-モノリス-2.png

ページは、DashboardButtonsFormsAlertsRatingsImagesPaginationTablesとあり、一つのリポジトリで管理します。プロジェクト構成は次の通りです。

dashboard-app
├── package.json
├── pages
│  ├── _app.js
│  ├── _document.js
│  ├── alerts.js
│  ├── buttons.js
│  ├── forms.js
│  ├── image.js
│  ├── index.js
│  ├── pagination.js
│  ├── rating.js
│  └── table.js
├── public
│  ├── favicon.ico
│  ├── static
│  └── vercel.svg
├── src
│  ├── components
│  ├── pages
│  ├── shared
│  ├── styles
│  └── theme
└── yarn.lock

プロジェクト構成は、Next.jsのテンプレートで展開されています。ページのルーティングは、pagesに定義し、実装コードはsrcに展開しています。APIエンドポイントを追加する場合は、apiフォルダに作成することができます。

モノリスの場合、パッケージや、共有モジュール、コンポーネントなど全てのコードを一つのフォルダに格納して管理します。これぐらいのページ数だと見通しが良く、管理しやすいと思います。しかし、もし数十ページと増えていくと、扱うコンポーネントも大量になるためメンテナンスが難しくなるでしょう。サービスやチームが大きくなるにつれて、プロジェクト構成も再検討する必要があります。

# マイクロフロントエンド

マイクロフロントエンドは、アプリケーションを細分化し、チームごとに独立させて開発するためのアーキテクチャです。 モノリスでは、コードベースやチームが大きくなるにつれて、メンテナンスコストが増加するというデメリットがありました。機能やページが増えるごとにシステムの複雑性が増し、リリースサイクルが遅れる可能性が発生します。

プロジェクトアーキテクチャ-マイクロフロントエンド-1.jpg

マイクロフロントエンドは、このようなモノリスの課題を解決するために登場しました。マイクロサービスの考え方をフロントエンドにも拡張し、単一のチームにバックエンド、フロントエンド、デザイナーが一緒になって独立した開発を実現すること目指します。

プロジェクトアーキテクチャ-マイクロフロントエンド-2.jpg

マイクロフロントエンドの最大のメリットは、チームを細分化することで、柔軟な開発体制を実現することです。機能ごと、あるいはページごとにチームを分割することで特定のドメインを持ったチームで開発することができます。そのため、設計コストや運用コストを抑えることができ、開発効率を上げることができます。各サービスが扱う技術やフレームワークもそれぞれが選ぶことができ、デプロイも独立して実行可能となります。

マイクロフロントエンドのリポジトリ構成は、ポリレポかモノレポを選択することができます。ポリレポの場合は、各サービスのリポジトリを独立して開発します。モノレポの場合は、一つのリポジトリでサービスを分割して開発します。それぞれメリット、デメリットがありますが、モノリスから移行する場合は、同じバックエンドAPIを持つことが多いので、モノレポの方が効率的な場合があります。例えば、バックエンドAPIの接続情報を共通化したり、LintやTypeScriptの設定を共有することができます。

モノリスで参考にしたアプリケーションを例に見てみましょう。

プロジェクトアーキテクチャ-モノリス-2.png

もし、ページごとにチームを分割する場合、DashboardButtonsFormsAlertsRatingsImagesPaginationTablesと分けることができます。

プロジェクトアーキテクチャ-モノリス-3.png

その場合、モノレポで作ると、次のようなプロジェクト構成で展開できます。

├── apps
│   ├── alerts
│   ├── buttons
│   ├── dashboard
│   ├── forms
│   ├── images
│   ├── pagination
│   ├── ratings
│   └── tables
├── package.json
├── pakcages
│   ├── components
│   ├── eslint
│   └── tsconfig
└── yarn.lock

apps以下は、それぞれのサービスを定義します。各サービスは独立して開発できるため、異なるフレームワークやライブラリを使用することができます。また、デプロイも単独で実行可能です。packagesには、共通のLintやTypeScriptの設定ファイルを定義します。また、横断的に使用する共通コンポーネントもここに配置します。

マイクロフロントエンドの場合、共通のコンポーネントやモジュール、設定ファイルが多くなる傾向があります。モノレポで構成する場合は、そのようなニーズを満たすことが可能となっています。だたし、分割する粒度やチームによって要件は異なるため、よく検討して導入する必要はあります。ポリレポとモノレポの詳細は、次のリポジトリ構成で詳しく見たいと思います。

マイクロフロントエンドの詳しい説明や実装方法は、アーキテクチャのMicro Frontendsの章をご確認ください。

# リポジトリ構成

# ポリレポ(Polyrepo)

ポリレポとは、マイクロサービスで、サービスごとにリポジトリを管理する手法です。バックエンドのマイクロサービスでは、一般的なリポジトリ構成になっています。バックエンドのマイクロサービスでは、異なる言語やフレームワークを選定する可能性があるため、リポジトリを分けていた方が運用しやすい場合があります。

フロントエンドにおいてもポリレポの構成は可能です。例えば、複数のサービスを展開するプロジェクトの場合、サービスごとにリポジトリを分けることができます。コンシューマー向けのアプリと、企業向けのCMSなど、別のリポジトリで管理するケースが当てはまるでしょう。 また、マイクロフロントエンドでも、各チームごとでリポジトリを分けるパターンも考えられます。

ポリレポは、リポジトリを完全に分けるため、チームの独立性を優先する場合に有効的です。共通のコンポーネントは、別のリポジトリで管理してNPMとして配信するなどのケースもあるでしょう。しかし、共通の処理が多かったり、同じバックエンドAPIを参照していたり、パッケージの管理を統一したいなどの場合はモノレポの方が効率的な場合があります。

このあたりはサービスの規模や要件によって異なるため、チームで検討する必要があるでしょう。

# モノレポ(Monorepo)

モノレポとは、複数のサービスを一つのリポジトリで管理する手法です。GoogleやMicrosoft、Stripeなどの企業で採用されているリポジトリ構成です。ポリレポがサービスごとにリポジトリを用意するのに対して、モノレポは全てのサービスを一つのリポジトリに集約し、モジュール化して管理します。一箇所で全てを管理する点ではモノリスと似ているかもしれませんが、モノレポは以下のような特徴を持っています。

  • モジュールの依存関係を適切に管理できる
  • サービスごとにデプロイできる
  • キャッシュでビルドの効率化ができる
  • 変更箇所の影響範囲を明確にできる

モノリスの場合、共通化したモジュールをどこでも使用することができるため、依存関係が複雑化しやすい傾向にあります。また、デプロイするたびにアプリケーション全体のビルドが必要になるため、リリースサイクルが遅くなる可能性があります。 しかし、モノレポでは、適切にモジュールの依存関係を整理し、共有することで、プロジェクト全体の効率化を実現することができます。

また、ポリレポではリポジトリを分けることで独立性を保てる反面、共通の設定ファイルがバラバラに管理されたり、新規サービスを一から立ち上げるコストなどの課題がありますが、モノレポは、共通の設定やコンポーネント、デプロイメントを共有できるため、サービスの立ち上げやしやすいというメリットがあります。

実際に、どのようにファイルが共有されるか見てみましょう。 一般的に、モノレポの構成は次のようになっています。

project-root
├── apps
│   ├── web
│   ├── api
│   └── cms
├── package.json
├── pakcages
│   ├── components
│   ├── eslint
│   └── tsconfig
└── yarn.lock

ルートのpackage.jsonで共通のライブラリを管理します。appsには各サービスを展開し、packagesはサービスで共有するための共通ファイルを配置します。例えば、apps/webpackages/tsconfigを使いたい場合は、packages.jsonに次のように書くことでインポートして使うことができます。

{
  "name": "@apps/web",
  "version": "1.0.0",

  "devDependencies": {
    "@packages/tsconfig": "*"
  }
}

NPMのパッケージをインストールするように、packages内のモジュールを使うことができます。サービスがNext.jsやAstroなど異なるフレームワークを使っていても、このようにインポートすることで共通の設定をプロジェクトで共有することができます。NPMパッケージとして公開すれば同じような構成をすることは可能ですが、変更を加える度にNPMの更新をしなければなりません。モノレポの方が、すぐに変更できるという点で、スピード感をもって開発することができます。

モノレポの実装は、npmやyarnのworkspaces (opens new window)機能を使いますが、それ以外にも次のような管理ツールがあります。

フロントエンド開発だと、Lerna、Nx、Turborepoを使うことが多いでしょう。この章では、Turborepoを例にモノレポの実装を見たいと思います。

# メリット

フロントエンド開発でのモノレポのメリットは、次のようなものがあります。

  • シンプルにできる
  • ライブラリの管理がしやすい
  • ESLintやTypescriptの設定を共有できる
  • 共通のモジュールを横断的に使える
  • テストの共通化ができる
  • フロントエンドとバックエンドのスキーマを共有できる
  • 新規サービスの立ち上げがしやすい

モノリスでは見通しが悪くなっていたコードを細かくモジュールに分けることで、シンプルな構成にすることができます。小さくする分、依存性が整理されてメンテナンス性が向上します。

モノレポはプロジェクトルートのpackage.jsonで依存関係の管理ができるため、共通で使用しているライブラリのバージョンアップがしやすくなります。また、ESLintやTypeScriptの設定ファイルを共有することで、開発の効率化を実現できます。

フロントエンドとバックエンドでTypeScriptを使っている場合、スキーマの型ファイルを共有することができます。ポリレポの構成でフロントとバックエンドが別の言語の場合、GraphQL Code Generator (opens new window)やOpen APIの自動生成、gRPCなどの仕組みを活用すれば同じことが実現可能ですが、言語を統一した方が実装コストを抑えて開発することができるでしょう。

プロジェクトによっては、複数のサービスを同時並行で進めることもあると思います。そのような場合、ポリレポでは一つ一つのサービスを立ち上げてセットアップする必要があります。ライブラリをインストールし、ESLintやTypeScript、テストの環境構築が必要になります。しかし、モノレポでは、すでに共有ファイルがあるのでそれらをインポートして使えば、同じ構成で新規サービスを始めることができます。そのため、プロジェクトの効率化とスピードを優先したい場合、モノレポの方が有効的になるでしょう。

# デメリット

次に、モノレポのデメリットを見てみましょう。

  • リポジトリが肥大化する
  • ビルドに時間がかかる
  • 複数の言語が混在すると、見通しが悪くなる

モノレポは、一つのリポジトリに全てのサービスやモジュールを管理するため、自然とコードベースが肥大化していきます。その分、関わるドメインも増えるため、IssueやPRの粒度、CIの調整が必要になります。

また、共通処理のビジネスロジックなどコアの部分が大きくなってくると、ビルド時間に影響を及ぼします。TurborepoやNxなどはキャッシュの活用でビルドの効率化をしていますが、全てのサービスで依存されているコードの場合、キャッシュが効かず遅くなる可能性があります。

また、なんとなくモノレポを導入して、関係のないドメインや複数の言語が入り混じるような状況になった場合、プロジェクト全体の見通しが悪くなる可能性があります。 モノレポで重要なことは、コードを適切に分けてシンプルに保つことです。コードをシンプルにすることで、依存関係を明確にし、影響範囲を限定的にコントロールできるようになります。共通化する必要のないものまでモジュール化しても、逆にメンテナンス性が下がる可能性があります。アプリケーションが複雑化し、メンテナンスするのが難しくなって初めてモノレポ化の検討をするのがいいでしょう。

# モノレポの実装

それでは、Turborepoを用いたモノレポの実装を見てみましょう。

Turborepoは、Vercelが開発したモノレポのビルドツールです。npm、yarn、pnpmのworkspacesをベースにモノレポを構築することができます。Turborepoは、モジュールの依存性の解決を自動的に行うため、実装者は依存関係の順番を意識することなくコードを書くことができます。また、各タスクのキャッシュ (opens new window)をすることで、ビルドやテスト、Linterなどの処理を効率化に実行することができます。

Turborepoでは、プロジェクト生成のCLIが用意されているためそれを使用します。

$ npx create-turbo@latest

生成されたファイルは次のような構成になっています。

apps
 - apps/docs
 - apps/web
packages
 - packages/eslint-config-custom
 - packages/tsconfig
 - packages/ui

appsがサービスで、packagesが共通で利用するモジュールになっています。apps/webpackage.jsonを見てみましょう。以下のようにuiモジュールがインストールされてます。






 


{
  "dependencies": {
    "next": "^13.4.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "ui": "*"
}

apps/web/app/page.tsxでは、このuiモジュールのコンポーネントをインポートして使うことができます

 










import { Button, Header } from "ui";

export default function Page() {
  return (
    <>
      <Header text="Web" />
      <Button />
    </>
  );
}

uiモジュールのButtonコンポーネントは以下のようなtsxファイルです。

"use client";

import * as React from "react";

export const Button = () => {
  return <button onClick={() => alert("boop")}>Boop</button>;
};

apps/webからは直接このButton.tsxファイルをインポートしてビルドしています。通常、WebpackやTurbopackなどのバンドラツールはnode_modulesやローカルパッケージをビルド対象としていないので、そのままtsxファイルを読み込むとエラーが発生してしまいます。

../../packages/ui/Button.tsx
Module parse failed: Unexpected token (3:9)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| import * as React from "react";
| export const Button = () => {
>   return <button>Boop</button>;
| };
|

これを回避すべく、Next.js13では、transpilePackages (opens new window)というオプションが用意されており、ここにパッケージを指定することで、ビルドを実行することができます。今回のようにuiモジュールをビルド対象とする場合は、next.config.jsで以下のように指定します。

module.exports = {
  transpilePackages: ["ui"],
};

サービス側でビルドを行わない場合は、uiモジュールでビルドを実行してjsファイルを配信する必要があります。今回の例だと、ui/package.jsonmainindex.jsに書き換えて、devスクリプトを追加します。




 




 












{
  "name": "ui",
  "version": "0.0.0",
  "main": "./index.js",
  "types": "./index.tsx",
  "license": "MIT",
  "scripts": {
    "lint": "TIMING=1 eslint \"**/*.ts*\"",
    "dev": "tsc --watch"
  },
  "devDependencies": {
    "@types/react": "^17.0.37",
    "@types/react-dom": "^17.0.11",
    "eslint": "^7.32.0",
    "eslint-config-custom": "*",
    "react": "^18.2.0",
    "tsconfig": "*",
    "typescript": "^4.5.2"
  }
}

これで、turbo run devすることでworkspaces内にあるdevスクリプトが実行され、uiモジュールのビルドも実行されます。yarn devすると次のように並列で実行されているのが分かります。

yarn dev

...
docs:dev: ready - started server on 0.0.0.0:3001, url: http://localhost:3001
web:dev: ready - started server on 0.0.0.0:3000, url: http://localhost:3000
ui:dev:
ui:dev: 10:27:16 - Starting compilation in watch mode...

このように事前にビルドしておくことで、サービス側は生成されたJavaScriptファイルをそのまま使用することができます。

続いて、スクリプトの依存関係やキャッシュの管理は、turbo.jsonで設定します。

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["lint", "build"]
    },
    "lint": {},
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Pipeline (opens new window)を設定することで、各スクリプトに対して依存関係のあるタスクを設定したり、キャッシュするアウトプット先を指定できます。

npmやyarnのworkspaces単体だと、ビルドや依存関係の解決などを自前で用意する必要がありますが、Turborepoではビルドの最適化やHMR、依存関係の解決などの機能を使うことができるので、効率的にモノレポの開発をすることができます。また、Remote Caching (opens new window)という機能もあり、クラウドサーバ上にキャッシュを保持し、複数の端末やCI環境でキャッシュを共有することができます。

プロジェクトアーキテクチャ-turborepo-1.webp

出典: Turborepo (opens new window)

# CIの実装

次に、モノレポにおけるCIの実装を見てみましょう。モノレポでは、フロントエンドのサービスやバックエンドのサービスが混在することがあります。その場合、変更がある度に全てのサービスをCI上で検証すると非効率になります。そのため、モノレポ環境では、変更されたファイルに応じてタスクを割り振る必要があります。この章では、GitHub ActionsとCircleCIでのモノレポの設定方法を見てみましょう。

# GitHub Actions

GitHub Actionsでは、pathsオプションを使うことで特定のファイルをフィルタリングすることができます。例えば、apps/webサービスの変更があったときだけビルドを実行したい場合は次のように書くことができます。

name: Web

on:
  pull_request:
    paths:
      - 'apps/web/**'

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v3
      - name: Setup Node.js environment
        uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: yarn

      - name: Cache node_modules
        uses: actions/cache@v2
        id: node_modules_cache_id
        with:
          path: node_modules
          key: v1-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
          restore-keys: |
            v1-yarn-

      - name: Install dependencies
        if: steps.node_modules_cache_id.outputs.cache-hit != 'true'
        run: yarn install --frozen-lockfile --silent

      - name: Lint
        run: yarn lint --filter=web

      - name: Build
        run: yarn build --filter=web

Turborepoでは、--filterオプションを使うことで、特定のタスクを実行することができます。今回のケースは、apps/webのビルドだけ実行したいので--filter=webと指定します。

      - name: Lint
        run: yarn lint --filter=web

      - name: Build
        run: yarn build --filter=web

同様に、apps/docsだけのworkflowを実行する場合は次のよう書くことができます。

name: Docs

on:
  pull_request:
    paths:
      - 'apps/docs/**'

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v3
      - name: Setup Node.js environment
        uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: yarn

      - name: Cache node_modules
        uses: actions/cache@v2
        id: node_modules_cache_id
        with:
          path: node_modules
          key: v1-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
          restore-keys: |
            v1-yarn-

      - name: Install dependencies
        if: steps.node_modules_cache_id.outputs.cache-hit != 'true'
        run: yarn install --frozen-lockfile --silent

      - name: Lint
        run: yarn lint --filter=docs

      - name: Build
        run: yarn build --filter=docs

それぞれのworkflowは、ファイルを分割して管理できるのでフロントエンド、バックエンド、モジュールごとのようにworkflowを設定することができます。

.github/workflows/
├── apps/frontend/docs.yml
├── apps/frontend/web.yml
├── apps/backend/api.yml
└── packages/ui.yml

# CircleCI

CircleCIで分割したタスクを実行するためには、ダイナミックコンフィグ (opens new window)circleci/path-filtering (opens new window)という機能を使います。ダイナミックコンフィグは、CI実行時に動的にconfigを生成する機能で、circleci/path-filteringは変更されたファイルに応じて特定のworkflowを実行するプラグインになります。

今回は、以下のようにサービスごとにconfigを管理し、変更があったコードに応じてサービスのbuildを実行したいと思います。

.circleci/
├── config.yml
└── workflows
    ├── docs.yml
    └── web.yml

複数のconfigをマージする機能は、circleci config packというCLIで実現可能ですが、ディレクトリ構成に制約があるため見送ります。より柔軟にマージできるように、yq (opens new window)というYAMLのマージツールを使用します。yqでworkflows以下のconfigをマージし、その設定ファイルをもとにCIを実行します。

まとめると全体的なフローは次の通りです。

  1. yqでworkflowsのconfigをマージする
  2. path-filtering/filterで変更のあったファイルをフィルタリングし、特定のworkflowを実行

TIP

CircleCIのダイナミックコンフィグを使うためには、.circleci/config.ymlsetup: trueを設定するのとProject Settingsで設定をONにする必要があります。

より詳しい情報は、CircleCI のダイナミックコンフィグの入門ガイド (opens new window)をご確認ください。

まずは、メインとなる.circleci/config.ymlを作成しましょう。

version: 2.1
setup: true
orbs:
  path-filtering: circleci/path-filtering@0.1.5

jobs:
  setup:
    docker:
      - image: cimg/go:1.20.4
    steps:
      - checkout
      - run:
          name: Install yq
          command: |
            go install github.com/mikefarah/yq/v4@latest
            yq --version
      - run:
          name: Merge config files
          command: |
            mkdir -p /tmp/
            yq eval-all '. as $item ireduce ({}; . * $item )' ./.circleci/workflows/*.yml > /tmp/merged.yml
            cat /tmp/merged.yml
      - persist_to_workspace:
          root: /tmp
          paths:
            - merged.yml

workflows:
  config:
    jobs:
      - setup
      - path-filtering/filter:
          requires:
            - setup
          workspace_path: .
          base-revision: main
          pre-steps:
            - attach_workspace:
                at: /tmp
          config-path: /tmp/merged.yml

          mapping: |
            apps/web/.* build-web true
            apps/docs/.* build-docs true

setupでは、Goのinstallでygをインストールし、workflowsのconfigをマージしています。実際にマージされたファイルは次の通りです。

version: 2.1
orbs:
  node: circleci/node@5.1.0
parameters:
  build-docs:
    type: boolean
    default: false
  build-web:
    type: boolean
    default: false
jobs:
  docs-build:
    executor: node/default
    steps:
      - checkout
      - node/install-packages:
          pkg-manager: yarn
      - run:
          command: yarn lint --filter=docs
          name: Run lint
      - run:
          command: yarn build --filter=docs
          name: Build app
  web-build:
    executor: node/default
    steps:
      - checkout
      - node/install-packages:
          pkg-manager: yarn
      - run:
          command: yarn lint --filter=web
          name: Run lint
      - run:
          command: yarn build --filter=web
          name: Build app
workflows:
  docs-build:
    when: << pipeline.parameters.build-docs >>
    jobs:
      - docs-build
  web-build:
    when: << pipeline.parameters.build-web >>
    jobs:
      - web-build

parametersに注目すると、build-docsbuild-webが設定されています。workflowsでは、このパラメータがあるときだけ以下のようにworkflowを実行するようになっています。



 



 



workflows:
  docs-build:
    when: << pipeline.parameters.build-docs >>
    jobs:
      - docs-build
  web-build:
    when: << pipeline.parameters.build-web >>
    jobs:
      - web-build

path-filtering/filterで、このマージされたconfigを指定します。mappingでは、変更されたファイルに応じてパラメータを設定します。

          mapping: |
            apps/web/.* build-web true
            apps/docs/.* build-docs true

これは、apps/web/以下のファイルに変更があった場合にbuild-webというパラメータにtrueを設定するという意味です。先ほどのマージされたconfigをもう一度見ると、build-webパラメータがあるときはweb-buildを実行するように設定しているのが分かります。

workflows:
  web-build:
    when: << pipeline.parameters.build-web >>
    jobs:
      - web-build

これにより、web-buildジョブでビルドが実行されるようになります。apps/docsに関しても同様です。例えば、apps/web/app/page.tsxを変更すると、次のようにworkflowが実行されます。

プロジェクトアーキテクチャ-ci-1.png

マージ前の.circleci/workflows/web.ymlも一応見ておきましょう。

version: 2.1

orbs:
  node: circleci/node@5.1.0

parameters:
  build-web:
    type: boolean
    default: false

jobs:
  web-build:
    executor: node/default
    steps:
      - checkout
      - node/install-packages:
          pkg-manager: yarn
      - run:
          command: yarn lint --filter=web
          name: Run lint
      - run:
          command: yarn build --filter=web
          name: Build app

workflows:
  web-build:
    when: << pipeline.parameters.build-web >>
    jobs:
      - web-build

apps/webだけのworkflowとjobを設定をすればいいので、見通しがよくなっているのが分かります。 このようにCircleCIではダイナミックコンフィグとcircleci/path-filteringを使うことで、モノレポのCIの実装が可能となっています。