複数のCloud Functionsをモノレポ化して1つのリポジトリで管理してみた
はじめに
Cloud Functionsを複数作成し運用していると、各Cloud Functionsごとにリポジトリを作成する必要があり、管理が大変になります。
そして、それぞれのCloud Functionsで共通して使うlinter, formatter, ビルドツールなどの周辺ツールの設定はリポジトリが別れているがために共通化することができません。また、CI/CDの設定も各リポジトリごとに作成する必要があります。
そこで今回は、複数のCloud Functionsをモノレポ化し、1つのリポジトリにまとめて、linter, formatter, CI/CDなどの設定を共通化してみます。
ちなみに、Cloud Functions for Firebaseを使っている場合は、モノレポ化する方法が公式ドキュメントに記載されているためこれに従い実装するのが良いと思います。
モノレポ(Monorepo)とは
複数プロジェクトのコードベースを同一レポジトリで管理すること。またはその管理下における開発手法のこと。プロジェクトは相互依存可能で、プロジェクト間のコード共有もできます。
Babelはモノレポを採用している代表的なリポジトリです。
複数のCloud Functionsをモノレポ化するメリット・デメリット
メリット
- すべてのCloud Functionsを単一リポジトリで管理できる。
- linter, formatter, ビルドツールなどの周辺ツールやCI/CDなどの設定を共通化できる。
- 型定義等のコードの共通がしやすい。
デメリット
- linterやformatterなどの周辺ツールを共通化しているため一部のCloud Functionsを別の言語で書くことが難しい。
モノレポを支える技術
npm workspaces
- npm自体に組み込まれた機能であり、公式のnpmコマンドラインツールで直接使用可能。
- 単一のルートパッケージで複数のパッケージを管理するための機能を提供します。異なるパッケージ間で依存関係を簡単に管理し、共通の依存関係を効率的に共有することができます。ただし、プロジェクト全体のビルドプロセスをカスタマイズするための機能は限られます。
Turborepo
- Vercel が開発した JavaScript/TypeScript のモノレポ環境に特化したビルドツールです。 yarn, npm, pnpm を利用している環境で利用可能。
- 管理しているパッケージ間の依存関係を考慮した順でビルドが可能。
- キャッシュを利用し、差分がある箇所のみのビルドを行うためビルド時間が高速。
ディレクトリ構成
Before
モノレポ化前の構成は各Cloud Functionsごとにリポジトリを作成していました。
# function-aリポジトリ
.
|--node_modules
|--.gitignore
|--.prettierrc
|--package.json
|--package-lock.json
|--tsconfig.json
|--.gitlab-ci.yml # CI/CDの設定ファイル
|--dist
| |--index.js # トランスパイルされたファイル。Cloud Functionsのエントリーポイント
|--src
| |--index.ts
# function-bリポジトリ
.
|--node_modules
|--.gitignore
|--.prettierrc
|--package.json
|--package-lock.json
|--tsconfig.json
|--.gitlab-ci.yml
|--dist
| |--index.js
|--src
| |--index.ts
# function-cリポジトリ
|--function-c
After
ルートの.gitlab-ci.yml
では、各Cloud Functionsディレクトリ内の.gitlab-ci.yml
をincludeして実行している。共通化するジョブはルートのymlファイルに記載し、個別の環境変数の設定やデプロイ先の設定は各Cloud Functionsディレクトリ配下のymlファイルに書くようにしている。
.
|--.gitignore
|--.npmrc # npm設定ファイル
|--.prettierrc
|--.vscode
|--README.md
|--packages # 各Cloud Functionsで共通して使うパッケージ。外部パッケージのラッパーなど
| |--common
| | |--package.json
| | |--src
| | | |--index.ts
| |--bigquery
|--functions
| |--function-a
| | |--.env
| | |--.env.template
| | |--package.json
| | |--.gitlab-ci.yml
| | |--src
| | | |--index.ts
| |--function-b
| |--function-c
| ・
| ・
| ・
|--node_modules
|--package-lock.json
|--package.json
|--tsconfig.base.json
|--turbo.json
|--.gitlab-ci.yml
動作環境
$ node -v
v18.14.0
$ npm -v
9.3.1
モノレポ化手順のポイント
各ワークスペースで共通で使うパッケージはルートディレクトリでインストールする
TypeScriptなど各ワークスペースで共通して使うパッケージはルートディレクトリでインストールすることで、npmの巻き上げ(hoisting)により、ルートのnode_modulesからワークスペースのシンボリックリンクができるため、各ワークスペースで個別にインストールする必要がない。
# /cloud-functions-monorepo-template
$ npm install -D typescript
functions/function-a
で他のワークスペース@package/common
を参照する設定
まずは@packcaes/common
の実装を追加します。
export const hello = (): string => {
return 'Hello World!';
};
@packages/common
を@functions/function-a
にインストールする場合下記コマンドでパッケージをインストールする。
# /cloud-functions-monorepo-template
$ npm install @packages/common -w @functions/function-a
*
に変更することで、依存関係の最新バージョンを参照することができます。これにより、パッケージのバージョンが変更された場合に、依存関係のバージョンを上げる必要がなくなります。
{
"dependencies": {
- "@packages/common": "^1.0.0"
+ "@packages/common": "*"
}
}
TypeScriptをJavaScriptへトランスパイルする。
ルートディレクトリでnpm run build
を実行すると、Turborepoにより各ワークスペースのnpm run build
が依存関係を考慮した順で実行される。
# /cloud-functions-monorepo-template
$ npm run build
{
"scripts": {
"build": "turbo run build"
}
}
{
"scripts": {
"build": "tsc --build"
}
}
他のワークスペースの関数をimportするにはreferences
を使い、packages/commonへの参照を設定する必要があります。
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"references": [
{
"path": "../../packages/common"
}
]
}
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
}
}
export const hello = (): string => {
return 'Hello World!';
};
import ff from '@google-cloud/functions-framework';
import { hello } from '@packages/common'; // 内部パッケージのインポート
export async function main(req: ff.Request, res: ff.Response) {
res.send(hello());
}
Cloud Functionsをローカルで動かすための設定
env-cmdを使い、環境変数を使えるように設定します。各ワークスペースで使うため、ルートでインストールします。
# /cloud-functions-monorepo-template
$ npm install -D env-cmd
Function Frameworkを使うと、Cloud Functionsをローカルで実行することが出来ます。
# /cloud-functions-monorepo-template
$ npm install @google-cloud/functions-framework
concurrentlyを使い、scriptを並列で実行できるようにします。
# /cloud-functions-monorepo-template
$ npm install -D concurrently
functions/function-a/package.json
を修正
{
"name": "@functions/function-a",
"version": "1.0.0",
"scripts": {
+ "build": "tsc --build",
+ "dev": "concurrently \"npm run dev:*\"",
+ "dev:build": "tsc -w",
+ "dev:functions": "env-cmd -r .env npx functions-framework --source=dist/src --target=main --signature-type=http"
}
}
Cloud Functionsの起動
# /cloud-functions-monorepo-template
$ npm run dev
@functions/function-a:dev: [functions] Serving function...
@functions/function-a:dev: [functions] Function: main
@functions/function-a:dev: [functions] Signature type: http
@functions/function-a:dev: [functions] URL: http://localhost:8080/
@functions/function-a:dev: [build]
@functions/function-a:dev: [build] 10:21:08 - Found 0 errors. Watching for file changes.
Cloud Functionsのデプロイ設定
Cloud Functionsをデプロイする際にTurborepoのturbo prune
コマンドを使い、ワークスペース間の依存関係を考慮したデプロイ用のディレクトリが作成し、それをgcloud
コマンドでデプロイします。
variables:
FUNCTION_NAME: 'function-a'
build:
stage: build
image: node:18.14
script:
- apt-get update && apt-get install -y jq
- npm ci
- npm run build
- npx turbo prune --scope=@functions/$FUNCTION_NAME --out-dir=build-function
- cp .gcloudignore ./build-function
- jq --arg deploy_function_name "$FUNCTION_NAME" '.main = "functions/" + $deploy_function_name + "/dist/src/index.js"' package.json > ./build-function/package.json
.
|--.gitignore
|--.npmrc # npm設定ファイル
|--.prettierrc
|--.vscode
|--README.md
|--packages
| |--common
| |--bigquery
|--functions
| |--function-a
| |--function-b
| |--function-c
+ |--build-functions # turbo pruneコマンドで作成されたデプロイ用のディレクトリ
+ | |--functions
+ | | |--function-a
+ | | |--package.json
+ | | |--.gitlab-ci.yml
+ | | |--src
+ | | | |--index.ts
build-functionsディレクトリのpackage.jsonはルートのpackage.jsonをコピーしたファイルになるため、Cloud Functions毎にエントリポイント(main)を動的にする必要がある。
そのためbuild script内でjqコマンドを使って動的に書き換えている。
(調べたかぎりでは、Turborepoの設定でいい感じにできる方法が見つからなかったためこのように対応)
{
- "main": ""
+ "main": "functions/function-a/dist/src/index.js"
}
モノレポ化した結果
良かった点
- 各Cloud Functionsで共通して使うパッケージをルートの
packages
ディレクトリにまとめることができた。 - linter, formatter, ビルドツールなどの周辺ツールやCI/CDなどの設定を共通化できた。
- 新しくCloud Functionsを作成する際の実装が楽になった。
イマイチだった点
- CI/CDで各Cloud Functionsをデプロイする際にエントリポイントを動的にするために、scriptを書いて対応する必要があった。
おわりに
複数のCloud Functionsをモノレポ化する方法について調査しましたが、実際に実装している例はあまりなかったため実装方法を調べるのに苦労しました。この方法がベストプラクティスであるかどうかはわからないため、他に良い方法があればコメントで教えていただけると嬉しいです。
ちなみに今回のソースコードは自分のリポジトリにありますで、参考にしてみてください。
https://github.com/seiya0429/cloud-functions-monorepo-template
この記事が誰かの参考になれば幸いです。