👏

GitHub Actions & Google CloudでつくるかんたんなCI/CD

に公開

こんにちは

先日ハッカソンで作成したものをガチでサービス化するためにインフラ経験ゼロの僕が,クラウドサービスを使ってCI/CDを整えてみました.GCPこれから触るよって人にぜひ読んでもらえたらと思います.

そもそもなんでやるの?

これに関しては,現在開発している成果物を「みんなに使ってもらいたい」,「安定した基盤の上で動かしたい」という理由があって今回Google CloudとActionsを使ってCI/CDを組んでみました.
まとめると以下のとおりです.

  • サービスを「公開・リリース」して「定期的に更新」したい
  • GitHubのmainブランチにマージしたら自動でアプリがアップデートされる
  • Gemini APIなど、有料API利用もあるので 構成は最小限かつ柔軟に
  • 開発フェーズが終わってから本格運用に備える形でもOK!

※ これを活用した制作物はこれ↓(宣伝)
https://github.com/work-in-progress-team/RepoInterviewer


Google Cloudを選んだ理由

比較項目 AWS Google Cloud
利用企業 多い 少なめ
難易度 高(ツールが多い) 低(シンプル)
無料枠 少ない 多い(Cloud Run など)
ドキュメント 多い(日本語も豊富) 少なめ
自由度 高(設計の幅が広い) 低〜中
コスト感 上がりやすい 抑えやすい

結論:Google Cloudにして良かった。
今はプロトタイプ開発フェーズなので、リリース・運用の経験値獲得が最優先!


今回作る全体構成図・技術構成

開発者 → GitHub Push(PR作成)
             ↓
    [GitHub Actions - test.yml]
        └── pytest(tests/unit)
             ↓
        ✅ 通過 → Merge可
        ❌ 失敗 → Merge 不可

mainブランチにマージ
             ↓
    [GitHub Actions - deploy.yml]
        ├── Docker build
        ├── integrationテスト
        └── Cloud Build → Cloud Runへ Deploy
             ↓
    [Cloud Run] FastAPI API
        ⇄ [Cloud Firestore]

今回は,mainブランチにマージしたら早速デプロイするようにしておりますが,僕らの環境では devブランチとmainブランチを用意して,それぞれにマージしたらstg環境,prod環境にデプロイするように作成しました.
また,しれっと「Pytest」「FastAPI」などと書いてありますが今回は以下の技術スタックでローカル環境を構築 → デプロイ をやってみます.

技術 備考
Python 書きやすい!簡単!
FastAPI PythonのWebフレームワーク
Firestore-emulator Firestoreをローカルで動かせるやつ

やっていく

今回は,実際にAPIを作ってそれをデプロイするといった具合でやってみます.
今回作るAPIは,人材紹介サービスのユーザープロフィール情報管理APIとしてみましょう.
できることとしては

  • ユーザー情報のCRUD処理
    • 必須カラム(名前,年齢,性別,職業,技術経験,自己紹介文)
    • 非必須カラム(趣味タグ,資格,希望職種,ポートフォリオ)

環境構築

Google Cloud関連を用意

Google Cloudの環境が必要なので,以下からアカウント登録&プロジェクトを作りましょう!
https://cloud.google.com/docs/get-started?hl=ja
ここで登録してから,「新しいプロジェクト」を押して

作成したプロジェクトを選択してから,gcloud-cliを入れます.これはローカルでGoogleCloudのプロダクトを操作するために必要なCLIツールです.以下からセットアップできます.
https://cloud.google.com/sdk/docs/install?hl=ja

そして

gcloud emulators firestore start

で動作が確認できたらOKです.

GitHub

完成版をGitHubに公開してあります!
https://github.com/cercil0605/sample_fastapi_cicd

ローカル環境で構築

前提条件

  • Python 3.10がローカルに入っていること
    これだけです.ではまず,作業ディレクトリを作ってPythonの仮想環境を作っちゃいましょう!
mkdir (任意)
python3 -m venv venv
source venv/bin/activate

次に,必要なライブラリを入れていきます.

touch requirements.txt
requirements.txt
fastapi 
uvicorn 
pydantic
google-cloud-firestore
dotenv
pip install -r requirements.txt

動作確認

これで整ったので,確認していきます.
まず,Firestore emulatorを動かします.

gcloud emulators firestore start --host-port=localhost:8090

次に以下のPythonファイルを作成,実行します.

test.py
import os
from dotenv import load_dotenv
from google.cloud import firestore

