Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 172 additions & 1 deletion .github/workflows/dev-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,19 @@ jobs:
run: |
go build ./...
go vet ./...
go test ./...
working-directory: ./app/functions-go

# sanpai/ranking/register/statusCacheBackfill等のFirestore統合テストは
# FIRESTORE_EMULATOR_HOST未設定時は自動スキップされる設計(README参照)。
# CIでも実際に検証されるよう、Firestoreエミュレータを起動した状態で
# go test を実行する(firebase emulators:exec が終了時に自動でエミュレータを
# 落とすため、後続ステップに影響しない)。
- name: go test (functions-go, with Firestore emulator)
run: |
firebase emulators:exec --project d-shrine-dev --only firestore \
'cd functions-go && go test ./...'
working-directory: ./app

# Go版Cloud Run functions(statusGo)のデプロイ。既存のstatus(Node)とは別関数として
# 共存させ、フロントエンドの切替タイミングを制御できるようにしている
# (詳細は docs/backend.md「status エンドポイントのGo移植」を参照)。
Expand Down Expand Up @@ -216,3 +226,164 @@ jobs:
--timeout=30s \
--set-env-vars=FUNC_BASE_URL=${{ env.OGP_FUNC_BASE_URL }},OGP_PROJECT_ID=d-shrine-dev
working-directory: ./app/functions-go

# スケジュール関数(Pub/Sub トリガー)のGo移植分。既存のNode版
# (rankingUpdate/rankingCache/statusCacheBackfill/scheduledOgpDelete)とは
# 別のCloud Scheduler ジョブ・Pub/Subトピック・関数名で完全に独立してデプロイし、
# 安定稼働を確認してからNode版を止める(詳細は docs/backend.md
# 「スケジュール関数のGo移植」を参照)。
#
# --trigger-topic に指定したPub/SubトピックはCloud Functions側が
# 存在しなければ自動作成するため、事前のトピック作成手順は不要。
# Cloud SchedulerジョブはApp Engineのロケーションに紐づくため、
# 既存プロジェクトの設定値を動的に取得して使う。
- name: enable required GCP services for scheduled Go functions
run: |
gcloud services enable \
pubsub.googleapis.com \
cloudscheduler.googleapis.com \
eventarc.googleapis.com \
--project=d-shrine-dev

# Cloud SchedulerジョブのlocationはプロジェクトのApp Engineアプリと
# 同一でなければならない(App Engineが存在するプロジェクトでは、それ以外の
# リージョンを指定すると `Location must equal <app-engine-location>` エラーになる)。
# `gcloud app describe` が返す locationId はレガシーなApp Engine表記
# (例: "us-central")の場合があり、Cloud Scheduler/Cloud Functions等の
# 通常のCloudリージョン表記(例: "us-central1")とは異なることがあるため、
# 既知の対応表で変換する。
- name: resolve App Engine location for Cloud Scheduler
run: |
set -eo pipefail
RAW_LOCATION=$(gcloud app describe --project=d-shrine-dev --format='value(locationId)')
case "$RAW_LOCATION" in
us-central) LOCATION=us-central1 ;;
europe-west) LOCATION=europe-west1 ;;
*) LOCATION="$RAW_LOCATION" ;;
esac
echo "resolved App Engine location: $RAW_LOCATION -> $LOCATION"
echo "APP_ENGINE_LOCATION=$LOCATION" >> "$GITHUB_ENV"

# timeout=300s: Node版はrunWith()を使わずデフォルト(60s)のままだが、
# 全ユーザーをスキャンする処理のため安全側に長めの値を明示指定する
# (実行時間そのものはNode版と同等の想定で、上限を厳しくしすぎて
# ユーザー数増加時にタイムアウトする事故を避けるのが目的)。
- name: deploy rankingUpdateGo (Cloud Run function, Go, Pub/Sub trigger)
run: |
gcloud functions deploy rankingUpdateGo \
--project=d-shrine-dev \
--gen2 \
--runtime=go125 \
--region=us-central1 \
--source=. \
--entry-point=RankingUpdateGo \
--trigger-topic=ranking-update-go \
--memory=256Mi \
--timeout=300s
working-directory: ./app/functions-go

