この記事について
- Honoで簡単なJSON API(Todo風CRUD)を作る
- Dockerでコンテナ化してローカル動作確認
- Google Cloud Runにデプロイして公開する
- Vitestでテストを書く
- GitHub Actionsでテスト自動化 + mainマージでCloud Runへ自動デプロイ
前回はCloudflare Workersでリンクページを作ったので、今回はGCPのCloud Runでコンテナベースのデプロイを体験してみます。写経しながら手を動かして理解する形式です。
全体構成
[開発]
Hono(Node.js) + TypeScript
│
│ docker build
▼
Dockerイメージ
│
│ push
▼
Artifact Registry(GCP)
│
│ deploy
▼
Cloud Run(本番)
[CI/CD]
git push
├─ GitHub Actions: npm test
└─ main反映後: docker build → Artifact Registryへpush → Cloud Runへdeploy
事前準備
以下を用意します。
-
Node.js 20以上(
node -vで確認) - Docker Desktop(インストール済みでない場合は公式サイトから)
- Google Cloudアカウント(無料枠あり。初回は$300分のクレジットが付与される)
- gcloud CLI(公式手順からインストール)
gcloud CLIをインストールしたら、ログインしておきます。
gcloud auth login
GCPプロジェクトの作成
gcloud projects create my-hono-app --name="my-hono-app"
gcloud config set project my-hono-app
my-hono-appの部分はGCP全体でユニークな名前にする必要があるので、重複したら別名にしてください(例: my-hono-app-20260920)。
課金アカウントの紐付けが必要です。GCPコンソールの「お支払い」からプロジェクトに課金アカウントを紐付けてください(無料枠内なら課金は発生しません)。
必要なAPIを有効化
gcloud services enable run.googleapis.com
gcloud services enable artifactregistry.googleapis.com
gcloud services enable cloudbuild.googleapis.com
1. Honoアプリの作成
1-1. プロジェクト作成
npm create hono@latest todo-api
テンプレートを聞かれたら nodejs を選んでください。Cloud Runは通常のNode.jsサーバーとしてコンテナを起動するので、Workers用テンプレートではなくNode.js用テンプレートを使います。
cd todo-api
npm install
1-2. 型とデータストアを作成
src/todos.ts を新規作成します。
export type Todo = {
id: number
title: string
done: boolean
}
let todos: Todo[] = [
{ id: 1, title: "Honoを学ぶ", done: false },
{ id: 2, title: "Cloud Runにデプロイする", done: false },
]
let nextId = 3
export function getAll(): Todo[] {
return todos
}
export function getById(id: number): Todo | undefined {
return todos.find((t) => t.id === id)
}
export function create(title: string): Todo {
const todo: Todo = { id: nextId++, title, done: false }
todos.push(todo)
return todo
}
export function remove(id: number): boolean {
const before = todos.length
todos = todos.filter((t) => t.id !== id)
return todos.length < before
}
インメモリの簡易ストアです(コンテナが再起動すると消えます。今回はデプロイ手順の学習が目的なのでこれでOKです)。
1-3. APIエンドポイントを作成
src/index.ts を以下の内容に書き換えます。
import { serve } from "@hono/node-server"
import { Hono } from "hono"
import { getAll, getById, create, remove } from "./todos.js"
const app = new Hono()
app.get("/", (c) => {
return c.json({ message: "Todo API is running" })
})
app.get("/api/todos", (c) => {
return c.json(getAll())
})
app.get("/api/todos/:id", (c) => {
const id = Number(c.req.param("id"))
const todo = getById(id)
if (!todo) {
return c.json({ error: "not found" }, 404)
}
return c.json(todo)
})
app.post("/api/todos", async (c) => {
const body = await c.req.json<{ title?: string }>()
if (!body.title) {
return c.json({ error: "title is required" }, 400)
}
const todo = create(body.title)
return c.json(todo, 201)
})
app.delete("/api/todos/:id", (c) => {
const id = Number(c.req.param("id"))
const ok = remove(id)
if (!ok) {
return c.json({ error: "not found" }, 404)
}
return c.json({ ok: true })
})
const port = Number(process.env.PORT) || 8080
serve({
fetch: app.fetch,
port,
})
console.log(`Server is running on port ${port}`)
ポイントは process.env.PORT を見ていることです。Cloud RunはコンテナにPORT環境変数を渡してリクエストを送ってくるので、これを見てlistenする必要があります(ローカルでは未設定なら8080番を使います)。
1-4. 動作確認
npm run dev
別のターミナルで確認します。
curl http://localhost:8080/api/todos
curl -X POST http://localhost:8080/api/todos -H "Content-Type: application/json" -d '{"title":"写経する"}'
curl http://localhost:8080/api/todos/1
curl -X DELETE http://localhost:8080/api/todos/1
それぞれ期待通りのJSONが返ってくればOKです。
2. テストを書く
2-1. Vitestのインストール
npm install -D vitest
package.jsonのscriptsに追加します。
{
"scripts": {
"test": "vitest run"
}
}
2-2. テストコード
src/todos.test.ts を新規作成します。
import { describe, it, expect } from "vitest"
import { getAll, getById, create, remove } from "./todos.js"
describe("todos", () => {
it("getAll returns an array", () => {
expect(Array.isArray(getAll())).toBe(true)
})
it("create adds a new todo", () => {
const before = getAll().length
const todo = create("テスト用タスク")
expect(getAll().length).toBe(before + 1)
expect(todo.title).toBe("テスト用タスク")
expect(todo.done).toBe(false)
})
it("getById finds the created todo", () => {
const todo = create("検索テスト")
const found = getById(todo.id)
expect(found?.title).toBe("検索テスト")
})
it("remove deletes a todo", () => {
const todo = create("削除される予定")
const ok = remove(todo.id)
expect(ok).toBe(true)
expect(getById(todo.id)).toBeUndefined()
})
it("remove returns false for non-existent id", () => {
const ok = remove(999999)
expect(ok).toBe(false)
})
})
実行して確認します。
npm test
全件パスすればOKです。
3. Dockerでコンテナ化
3-1. Dockerfile作成
プロジェクト直下にDockerfileを作成します。
# ビルド用ステージ
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 実行用ステージ
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
ENV NODE_ENV=production
EXPOSE 8080
CMD ["node", "dist/index.js"]
マルチステージビルドにすることで、本番イメージには開発用の依存関係やソースコードを含めず、ビルド成果物だけを含めた軽量なイメージになります。
package.jsonのscriptsにbuildがなければ追加してください(テンプレートによっては既にある場合があります)。
{
"scripts": {
"build": "tsc"
}
}
tsconfig.jsonのoutDirがdistになっているか確認してください。
3-2. .dockerignore作成
.dockerignoreを作成し、不要なファイルをビルドコンテキストから除外します。
node_modules
dist
.git
*.md
.env
3-3. ローカルでビルド・起動確認
docker build -t todo-api .
docker run -p 8080:8080 todo-api
別ターミナルから確認します。
curl http://localhost:8080/api/todos
ローカルのnpm run devと同じ結果が返ってくればOKです。Ctrl+Cでコンテナを止めておきます。
4. Cloud Runへ手動デプロイ
4-1. Artifact Registryにリポジトリを作成
Dockerイメージの置き場所を作ります。
gcloud artifacts repositories create todo-api-repo \
--repository-format=docker \
--location=asia-northeast1 \
--description="todo-api docker images"
4-2. Dockerの認証設定
gcloud auth configure-docker asia-northeast1-docker.pkg.dev
4-3. イメージのビルド・タグ付け・push
macOS/Linux(bash/zsh)の場合:
export PROJECT_ID=$(gcloud config get-value project)
docker build -t asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-api-repo/todo-api:latest .
docker push asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-api-repo/todo-api:latest
Windows(PowerShell)の場合:
$env:PROJECT_ID = (gcloud config get-value project)
docker build -t asia-northeast1-docker.pkg.dev/$env:PROJECT_ID/todo-api-repo/todo-api:latest .
docker push asia-northeast1-docker.pkg.dev/$env:PROJECT_ID/todo-api-repo/todo-api:latest
PowerShellでは
exportは使えず、$PROJECT_IDのような書き方も反映されません。環境変数は$env:変数名でセット・参照します。また環境変数はそのターミナルのセッション内でしか有効でないため、別のターミナルタブやウィンドウを開くと消える点にも注意してください。以降のコマンドでも$PROJECT_IDはPowerShellでは$env:PROJECT_IDと読み替えてください。
4-4. Cloud Runへデプロイ
gcloud run deploy todo-api \
--image=asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-api-repo/todo-api:latest \
--region=asia-northeast1 \
--platform=managed \
--allow-unauthenticated
--allow-unauthenticatedは未認証アクセスを許可するオプションです(誰でもアクセスできる公開APIにする場合はこれが必要)。
デプロイが完了すると、以下のようなURLが表示されます。
Service URL: https://todo-api-xxxxxxxxxx-an.a.run.app
このURLにアクセスして確認します。
curl https://todo-api-xxxxxxxxxx-an.a.run.app/api/todos
ローカルと同じ結果が返ってくれば手動デプロイは成功です。
5. GitHub Actionsでテスト自動化
5-1. GitHubリポジトリの作成とプッシュ
git init
git add .
git commit -m "Initial commit: todo-api"
git branch -M main
git remote add origin https://github.com/<あなたのユーザー名>/todo-api.git
git push -u origin main
5-2. テスト用ワークフロー
.github/workflows/ci.ymlを作成します。
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm test
-
push/pull_requestのどちらでもmainブランチ向けなら実行されます -
npm ciはロックファイル通りに厳密インストールする、CI向けのコマンドです - テストが落ちればここで検知でき、以降のデプロイに進みません
コミットしてpushし、GitHubのActionsタブで緑のチェックが付くことを確認してください。
git add .github/workflows/ci.yml
git commit -m "Add CI workflow"
git push
6. GitHub ActionsでCloud Runへ自動デプロイ
6-1. サービスアカウントの作成
デプロイ用の専用サービスアカウントを作ります。
gcloud iam service-accounts create github-deployer \
--display-name="GitHub Actions Deployer"
必要な権限を付与します。名前は必ず直前のcreateコマンドで作成したサービスアカウント名と一致させてください(github-deployerの部分を書き換えていたら、以降も同じ名前で揃えます)。
macOS/Linux(bash/zsh)の場合:
export SA_EMAIL=github-deployer@$PROJECT_ID.iam.gserviceaccount.com
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/run.admin"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/artifactregistry.writer"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/iam.serviceAccountUser"
Windows(PowerShell)の場合:
$env:SA_EMAIL = "github-deployer@$env:PROJECT_ID.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding $env:PROJECT_ID `
--member="serviceAccount:$env:SA_EMAIL" `
--role="roles/run.admin"
gcloud projects add-iam-policy-binding $env:PROJECT_ID `
--member="serviceAccount:$env:SA_EMAIL" `
--role="roles/artifactregistry.writer"
gcloud projects add-iam-policy-binding $env:PROJECT_ID `
--member="serviceAccount:$env:SA_EMAIL" `
--role="roles/iam.serviceAccountUser"
PowerShellで文字列を組み立てるときは
""で囲む必要があります。囲まずに$env:SA_EMAIL = github-deployer@...と書くと、コマンドとして実行しようとしてエラーになります。
6-2. 認証キーの発行
macOS/Linux(bash/zsh)の場合:
gcloud iam service-accounts keys create key.json \
--iam-account=$SA_EMAIL
Windows(PowerShell)の場合:
gcloud iam service-accounts keys create key.json `
--iam-account=$env:SA_EMAIL
key.jsonが手元に生成されます。このファイルは絶対にGitにコミットしないでください。
サービスアカウントキーは漏洩リスクがあるため、本来はWorkload Identity連携(キーレス認証)が推奨されます。今回は入門向けにシンプルなキー方式で進めますが、本番運用するなら移行を検討してください。
6-3. GitHub Secretsへ登録
GitHubリポジトリの Settings > Secrets and variables > Actions から、以下のSecretを登録します。
| Secret名 | 値 |
|---|---|
GCP_PROJECT_ID |
$PROJECT_IDの値 |
GCP_SA_KEY |
key.jsonの中身をそのままコピー&ペースト |
登録し終えたら、ローカルのkey.jsonは削除しておきましょう。
rm key.json
6-4. デプロイ用ワークフロー
.github/workflows/deploy.ymlを作成します。
name: Deploy to Cloud Run
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/auth@v2
with:
credentials_json: "${{ secrets.GCP_SA_KEY }}"
- uses: google-github-actions/setup-gcloud@v2
- name: Configure Docker
run: gcloud auth configure-docker asia-northeast1-docker.pkg.dev
- name: Build and push image
run: |
docker build -t asia-northeast1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/todo-api-repo/todo-api:${{ github.sha }} .
docker push asia-northeast1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/todo-api-repo/todo-api:${{ github.sha }}
- name: Deploy to Cloud Run
run: |
gcloud run deploy todo-api \
--image=asia-northeast1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/todo-api-repo/todo-api:${{ github.sha }} \
--region=asia-northeast1 \
--platform=managed \
--allow-unauthenticated \
--project=${{ secrets.GCP_PROJECT_ID }}
ポイント:
-
testジョブが失敗するとdeployジョブは実行されません(needs: test) - イメージタグに
${{ github.sha }}(コミットハッシュ)を使うことで、どのコミットのイメージがデプロイされたか追跡できます -
google-github-actions/authでサービスアカウントキーを使って認証し、setup-gcloudでgcloud CLIをセットアップします
コミットしてpushします。
git add .github/workflows/deploy.yml
git commit -m "Add Cloud Run deploy workflow"
git push
GitHubのActionsタブでtest→deployの順に緑になり、Cloud RunのURLにアクセスして最新の変更が反映されていれば成功です。
7. CI/CDの役割分担
git push (main)
│
├─ test ジョブ
│ ├─ npm ci
│ └─ npm test ← ここで落ちればdeployは実行されない
│
└─ deploy ジョブ(testの後)
├─ docker build
├─ Artifact Registryへpush
└─ Cloud Runへdeploy
前回のCloudflare Workers版では「GitHub Actionsでテスト」「Cloudflareのビルドコマンドで最終ゲート」という二段構えでしたが、今回はGitHub Actions一本でtest→deployまで完結させる構成です。Cloud RunにはCloudflare Pagesのようなビルド時テスト機構がないため、CI側でゲートを持たせる必要があります。
まとめ
- Hono(Node.jsテンプレート) +
@hono/node-serverでCloud Run向けのAPIサーバーを作成 - マルチステージDockerfileでビルド成果物のみを含む軽量イメージを作成
- Artifact Registryにイメージをpushし、Cloud Runへデプロイ
- Vitestでロジックのテストを書き、GitHub Actionsでtest→build→deployを自動化
- サービスアカウントキーでの認証は入門向けの簡易構成。本番ではWorkload Identity連携への移行を検討する
前回のCloudflare Workers版と比べると、Cloud Runは「コンテナで動く」という自由度の高さがある一方、Dockerfileの管理やイメージのビルド・pushといった一手間が増える構成になっています。