# .env の読み込み
load_dotenv()
# google cloudに設定したproject名とローカルホスト先
project_id = os.getenv("FIRESTORE_PROJECT_ID")
emulator_host = os.getenv("FIRESTORE_EMULATOR_HOST")

if not emulator_host:
    raise RuntimeError("FIRESTORE_EMULATOR_HOST is not set in .env")

print(f"Connecting to Firestore Emulator at {emulator_host}, project={project_id}")

db = firestore.Client(project=project_id)

# --- 書き込みテスト ---
doc_ref = db.collection("test_collection").document("test_doc")
doc_ref.set({
    "message": "Hello Firestore!",
    "status": "ok"
})
print("✅ Document written to Firestore Emulator.")

# --- 読み込みテスト ---
doc = doc_ref.get()
if doc.exists:
    print("✅ Document read from Firestore Emulator:")
    print(doc.to_dict())
else:
    print("❌ Failed to read document from Firestore Emulator.")

実行結果

(venv) ╭─kei@Keis-MacBook-Pro ~/Documents/sample_fastapi_cicd 
╰─$ /Users/kei/Documents/sample_fastapi_cicd/venv/bin/python /Users/kei/Documents/sample_fastapi_cicd/test.py                              1 ↵
Connecting to Firestore Emulator at localhost:8090, project=sample-cicd
✅ Document written to Firestore Emulator.
✅ Document read from Firestore Emulator:
{'status': 'ok', 'message': 'Hello Firestore!'}

ここまでできたらOKです.

API構築

さて,ここからAPIを構築していきます.
今回は,人材紹介サービスのユーザープロフィール情報管理APIを作ってみます.
pytestやCI/CDを組みこむので少し本格的なフォルダ構成でやってみます.

ディレクトリ&ソースコード

├── app
│   ├── api
│   │   └── routers
│   │       └── users.py
│   ├── domain
│   │   └── schema.py
│   ├── infra
│   │   ├── firestore_client.py
│   │   └── repository.py
│   ├── main.py
│   └── usecase
│       └── user_usecase.py
├── requirements.txt
├── test.py
└── tests
    ├── integration
    │   └── test_users_api.py
    └── unit
        └── test_usecases.py

ソースコードはGitHubを貼っておくのでここのやつを使ってください.

https://github.com/cercil0605/sample_fastapi_cicd

実際にlocalhost:8000/docsにアクセスしてSwaggerが出たらOKです.

いくつかのAPIにリクエストを送って反応を確認してください.

テストを実行する

今回は,CIの段階でPytestを稼働させています.つまり,PullRequestを出すごとにActions側で/testsのテストが全て実行されるようにします.

用意したテストは

  • unitテスト(全ユースケース)
  • integrationテスト(API丸ごとテスト)

に簡単に用意しておきました.これをルートディレクトリで

PYTHONPATH=. pytest -q

とやって通ることを確認してください.

今回はちょっとしっかりめなAPIを構築したため,それぞれのユースケースに応じて動作確認することをいちいち手動で確認してられないので,テストを用いてしっかりと確認してから本番環境にあげられるようにするためです!

CI構築

それでは,CIを構築していきます.ActionsにPRがあるたびにテストしてほしいので,以下のようなファイルを作成・レポジトリに上げます.

.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  CI:
    name: check-CI
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.10"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run tests
        run: |
          PYTHONPATH=. pytest -q --disable-warnings --maxfail=1

そうすると勝手にレポジトリでこのように動きます.

そして,絶対にGitHubのブランチルールを以下の記事などを参考にテストが通らないとマージできないようにしてください.
https://zenn.dev/tomona/articles/fc74427246500e

試しに,ブランチを切ってテストが落ちるようなことをしてみると...

PRを出してもマージできないですね.これでOKです.

CD構築

長いですね....お待たせしました本番です.
ここでやりたいことは

  • 自動で本番環境にデプロイする仕組みを作る
  • いつアクセスしても動くようにする
    この2つになります.

まず,GCP上で構築したい仕組みの図を示します.

フローとしては

  1. mainブランチにpushされた内容を検知
  2. Cloud BuildでDockerイメージを構築
  3. Artifact Registryに完成したイメージを保存
  4. Cloud Run・Firestoreで完成したイメージを元に動かす.

といった具合です.

CloudRun構築まで

こちらを参考に行っていきます.
https://cloud.google.com/build/docs/building/build-containerize-python?hl=ja
https://cloud.google.com/build/docs/build-push-docker-image?hl=ja
以下3つのAPIを有効化してください.

  • Cloud Build API
  • Compute Engine API
  • Artifact Registry API