- name: create/update Cloud Scheduler job for rankingUpdateGo
run: |
gcloud scheduler jobs update pubsub ranking-update-go \
--project=d-shrine-dev --location=${{ env.APP_ENGINE_LOCATION }} \
--schedule="0 * * * *" \
--topic=ranking-update-go \
--message-body="{}" \
--time-zone=Etc/UTC \
|| gcloud scheduler jobs create pubsub ranking-update-go \
--project=d-shrine-dev --location=${{ env.APP_ENGINE_LOCATION }} \
--schedule="0 * * * *" \
--topic=ranking-update-go \
--message-body="{}" \
--time-zone=Etc/UTC

# timeout=300s: rankingUpdateGoと同様の理由(全ユーザースキャンのため安全側)。
- name: deploy rankingCacheGo (Cloud Run function, Go, Pub/Sub trigger)
run: |
gcloud functions deploy rankingCacheGo \
--project=d-shrine-dev \
--gen2 \
--runtime=go125 \
--region=us-central1 \
--source=. \
--entry-point=RankingCacheGo \
--trigger-topic=ranking-cache-go \
--memory=256Mi \
--timeout=300s
working-directory: ./app/functions-go

- name: create/update Cloud Scheduler job for rankingCacheGo
run: |
gcloud scheduler jobs update pubsub ranking-cache-go \
--project=d-shrine-dev --location=${{ env.APP_ENGINE_LOCATION }} \
--schedule="0 */2 * * *" \
--topic=ranking-cache-go \
--message-body="{}" \
--time-zone=Etc/UTC \
|| gcloud scheduler jobs create pubsub ranking-cache-go \
--project=d-shrine-dev --location=${{ env.APP_ENGINE_LOCATION }} \
--schedule="0 */2 * * *" \
--topic=ranking-cache-go \
--message-body="{}" \
--time-zone=Etc/UTC

- name: deploy statusCacheBackfillGo (Cloud Run function, Go, Pub/Sub trigger)
run: |
gcloud functions deploy statusCacheBackfillGo \
--project=d-shrine-dev \
--gen2 \
--runtime=go125 \
--region=us-central1 \
--source=. \
--entry-point=StatusCacheBackfillGo \
--trigger-topic=status-cache-backfill-go \
--memory=512Mi \
--timeout=300s
working-directory: ./app/functions-go

- name: create/update Cloud Scheduler job for statusCacheBackfillGo
run: |
gcloud scheduler jobs update pubsub status-cache-backfill-go \
--project=d-shrine-dev --location=${{ env.APP_ENGINE_LOCATION }} \
--schedule="*/30 * * * *" \
--topic=status-cache-backfill-go \
--message-body="{}" \
--time-zone=Etc/UTC \
|| gcloud scheduler jobs create pubsub status-cache-backfill-go \
--project=d-shrine-dev --location=${{ env.APP_ENGINE_LOCATION }} \
--schedule="*/30 * * * *" \
--topic=status-cache-backfill-go \
--message-body="{}" \
--time-zone=Etc/UTC

# timeout=300s: OGPキャッシュファイル数が多い場合でも一覧・削除が
# 途中で打ち切られないよう安全側に長めの値を明示指定する。
- name: deploy scheduledOgpDeleteGo (Cloud Run function, Go, Pub/Sub trigger)
run: |
gcloud functions deploy scheduledOgpDeleteGo \
--project=d-shrine-dev \
--gen2 \
--runtime=go125 \
--region=us-central1 \
--source=. \
--entry-point=ScheduledOgpDeleteGo \
--trigger-topic=scheduled-ogp-delete-go \
--memory=256Mi \
--timeout=300s \
--set-env-vars=STORAGE_BUCKET_NAME=d-shrine-dev.appspot.com
working-directory: ./app/functions-go

