🌱

Spring BootプロジェクトをDocker上で動かす

2021/12/04に公開
1

概要と前提

タイトルの実現方法を解説していきます。
 Spring Initializrで作成したプロジェクトを、Docker上でビルド・起動し、デバッグもできるようにします。データベースとDBクライアントも一緒に立ち上げます。
 また、編集環境はVSCodeの拡張機能Remote - Containersを使って用意します。
 ひとまずプロジェクトを起動するだけならGitHubリポジトリのREADMEを、構築手順が知りたい場合はこの記事を読んでください。

(GitHubにあげたコード...springboot-mysql-onDocker-withRemotecontainers)

この記事を読むにあたって

  • GradleやSpringを扱ったことがある
  • Dockerの基礎的な知識がある Docker Composeの基本的な記法がわかる

あたりが前提となります。
記事について指摘や提案があれば、是非コメントください!

プロジェクトのスペック

  • 環境:Docker Desktop for Windows
  • サーバー:Java(Spring Boot)
  • プロジェクト管理:Gradle
  • DB:MySQL
  • エディタ:VSCode(Remote - Containers)

環境構築手順のあらすじは

といった流れになります。

ディレクトリ構成

├─.devcontainer # Remote - Containersのconfig
│      devcontainer.json
│
├─docker # Docker関連
│  │  docker-compose.yml
│  │
│  └─mysql # データ永続化のためのディレクトリ
└─spring_prj # Spring Bootプロジェクト本体

1.実行環境の用意

Docker Composeで環境の用意をします。

docker-compose.ymlの用意

構築するサーバーは、MySQL/Java/DBクライアント用の3つです。
それぞれmysql/java/dbclientというコンテナ名で用意します。
設定はdocker-compose.ymlの中にすべて一緒に書かれていますが、コンテナごとに分けてみていきます。

MySQL

docker-compose.yml
services:
  mysql:
    build: ./mysql
    container_name: mysql
    env_file:
      - ./mysql/db.env # MySQL設定ファイル
    volumes:
      - ./mysql/data:/var/lib/mysql # 実データの永続化
      - ./mysql/log:/var/log/mysql # logの永続化
    ports:
      - 3306:3306
  • volumes:
    実データは/var/lib/mysql、ログは/var/log/mysqlに永続化させてあります。

  • build:
    imageの指定ではなく、Dockerfileを用意しています。

Dockerfile
FROM mysql:5.7

EXPOSE 3306
# 設定ファイルをコンテナにコピー
COPY ./my.cnf /etc/mysql/my.cnf
# 設定ファイルの権限を変更
RUN chmod 644 /etc/mysql/my.cnf
# データの初期化を行うDDLをコンテナにコピー
COPY ./sql_init /docker-entrypoint-initdb.d

Dockerfileの内容は、docker-compose.ymlでも指定できるものですが、上記のように切り出しています。理由は、ビルドの過程で必要な情報であるからです。
docker-compose.ymlに記載した場合、コンテナへのマウントはビルドが完了した後になります。Dockerfileに記載しておけば、ビルドの過程でファイルを読み取ってくれます。

DBクライアント

docker-compose.yml
  dbclient:
    image: phpmyadmin/phpmyadmin
    container_name: dbclient
    environment:
      - PMA_ARBITRARY=1 # 任意のサーバーへの接続を許可
      - PMA_HOST=mysql # 接続先ホスト名 ここではdbサーバーのコンテナ名を指定
      - PMA_USER=root # MySQLの設定と合わせておく
      - PMA_PASSWORD=root # MySQLの設定と合わせておく
    links:
      - mysql
    ports:
      - 4200:80
    volumes:
      - ./dbclient/sessions:/sessions
    depends_on:
      - mysql # 「mysql」の後で起動

DBクライアントツールにはphpMyAdminを採用しました。
DBへの接続設定がメインなので特筆すべき点はないと思います。
ほかにもクライアントツールはありますが、ブラウザで操作でき、作業者ごとにインストールが不要なので採用しました。

