はじめに
概要
DBを利用するWebアプリケーションにおいて、AWSを使用した本番環境の構築・コンテナ化アプリのデプロイ方法について取り上げます。
前提条件
- 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を構築している想定です。

左側の開発環境の構築を前編のゴールとして進めていきます。
1. サンプルアプリケーション・開発用DBの作成
1) アプリケーション作成
Spring InitializrからMavenプロジェクトを作成し、以下の依存関係を追加します。
- Spring Boot DevTools
- Spring Web
- Thymeleaf
- Spring Data JPA
- MySQL Driver
続いて、以下の通り実装します。
エンティティクラス
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
}
リポジトリインタフェース
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> {
}
サービスクラス
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();
}
}
コントローラークラス
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;
}
}
テンプレート
<!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によるデータアクセスの方法は本記事の主題ではないので、今回は参照処理のみと単純な作りにします)

実際には、DBから取得した情報が表形式で表示されるイメージです。
この段階では、Spring Data JPAの依存関係を追加しているにも関わらず、DB接続情報の設定などは行っていないため、アプリケーションの起動には失敗します。
[進捗状況]

ここまでで、ひとまず基本的なアプリケーションの作成だけ完了しました。
2) dbサービス作成(Docker Compose)
アプリケーションのルートフォルダに、以下の通り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番ポートにバインドします。
続いて、接続情報を保持する環境変数ファイルをルートフォルダに作成します。
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ファイルを作成するイメージです。
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.
3) 開発用DB接続情報の追加
DBの接続情報などの情報は環境ごとに異なるので、Springのプロファイルを使用して切り替えられるようにします。
開発環境のローカルマシン上で動くアプリケーションから開発用DBへのアクセス用には、devプロファイルを用意します。
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アプリケーションが環境変数として読み込むようにするため、以下の依存関係を追加します。
<dependency>
<groupId>me.paulschwarz</groupId>
<artifactId>spring-dotenv</artifactId>
<version>3.0.0</version>
</dependency>
最後に、デフォルトのプロファイルをdevにするよう設定します。
spring.profiles.active=dev
この段階でも、まだアプリケーションの起動は失敗します。
Hibernateの整合性チェックにより、エンティティであるUserクラスが参照するはずのusersテーブルが存在しないことが分かるからです。
この後作成します。
2. 開発用テーブルの作成
開発用データベース内にテーブルを作成していきます。
今回はエンティティをもとにマイグレーションファイルを作成し、マイグレーションを実行することでテーブルを作成します。

具体的には、Spring Data JPAにより、エンティティの情報からマイグレーションファイルを自動で作成します。(中身はDDLです)
続いて、この後追加するマイグレーションツールであるFlywayによりマイグレーションを実行することでテーブルが作成されます。
実際に作業をしていきます。
1) マイグレーションファイルの作成
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で定められた命名規則に則っています。
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を依存関係に追加します。
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
また、application.propertiesにFlywayの設定を追加します。
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

正常にDBから取得したデータを画面に表示していることが分かります。
[進捗状況]

ここまでで、ローカルマシン上のアプリケーションと開発用DBを使用したデバッグ環境が完成しました。
3. コンテナ内実行環境の作成
開発環境において、コンテナ内でアプリケーションをデバッグ実行できるようにします。
これにより、(デバッグ機能の有無を除き)本番環境と全く同一の環境での動作確認ができるようになります。
1) 開発用DB接続情報の追加(コンテナ内アプリからの接続用)
開発環境のコンテナ内で動くアプリケーションから開発用DBへのアクセス用には、dockerプロファイルを用意します。
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ステージ指定時のビルドでのみ実行するようにします。
[進捗状況]

ひとまずDockerfileの作成のみ完了です。
この後、Docker ComposeからこちらのDockerfileを使用する処理を定義していきます。
3) appサービス作成(Docker Compose)
作成済みの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

正常に動いています。
デバッグの実施
ここではVSCodeを使用したデバッグ方法を取り上げますが、他のIDEでも同様の方法で実施できるはずです。
.vscode/launch.jsonファイルを以下の通り用意します。
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Container App",
"request": "attach",
"hostName": "localhost",
"port": 5005
}
]
}
nameの値は任意です。
上記設定により、デバッグを待ち受けるlocalhostの5005番ポートにアタッチしてくれます。

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

これにより、ブレークポイントを張るなどして、好きなようにデバッグを実施できます。
・アプリケーション停止(※サービス名未指定の場合、全てのサービスが停止)
docker compose down app
・更新後アプリケーションの反映
# パッケージング
./mvnw clean package
# 強制的にイメージを再ビルド
docker compose up -d --build app
[進捗状況]

ここまでで、上記の通り開発環境として必要な物は全て用意しました。
JARパッケージを用意し、Docker ComposeによりDockerfileを使用したイメージの作成からコンテナの実行までを行っています。
また、コンテナ内でのアプリケーション実行でもデバッグを実施できる環境を整えました。
おわりに
以上で、開発環境でのアプリ・DB構築が完了しました。
DBはDocker Composeを使用してコンテナ内で実行しています。
アプリケーションはローカルマシン上で直接実行するだけでなく、Dockerコンテナ内で実行する環境も用意しました。
前者は比較的軽く実行でき、後者はより本番環境に近い環境でアプリケーションを動かすことができます。
併せて、Spring Bootのプロファイルを利用して、DBの接続先を切り替えられるようにしました。
開発環境において、接続先は同一のDBであっても、ローカルマシン上からとアプリケーションコンテナからとではエンドポイントが異なるため、それらを切り替えられるようにしています。
また、Spring Data JPAによりDBとの接続やマイグレーションファイルの作成を行い、Flywayによりマイグレーションを管理しています。
後編では、AWSで本番環境を構築し、アプリケーションをデプロイして実際に動かすところまで実施していきます。