- name: create/update Cloud Scheduler job for scheduledOgpDeleteGo
run: |
gcloud scheduler jobs update pubsub scheduled-ogp-delete-go \
--project=d-shrine-dev --location=${{ env.APP_ENGINE_LOCATION }} \
--schedule="0 */1 * * *" \
--topic=scheduled-ogp-delete-go \
--message-body="{}" \
--time-zone=Etc/UTC \
|| gcloud scheduler jobs create pubsub scheduled-ogp-delete-go \
--project=d-shrine-dev --location=${{ env.APP_ENGINE_LOCATION }} \
--schedule="0 */1 * * *" \
--topic=scheduled-ogp-delete-go \
--message-body="{}" \
--time-zone=Etc/UTC
82 changes: 75 additions & 7 deletions app/functions-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ functions-go/
register_test.go
ogp_rewrite.go # ogpRewriteGo エンドポイント
ogp_rewrite_test.go
ranking_update.go # rankingUpdateGo (Pub/Subトリガー、スケジュール関数)
ranking_update_test.go
ranking_cache.go # rankingCacheGo (Pub/Subトリガー、スケジュール関数)
ranking_cache_test.go
status_cache_backfill.go # statusCacheBackfillGo (Pub/Subトリガー、スケジュール関数)
status_cache_backfill_test.go
scheduled_ogp_delete.go # scheduledOgpDeleteGo (Pub/Subトリガー、スケジュール関数)
scheduled_ogp_delete_test.go
cmd/
main.go # ローカル動作確認専用(デプロイでは使わない)
internal/
Expand All @@ -46,9 +54,7 @@ functions-go/
performance_test.go # performance.test.js と同一の入出力を検証
```

`userOGP`(OGP画像生成)とPub/Subスケジュール関数(`rankingUpdate`/
`rankingCache`/`statusCacheBackfill`/`scheduledOgpDelete`)は対象外
(理由は `docs/backend.md`「Go移植を見送った機能」を参照)。
`userOGP`(OGP画像生成)は対象外(理由は `docs/backend.md`「Go移植を見送った機能」を参照)。

## 関数の命名規則(既存Node関数との共存)

Expand Down Expand Up @@ -81,10 +87,12 @@ PORT=8090 \
go run ./cmd
```

`sanpai_test.go` は `FIRESTORE_EMULATOR_HOST` が未設定の場合は自動的にスキップする
ため、通常のCI(`go test ./...`)には影響しない。GitHub Events APIはテスト用の
`httptest` モックサーバーに差し替えており(`githubAPIBaseURL` 変数)、実際の
GitHub APIやFirebase Authへの通信は発生しない。
Firestoreエミュレータ統合テスト(`sanpai_test.go`等)は `FIRESTORE_EMULATOR_HOST`
が未設定の場合は自動的にスキップするが、CI(`dev-deploy.yml`)では
`firebase emulators:exec --only firestore 'go test ./...'` の形でFirestore
エミュレータを起動した状態で実行しているため、実際にスキップされず検証される。
GitHub Events APIはテスト用の `httptest` モックサーバーに差し替えており
(`githubAPIBaseURL` 変数)、実際のGitHub APIやFirebase Authへの通信は発生しない。

## デプロイ

Expand Down Expand Up @@ -123,6 +131,66 @@ gcloud functions deploy statusGo \
`functions.config().func.base_url` 相当)と `OGP_PROJECT_ID` を
`--set-env-vars` で渡す必要がある。

## スケジュール関数(Pub/Subトリガー)のデプロイ

`rankingUpdateGo`/`rankingCacheGo`/`statusCacheBackfillGo`/`scheduledOgpDeleteGo`
はHTTPトリガーではなくPub/Sub(Cloud Scheduler経由)トリガーのため、
デプロイ方法が異なる。

