🍮

10日で作る Go Basic API

に公開

「とりあえず動く API」を超えて、10日でどこまで“ちゃんとした”バックエンドを作れるのか。

このエントリでは、リンク集サービス Tsudzuri のバックエンド API を約10日間で作り上げたプロセスを、アーキテクチャ、ツール、CI/CD、Google Cloud、そして GitHub Copilot(AI エージェント) の活用という観点から振り返ります。

「Go で Web API を作ってみたい」「趣味開発でも設計と運用まで含めてきちんとやってみたい」という人向けに、コンパクトに全体像を掴めることを目指しています。


サービス概要と目標設定

Tsudzuri とは?

Tsudzuri は、複数人で共同編集できる リンク集(綴り) を作るためのサービスです。

  • 匿名ゲストで開始可能
  • 「綴り (Page)」を作成してリンクを追加・編集・並べ替え
  • 招待コード/招待リンクで共有し、参加者全員が閲覧・編集可能

今回の記事では、フロントではなく バックエンド API サーバー(Go) にフォーカスします。

ゴール設定

ゴールは以下に絞り込みました。

  • 最低限必要な Page / User のユースケース を一通り実装
  • gRPC + gRPC-Gateway ベースで、HTTP と gRPC の両方から叩ける API
  • PostgreSQL + ent でドメインを壊さない永続化
  • OpenTelemetry + GCP を前提にしたトレーシング/ロギングの仕組み
  • ローカル開発は Docker Compose + Makefile でワンコマンド起動
  • GitHub Actions で CI(Lint/Test)と CD(Cloud Run デプロイ)までつなぐ

「あとから雑に作り直さなくていい」最小構成を狙い、設計と運用の“ライン”をはじめから決めて進めたのがポイントでした。


アーキテクチャ概要

全体構成

リポジトリ構成は、クリーンアーキテクチャをベースにしたレイヤード構成です。

cmd/api        # エントリポイント (main, DI, OTEL)
config/        # 設定・環境変数
api/           # protobuf 定義 & 生成物
  ├── protobuf/
  └── tsudzuri/
domain/        # ドメインモデル, リポジトリIF
  ├── page/
  └── user/
usecase/       # ユースケース (アプリケーションサービス)
  ├── page/
  └── user/
infrastructure/# DB, Firebase などの実装
  ├── api/
  └── db/
presentation/  # gRPC ハンドラ, エラーコード
  ├── grpc/
  └── errcode/
pkg/           # 共通ライブラリ (log, trace, cache, uuid…)
resources/     # DBマイグレーション

レイヤごとに責務をはっきり分離しています。

  • presentation: gRPC / HTTP の入り口。リクエスト/レスポンス変換とエラーマッピング
  • usecase: ユースケース単位のアプリケーションサービス
  • domain: ビジネスルールとエンティティ、リポジトリインターフェース
  • infrastructure: DB や外部サービス(Postgres, Firebase)の実装

Page / User ドメインの切り方

ドメインは PageUser に絞りました。

  • domain/page

    • Page エンティティ(タイトル、リンクのリスト、並び順など)
    • Link エンティティ(URL, タイトル, メモ…)
    • PageRepository インターフェース
  • domain/user

    • User エンティティ(ID, DisplayName, アイコンなど)
    • UserRepository インターフェース

これに対して、

  • usecase/page に Page 作成・編集・一覧・参加(join)・リンク操作など
  • usecase/user に User 作成・ログイン・取得など

のユースケースを配置し、さらに presentation/grpc/page, presentation/grpc/user でハンドラを実装しています。

ポイント

  • ent のスキーマはインフラ層に閉じ込め、domain からは見えないようにする
  • ハンドラは usecase のインターフェースだけを知る
  • domain は application / infrastructure の詳細を一切知らない

これにより、「とりあえず動く」ではなく、「拡張しやすい」構成にできました。


開発フローとツール群

ローカル開発は Makefile + Docker Compose に寄せています。

  • make dev : air でホットリロード開発
  • make up / make down : API / DB / Swagger UI の起動・停止
  • make test : テスト用 DB コンテナを立ち上げてから go test ./...
  • make lint / make fmt : golangci-lintgofumpt、および buf format
  • make generate : go generate + buf generate で ent / protobuf 周りをまとめて生成

