1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ECS + RDS】コンテナ化Spring Boot/MySQLアプリの本番環境を構築 [前編] - 開発環境でのアプリ・DB構築まで

Posted at

はじめに

概要

DBを利用するWebアプリケーションにおいて、AWSを使用した本番環境の構築・コンテナ化アプリのデプロイ方法について取り上げます。

image.png

前提条件

  • Java/Spring Boot環境構築済み
  • Docker環境構築済み
  • AWSアカウント作成済み

リポジトリ

動作環境

  • Windows 11 Home(24H2)
  • Java 21
  • Maven 3.9.11
  • Spring Boot 3.5.4
  • MySQL 8.4.5
  • Docker 27.3.1
  • Docker Desktop 4.36.0

本手順

前編では、開発用DBを構築し、アプリケーションを動かすところまで実施します。

DB構成

以降の手順では、DB構成は以下の通り進めます。

開発用DB

項目 内容
サーバ ローカルマシン(Dockerコンテナ)
データベース名 qiita_spring_ecs_dev
ユーザ名 qiita_spring_ecs_dev_user
パスワード devpassword

本番用DB

項目 内容
サーバ DBサーバ(RDS)
データベース名 qiita_spring_ecs_prod
ユーザ名 qiita_spring_ecs_prod_user
パスワード prodpassword

環境全体像

開発環境・本番環境の大まかな全体像は以下の通りです。
ローカルマシンはWindowsを前提に、WSLでDockerを構築している想定です。
image.png

左側の開発環境の構築を前編のゴールとして進めていきます。

1. サンプルアプリケーション・開発用DBの作成

1) アプリケーション作成

Spring InitializrからMavenプロジェクトを作成し、以下の依存関係を追加します。

  • Spring Boot DevTools
  • Spring Web
  • Thymeleaf
  • Spring Data JPA
  • MySQL Driver

続いて、以下の通り実装します。

エンティティクラス

User.java
package com.example.demo.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false, length = 50)
    private String name;

    @Column(name = "age", nullable = false)
    private int age;

    public User() {
    }

    // getter, setter
}

リポジトリインタフェース

UserRepository.java
package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

}

サービスクラス

UserService.java
package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;

@Service
@Transactional(readOnly = true)
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public List<User> findAll() {
        return userRepository.findAll();
    }
}

コントローラークラス

UserController.java
package com.example.demo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;

@Controller
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping({ "", "/" })
    public ModelAndView index(ModelAndView mav) {
        List<User> users = userService.findAll();

        mav.addObject("users", users);
        mav.setViewName("users/list");
        return mav;
    }
}

テンプレート

templates/users/list.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>UserList</title>
  </head>
  <body>
    <h1>ユーザ一覧</h1>
    <div>
      <table border="1">
        <tr>
          <th>ID</th>
          <th>名前</th>
          <th>年齢</th>
        </tr>
        <tr th:each="user : ${users}">
          <td th:text="${user.id}"></td>
          <td th:text="${user.name}"></td>
          <td th:text="${user.age}"></td>
        </tr>
      </table>
    </div>
  </body>
</html>

DBに格納しているユーザ一覧の情報を取得して画面に表示するアプリケーションです。
(Springによるデータアクセスの方法は本記事の主題ではないので、今回は参照処理のみと単純な作りにします)

image.png
実際には、DBから取得した情報が表形式で表示されるイメージです。

この段階では、Spring Data JPAの依存関係を追加しているにも関わらず、DB接続情報の設定などは行っていないため、アプリケーションの起動には失敗します。

[進捗状況]
image.png
ここまでで、ひとまず基本的なアプリケーションの作成だけ完了しました。

2) dbサービス作成(Docker Compose)

アプリケーションのルートフォルダに、以下の通りdocker-compose.ymlを作成します。

docker-compose.yml
services:
  db:
    image: mysql:8.4.5
    container_name: mysql-dev
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${DEV_DB_ROOTPASS}
      MYSQL_DATABASE: ${DEV_DB_DATABASE}
      MYSQL_USER: ${DEV_DB_USERNAME}
      MYSQL_PASSWORD: ${DEV_DB_PASSWORD}
      LANG: C.UTF-8
    ports:
      - '3308:3306'
    volumes:
      - db-data:/var/lib/mysql

volumes:
  db-data:

