TD;DR
2022年末にこんなアプリを作りました。
そして、転職先でJava(Kotlin)をメインで使うということがあったので、まずはこのPythonのCloud Functionsとスプレッドシートを使った構成から、Spring(Kotlin)のAppEngineとCloudSQLを使った構成に置き換えます。

そして、Reactから通知を飛ばす番組を選択できるようにします。
また通知先はメールからSlackに変更予定です。
この構成を作るにあたって、今回はSpring(Kotlin)とAppEngineとCloudSQLに置き換えるところまでやりました。
そして、めちゃくちゃハマりました…
というわけで、今回はSpring(Kotlin/Gradle)×AppEngine×CloudSQLを使ったアプリの構築手順について書きたいと思います。
なお、今回のアプリケーションはREST APIを処理するためのアプリケーションです。
環境
- アプリケーション環境
- Spring Boot: 3.0.2
- Kotlin(Java: 17)
- Gradle - Kotlin
- MySQL: 8.0
- GCP環境
- AppEngine: Java17 スタンダード環境
- Cloud SQL: MySQL8.0, プライベートIPを付与
- VPCネットワーク: サーバーレスVPCコネクタ
プロジェクトの初期構成はSpring initializrで作成します。
この時ですが、PackagenameにはJarを選択しておいてください。
(WarだとTomcatを含めてAppEngineへデプロイするための別途設定が必要になってきます。)
ここで解説すること
Springのアプリケーション構築自体には言及しません。
Spring/JPAを使ったアプリの構築については、公式ドキュメントやこちらのサイトの内容が参考になることでしょう。
今回はローカル環境にてSpring/JPAを使ってMySQLとのやりとりができている前提で、ではそれをAppEngineとCloud SQLで動かそうという試みです。
ざっくりした手順
- AppEngineの準備
- VPCの作成とサーバーレスVPCアクセスコネクタの作成
- Cloud SQLの準備
- 本番用の設定ファイルの準備
- GradleでJarファイルをビルド
- デプロイ
という感じです。
次から詳しく行きます。
本格的に手順
AppEngineの準備
GCPのコンソールからプロジェクトを作成し、メニューのAppEngineからデプロイ先となるサービスを作成します。
このとき注意が必要なのはリージョンです。
作成するVPC,Cloud SQLとおなじリージョンにAppEngineが存在しないとダメです。
AppEngine側では、この時点ではこれ以上何もする必要がありません。
VPCの作成とサーバーレスVPCアクセスコネクタの作成
CloudSQLにプライベートIPを付与するために必要なVPCネットワークを作成します。
このときリージョンを選択する必要がありますが、このリージョンはAppEngineを作成したリージョンと同じである必要があります。

またサブネット範囲を入力するよう言われますが、今回はサブネット内部のIPの数はCloudSQLが使う分だけあれば今は大丈夫なので最小限でいいでしょう。
他の選択値についてはデフォルトのままで大丈夫です。
VPCが作成できたら、サーバーレスVPCアクセスコネクタを作成します。
同じくVPCのメニューにあります。

ここでリージョンはAppEngineとVPCと同じリージョン。
ネットワークは先ほど作成したVPC名が表示されるはずなので、それを選択します。
Cloud SQLの準備
CloudSQLでDBを作成します。

このとき気をつけるのが、「インスタンスのカスタマイズ」→「接続」にある「プライベートIP」のチェックをONにし、「ネットワーク」に表示されている先ほど作成したVPCを選択します。
これで先ほど作成したVPCサブネット内で利用できる内部IPアドレスがこのDBインスタンスに紐づきます。
イメージとしてはこういう感じ。