テスト実行時は docker compose で Postgres を自動起動し、pg_isready で待ち合わせてから go test を叩くようにしているので、テスト前の手作業がほとんど要らないのがポイントです。

DB は ent + PostgreSQL で、スキーマは infrastructure/db/ent、マイグレーション SQL は resources/migration/ にまとめています。


Google Cloud 上での運用設計

ここからは、「個人開発でも現実的に払えるコスト」 を意識しながら設計した GCP 周りを少し踏み込んで紹介します。

Cloud Run — コンテナ実行基盤の中心

この API は Cloud Run で動かすことを前提に設計しました。

  • 完全マネージドで OS・パッチ・スケーリングを丸投げできる
  • コンテナさえ作ればそのままデプロイできる
  • 個人開発でも Free Tier + 従量課金で現実的なコスト

構成としては、Artifact Registry に push したイメージを Cloud Run サービス(tsudzuri など)で起動する、ごく標準的な形です。

個人利用なので、

  • 最小インスタンス数 0(アイドル時コストゼロ、cold start は割り切る)
  • 最大インスタンス数・同時実行数は控えめ
  • CPU はリクエスト処理中のみ割り当て

といった「とりあえず安く動かす」設定からスタートしています。

Cloud SQL — 共通コアでできるだけ安い構成を目指す

DB は Cloud SQL for PostgreSQL を採用しています。ここでもコストをかなり意識しました。

  • 1 プロジェクトに 「共通コア」的な Cloud SQL インスタンス を 1 つだけ作る
  • その中に tsudzuri 専用のデータベースを作成
  • 本番・検証を分けたい場合も、まずは同一インスタンス内に別 DB として切る

インスタンス自体は小さめのマシンタイプを選び、

  • 自動バックアップ有効
  • 高可用性(マルチゾーン)は最初は切っておく(RTO/RPO 要件を満たせるなら後から有効化)

といったあたりで「コストと安心感のバランス」を取っています。

開始/停止のスケジューリング

さらにコストを削りたい場合、夜間や使わない時間帯に Cloud SQL を止める選択肢もあります。

Cloud Scheduler + Cloud Functions(または Cloud Run Jobs)+ Cloud SQL Admin API を組み合わせることで、

  • 「毎日 1:00 に停止」「毎朝 9:00 に起動」

といったスケジュール運用が可能です。

Service Account 設計 — CI/CD と Cloud Run / Cloud SQL

サービスアカウントは、ざっくり次の 2 役に分けています。

  1. CI/CD 用サービスアカウント
  • GitHub Actions から Artifact Registry / Cloud Run にアクセスするための SA
  • 例: roles/artifactregistry.writer, roles/run.admin, roles/iam.serviceAccountUser
  1. Cloud Run 実行用サービスアカウント
  • 実際に API コンテナが動くときに紐づける SA
  • Cloud SQL 用に roles/cloudsql.client、Secret Manager 用に roles/secretmanager.secretAccessor などを付与

CI 側の SA キーは GitHub Actions の GCP_SA_KEY シークレットとして登録し、.github/workflows/ci.yaml から secrets: inherit で渡しています。

Artifact Registry — イメージ管理とライフサイクル

コンテナイメージは Artifact Registry に格納し、GitHub Actions からタグ付きで push しています。

古いイメージでストレージ課金が増えがちなので、Artifact Registry のライフサイクルポリシーで、

  • 「最新数個だけ残し、それ以外は一定期間で削除」

といったルールを 1 つ入れておくと安心です。

Dockerfile は multi-stage build で、builder ステージでビルド → runtime ステージにバイナリだけコピーするごく基本的な構成ですが、それだけでもイメージサイズと cold start をだいぶ抑えられます。

OpenTelemetry / Logging — 見える化の土台

ファイルとしては cmd/api/otel.go, pkg/trace/, pkg/log/ あたりに設定がまとまっています。

  • gRPC / HTTP ハンドラに OpenTelemetry のミドルウェアを挿入
  • GCP のリソース情報(プロジェクト ID, サービス名など)を自動検出
  • ローカル開発時は stdout exporter、本番は Cloud Trace exporter などに切り替え可能な構成