Java

docker-compose.yml
  java:
    image: openjdk:15
    container_name: java
    env_file:
      - ./mysql/db.env # mysqlと同じものを指定
    tty: true
    working_dir: /app
    volumes:
      - ../spring_prj:/app # Spring Bootのプロジェクト
    ports:
      - 8080:8080 # 通常実行
      - 5050:5050 # デバッグ用
    depends_on:
      - mysql # 「mysql」の後で起動

これも特に目新しいことはありません。

~ちょっと動作確認~
ここで一度動作確認します。Javaはまだ動かすものがないので、DBとクライアントに問題がないかを確認します。
次のコマンドでmysqldbclientのコンテナを起動します。

$ cd docker
$ docker-compose up -d dbclient
...
Creating mysql     ... done # この表示で起動完了
Creating dbclient  ... done

コンテナ名はdbclientだけで十分です。depends_onでmysqlを指定しているので、依存先のDBサーバーが勝手に先に立ち上がってくれます。
起動が完了したら、ローカルホストの4200番に接続して、DBが起動しているか、初期データが投入されているか確認してみます。初期データ投入のSQL文は、mysqlコンテナのDockerfileで指定した、docker/mysql/sql_init以下にあるファイルに記載されています。
01_ddl.sqlでスキーマとテーブルを作成し、02_initial-data.sqlでデータを投入しています。その内容通りに、sample_schema.accountに3つのレコードが登録されていれば確認はOKです。

2.Javaプロジェクトの起動

Spring Initializrでプロジェクト作成

Spring Initializrを使うと、GUIで簡単にSpring Bootのプロジェクトを作成することができます。今回は以下の添付画像のような仕様にしました。
JPAを使います。

適宜入力・選択した後は「GENERATE」を押すとzipがダウンロードされます。
展開したものを、spring_prj直下に配置します。

コンテナ上でビルド

コンテナ上でビルドします。以下のような流れになります。
まずjavaコンテナを起動します。

$ cd docker
$ docker-compose up -d
...
Creating mysql     ... done
Creating dbclient  ... done
Creating java      ... done

コンテナが起動したらjavaのプロセスに入ります。

$ docker exec -it java /bin/bash
# コンテナが正常に動いていれば、以下のように今いるディレクトリを確認
bash-4.4\# pwd
/app # appディレクトリにいる 

すると、コンテナ内のappディレクトリにいるはずです。Gradleでビルドします。

$ sh gradlew build
# ビルドが開始され数分かかる
......
BUILD SUCCESSFUL in *m *s

とりあえずビルドが成功すればOKです。まだコントローラーなど何も用意していないので、何か簡単な機能を用意してからプロジェクトを起動し、動作確認をします。

簡単なDB検索機能を用意する

JPAを使って、検索機能を実装します。
必要なものは、spring_prj/src/main/java/com/example/sample以下の、下記のファイルたちです。

  • entity/AccountEntity.java
    あるテーブルの1レコードを表現するクラス。
    そのテーブルのデータをやり取りする上での入れ物にもなります。

  • repository/AccountRepository.java
    JpaRepositoryを拡張して実装するインターフェイスで、一つのEntityに対応しています。
    この拡張クラスを作るだけで、基本的なCRUDメソッドの実装を省略できます。

  • service/AccountService.java
    上記のJpaRepositoryを継承したインターフェイスから、accountテーブルのレコードを全件取得するfindAllメソッドを呼び出します。

  • controller/AccountController.java
    上記AccountServiceの検索メソッドを呼び出すコントローラーです。

上記のクラスたちで検索機能が成り立っています。
では、もう一度ビルドして、プロジェクトを起動し動作確認をしましょう。

ビルドと起動

$ sh gradlew build
# …ビルド完了したらjarを実行してアプリケーションを起動
$ java -jar build/libs/sample-0.0.1-SNAPSHOT.jar