次に,サービスアカウントに権限を与えていきます.このアカウントは,僕らがGoogle Cloudにアクセスしているアカウントではなく,デフォルトで設定されているGoogle Cloudの処理を実行してくれるアカウントです.

# 各自PCのCLI or gcloud cliで実行
gcloud config set project sample-cicd
# 権限付与
# storage.objectUserに追加
gcloud projects add-iam-policy-binding sample-cicd-468503 \
    --member=serviceAccount:$(gcloud projects describe sample-cicd-468503 \
    --format="value(projectNumber)")-compute@developer.gserviceaccount.com \
    --role="roles/storage.objectUser"

# Artifact Registryに書き込み権限
gcloud projects add-iam-policy-binding sample-cicd-468503 \
 --member=serviceAccount:$(gcloud projects describe sample-cicd-468503 \
 --format="value(projectNumber)")-compute@developer.gserviceaccount.com \
 --role="roles/artifactregistry.writer"

# loggingに書き込み権限
gcloud projects add-iam-policy-binding sample-cicd-468503 \
 --member=serviceAccount:$(gcloud projects describe sample-cicd-468503 \
 --format="value(projectNumber)")-compute@developer.gserviceaccount.com \
 --role="roles/logging.logWriter"

gcloud iam service-accounts add-iam-policy-binding $(gcloud projects describe sample-cicd-468503 \
    --format="value(projectNumber)")-compute@developer.gserviceaccount.com \
    --member=serviceAccount:$(gcloud projects describe sample-cicd-468503 \
    --format="value(projectNumber)")-compute@developer.gserviceaccount.com \
    --role="roles/iam.serviceAccountUser" \
    --project=sample-cicd-468503

# Firestoreの有効化とDB作成
gcloud services enable firestore.googleapis.com && \
gcloud firestore databases create \
  --region=asia-northeast1 \
  --type=firestore-native

そして,Cloud BuildはDockerfileからイメージを構築し,Cloud Runで動かすという流れになるため,Dockerfileをローカルで作成します.(配布レポジトリではすでに作成済)

Dockerfile
FROM python:3.10-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

次に,Dockerイメージを保存・Cloud Runに渡すためにArtifact Registryをセットアップします.(うまくいかないときはGUIで試してもOKです!)

gcloud artifacts repositories create sample-fastapi-cicd-docker  --repository-format=docker \
    --location=asia-northeast1 --description="Docker repository for fastapi"

そして最後に,CloudBuildのためにcloudbuild.ymlを作成します.

cloudbuild.yml
steps:
  # Install dependencies
  - name: python
    entrypoint: pip
    args: ["install", "-r", "requirements.txt", "--user"]
  # Docker Build
  - name: "gcr.io/cloud-builders/docker"
    args:
      [
        "build",
        "-t",
        "asia-northeast1-docker.pkg.dev/${PROJECT_ID}/sample-fastapi-cicd-docker/backend:${SHORT_SHA}",
        ".",
      ]
  # Docker push to Google Artifact Registry
  - name: "gcr.io/cloud-builders/docker"
    args:
      [
        "push",
        "asia-northeast1-docker.pkg.dev/${PROJECT_ID}/sample-fastapi-cicd-docker/backend:${SHORT_SHA}",
      ]
  # Deploy to Cloud Run
  - name: google/cloud-sdk
    args:
      [
        "gcloud",
        "run",
        "deploy",
        "sample-cicd",
        "--image=asia-northeast1-docker.pkg.dev/${PROJECT_ID}/sample-fastapi-cicd-docker/backend:${SHORT_SHA}",
        "--region",
        "asia-northeast1",
        "--platform",
        "managed",
        "--allow-unauthenticated",
      ]

options:
  logging: CLOUD_LOGGING_ONLY

これで完成しました!

そしたら「Cloud Build」にて以下を行ってください.

まず,サービスアカウントの権限を与えましょう.

こうしないとデプロイできなくなります!

次に,mainにマージしたらデプロイされるように構成します.

これで,mainブランチにマージされたらデプロイが実行されます.

CloudBuildの履歴が

こんな感じに成功になっていればOKです.

次に,CloudRunにアクセスして,デプロイしたコンテナ -> セキュリティ -> 認証 から「公開アクセスを許可する」を有効にしたらURLからアクセスできます!

/healthにアクセスして

完璧です!

さいごに

ここまで読んでくださりありがとうございました!
初めてマネージドサービスを複数用いて構築しましたが,かなり苦労したのでこの記事を読んで困らない人が増えれば嬉しいです!

Discussion