ログは zap ベースで JSON 形式にまとめておき、Cloud Run 上ではそのまま Cloud Logging に取り込まれます。
エラー時にはトレース ID をログに埋め込むようにしておくことで、

  • 「このリクエストで何が起きたか」をログから辿る
  • 同じ ID で Cloud Trace 上のスパンも追いかける

といった、運用時に嬉しいクロス参照がしやすくなります。

「最初から観測可能性を入れておく」ことで、後からのトラブルシュートやパフォーマンスチューニングが格段に楽になります。


CI / CD パイプライン

Lint & Test(CI)

.github/workflows/lint-and-test.yaml で、feature/** ブランチへの push をトリガーに CI を走らせています。

  • make install-tools.bin/ にツールインストール
  • make lint で静的解析
  • make test でユニットテスト

チーム開発を想定して、feature ブランチに push したら自動で検証が走る流れを最初から仕込んでいます。

Cloud Run へのデプロイ(CD)

CD は、再利用可能なワークフロー .github/workflows/ci.yaml を中心に構成しています。

  • Docker イメージをビルド
  • Artifact Registry へ push
  • Cloud Run へデプロイ
  • 必要に応じてトラフィックの切り替え

環境ごとにラッパーを用意することで、同じ CI ロジックを dev / prod で使い回せるようにしました。

  • dev-precheck.yaml
    • precheck ブランチへの push or 手動実行で dev 環境へデプロイ
  • cd.yaml
    • v*.*.* タグの push をトリガーに prod 環境へデプロイ

Cloud Run に渡す環境変数は .github/env/dev.yaml / .github/env/prod.yaml のような YAML で管理し、 ci.yaml の inputs として渡します。


GitHub Copilot / AI エージェントを使った開発フロー

今回の開発では、GitHub Copilot(特に Chat / エージェント機能)をトライアルで登録しかなり積極的に使いました。正直これが無いと厳しかったと思います。

Copilot に任せたこと

  • 既存のコードに合わせたテスト生成
    • 例: usecase/pagedomain/user のテーブルドリブンテスト
  • Protobuf や ent スキーマの初期雛形
  • OpenTelemetry の設定や gRPC ミドルウェアの配線案
  • コード補完

特に、「このレイヤの責務はこうだから、ここにこういう関数を追加したい」 と説明すると、既存コードからパターンを学習してかなり良い初期案を出してくれます。

人間がやるべきこと

一方で、以下は人間側がしっかり判断したところです。

  • ドメインモデリング(Page / User / Link の境界、ユースケースの切り方)
  • トランザクション境界の設計(どこまでを 1 トランザクションにするか)
  • エラー設計(presentation/errcode の粒度やマッピング方針)
  • CI/CD・GCP 全体像の設計

Copilot はコードを書くのは得意ですが、「そもそも何を作るべきか」「どんな責務の分割が美しいか」という設計は、やはり人間が主導した方がうまくいきました。

実際のやり取りイメージ

  • usecase/page に Page の join ユースケースを追加したい。既存の create/list と同じテストスタイルでテストも追加して」
  • 「この gRPC ハンドラから usecase を呼ぶ部分でエラー変換がダサいので、presentation/errcode に共通関数を足して整理して」
  • 「Cloud Run 用の GitHub Actions を作りたい。.github/workflows/ci.yaml を再利用ワークフローとして切り出して」

といった依頼を投げると、コードとワークフローを書いてくれます。そこに対して、「ここはトラフィック切り替えは別でやるから enable_traffic_update: false にして」など、プロダクトの事情を踏まえた微調整を人間が行う、という形でコラボしました。


まとめ

  • 10日という短期間でも、設計と運用の「最低ライン」を最初に決めることで、後から伸ばせるバックエンド API を作れる
  • Go / gRPC / ent / OpenTelemetry / Cloud Run / GitHub Actions という組み合わせは、個人開発でも十分現実的な選択肢
  • GitHub Copilot などの AI エージェントを「ペアプロ相手」として使うと、実装や設定の初期案づくりが爆速になる

この記事が、開発を 「10日で形にしてみる」 ための参考になれば嬉しいです。

リポジトリ: https://github.com/naka-sei/tsudzuri

Discussion