起動したら、先ほど実装した検索機能を使って動作確認をします。
APIクライアントがあればそれを使ってもよいですが、下記のようにcurlコマンドで簡単に確認できます。

$ curl http://localhost:8080/showaccount
[{"account_id":1,"email":"mng@hoge","password":"mngpass","user_name":"manager"},
{"account_id":2,"email":"user1@hoge","password":"user1pass","user_name":"MR.user1"},
{"account_id":3,"email":"user2@hoge","password":"user2pass","user_name":"MR.user2"}]

このようにaccountテーブルの全データが表示されればOKです。

3.VSCodeでコンテナ上のコードを開く

ここまでの作業で、コンテナ上でプロジェクトを動かす環境が整いました。なので、順次開発に取り掛かっていきたいところですが、このまま進めていくと大きな難点があることに気が付きます。
 現状でコードの編集をしようとすると、ローカルのspring_prjディレクトリに配置してあるものを開くことになるとおもいます。しかし、現在ローカル環境にあるのはこのコードだけです。
 実物として動いている(ビルド対象となる)コードはコンテナ上にマウントされている方ですし、SDKもコンテナ上に存在し、VSCodeのlanguage serverはそれを参照することができない状態にあります。(※1)これにより以下のことが起き、不便な状態です。

  • ライブラリの参照先が見つからない
  • コード補完が効かない
  • デバッグができない

※1)このことがあまりピンとこない場合は、この辺りを読んでみてください。

Dockerの導入で実行環境の統一ができたのはいいですが、編集環境と実行環境が分離してしまいこのようなことになっています。
 
以上のことから、環境の用意されているコンテナにマウントしたコードをエディタで開けばそんな不都合は起きないと言えます。そして、それを実現してくれるのがRemote - Containersです。
 Remote - Containersを使えば、コンテナ内の環境でコードを編集できるため、ローカルで環境を用意した時と同じように完全な状態でエディタの支援を受けて開発が行えます。

Remote - Containersについてもう少し詳しく知りたい方はこの辺をどうぞ。

では、VSCodeでコンテナ内のファイルを開くための準備を行っていきます。
(一旦アプリケーションを終了して、docker-compose downでコンテナを停止しておく)

devcontainer.jsonの用意

Remote - Containers向けの設定ファイルを用意します。
場所は.devcontainer/devcontainer.jsonです。

devcontainer.json
{
    "name": "remote-java",                                 // 任意の名前
    "dockerComposeFile": "../docker/docker-compose.yml",   // DockerComposeFileを指定
    "service": "java",                                     // DockerComposeFileにあるservice名を指定
    "workspaceFolder": "/app",                             // コンテナに入ったときの作業ディレクトリ
    "settings": {
        "terminal.integrated.defaultProfile.linux": "bash" // bashでターミナルを起動
    },
    "extensions": [                                        // コンテナ内で使いたい拡張機能
      "vscjava.vscode-java-pack",                          // Java関連の拡張機能パック
      "pivotal.vscode-boot-dev-pack",                      // Spring Boot関連の拡張機能パック
-     "gabrielbb.vscode-lombok",                          // (2022/07/22修正 記事コメント参照)
+     "vscjava.vscode-lombok"
    ]
  }

"extensions"に記載するのはExtension IDというものですが、VSCodeで拡張機能のページを開いたときにアイコンの右にある歯車のマークをコピーすると、Copy Extension IDの選択肢が出るのでそこでコピーできます。

また、Extensionsの検索窓にExtension IDを入れても検索できます。それぞれどんなものが入っているか見ておくといいとおもいます。

Remote - Containersで開く

設定ファイルの用意ができたので、いよいよコンテナの環境をVSCodeで開きます。
まずは通常通りにコンテナを立ち上げます。

# dockerディレクトリに移動してから
$ docker-compose up -d
......
Creating mysql ... done
Creating java  ... done

