![見出し画像](https://assets.st-note.com/production/uploads/images/37944077/rectangle_large_type_2_c56c86788fc5718736d0aa329002e834.jpeg?width=1200)
APIサービスプロジェクトのビルドツールをsbtからGradleに切り替えた
はじめに
はじめまして、株式会社POLでエンジニアをしている高橋です。10月初旬に弊社のAPIサービスのプロジェクトでJavaのビルドツールをsbtからGradleに切り替える変更を実施しましたので、切り替えるモチベーションや実際の変更内容について紹介したいと思います。
弊社のバックエンドプロジェクトの構成について
まずは株式会社POLで運営している、理系学生に特化した採用サービス LabBaseのバックエンドプロジェクトの構成について簡単に説明します。フレームワークはJavaのPlay frameworkを使用しており、ビルドツールは記事タイトルにも書いてあるsbtを使っています。3年以上サービス運営してきた経緯や途中でのアーキテクチャの見直しもあって、現在では以下の4プロジェクトによってサービスの処理を管理しています。
・リポジトリプロジェクト
・共通処理プロジェクト
・LabBaseサービスプロジェクト
・アドミンプロジェクト
4つのプロジェクトについて簡単に説明すると、リポジトリプロジェクトはサービスのDDL, DMLや、それらを使ってJOOQで生成したモデルを管理するJavaプロジェクトです。DDL, DMLはFlywayでマイグレーションしているため、Flywayによってバージョン管理ができるように命名して管理してます。
リポジトリプロジェクト
├── README.md
├── build.sbt
├── conf
│ └── resources
│ ├── README.md
│ └── sql
│ ├── V20190815_0_1__create_database.sql
│ ├── V20190815_1_1__foo_ddl.sql
│ ├── V20190815_1_2__foo_dml.sql
│ ├── V20191011_2_1__bar_ddl.sql
│ ├── V20191011_2_2__bar_dml.sql
│ ├── etc...
└── app
└── JOOQで生成されたコードが格納される
共通処理プロジェクトはPlay frameworkで動作する認証等の共通処理を担うRest API Javaプロジェクトで、リポジトリプロジェクトに依存しています。
LabBaseサービスプロジェクトは、Play frameworkで動作する理系学生とのマッチング固有の処理を担うRest API Javaプロジェクトで、リポジトリプロジェクト、共通処理プロジェクトに依存しています。
アドミンプロジェクトは、Play frameworkで動作する社内での業務向けの機能提供を担うJavaプロジェクトで、リポジトリプロジェクト、共通処理プロジェクト、LabBaseサービスプロジェクトに依存しています。
# 依存関係
リポジトリ <- 共通処理 <- LabBaseサービス <- アドミン
多段でプロジェクト依存していて非常につらそうですね😊
今回はこういった構成であるために色々つらみがあったのと、今後やりたいことをできるようにしようと思い、まずはやりやすいビルド周りを見直して最終的にGradleに切り替えることにしました。以下でつらみや今後やりたいことについてお話します。
ビルドツールを変えるモチベーション①〜ビルド周りがつらい
多段でプロジェクト依存していることから予想される通り、やはりビルド周りには問題があって、具体的にはローカルでビルドが終わらないことがある、CIでのJavaプロジェクトのビルド時間が10分を超えている、テスト実行時間もビルド時間に引きづられて長くなっている等、エンジニアにとって嬉しくない開発体験を与えていました。定量的な観点としては、弊社ではDX Criteriaを推進しており、その中の項目である、「すべてのインテグレーションテストにかかる時間が計測されており、それは30分以内に完了するか。」や、「デプロイ工程が自動化されておらず、本番反映に1時間以上かかっていたり、特定の時間帯しかできないなどの制約事項がかかっている。」の達成の妨げになってしまうので、「なんとかしないとなあ」という状況でした。
ビルドツールを変えるモチベーション②〜kotlin +Spring Frameworkを使いたいがsbtだと難しい
以前弊社のゲバさんがAWS Amplifyを導入した記事を記載してくれていましたが、現在弊社では新技術をトライして技術の幅を広げようとしており、バックエンドに関してもJava以外の開発言語の選択肢を僕が中心となって検討していました。個人的にはScalaを使いたいなと思いつつも、社内のメンバーはPlay frameworkよりもSpring Frameworkに知見のある人が多い、現状のチームの実力だとScalaに踏み切るのはリスクあると考え、kotlin + Spring Frameworkを採用することにしました。モチベーション①だけだとsbtからGradleに切り替える必要はないのですが、kotlin周りのsbtプラグインのメンテナンス状況や(sbtでもkotlinを使えるプラグインはありそれは問題なく動くのですが、ktlintのsbtプラグインが2〜3年ほど更新されてなくいろいろ試したけどsbtではktlintを有効化できませんでした…)、Spring FrameworkではGradleを使うことが主流なこともあり、sbtからGradleに切り替えることにしました。
Gradleに切り替える際にやったことを幾つか以下で共有します。
sbt→Gradle変更でやったこと①〜プロジェクト構成の変更
まずはGradleの公式ドキュメントに従って、複数プロジェクトでのビルド設定を行いました。具体的には既存の4プロジェクト、リポジトリプロジェクト、共通処理プロジェクト、LabBaseサービスプロジェクト、アドミンプロジェクトをサブプロジェクトとして、新たにビルド全体を統括する「バックエンドビルドプロジェクト」をメインプロジェクトとして追加しました。
# プロジェクトの構成
│ # バックエンドビルドプロジェクト
├── labbase-api-build
│ ├── build
│ │ └── kotlin
│ │ └── sessions
│ ├── build.gradle
│ ├── gradle
│ │ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ └── settings.gradle
│ # リポジトリプロジェクト
├── labbase-repository
│ ├── build.gradle
│ ├── ...
│ # 共通処理プロジェクト
├── labbase_common_api
│ ├── build.gradle
│ ├── ...
│ # LabBaseサービスプロジェクト
├── labbase_compass_api
│ ├── build.gradle
│ ├── ...
│ # アドミンプロジェクト
├── labbase_compass_adminapi
│ ├── build.gradle
│ ├── ...
バックエンドビルドプロジェクトのsetttings.gradleにサブプロジェクトの設定を行います。Javaプロジェクト以外もモノレポ上で管理しているので、フラットなマルチプロジェクトでの設定を行うことにしました。
rootProject.name = 'labbase-api-build'
# 既存のプロジェクトをサブプロジェクトとして追加
include 'labbase-repository'
include 'labbase_common_api'
include 'labbase_compass_api'
include 'labbase_compass_admin'
project(':labbase-repository').projectDir = new File('../labbase-repository')
project(':labbase_common_api').projectDir = new File('../labbase_common_api')
project(':labbase_compass_api').projectDir = new File('../labbase_compass_api')
project(':labbase_compass_admin').projectDir = new File('../labbase_compass_admin')
sbt→Gradle変更でやったこと②〜JOOQのコード生成の置き換え
これまではsbtプロジェクトとしてのインポートとJOOQによるJavaコード生成を実現するために、①共通処理プロジェクト等でインポートするためのbuild.sbt、②DDL, DMLの実行とJavaのJOOQコード生成をするためのpom.xmlのように、mavenとsbtを併用して何とか実現していました。設定ファイルが2種類あってイケてないのと、①、②どちらともGradleで実現できそうだったので、今回の変更を機にgradle-jooq-pluginとflyway-gradle-pluginを使いGradle側に寄せました。
// リポジトリプロジェクトのbuild.sbt
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath('mysql:mysql-connector-java:5.1.47')
classpath('org.jooq:jooq-codegen:3.12.3')
classpath('org.flywaydb:flyway-gradle-plugin:6.5.5')
}
}
plugins {
id "org.flywaydb.flyway" version "6.5.5"
id 'nu.studer.jooq' version '4.0'
id "java"
}
repositories {
mavenCentral()
}
dependencies {
jooqRuntime 'mysql:mysql-connector-java:5.1.47'
compile group: 'org.jooq', name: 'jooq', version: '3.12.3'
compile group: 'org.jooq', name: 'jooq-meta', version: '3.12.3'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
// gradle-flyway-pluginの設定
flyway {
url = "jdbc:mysql://${System.getenv()['MYSQL_DOMAIN']}:${System.getenv()['MYSQL_PORT']}/${System.getenv('MYSQL_SCHEMA')}?useSSL=false"
user = 'user'
password = 'password'
locations = ["filesystem:conf/resources/sql"]
}
// gradle-jooq-pluginの設定
jooq {
version = '3.12.3'
common(sourceSets.main) {
jdbc {
driver = 'com.mysql.jdbc.Driver'
url = "jdbc:mysql://${System.getenv()['MYSQL_DOMAIN']}:${System.getenv()['MYSQL_PORT']}/${System.getenv('MYSQL_SCHEMA')}"
user = 'user'
password = 'password'
properties {
property {
key = 'useSSL'
value = 'false'
}
}
}
generator {
name = 'org.jooq.codegen.JavaGenerator'
database {
name = 'org.jooq.meta.mysql.MySQLDatabase'
inputSchema = 'common_database_name'
includes = '.*'
excludes = 'flyway_schema_history'
}
generate {
relations = true
records = true
instanceFields = true
pojos = true
daos = true
}
target {
packageName = 'jp.co.pol.jooq.db.common'
directory = 'src/main/java'
}
}
}
compass(sourceSets.main) {
jdbc {
driver = 'com.mysql.jdbc.Driver'
url = "jdbc:mysql://${System.getenv()['MYSQL_DOMAIN']}:${System.getenv()['MYSQL_PORT']}/${System.getenv('MYSQL_SCHEMA')}"
user = 'user'
password = 'password'
properties {
property {
key = 'useSSL'
value = 'false'
}
}
}
generator {
name = 'org.jooq.codegen.JavaGenerator'
database {
name = 'org.jooq.meta.mysql.MySQLDatabase'
inputSchema = 'compass_database_name'
includes = '.*'
excludes = 'flyway_schema_history'
}
generate {
relations = true
records = true
instanceFields = true
pojos = true
daos = true
}
target {
packageName = 'jp.co.pol.jooq.db.compass'
directory = 'src/main/java'
}
}
}
}
また、サブプロジェクトとしてリポジトリプロジェクトを依存させた際、依存する側のビルド時にJOOQのコード生成が実行されてしまうのを防ぐために、以下の設定を追加しリポジトリプロジェクト以外ではJOOQのコード生成を行わないように修正しました。
// リポジトリプロジェクトのbuild.sbtに設定を追加
if(project.gradle.startParameter.getTaskNames().size() > 0 && !project.gradle.startParameter.getTaskNames().get(0).contains('labbase-repository')) {
// labbase-repository以外ではjooqのコード生成を飛ばす
project.gradle.startParameter.excludedTaskNames.add('generateCommonJooqSchemaSource')
project.gradle.startParameter.excludedTaskNames.add('generateCompassJooqSchemaSource')
}
Gradleのプラグインを使うことで、sbtとmavenで重複していた設定をbuild.gradleに一箇所にまとめることができました😊
# コマンドによるDBセットアップ→JOOQコード生成。Dockerコンテナ上で以下のコマンドが実行される。
./gradlew :labbase-repository:flywayMigrate
./gradlew :labbase-repository:cleanRepositoryCode
./gradlew :labbase-repository:generateCommonJooqSchemaSource :labbase-repository:generateCompassJooqSchemaSource
./gradlew :labbase-repository:jar
sbt→Gradle変更でやったこと③〜Dockerイメージの作成
Javaプロジェクトのデプロイ、サービスの実行にDockerコンテナを使っており、sbt-native-packagerを使ってDockerイメージの作成を行っておりました。build.sbtに対して以下のような設定を行うと、
// build.sbt
lazy val root = Project(id = "labbase_common", base=file("."))
.enablePlugins(PlayJava, DockerPlugin, JavaAppPackaging)
.settings(
dockerBaseImage := "amazoncorretto:11.0.8-al2",
dockerEntrypoint := Seq("/opt/docker/bin/labbase_common"),
dockerExposedPorts := Seq(9000, 17264),
name := "labbase_common",
scalaVersion := "2.12.8",
dockerRepository := Some("localhost"),
packageName in Docker := "labbase-common",
)
.dependsOn(repo)
設定内容に対応して以下のDockerfile定義に一致するDockerイメージが作成されます。
# build.sbtの設定に対応するDockerfile
FROM amazoncorretto:11.0.8-al2
WORKDIR /opt/docker
ADD --chown=daemon:daemon opt /opt
EXPOSE 9000 17264
USER daemon
ENTRYPOINT ["/opt/docker/bin/labbase_common"]
CMD []
元のDockerfileと同じ内容のものをGradleによって実現する必要があり、今回はgradle-docker-pluginのdocker-remote-apiを利用して対応しました。
// バックエンドビルドプロジェクトのbuild.gradle(抜粋)
plugins {
id 'java'
id 'idea'
id 'org.gradle.playframework' version '0.9' apply false
id 'com.bmuschko.docker-remote-api' version '6.6.1' apply false
}
// プロジェクト名と実行シェルスクリプト名のマッピング
Map<String, String> applicationTagMap = [
"labbase_common": 'labbase-common',
]
allprojects {
plugins.withType(com.bmuschko.gradle.docker.DockerRemoteApiPlugin).whenPluginAdded {
// ビルドのバージョン
def version = System.getProperty("version", "1.0.0")
// Dockerfileの作成タスク
task createDockerfile(type: Dockerfile) {
def _applicationName = '';
tasks.withType(CreateStartScripts) {
_applicationName = applicationTagMap.get(project.name)
}
destFile.set(file("$projectDir/Dockerfile"))
// ADD時にchownを指定する
Dockerfile.File f = new Dockerfile.File("build/stage/main", '/opt/docker').withChown('daemon:daemon')
// ここからDockerfileの定義
from 'amazoncorretto:11.0.8-al2'
label(['maintainer': 'POL, Inc. "社内で使うメールアドレスなので削除"'])
workingDir('/opt/docker')
runCommand('chown -R daemon:daemon /opt')
addFile(f)
exposePort(9000, 17264)
user('daemon')
entryPoint("/opt/docker/bin/${_applicationName}")
defaultCommand('')
}
// docker imageを作成するタスク
task buildDockerImage(type: DockerBuildImage) {
def _applicationName = '';
tasks.withType(CreateStartScripts) {
_applicationName = applicationTagMap.get(project.name)
}
inputDir.set(file("$projectDir"))
dependsOn createDockerfile
images.add("localhost/${_applicationName}:${version}")
}
}
}
上記のbuild.gradleの設定によって、サブプロジェクト毎にcreateDockerfileタスクを実行することで以下のようなDockerfileが生成されます。
FROM amazoncorretto:11.0.8-al2
LABEL maintainer="POL, Inc. \"社内で使うメールアドレスなので削除"\""
WORKDIR /opt/docker
RUN chown -R daemon:daemon /opt
ADD --chown=daemon:daemon build/stage/main /opt/docker
EXPOSE 9000 17264
USER daemon
ENTRYPOINT ["/opt/docker/bin/labbase-common"]
CMD [""]
サブプロジェクトのstageタスクによってAPIサーバーとして実行可能な資材が生成されるので、CIでstageタスク→buildDockerImageタスクの順で実行することでsbt-native-packegerと同等のDockerイメージの生成を実現させました。
# circleci.ymlのコマンド
./gradlew ':labbase_common_api:stage' && ./gradlew ':labbase_common_api:buildDockerImage' -Dversion=<< parameters.version >>
sbt→Gradle変更の結果と振り返り
上記以外にも、ローカル環境で開発サーバーを立ち上げられる、sbt→Gradleの切り替えをしたらアドミンプロジェクトだけビルドエラーが大量発生したので要らないコードを大量に削除する(これが一番大変だった…)等々、色々対応した結果…何とかリリースまでに至りました。
Gradle変更した結果どうなったか、やってみて気づいた点について以下に記載します。
まず最初に、今回のGradle変更はテストコードの存在にかなり助けられました。ビルドツールを切り替えた影響をテストが通らなくなった箇所が幾つかあり、それによって挙動の変更に気づくことができたり不具合を発生させることなくリリースすることができました。こういった変更を人力で網羅することは労力がかかるし漠然とした不安が残ってしまうものなので、テストコードが存在することで比較的安心してGradleに切り替えることができました。※去年テストコードがほぼない状態だったのをC0 60%〜70%まで上げていて、その話について後日書こうと思います。
次にビルドの実行時間ですが、今回のGradle変更によってビルドの実行時間が短縮されました。CI上で最もビルドに時間の掛かっていたアドミンプロジェクトでは、今回の対応によってビルド時間が約14分から約6分まで削減されました。テスト実行時間もビルド時間の短縮によって1分程度短くなり、まだまだ実行時間を削減できそうですが、改善の第一歩としてはまずまずの成果を得られました。
また今回の変更によってGradleに結構詳しくなれました。プラグインのコードベースでの仕様の調査方法(tasks配下を見れば設定できる項目がわかるなど)が身についたので、Gradleを使うのも怖くなくなりました😊
最後に開発チームへの導入についてですが、大きな開発がGradle変更リリース時期の前後に存在していてそこまでに間に合わせようとしたため、ローカル環境での開発検証やドキュメント周りが不足してしまい、リリース直後にローカルで環境が立ち上がらないという問い合わせや、sbtでできていたことができない等の問題を発生させてしまいました。開発チームで多少混乱が発生したので、次回の改善ポイントとして対応しようと思います。
今後について
今回のGradle変更で、当初の目的であったkotlinがやっと使える!という状況になりました。今後はkotlinも含めた新しいアーキテクチャでの開発の導入を進めていく予定です。今回Gradleに変更したことでkotlinも使えるようになりましたが、他にもGradleプラグインを追加して色々できるようになりました。今後開発チームの規模が大きくなっていくことが予想され、それに耐えうる状態にしていきたいので、これを機に新しい構成でバックエンドプロジェクトを作成、変更して行こうとしています。具体的にはOpenAPI + kotlin + Spring Frameworkの様な構成で、既存のAPIプロジェクトも少しずつ置き換えしていけるような形で作成していこうとしています。この辺が進捗があったらまた記事で紹介しようと思います。
最後に
最後まで読んで頂きありがとうございました。
POLでは新しい仲間を募集中です。カジュアル面談担当なので、詳しく聞きたいことがあればお話できると思いますので、気になる点があれば聞いてください。
この記事が気に入ったらサポートをしてみませんか?