今回はMySQL 8.4.5のイメージを使用します。
接続情報などは環境変数を参照するようにします。
また、私のローカルマシンでは3306番ポートは既に使用しているため、コンテナの3306番ポート(MySQLが使用するポート)をローカルマシンの3308番ポートにバインドします。

続いて、接続情報を保持する環境変数ファイルをルートフォルダに作成します。

.env
DEV_DB_ROOTPASS=rootpassword
DEV_DB_DATABASE=qiita_spring_ecs_dev
DEV_DB_USERNAME=qiita_spring_ecs_dev_user
DEV_DB_PASSWORD=devpassword

当ファイルは機密情報を含むので、Git管理から除外します。

また、.envファイルのテンプレートとして、.env.exampleファイルを作成します。
これをもとに、各開発者が実際の値を設定した.envファイルを作成するイメージです。

.env.example
DEV_DB_ROOTPASS=your_root_password
DEV_DB_DATABASE=your_database
DEV_DB_USERNAME=your_username
DEV_DB_PASSWORD=your_password

実際に、以下コマンドでdbサービス(MySQL 8.4.5コンテナ)を立ち上げます。

docker compose up -d

以下の通りログインし、正常に起動していることを確認します。

docker exec -it mysql-dev mysql -u qiita_spring_ecs_dev_user -p qiita_spring_ecs_dev

# ローカルマシンにMySQLがインストールされている場合、以下でも可
# mysql -u qiita_spring_ecs_dev_user -P 3308 -p qiita_spring_ecs_dev

> Welcome to the MySQL monitor.

[進捗状況]
image.png
開発用DBが作成されました。

3) 開発用DB接続情報の追加

DBの接続情報などの情報は環境ごとに異なるので、Springのプロファイルを使用して切り替えられるようにします。

開発環境のローカルマシン上で動くアプリケーションから開発用DBへのアクセス用には、devプロファイルを用意します。

application-dev.properties
spring.datasource.url=jdbc:mysql://localhost:3308/${DEV_DB_DATABASE}
spring.datasource.username=${DEV_DB_USERNAME}
spring.datasource.password=${DEV_DB_PASSWORD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=validate

Spring Data JPAにより、開発用DBと接続するようにしています。
コンテナ内で動く開発用DBはローカルマシンの3308番ポートにバインドしているので、URLに指定するエンドポイントは「localhost:3308」となります。
接続情報の一部は、先ほど同様.envファイルに定義した情報を参照しています。

続いて、.envファイルに定義した接続情報をSpringアプリケーションが環境変数として読み込むようにするため、以下の依存関係を追加します。

pom.xml
<dependency>
	<groupId>me.paulschwarz</groupId>
	<artifactId>spring-dotenv</artifactId>
	<version>3.0.0</version>
</dependency>

最後に、デフォルトのプロファイルをdevにするよう設定します。

application.properties
spring.profiles.active=dev

この段階でも、まだアプリケーションの起動は失敗します。
Hibernateの整合性チェックにより、エンティティであるUserクラスが参照するはずのusersテーブルが存在しないことが分かるからです。
この後作成します。

2. 開発用テーブルの作成

開発用データベース内にテーブルを作成していきます。
今回はエンティティをもとにマイグレーションファイルを作成し、マイグレーションを実行することでテーブルを作成します。

image.png
具体的には、Spring Data JPAにより、エンティティの情報からマイグレーションファイルを自動で作成します。(中身はDDLです)
続いて、この後追加するマイグレーションツールであるFlywayによりマイグレーションを実行することでテーブルが作成されます。

実際に作業をしていきます。

1) マイグレーションファイルの作成

application-dev.propertiesに以下を一時的に追加します。

application-dev.properties
spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create
spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-target=src/main/resources/db/migration/V1__Initial_schema.sql
spring.jpa.properties.hibernate.hbm2ddl.delimiter=;

この状態でアプリケーションを実行します。

./mvnw spring-boot:run

すると、指定したパスに以下の通りマイグレーションファイルが作成されます。
Vn__xxxというファイル名は、Flywayで定められた命名規則に則っています。

V1__Initial_schema.sql
create table users (age integer not null, id bigint not null auto_increment, name varchar(50) not null, primary key (id)) engine=InnoDB;

マイグレーションファイル作成後、先ほどapplication-dev.propertiesに加えた変更を取り消しておきます。