立ち上がったら、VSCodeのウインドウ左下の、緑色のマークをクリックします。

すると選択肢が出るので、Attach to Running Containerを選択します。

また選択肢がでるので、/javaを選択します。

これで、今動いているjavaコンテナの環境がVSCodeで開かれました!

(初回だけ開くディレクトリを聞かれるかもしれません。その場合は/appと指定してOKを押して下さい。)

4.デバッグ

今はRemote - Containersでコンテナ内のコードが展開されている状態です。ターミナルも同じくです。なので、通常実行する場合は、上記手順でやったようにjarを実行すればOKです。
 さて、デバッグの方法ですが、これもローカルでやることと一緒です。まずはGradleの設定から。以下を既存のものに追記します。

Gradleの設定

デバッグモードで実行できるように以下のように追記します。

build.gradle
jar { // plan.jarは出力しない
	enabled = false
}

bootJar { // archiveFileNameで一項目にまとめることもできる
	archiveBaseName = "sample"
	version = "0.0.1"
	archiveClassifier = 'SNAPSHOT'
	archiveExtension = 'jar'
}

bootRun { //debug用にgradleからJVMへ引数を渡す
	systemProperties = System.properties // gradleのシステムプロパティをjavaに渡す
	// 上記の記述で、以下の引数がjdkに渡される
	jvmArgs=["-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5050"]
}

bootJar内では実行jarの名前を設定しています。build/libsに指定した名前で出力されます。
bootRun内ではbootRunタスク実行時に、Javaに渡す引数を指定しています。後でbootRun実行したときにこのオプションが付加されます。
引数のうち重要なのは

  • suspend=yでデバッガーがアタッチされるまで起動を待つ
  • address=*:5050host:portを指定

の二点です。
suspend=nにしておけばデバッガーを待たずにアプリケーションが立ち上がります。また、ホストの指定(今回は*)は必須で省略すると起動できません。

VSCodeの設定

次に、 spring_prj/.vscode/launch.jsonを用意します。
VSCodeのデバッガ構成を記すファイルです。

launch.json
{
 "version": "0.2.0",
 "configurations": [
   {
     "type": "java",           // 言語を指定
     "name": "java (Attach)",  // 任意の名前
     "request": "attach",      // すでに起動しているプログラムにアタッチ
     "hostName": "java",       // コンテナ名を指定
     "port": 5050              // 接続するポートを指定
   }
 ]
}

デバッグしてみる

まずビルドしなおしておきます。

$ sh gradlew clean
...
BUILD SUCCESSFUL 
$ sh gradlew build
...
BUILD SUCCESSFUL 

プロジェクトを起動します。

$ sh gradlew bootRun

VSCodeでデバッグビューを開くと、先ほど指定したjava (Attach)という名前のデバッグ構成がプルダウンの一覧にあるはずです。

それを選択した状態で開始ボタンを押します。すると、アプリケーションもデバッグモードで起動し、デバッグができるようになりました。
AccountService.javaなどにブレークポイントを貼って、上記でやったようにcurlコマンドをたたいたりしてちゃんとデバッグできるか確認しておきます。

終わるときは…
赤いボタンでデバッガーを切り離し、ctrl+Cでアプリケーションを停止します。

コンテナ画面の閉じ方は、開くときに押した緑色のボタンを押し、Close Remote Connectionを選択すると終了します。

おわり

以上のような手順でプロジェクトを立ち上げました。
指摘・提案のコメント歓迎です!

感想

DockerでサーバーやDBの実行環境を用意したりGradleでビルド設定書くのは初めてだけど頑張りました。環境構築であれこれつまずいて開発作業に入れないのがつらい…。でもこれでフロント、サーバー、DBとそろったのでやっと開発できる。
以前書いたReactの環境構築に続いて、自分が忘れないためのメモとして書いてますが、拙いなりに一部でも参考になってたら嬉しいです。

Discussion