```bash
gcloud functions deploy rankingUpdateGo \
--project=d-shrine-dev \
--gen2 \
--runtime=go125 \
--region=us-central1 \
--source=. \
--entry-point=RankingUpdateGo \
--trigger-topic=ranking-update-go \
--memory=256Mi \
--timeout=300s
```

`--trigger-topic` に指定したPub/Subトピックは存在しなければCloud Functions側が
自動作成するため、事前のトピック作成は不要。ただし実際に定期実行するには
別途Cloud Schedulerジョブが必要で、CIでは「更新を試みて、ジョブが無ければ
作成する」形で冪等にセットアップしている(`update`は対象ジョブが存在しない場合
失敗するため、`||`で`create`にフォールバックする)。

Cloud SchedulerジョブのlocationはプロジェクトのApp Engineアプリと同一の
リージョンでなければならない制約があるため、`gcloud app describe`から動的に
取得している。ただし `gcloud app describe` が返す `locationId` はレガシーな
App Engine表記(例: `us-central`)の場合があり、Cloud Scheduler側が要求する
通常のCloudリージョン表記(例: `us-central1`)と異なることがあるため、
既知のパターンを変換してから使用する(詳細はCIのコメント参照)。

```bash
RAW_LOCATION=$(gcloud app describe --project=d-shrine-dev --format='value(locationId)')
# us-central -> us-central1 等、既知の表記ゆれを変換した値をLOCATIONとする
gcloud scheduler jobs update pubsub ranking-update-go \
--project=d-shrine-dev --location="$LOCATION" \
--schedule="0 * * * *" \
--topic=ranking-update-go \
--message-body="{}" \
--time-zone=Etc/UTC \
|| gcloud scheduler jobs create pubsub ranking-update-go \
--project=d-shrine-dev --location="$LOCATION" \
--schedule="0 * * * *" \
--topic=ranking-update-go \
--message-body="{}" \
--time-zone=Etc/UTC
```

`statusCacheBackfillGo` はNode版の `runWith({timeoutSeconds:300, memory:"512MB"})`
に合わせて `--memory=512Mi --timeout=300s` を指定している。`scheduledOgpDeleteGo`
はCloud Storageを操作するため `STORAGE_BUCKET_NAME`(Node版の
`${projectID}.appspot.com` 相当)を `--set-env-vars` で渡している。

各スケジュールの間隔・タイムゾーンはNode版と揃えているが、固定間隔
(`every N minutes`)の実行に対してタイムゾーンの選択は実行時刻そのものには
影響しない(例えば「毎時0分」はUTCでもAsia/Tokyoでも同じ実時刻に発火する。
Asia/Tokyoは夏時間の無い固定オフセットのため)ため、CI側は全て `Etc/UTC` を
明示指定している。

## Node版との等価性の確認方法

新しく移植する際は、Firestoreエミュレータに同一のテストデータを投入し、
Expand Down
22 changes: 11 additions & 11 deletions app/functions-go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ go 1.25.8

require (
cloud.google.com/go/firestore v1.22.0
cloud.google.com/go/storage v1.63.0
firebase.google.com/go/v4 v4.20.0
github.com/GoogleCloudPlatform/functions-framework-go v1.9.2
github.com/cloudevents/sdk-go/v2 v2.15.2
google.golang.org/api v0.287.0
google.golang.org/grpc v1.81.1
)
Expand All @@ -20,13 +22,11 @@ require (
cloud.google.com/go/iam v1.11.0 // indirect
cloud.google.com/go/longrunning v1.0.0 // indirect
cloud.google.com/go/monitoring v1.29.0 // indirect
cloud.google.com/go/storage v1.62.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudevents/sdk-go/v2 v2.15.2 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
Expand All @@ -49,11 +49,11 @@ require (
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/atomic v1.4.0 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.10.0 // indirect
Expand All @@ -65,8 +65,8 @@ require (
golang.org/x/text v0.38.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect
google.golang.org/genproto v0.0.0-20260519071638-aa98bba5eb94 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
Loading
Loading