要はAppEngineとCloudSQLはどちらもGCP内部にあるサービスなので、わざわざパブリックネットワーク経由で通信するのではなく、内部IPで通信すればいいよねというわけです。
ただし、AppEngineのサービスはリクエストのたびにどこかのマシン上にホストされて起動することになるわけなので、リージョンは決まっていてもどのゾーンで起動するかはわからないわけです。
そこでAppEngineにサーバーレスVPCアクセスコネクタの情報を教えてあげて、内部リソースへの通信経路を教えてあげます。
その情報をもとにサーバーレスVPCアクセスコネクタの先(=作成したVPCに紐づけられたサービス=今回の例だとCloudSQL)のIPが振られているサービスと通信するというわけです。
ここまででクラウド側の準備は完了です。
本番用設定ファイルの準備
ライブラリの追加
まずAppEngineからCloudSQLへ接続するためのライブラリを入れます。
dependencies {
↓これを追加
implementation("com.google.cloud:spring-cloud-gcp-starter-sql-mysql:3.0.0")
}
これがAppEngineにデプロイされた場合に、application.ymlに記載した設定内容をもとにCloudSQLへの接続をよしなにしてくれるライブラリです。
これがないとAppEngineでアプリを起動した際にエラーとなって落ちます。
application.ymlの設定
別途application-dev.ymlをsrc/main/resourcesフォルダに作成し、デフォルトのapplication.ymlの内容をこのファイルに貼り付けます。
これは開発環境用の設定ファイルとして利用します。
そして、今度は本番用にapplication-prod.ymlを作成します。
こちらの内容は以下のような感じです。
server:
port: 8080
database: mysql
cloud:
gcp:
sql:
database-name: CloudSQLに作成したデータベース名
instance-connection-name: CloudSQLの接続名
spring:
cloud:
gcp:
sql:
database-name: CloudSQLに作成したデータベース名
instance-connection-name: CloudSQLの接続名
datasource:
url: jdbc:mysql://CloudSQLに割り当てられた内部IP:3306/CloudSQLに作成したデータベース名
username: ユーザー名
password: パスワード
driver-class-name: com.mysql.jdbc.Driver
database-name: CloudSQLに作成したデータベース名
instance-connection-name: CloudSQLの接続名
何回同じことを書くんだという声が聞こえてきそうなのですが、これらを全て書かないと起動時に落ちてしまいます。
落ちている理由なのですが、一つ前の手順で入れたライブラリのうちのクラスがdatabase-nameとinstance-connection-nameをこの3ヶ所分見ているようで、そのクラスがnewされるタイミングでこの設定値をとりにくるようです。
そして、設定値が見つからない場合は例外を吐いて止まっていました。
なぜこの3箇所を別に見ているのかまではわかりませんでしたが、とにかく書かないと落ちるので仕方ない…
application-prod.ymlが作成できたら、元のapplication.ymlの内容を以下のように書き換えます。
spring:
profiles:
active: prod
これはここで作ったapplication-dev.ymlとapplication-prod.ymlを切り替えるための設定です。
activeにdevを設定するとapplication-dev.ymlが、prodに設定するとapplication-prod.ymlが適用されてアプリケーションがビルドされます。
必要に応じてこの設定は切り替えてください。
GradleでJarファイルをビルド
プロジェクトのルートディレクトリで以下のコマンドを実行
gradle build
これでビルドに成功すると、./build/libsにJarが出来上がります。
AppEngineへのデプロイには、plainがついていないJarファイルを使います。
デプロイ
デプロイ用のフォルダとapp.yamlの作成
AppEngineスタンダード環境へのデプロイを行うには、実行用JarとAppEngine自体の設定ファイルであるapp.yamlの二つが含まれるディレクトリが必要です。
なので、ディレクトリを作ってあげる必要があります。
今回は、buildフォルダの直下にstaged-appという名前のフォルダを作ります。
それから、AppEngine用設定ファイルのapp.yamlを作ります。
app.yamlは以下の内容でプロジェクトの直下に作ります。
ちなみにapp.yamlの拡張子はymlではなくyamlじゃないとデプロイ時に怒られるので注意。
それとインスタンスクラスにはF2を指定しています。
今回はデータベースへの接続等をやるので256MBではメモリが足りませんでした。
(筆者環境だと286MBを使って落ちました)
なので最初からF2に設定しておきます。(オートスケールの設定をする方はF1でも問題ないと思います。)
AppEngineのマシンスペックは別途こちらをご参照ください。
runtime: java17
vpc_access_connector:
name: projects/プロジェクトID/locations/作成したサーバーレスVPCアクセスコネクタのリージョン/connectors/作成したサーバーレスVPCアクセスコネクタの名前
instance_class: F2
だいぶごちゃごちゃしてきたので今のプロジェクト構成を見せるとこんな感じ。

ここで、app.yamlをバージョン管理対象にしつつデプロイまでを楽にしたいのでシェルファイルを作っておきます。
デプロイ用シェル
以下のシェルを実行することで、Jarのビルドからデプロイまでを勝手にやってもらおうという魂胆です。
プロジェクト直下にapp.yamlを作っておくことでバージョン管理対象にしつつ、このシェルで毎回デプロイのたびにデプロイ用のフォルダに集めてもらおうというわけです。
#!/bin/bash
rm -f ./build/libs/UP対象のJarファイル
rm -f ./build/staged-app/UP対象のJarファイル
rm -f ./build/staged-app/app.yaml
gradle build
cp ./build/libs/UP対象のJarファイル ./build/staged-app
cp ./app.yaml ./build/staged-app
cd ./build/staged-app
gcloud app deploy
デプロイ
Jarファイルをapp.yamlをデプロイ用のディレクトリの下に配置して「gcloud app deploy」コマンドを叩く、もしくは先のシェルを実行するとデプロイが起こります。
デプロイが終わったら、あとはブラウザから正常にリクエストが実行されることを確認してみてください。
所感
ひとまずAppEngineにデプロイすることができたのでよかったです。
ここからは
Spring×Kotlin×Gradleで作成したアプリをAppEngineスタンダード環境で動かす記事が検索しても有用なものが全く出てこなかったので、この記事が後の誰かの役に立てばとても嬉しいです。
おそらくみなさんCloudRunを使ってるのかなと思っています。