モノレポで Cloud Functions を管理している実践記録

こんにちは!

広告事業本部でリードウェブアプリケーションエンジニアをしている森田です!

最近ありがたいことに社内向けサービスの0→1の開発に携わる機会が2度ありました。 それらのサービスでは Google Cloud Workflows x Cloud Functions(第2世代)を採用しています。 私のチームは主に AWS EC2 上の Ruby on Rails でウェブアプリケーションの構築をメインとしてきたチームです。 Cloud Functions や Workflows、ひいては Google Cloud の知見があまりない状況でした。 そんな中で一年近くの開発を経てユーザー検証も終わりが見え、正式リリース目前となった今、現状の工夫と課題感について残していければと思います。

はじめに

今回開発しているサービスは Slack、kintone をユーザーインターフェースとする構成です。 バックエンドの Workflows x Cloud Functions からいくつかの広告プラットフォームのAPIを叩くような構成のものです。 現在 Cloud Functions のエンドポイントが Slack をインターフェースとするサービスに54個、kintone をインターフェースとするサービスに37個あります。 ここからはそれぞれモノレポで管理している構成と、デプロイの工夫について述べていきます。

モノレポ構成

モノレポ構成を選択した背景としては、開発初期から Cloud Functions のエンドポイントが多くなることを想定していました。

これらのエンドポイント間で、

  • デプロイ設定
  • 開発環境設定
    • リンターやフォーマッタ
    • CI/CD
  • DB接続

など、各エンドポイントで共通で利用したいものが多くありモノレポを選択しました。

その構成は下記のようになっています。 Cloud Functionsのエントリーポイントをfunctions/functions/**/main.pyに配置し、functions/**/*.py に共有コードを配置しています。

functions/                        # 共有コードを格納するディレクトリ
├── functions/                    # エントリーポイント(各関数の main.py)
│   ├── path/
│   │   └── to/
│   │       ├── hoge/
│   │       │   ├── endpoint_1/
│   │       │   │   └── main.py
│   │       │   └── endpoint_2/
│   │       │       └── main.py
│   │       └── fuga/
│   └── piyo/
│
├── models/                       # 共有コード(全関数から参照)
├── repositories/
├── services/
├── usecases/
├── storages/
└── utils/

ディレクトリパスから Cloud Functions 名を自動生成

前述の通り、Cloud Functions を多数管理しているので、その名前の管理も大変です。 この問題を解消するために、エントリーポイントのパスから Cloud Functions の名前を自動生成するようにしています。

例えば、functions/functions/path/to/hoge/endpoint_1/main.py の Cloud Functions 名は path-to-hoge-endpoint-1 になります。

これは下記のようなスクリプトでデプロイ時に生成するようにしています。

FUNCTIONS_ENDPOINT_DIR='functions/functions/'