初期状態のテーブルを作成するマイグレーションファイル生成処理は、今回の一回のみ動かしたいので、コミットはしません。(マイグレーションファイル自体はコミットします)
以降、テーブル定義に変更を加える場合、エンティティの更新、およびマイグレーションファイルを手動で作成し、アプリケーションを実行することでテーブル定義を変更する方針を想定します。

2) マイグレーション実行

マイグレーションツールであるFlywayを依存関係に追加します。

pom.xml
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-mysql</artifactId>
</dependency>

また、application.propertiesにFlywayの設定を追加します。

application.properties
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.locations=classpath:db/migration

これにより、開発・本番の両環境で、アプリケーション実行時に指定したパス(classpath:db/migration)に含まれるマイグレーションファイルの情報をもとに、FlywayがDBマイグレーションを実行してくれます。

実際にアプリケーションを実行してみます。

./mvnw spring-boot:run

(↓一部抜粋)
> o.f.c.i.s.JdbcTableSchemaHistory     : Creating Schema History table `qiita_spring_ecs_dev`.`flyway_schema_history` ...
> o.f.core.internal.command.DbMigrate  : Current version of schema `qiita_spring_ecs_dev`: << Empty Schema >>
> o.f.core.internal.command.DbMigrate  : Migrating schema `qiita_spring_ecs_dev` to version "1 - Initial schema"
> o.f.core.internal.command.DbMigrate  : Successfully applied 1 migration to schema `qiita_spring_ecs_dev`, now at version v1 (execution time 00:00.081s)

Flywayが開発用DBの状態を見てマイグレーションを実行しています。

show tables;
+--------------------------------+
| Tables_in_qiita_spring_ecs_dev |
+--------------------------------+
| flyway_schema_history          |
| users                          |
+--------------------------------+
2 rows in set (0.01 sec)

開発用DBを確認すると、意図した通りテーブルが作成されていることが分かります。
また、Flywayの履歴管理テーブルも自動で作成されます。
これにより、実行済みのマイグレーションファイルの情報が管理され、以降は、既に実行したマイグレーションはスキップしてくれます。

3) 動作確認

以下の通りデータを登録してアプリケーションにアクセスしてみます。

INSERT INTO users (
    name,
    age
)
VALUES
    ('田中太郎', 24),
    ('佐藤花子', 35),
    ('鈴木一郎', 46);

http://localhost:8080/users
image.png
正常にDBから取得したデータを画面に表示していることが分かります。

[進捗状況]
image.png
ここまでで、ローカルマシン上のアプリケーションと開発用DBを使用したデバッグ環境が完成しました。

3. コンテナ内実行環境の作成

開発環境において、コンテナ内でアプリケーションをデバッグ実行できるようにします。
これにより、(デバッグ機能の有無を除き)本番環境と全く同一の環境での動作確認ができるようになります。

1) 開発用DB接続情報の追加(コンテナ内アプリからの接続用)

開発環境のコンテナ内で動くアプリケーションから開発用DBへのアクセス用には、dockerプロファイルを用意します。

application-docker.properties
spring.datasource.url=jdbc:mysql://db:3306/${DEV_DB_DATABASE}
spring.datasource.username=${DEV_DB_USERNAME}
spring.datasource.password=${DEV_DB_PASSWORD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=validate

URLのホスト名ですが、アプリケーション用のコンテナから見ると、開発用DBのコンテナは自身とは異なるマシンになるので、「localhost」ではなく「db」(Docker Composeのサービス名)となります。
また、開発用DBのコンテナ内ではMySQLのアプリケーションは3306番ポートで動いているため、指定するエンドポイントは「db:3306」となります。

例によって接続情報の一部は.envファイルの情報を参照するので、当該ファイルはコンテナ内にもコピーするよう、この後のDockerfile作成で設定するようにします。

2) 開発用イメージ定義作成(Dockerfile)

アプリケーションのルートフォルダに、以下の通りDockerfileを作成します。

FROM amazoncorretto:21 AS base
WORKDIR /app
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
COPY .env .env
EXPOSE 8080

FROM base AS dev
EXPOSE 5005
ENTRYPOINT ["java", "-Djava.net.preferIPv4Stack=true", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "/app/app.jar", "--spring.profiles.active=docker"]

ベースイメージは「Amazon Corretto 21」を使用します。
Mavenによるパッケージングで作成されるJARファイルと、接続情報が定義されている.envファイルをコピーするようにします。

