はじめに
Spring Bootで作成したREST APIをDocker化し、Google CloudのCloud Runへデプロイしました。
今回使用した主な構成は以下です。
- Java 21
- Spring Boot 4.1
- PostgreSQL
- Flyway
- Docker
- Cloud Run
- Cloud SQL for PostgreSQL
- Artifact Registry
- Secret Manager
- OpenAPI / Swagger UI
実際にデプロイすることはできましたが、最初はコマンドを順番に実行しているだけで、それぞれが何をしているのか十分に理解できていませんでした。
そこで本記事では、実際に行った手順に加えて、各サービスやコマンドの役割も整理します。
また、非公開のCloud Run上でSwagger UIを確認した際に発生した、以下のエラーについても解説します。
Failed to fetch.
Possible Reasons:
CORS
Network Failure
URL scheme must be "http" or "https" for CORS request.
完成後の構成
最終的な構成は以下のようになります。
周辺では、以下のGoogle Cloudサービスも使用します。
Artifact Registry
└─ Dockerイメージを保存する
Secret Manager
└─ DBパスワードを保存する
サービスアカウント
└─ Cloud Runに必要な権限を与える
それぞれの役割は以下のとおりです。
| サービス | 役割 |
|---|---|
| Cloud Run | Spring Bootアプリを実行する |
| Cloud SQL | PostgreSQLを実行する |
| Artifact Registry | Dockerイメージを保存する |
| Secret Manager | DBパスワードなどの機密情報を保存する |
| サービスアカウント | Cloud RunがほかのGoogle Cloudサービスを利用するための権限を持つ |
前提条件
本記事では、以下の状態まで完了していることを前提とします。
- Spring Bootアプリがローカルで起動できる
- PostgreSQLを使用している
- FlywayでDBマイグレーションを管理している
- Dockerfileを作成済み
- Dockerイメージをローカルでビルドできる
- Swagger UIがローカルで表示できる
- Google Cloudプロジェクトを作成済み
- Google Cloud CLIをインストール済み
Google Cloud CLIが利用できるか確認します。
gcloud --version
Dockerが利用できるかも確認します。
docker --version
1. Google Cloudへログインする
Google Cloud CLIからログインします。
gcloud auth login
ブラウザが開くので、Google Cloudを使用するGoogleアカウントでログインします。
ログイン状態は以下で確認できます。
gcloud auth list
ここで行っているのは、Google Cloud CLIに対して、
このGoogleアカウントとしてGoogle Cloudを操作する
という認証を行う作業です。
2. Google Cloudプロジェクトを設定する
アクセス可能なプロジェクトを一覧表示します。
gcloud projects list
表示例です。
PROJECT_ID NAME PROJECT_NUMBER
freelance-manager-123456 Freelance Manager 123456789012
使用するのはPROJECT_IDです。
PowerShell変数へ設定します。
$PROJECT_ID = "freelance-manager-123456"
Google Cloud CLIが操作するプロジェクトを設定します。
gcloud config set project $PROJECT_ID
設定されたプロジェクトは以下で確認できます。
gcloud config get-value project
この設定によって、
これ以降のgcloudコマンドは、このプロジェクトに対して実行する
と指定しています。
3. 作業用のPowerShell変数を設定する
以降のコマンドを短くするため、使用する名前をPowerShell変数へ設定します。
$PROJECT_ID = "freelance-manager-123456"
$REGION = "asia-northeast1"
$REPOSITORY = "freelance-manager"
$IMAGE = "freelance-manager-api"
$SERVICE = "freelance-manager-api"
$INSTANCE = "freelance-manager-db"
$DB_NAME = "freelance_manager"
$DB_USER = "app"
$SERVICE_ACCOUNT_NAME = "freelance-manager-run"
$SERVICE_ACCOUNT_EMAIL = "${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
$SECRET_NAME = "freelance-manager-db-password"
asia-northeast1は東京リージョンです。
設定内容を確認します。
Write-Host "PROJECT_ID: $PROJECT_ID"
Write-Host "REGION: $REGION"
Write-Host "SERVICE: $SERVICE"
Write-Host "INSTANCE: $INSTANCE"
Write-Host "SERVICE_ACCOUNT_EMAIL: $SERVICE_ACCOUNT_EMAIL"
これらはGoogle Cloud上へ登録される設定ではなく、現在開いているPowerShell内だけで使用する一時的な変数です。
PowerShellを閉じると変数は消えますが、Google Cloud上に作成したリソースは消えません。
4. 使用するGoogle Cloud APIを有効化する
今回使用するサービスのAPIを有効化します。
gcloud services enable `
run.googleapis.com `
artifactregistry.googleapis.com `
sqladmin.googleapis.com `
secretmanager.googleapis.com
有効化しているAPIの役割は以下です。
| API | 用途 |
|---|---|
| Cloud Run API | Cloud Runサービスの作成と管理 |
| Artifact Registry API | Dockerイメージの保存 |
| Cloud SQL Admin API | Cloud SQLの作成と管理 |
| Secret Manager API | シークレットの作成と管理 |
Google Cloudでは、サービスを使用する前に対応するAPIをプロジェクト単位で有効化する必要があります。
5. Cloud SQL Java Connectorを追加する
Spring BootからCloud SQLへ接続するため、Cloud SQL Java Connectorを追加します。
build.gradleのdependenciesへ追加します。
implementation 'com.google.cloud.sql:postgres-socket-factory'
PostgreSQL JDBC Driverも必要です。
runtimeOnly 'org.postgresql:postgresql'
Cloud SQL Java Connectorは主に以下を担当します。
- Cloud SQLインスタンスの接続先解決
- IAMを利用した認証
- 通信の暗号化
- 接続情報の更新
アプリ側でCloud SQLのIPアドレスやSSL証明書を直接管理せずに接続できます。
6. Spring Bootの本番用設定を作成する
本番環境用に、以下のファイルを作成します。
src/main/resources/application-prod.yml
設定例です。
spring:
datasource:
url: jdbc:postgresql:///${DB_NAME}?cloudSqlInstance=${INSTANCE_CONNECTION_NAME}&socketFactory=com.google.cloud.sql.postgres.SocketFactory&cloudSqlRefreshStrategy=lazy
username: ${DB_USER}
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 5
minimum-idle: 0
jpa:
hibernate:
ddl-auto: validate
flyway:
enabled: true
DB接続情報を設定ファイルへ直接書かず、環境変数から受け取ります。
username: ${DB_USER}
password: ${DB_PASSWORD}
これにより、DBパスワードをGitHubへコミットせずに済みます。
Spring Profileによる環境分離
Spring Bootでは、環境ごとに設定ファイルを分けられます。
application.yml
└─ 共通設定
application-local.yml
└─ ローカル開発用
application-prod.yml
└─ Cloud Run用
Cloud Runには、後述するデプロイコマンドで以下の環境変数を設定します。
SPRING_PROFILES_ACTIVE=prod
これにより、Cloud Run上ではapplication-prod.ymlが読み込まれます。
7. Cloud Run用のポート設定を確認する
Cloud Runでは、環境変数PORTで指定されたポートでHTTPリクエストを待ち受ける必要があります。
application.ymlなどに以下を設定します。
server:
port: ${PORT:8080}
ローカルでは環境変数PORTがないため、8080が使用されます。
Cloud Runでは、Cloud Runから渡されたPORTの値が使用されます。
8. Cloud Run用サービスアカウントを作成する
Cloud Run専用のサービスアカウントを作成します。
gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME `
--display-name="Freelance Manager Cloud Run"
作成されるメールアドレスは、次のような形式です。
freelance-manager-run@プロジェクトID.iam.gserviceaccount.com
サービスアカウントは、人間ではなくアプリケーション用のGoogle Cloudアカウントです。
今回、Cloud Runはこのサービスアカウントとして動作します。
9. Cloud SQLへの接続権限を付与する
Cloud RunからCloud SQLへ接続するため、サービスアカウントへCloud SQL Clientロールを付与します。
gcloud projects add-iam-policy-binding $PROJECT_ID `
--member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" `
--role="roles/cloudsql.client"
これは、
このサービスアカウントはCloud SQLへ接続してよい
という権限を付与しています。
管理者権限を丸ごと与えるのではなく、必要な権限だけを付与します。
10. Cloud SQL for PostgreSQLを作成する
Cloud SQLインスタンスは、Google Cloudコンソールから作成しました。
設定例は以下です。
データベースエンジン:PostgreSQL
インスタンスID:freelance-manager-db
リージョン:asia-northeast1
用途:開発用
可用性:シングルゾーン
Cloud RunとCloud SQLは、同じリージョンに作成します。
インスタンス、データベース、ユーザーの違い
Cloud SQLでは、以下の要素を作成します。
Cloud SQLインスタンス
└─ PostgreSQLサーバーそのもの
データベース
└─ アプリが使用するデータ領域
DBユーザー
└─ Spring BootがDBへ接続するときに使うユーザー
データベースを作成します。
gcloud sql databases create $DB_NAME `
--instance=$INSTANCE
DBユーザーを作成します。
gcloud sql users create $DB_USER `
--instance=$INSTANCE `
--password="任意の安全なパスワード"
このパスワードは、ソースコードやREADMEへ記載しないようにします。
11. DBパスワードをSecret Managerへ保存する
Cloud SQLのDBパスワードをSecret Managerへ登録します。
シークレット名は以下としました。
freelance-manager-db-password
Secret Managerには、Cloud SQLユーザー作成時に設定したパスワードを保存します。
続いて、Cloud Run用サービスアカウントへ、シークレットを読み取る権限を付与します。
gcloud secrets add-iam-policy-binding $SECRET_NAME `
--member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" `
--role="roles/secretmanager.secretAccessor"
これにより、Cloud Runがサービスアカウントを通してSecret ManagerからDBパスワードを取得できます。
Spring Boot側では、取得した値がDB_PASSWORD環境変数として渡されます。
password: ${DB_PASSWORD}
12. Artifact Registryを作成する
Dockerイメージを保存するため、Artifact Registryのリポジトリを作成します。
gcloud artifacts repositories create $REPOSITORY `
--repository-format=docker `
--location=$REGION `
--description="Freelance Manager container images"
DockerからArtifact Registryへログインできるように設定します。
gcloud auth configure-docker "${REGION}-docker.pkg.dev"
Artifact Registryは、Dockerイメージを保存するためのサービスです。
GitHubがソースコードを保存する場所だとすると、Artifact Registryは実行可能なDockerイメージを保存する場所です。
13. Dockerイメージをビルドする
Artifact Registry上のイメージ名を作成します。
$IMAGE_URI = "${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE}:v1"
Dockerイメージをビルドします。
docker build -t $IMAGE_URI .
このコマンドでは、Dockerfileをもとに以下を行います。
ビルドされたイメージを確認します。
docker images
14. DockerイメージをArtifact Registryへpushする
作成したDockerイメージをArtifact Registryへアップロードします。
docker push $IMAGE_URI
ここまでの流れは以下です。
Artifact Registryへ登録されたイメージを確認します。
gcloud artifacts docker images list `
"${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE}" `
--include-tags
15. Cloud SQLのインスタンス接続名を取得する
Cloud SQL Java Connectorが接続先を特定するため、インスタンス接続名が必要です。
$INSTANCE_CONNECTION_NAME = gcloud sql instances describe $INSTANCE `
--format="value(connectionName)"
確認します。
$INSTANCE_CONNECTION_NAME
次のような形式です。
プロジェクトID:リージョン:インスタンス名
例です。
freelance-manager-123456:asia-northeast1:freelance-manager-db
16. Cloud Runへデプロイする
Artifact RegistryのDockerイメージをCloud Runへデプロイします。
gcloud run deploy $SERVICE `
--image=$IMAGE_URI `
--region=$REGION `
--service-account=$SERVICE_ACCOUNT_EMAIL `
--no-allow-unauthenticated `
--memory=1Gi `
--max-instances=2 `
--set-env-vars="SPRING_PROFILES_ACTIVE=prod,DB_NAME=$DB_NAME,DB_USER=$DB_USER,INSTANCE_CONNECTION_NAME=$INSTANCE_CONNECTION_NAME" `
--update-secrets="DB_PASSWORD=${SECRET_NAME}:1"
各オプションの意味は以下です。
| オプション | 意味 |
|---|---|
--image |
実行するDockerイメージ |
--region |
Cloud Runを配置するリージョン |
--service-account |
Cloud Runが使用するサービスアカウント |
--no-allow-unauthenticated |
未認証アクセスを禁止 |
--memory |
コンテナに割り当てるメモリ |
--max-instances |
最大インスタンス数 |
--set-env-vars |
Spring Bootへ渡す環境変数 |
--update-secrets |
Secret Managerの値を環境変数として渡す |
SPRING_PROFILES_ACTIVE=prodはどこで設定しているか
以下の部分です。
--set-env-vars="SPRING_PROFILES_ACTIVE=prod,..."
Cloud Runがコンテナを起動すると、コンテナ内へ次の環境変数が渡されます。
SPRING_PROFILES_ACTIVE=prod
Spring Bootはこの値を読み取り、application-prod.ymlを有効化します。
Cloud Runの環境変数
↓
SPRING_PROFILES_ACTIVE=prod
↓
Spring Bootがprodプロファイルを有効化
↓
application-prod.ymlを読み込む
DB_PASSWORD=${SECRET_NAME}:1の:1とは何か
以下の設定では、末尾の:1がSecret Managerのバージョン番号を表します。
--update-secrets="DB_PASSWORD=${SECRET_NAME}:1"
分解すると以下の意味です。
DB_PASSWORD
→ Cloud Runコンテナ内の環境変数名
${SECRET_NAME}
→ Secret Managerのシークレット名
1
→ 使用するシークレットのバージョン番号
Secret Managerでは、同じシークレット名のまま複数の値を履歴管理できます。
freelance-manager-db-password
├─ version 1:最初のDBパスワード
├─ version 2:変更後のDBパスワード
└─ version 3:さらに変更したDBパスワード
:1を指定すると、Cloud Runはコンテナ起動時にバージョン1の値を取得し、DB_PASSWORD環境変数として設定します。
Cloud Runがコンテナを起動
↓
Secret Managerのバージョン1を取得
↓
DB_PASSWORDへ設定
↓
Spring BootがDB接続に使用
latestを指定することもできますが、環境変数として利用する場合は、動作確認済みのバージョン番号へ固定した方が管理しやすくなります。
17. Cloud Runのログを確認する
ログを確認します。
gcloud run services logs read $SERVICE `
--region=$REGION `
--limit=100
主に以下を確認します。
- Spring Bootが正常に起動した
- Cloud SQLへ接続できた
- Flywayが実行された
- Hibernateの検証が成功した
- ポートが正常に開かれた
初回起動時は、FlywayがCloud SQL内にテーブルを作成します。
flyway_schema_historyには、実行済みマイグレーションの履歴が保存されます。
18. Cloud RunのURLを取得する
サービスURLを取得します。
$SERVICE_URL = gcloud run services describe $SERVICE `
--region=$REGION `
--format="value(status.url)"
確認します。
$SERVICE_URL
Cloud RunはHTTPSのURLを自動的に発行します。
19. 非公開のCloud RunへAPIリクエストを送る
今回は以下のオプションを指定しています。
--no-allow-unauthenticated
そのため、Google Cloudの認証なしではアクセスできません。
認証トークンを取得します。
$TOKEN = gcloud auth print-identity-token
APIへアクセスします。
Invoke-RestMethod `
-Uri "$SERVICE_URL/api/clients" `
-Headers @{ Authorization = "Bearer $TOKEN" } `
-Method Get
Authorizationヘッダーへトークンを付けることで、
Google Cloudへログイン済みのユーザーからのリクエスト
であることを証明しています。
20. 非公開Cloud RunのSwagger UIを確認する
通常のブラウザでCloud RunのURLを直接開いても、Cloud Run用の認証トークンが付かないため、非公開サービスにはアクセスできません。
Swagger UIを確認する場合は、Cloud Runプロキシを利用します。
gcloud run services proxy $SERVICE `
--region=$REGION `
--port=8080
その後、ブラウザで以下を開きます。
http://localhost:8080/swagger-ui.html
通信の流れは以下です。
プロキシを通すことで、ブラウザ側でCloud RunのIDトークンを設定せずにSwagger UIを確認できます。
21. Swagger UIでFailed to fetchが発生した
Swagger UIは表示できましたが、APIを実行すると以下のエラーになりました。
Failed to fetch.
Possible Reasons:
CORS
Network Failure
URL scheme must be "http" or "https" for CORS request.
Swagger UIのServers欄を確認すると、以下の値が表示されていました。
http://freelance-manager-api-xxxxx-an.a.run.app
この状態では、Swagger UI自体は以下から開いています。
http://localhost:8080/swagger-ui.html
しかし、APIリクエストはCloud RunのURLへ直接送信されていました。
Swagger UI
http://localhost:8080
│
│ API実行
▼
http://xxxxx.run.app/api/clients
これには以下の問題があります。
- Cloud RunのURLが
httpになっている - Swagger UIとAPIのオリジンが異なる
- APIリクエストがCloud Runプロキシを通らない
- Cloud RunのIAM認証情報が付かない
今回の構成では、Swagger UIからのAPIリクエストもlocalhost:8080へ送信し、Cloud Runプロキシを通す必要があります。
22. OpenAPIのServer URLを相対パスへ変更する
OpenApiConfigで、OpenAPIのServer URLを/に設定しました。
package com.example.freelancemanager.config;
import java.util.List;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI freelanceManagerOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Freelance Manager API")
.description("フリーランス向け案件・取引先・作業記録管理API")
.version("1.0.0"))
.servers(List.of(
new Server()
.url("/")
.description("Current server")
));
}
}
重要なのは以下の部分です。
new Server().url("/")
/は、Swagger UIを開いている現在のホストを基準にする相対URLです。
Swagger UIを以下で開いている場合、
http://localhost:8080/swagger-ui.html
APIの実行先も同じホストになります。
http://localhost:8080/api/clients
その後、Cloud Runプロキシがリクエストを非公開Cloud Runへ転送します。
Swagger UI
http://localhost:8080
│
│ /api/clients
▼
gcloud run services proxy
│
│ 認証情報を付加
▼
非公開Cloud Run
これにより、Swagger UIからAPIを正常に実行できるようになりました。
23. プロキシ環境用の設定
必要に応じて、application-prod.ymlへ以下も追加します。
server:
forward-headers-strategy: framework
springdoc:
cache:
disabled: true
forward-headers-strategyは、リバースプロキシが付与するヘッダーをSpring Bootが処理するための設定です。
springdoc.cache.disabledは、プロキシ経由と直接アクセスでURLが異なる場合に、OpenAPI定義のキャッシュが影響することを避けるための設定です。
ただし、今回の問題を直接解決したのは、OpenAPIのServer URLを/へ設定したことでした。
24. 修正したDockerイメージを再デプロイする
コードを修正しただけでは、Cloud Run上のアプリには反映されません。
新しいイメージタグを作成します。
$IMAGE_URI = "${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE}:v2"
Dockerイメージを再ビルドします。
docker build -t $IMAGE_URI .
Artifact Registryへpushします。
docker push $IMAGE_URI
Cloud Runを更新します。
gcloud run deploy $SERVICE `
--image=$IMAGE_URI `
--region=$REGION
プロキシを起動し直します。
gcloud run services proxy $SERVICE `
--region=$REGION `
--port=8080
ブラウザで以下を開きます。
http://localhost:8080/swagger-ui.html
Swagger UIのServers欄が以下になっていることを確認します。
/
API実行時のRequest URLが以下になれば成功です。
http://localhost:8080/api/clients
ローカル環境とGoogle Cloud環境の違い
今回の構成を比較すると、以下のようになります。
| 項目 | ローカル | Google Cloud |
|---|---|---|
| Spring Boot | Docker Compose | Cloud Run |
| PostgreSQL | Docker Compose | Cloud SQL |
| Dockerイメージ | ローカルDocker | Artifact Registry |
| DBパスワード | ローカル設定 | Secret Manager |
| DB接続 |
localhostまたはdb
|
Cloud SQL Java Connector |
| Spring Profile | local |
prod |
| DBマイグレーション | Flyway | Flyway |
| Swagger UI | 直接アクセス | Cloud Runプロキシ経由 |
アプリケーションコード自体は同じです。
Spring Profileと環境変数によって、接続先や設定を切り替えています。
コードを更新した場合の再デプロイ
Javaコードを変更した場合は、以下の流れで再デプロイします。
1. コードを修正する
2. テストを実行する
3. Dockerイメージを再ビルドする
4. Artifact Registryへpushする
5. Cloud Runへデプロイする
テストを実行します。
gradlew.bat test
新しいイメージタグを設定します。
$IMAGE_URI = "${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE}:v3"
再ビルドします。
docker build -t $IMAGE_URI .
Artifact Registryへpushします。
docker push $IMAGE_URI
Cloud Runへデプロイします。
gcloud run deploy $SERVICE `
--image=$IMAGE_URI `
--region=$REGION
本格的に運用する場合は、v1やv2の代わりにGitのコミットIDをイメージタグとして使用する方法もあります。
料金に関する注意
Google Cloudのリソースは、作成したままにすると料金が発生する場合があります。
Cloud Run
Cloud Runは、最小インスタンス数が0であれば、アクセスがない間はインスタンス数をゼロまで減らせます。
開発用であれば、Cloud Runサービスを残したままでも比較的管理しやすいです。
Cloud SQL
Cloud SQLは、起動中はアクセスがなくても料金が発生します。
使用しない時間は、インスタンスを停止できます。
gcloud sql instances patch $INSTANCE `
--activation-policy=NEVER
再開する場合は以下です。
gcloud sql instances patch $INSTANCE `
--activation-policy=ALWAYS
停止中でも、ストレージなどの料金が発生する場合があります。
今後使用しない場合は、必要なデータを確認したうえで削除を検討します。
今回理解できたこと
今回の作業では、以下を経験できました。
- Spring BootアプリのDocker化
- Artifact RegistryへのDockerイメージ保存
- Cloud Runでのコンテナ実行
- Cloud SQLでのPostgreSQL運用
- Secret Managerでのパスワード管理
- サービスアカウントとIAMによる権限管理
- Spring Profileによる環境分離
- 環境変数による設定の外部化
- Flywayによる本番DBの初期化
- 非公開Cloud Runへの認証付きアクセス
- Cloud Runプロキシを利用したSwagger UIの確認
- OpenAPIのServer URLとリバースプロキシの関係
単にSpring BootのAPIを作成するだけでなく、
Dockerイメージを作成し、
クラウドへ保存し、
Cloud Runで実行し、
Cloud SQLへ安全に接続する
ところまで経験できました。
また、Swagger UIのServers設定によって、APIリクエストの送信先が変わることも理解できました。
非公開Cloud Runをプロキシ経由で確認する場合は、OpenAPIのServer URLを/へ設定し、Swagger UIとAPIリクエストの両方を同じプロキシへ通す必要があります。
まとめ
今回構築した処理の全体像は、以下のとおりです。
DBパスワードはSecret Managerに保存し、Cloud Run専用のサービスアカウントに必要な権限だけを与えました。
Swagger UIはCloud Runプロキシを通して確認し、OpenAPIのServer URLを相対パス/に設定することで、非公開Cloud Run上のAPIを正常に実行できました。
最初はコマンドを順番に実行しているだけでしたが、整理してみると、それぞれのサービスが明確な役割を持って連携していることが分かりました。
今後は、このCloud Run上のAPIを利用するReactフロントエンドを作成し、実際に画面から操作できるフルスタックアプリへ発展させる予定です。