filepath_to_name() {
  local func_filepath=$1
  
  func_dir=$(dirname "$func_filepath") # functions/functions/path/to/hoge/endpoint_1
  relative_path=${func_dir#*${FUNCTIONS_ENDPOINT_DIR}/} # path/to/hoge/endpoint_1
  # ディレクトリ区切りをハイフンに変換
  echo "$relative_path" | sed 's/\//-/g' # path-to-hoge-endpoint-1
}

Cloud Functions 名の長さ制限

Cloud Functions 名を自動生成するようにしたのですが、Cloud Functions 名には当然長さ制限があります。 こうした長さ制限は余裕がある長さになっているようなイメージがありましたが、今回は引っかかってしまいました。

具体的には、Cloud Functions の function_id 上限は 63文字 で制限されています。

この制限についてはドキュメントにはあまり書かれていなくて、Compare Cloud Run functions | Google Cloud Documentation

The function name must start with a letter followed by up to 62 letters, numbers, hyphens, or underscores, and must end with a letter or a number.

との記載があったり、Package google.cloud.functions.v2 | Cloud Run functions | Google Cloud Documentationfunction_id なる値が

This value should be 4-63 characters, and valid characters are /[a-z][0-9]-/.

と記載されていたりします。

残念ながら実際にエラーが発生するまで気づくことができませんでした…

この長さ制限をファイルパスから目視で確認するのはあまりにも非効率なので、CI で自動化しています。

GitHub Actions の workflow から抜粋

env:
  MAX_FUNCTIONS_NAME_LENGTH: 63

# PR の変更ファイルから関数名を生成し、63文字超をエラーにする
for file in $changed_files; do
  function_name=$(bash scripts/util/functions_filepath_to_name.sh "$file")
  if [ "${#function_name}" -gt "$MAX_FUNCTIONS_NAME_LENGTH" ]; then
    # PR コメントで通知して CI 失敗
  fi
done

この処理、軽量なので PostToolUse hook の Edit|Write でエラーにしてしまってもいいですね。 ハーネス強化余地発見しました!!

デプロイ

ここからはモノレポ構成でのデプロイの工夫について触れていきます。 主に Cloud Functions デプロイ時の Cloud Build の Rate Limit に苦しめられています。

デプロイ処理は下記のスクリプト群で行っています

scripts/
├── deploy.sh                         # バッチオーケストレーター
├── deploy_cloud_functions.sh         # 1Cloud Functions ぶんのデプロイ処理
├── deploy_workflows.sh               # Workflows のデプロイ
└── util/
    ├── constants.sh                  # 定数定義
    ├── find_functions_endpoint.sh    # main.py を全件 find
    └── functions_filepath_to_name.sh # パス→関数名 変換

Cloud Functions は何度も触れていますが量が多いので並列でデプロイしています。 ただ、Cloud Functions デプロイ時に内部で処理される Cloud Build に Rate Limit があり、これに抵触してしまいました。 下記画像や参考記事にあるように単一リージョンで並列にビルドできるのは 30 までです。

参考記事: Quotas and limits | Cloud Build | Google Cloud Documentation

Rate Limit回避

ここでは Rate Limit 回避のために Cloud Functions のデプロイを 30 ずつの並列でバッチ処理している例を紹介します。

# scripts/deploy.sh
BATCH_SIZE=30
SLEEP_SEC=60

process_batch() {
  local pids=()
  local batch_results=()

  for (( i=start_index; i<end_index; i++ )); do
    ./scripts/deploy_cloud_functions.sh "${deploy_file_paths[i]}" &
    pids+=($!)
  done

  for pid in "${pids[@]}"; do
    if wait "$pid"; then
      batch_results+=("0")
    else
      batch_results+=("$?")
      echo "ERROR: Deployment for PID $pid failed"
    fi
  done
  printf '%s\n' "${batch_results[@]}"
}

# バッチ間は RateLimit 回避のため SLEEP_SEC 秒待機
wait_for_next_batch() {
  if [ $((i+BATCH_SIZE)) -lt $total ]; then
    echo "バッチ ${batch_index} 完了。RateLimit 回避のため ${SLEEP_SEC} 秒待機します..."
    sleep "${SLEEP_SEC}"
  fi
}

上記のように各バッチ内で 1 Cloud Functions のデプロイを bash バックグラウンドジョブで並列実行しています。 各プロセスの pid を保持し、これらが全完了するまで待機して結果を集約、1デプロイでも失敗すれば exit 1 で GitHub Actions のステップを失敗にしています。

デプロイ時間

参考までに、54本の Cloud Functions をこの30並列バッチデプロイしている処理にかかる時間の実績値をご紹介いたします。

過去1か月(2026/3〜4)の GitHub Actions の実行ログ(71件)から集計した結果は以下のとおりです。

指標 Deploy Functions ステップ deploy_functions ジョブ全体
平均 7.2分 (434秒) 8.1分 (488秒)
中央値 6.9分 (416秒) 7.9分 (473秒)
最小 6.3分 (379秒) 7.2分 (431秒)
最大 8.3分 (499秒) 9.4分 (562秒)
90パーセンタイル 8.1分 (484秒)
  • Deploy Functions ステップ: スクリプトの実行時間(バッチ1・待機60秒・バッチ2)
  • deploy_functions ジョブ全体: ステップに加え、リポジトリの checkout・Python/Poetry セットアップ・gcloud 認証などを含む

1 Cloud Functions あたりのデプロイ時間は、バッチ内での並列実行中に最初の関数が完了するまでの時間として計測すると 約2〜3分 です。

モノレポのメリット・デメリット

ここまでモノレポ構成の工夫を紹介してきましたが、改めてモノレポを選択したメリットと、実際に感じているデメリットを整理します。

メリット

モノレポを選んだ最大の理由はコード共有のしやすさです。 今回のサービスではドメインモデルやリポジトリ・サービス層のロジックは複数の Function で使いまわすことができています。 ポリリポ構成だとこの共有コードをコピーするかパッケージとして公開するかの二択になるかと思いますが、モノレポなら共有コードへの変更が1コミットで全 Function に即時反映されます。

また、依存管理の一元化も大きなメリットです。 単一の pyproject.toml + poetry.lock で54本すべての依存が固定されるため、バージョン不整合によるランタイムエラーを構造的に防げます。 セキュリティパッチの適用も poetry update → テスト → マージの1サイクルで済みます。

加えて、リンター・フォーマッタの設定・CI/CD パイプライン・DB接続設定なども一元管理できるので、新しい Function を追加するときにこれらを用意するコストが限りなくゼロです。

デメリット

デメリットも実際にいくつか感じています。

まず、デプロイ単位が大きくなってしまいます。 共有コードを変更すると全54本を再デプロイする必要があります。 前述のバッチデプロイで約7〜8分かかっているのですが、小さな共有コードの修正でもこの時間がかかります。

また、不要な依存が含まれることもあります。 全 Function が同じ pyproject.toml を参照しているため、ある Function にしか使わないライブラリも全 Function のデプロイパッケージに含まれます。 Cloud Functions のコールドスタート時間はパッケージサイズに影響すると思われるため、依存が増えるほど起動が遅くなる可能性があります。

これらのデメリットを受け入れた上でモノレポを選んでいるのは、チーム規模が小さく・Function 間の結合度が高い今のフェーズではコード共有の恩恵がデメリットを上回ると判断しているためです。

まとめ

ここまで、Cloud Functions のモノレポでの管理を実践した内容について記載してきました。 あらためて、チームとしてモノレポの恩恵を大きく感じております。 AIコーディングするにあたってコンテキストに与えるコードやドキュメント、Agent Skills やカスタム指示なども共用できていることは時代にあった大きなメリットだと感じています。 Cloud Functions のモノレポ構成の選定や実践に参考になれると幸いです!!