javaコマンドによるJARファイル実行時、5005番ポートでデバッグを受け付けるようにします。
また、Spring Bootのdockerプロファイルを使用して実行するよう指定します。

Dockerのマルチステージビルド機能を使用し、デバッグやdockerプロファイルの使用は、開発環境用のdevステージ指定時のビルドでのみ実行するようにします。

[進捗状況]
image.png
ひとまずDockerfileの作成のみ完了です。
この後、Docker ComposeからこちらのDockerfileを使用する処理を定義していきます。

3) appサービス作成(Docker Compose)

作成済みのdocker-compose.ymlファイルを以下の通り変更します。

docker-compose.yml
  services:
+   app:
+     build:
+       context: .
+       target: dev
+     image: qiita-spring-ecs-rds-app-dev:latest
+     depends_on:
+       - db
+     ports:
+       - '8080:8080'
+       - '5005:5005'
+     networks:
+       - app-network
  
    db:
      image: mysql:8.4.5
      container_name: mysql-dev
      restart: unless-stopped
      environment:
        MYSQL_ROOT_PASSWORD: ${DEV_DB_ROOTPASS}
        MYSQL_DATABASE: ${DEV_DB_DATABASE}
        MYSQL_USER: ${DEV_DB_USERNAME}
        MYSQL_PASSWORD: ${DEV_DB_PASSWORD}
        LANG: C.UTF-8
      ports:
        - '3308:3306'
      volumes:
        - db-data:/var/lib/mysql
+     networks:
+       - app-network
  
  volumes:
    db-data:
  
+ networks:
+   app-network:

アプリケーション本体であるappサービスを追加しています。
イメージは上記のDockerfileを使用して作成するようにし、devステージを使用してビルドするようにします。
また、アプリケーションから開発用DBにアクセスできるようにするため、appサービスとdbサービスを同一ネットワークで動かすようにします。

4) 動作確認

はじめに、Mavenでアプリケーションのパッケージングを行います。

./mvnw clean package

続いて、Docker Composeでサービスを立ち上げます。
dbサービスに変更を加えているので、--buildオプションを付与してイメージを強制的に再作成します。

docker compose up -d --build

http://localhost:8080/users
image.png
正常に動いています。

デバッグの実施
ここではVSCodeを使用したデバッグ方法を取り上げますが、他のIDEでも同様の方法で実施できるはずです。
.vscode/launch.jsonファイルを以下の通り用意します。

launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "Container App",
            "request": "attach",
            "hostName": "localhost",
            "port": 5005
        }
    ]
}

nameの値は任意です。
上記設定により、デバッグを待ち受けるlocalhostの5005番ポートにアタッチしてくれます。

image.png
appサービスは実行中のまま、VSCodeの実行とデバッグビューから、実行ボタンを押下します。

image.png
これにより、ブレークポイントを張るなどして、好きなようにデバッグを実施できます。

・アプリケーション停止(※サービス名未指定の場合、全てのサービスが停止)

docker compose down app

・更新後アプリケーションの反映

# パッケージング
./mvnw clean package

# 強制的にイメージを再ビルド
docker compose up -d --build app

[進捗状況]
image.png
ここまでで、上記の通り開発環境として必要な物は全て用意しました。
JARパッケージを用意し、Docker ComposeによりDockerfileを使用したイメージの作成からコンテナの実行までを行っています。
また、コンテナ内でのアプリケーション実行でもデバッグを実施できる環境を整えました。

おわりに

以上で、開発環境でのアプリ・DB構築が完了しました。

DBはDocker Composeを使用してコンテナ内で実行しています。
アプリケーションはローカルマシン上で直接実行するだけでなく、Dockerコンテナ内で実行する環境も用意しました。
前者は比較的軽く実行でき、後者はより本番環境に近い環境でアプリケーションを動かすことができます。

併せて、Spring Bootのプロファイルを利用して、DBの接続先を切り替えられるようにしました。
開発環境において、接続先は同一のDBであっても、ローカルマシン上からとアプリケーションコンテナからとではエンドポイントが異なるため、それらを切り替えられるようにしています。
また、Spring Data JPAによりDBとの接続やマイグレーションファイルの作成を行い、Flywayによりマイグレーションを管理しています。

後編では、AWSで本番環境を構築し、アプリケーションをデプロイして実際に動かすところまで実施していきます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?