Spring Boot + Thymeleaf + PostgreSQL の管理画面アプリを AWS ECS/Fargate + RDS にデプロイするときの構成・運用・セキュリティ
Spring Boot + Thymeleaf で作成した管理画面アプリを AWS 上で動作させるにあたり、どのような構成にするか、どこを守るべきか、どう運用すべきかを整理しながら構築を進めました。
対象は、Java / Spring Boot / Thymeleaf / PostgreSQL で作った業務アプリを、AWS ECS Fargate と RDS for PostgreSQL へデプロイしたいケースです。
今回の記事の主題は、単に AWS に載せたという結果ではなく、管理画面アプリを継続運用する前提で、どのような構成判断を行ったかにあります。
特に重視したのは次の点です。
・アプリケーション本体をどこに置くか ・データベースをどの位置に置くか ・外部公開範囲をどこまで絞るか ・再デプロイや確認をどのような手順で安全に進めるか ・構築後に知見をどう記録し、再利用できるようにするか
課金の見直しは途中で重要な論点になりましたが、それはあくまで構成判断の中の一要素です。記事の中心は、AWS 上でこのアプリを安全かつ運用しやすい形で成立させるために、どのような設計を採ったかという点にあります。
Spring Boot アプリを AWS ECS Fargate へデプロイしたい、PostgreSQL を RDS for PostgreSQL で運用したい、Thymeleaf ベースの管理画面を AWS へ載せたい、という検索意図に対して、構成、初期化、確認手順、運用の観点をまとめています。
この記事で分かること
・Spring Boot + Thymeleaf + PostgreSQL の管理画面アプリを AWS ECS/Fargate + RDS に置く構成の考え方 ・ALB、ECS Fargate、RDS for PostgreSQL、NAT Gateway をどう分離したか ・Docker とコマンドラインを使って AWS デプロイを再現できるようにした考え方 ・データベース初期化や動作確認をどう運用へ組み込んだか ・デプロイ時にハマりやすいポイントと確認手順 ・検証用途で課金を抑えるための考え方
作成したもの
今回作成したのは、DVD レンタル業務を想定した管理画面アプリです。
このアプリの元データとして使った PostgreSQL のサンプル DB は、次のページで確認できます。
アプリ本体をどのように組み立てたかは別記事にまとめているので、画面構成や実装方針まで含めて見たい場合は、こちらもあわせて読むとつながりやすいです。
PostgreSQL のサンプル DB をベースに DVD レンタル管理アプリを作った話
主な機能は次のとおりです。
・ログイン ・ダッシュボード ・顧客管理 ・店舗管理 ・スタッフ管理 ・在庫管理 ・支払い管理 ・レンタル管理 ・売上レポート
画面は Spring Boot + Thymeleaf によるサーバー描画型で構成し、検索、詳細、登録、更新、確認画面を持つ、比較的クラシックな業務アプリ構成にしています。




技術スタック
今回の主な技術スタックは次のとおりです。
・Java 21 ・Spring Boot ・Thymeleaf ・Spring Security ・Spring Data JPA ・PostgreSQL ・Docker ・AWS ECS Fargate ・AWS RDS for PostgreSQL ・AWS Application Load Balancer ・AWS CDK
フロントエンドは SPA ではなく、サーバー描画を前提にしています。 そのため、クライアントサイドの大きな状態管理ではなく、Spring MVC とサーバー側の状態保持を中心に設計しました。
コマンドラインで再現できる形にした
今回の AWS 対応で一番重要だったのは、コンソール画面を手で触らないと再現できない構成にしなかったことです。
このアプリは、Docker とコマンドラインを使って、ビルド、デプロイ、起動確認まで追える形に寄せています。ここをちゃんと作っておかないと、AWS へ 1 回載せることはできても、再デプロイや修正反映のたびに手順がぶれていきます。
今回やったのは、単に docker build と cdk deploy を打てるようにしただけではありません。実際に実行するときに使うファイルを分けて、それぞれに必要な中身を書きました。
実行時に本当に使うファイルは、主に次の 4 つです。
・Dockerfile
このファイルは Docker イメージを作るための本体です。実際には build 用イメージと runtime 用イメージの 2 段構成にしていて、前半では Maven で jar を作り、後半では実行に必要なものだけを残しています。
書いてある内容をそのまま見ると、要点は次のとおりです。
FROM maven:3.9.11-eclipse-temurin-25 AS build
RUN mvn -q -DskipTests package
FROM eclipse-temurin:25-jre-jammy
RUN apt-get install -y --no-install-recommends postgresql-client
COPY --from=build /workspace/target/dvd-rental-admin-0.0.1-SNAPSHOT.jar /app/app.jar
COPY docker/postgres/init/ /app/initdb/
COPY docker/aws-entrypoint.sh /app/aws-entrypoint.sh
ENTRYPOINT ["/app/aws-entrypoint.sh"]
つまり、この 1 ファイルの中に次の内容が入っています。
・maven:3.9.11-eclipse-temurin-25 で build すること
・dvd-rental-admin-0.0.1-SNAPSHOT.jar を /app/app.jar へ置くこと
・DB 初期化 SQL 一式を /app/initdb/ へ入れること
・起動コマンドを java -jar 直打ちではなく aws-entrypoint.sh 経由にすること
・runtime イメージに postgresql-client を入れて psql と pg_isready を使えるようにすること
・ポートを 8080 で公開すること
つまり、Dockerfile は単なるコンテナ化設定ではなく、「AWS で起動したあとに DB 初期化までできる実行イメージ」を組み立てるファイルです。
・docker/aws-entrypoint.sh
このファイルはコンテナ起動直後に実行する処理です。ここがかなり重要で、AWS 上の RDS をローカルに近い状態へそろえてからアプリを起動しています。
中身の流れはほぼ次のとおりです。
wait_for_database
dataset_version="$({ cat /app/initdb/01-dvdrental-full.sql; cat /app/initdb/02-convert-currency-to-jpy.sql; } | sha256sum | awk '{print $1}')"
if [ "$current_version" = "$dataset_version" ]; then
log 'database already matches the development dataset version'
return
fi
run_psql <<'SQL'
DROP SCHEMA IF EXISTS public CASCADE;
SQL
run_psql -f /app/initdb/01-dvdrental-full.sql
run_psql -f /app/initdb/02-convert-currency-to-jpy.sql
exec sh -c 'java $JAVA_OPTS -jar /app/app.jar'
実際に書いてある内容を整理すると、次の役割があります。
・pg_isready で RDS が ready になるまで最大 60 回待つ
・APP_INIT_FULL_DATASET=true のときだけ dataset 反映処理へ進む
・APP_DB_SCHEMA=public 以外は許可しない
・POSTGRESQL_HOST、POSTGRESQL_USER、POSTGRESQL_PASSWORD、POSTGRESQL_DATABASE がない場合は即終了する
・01-dvdrental-full.sql と 02-convert-currency-to-jpy.sql の SHA-256 から dataset version を作る
・public.app_dataset_metadata に保存済み version が一致していれば再投入しない
・version が違えば DROP SCHEMA IF EXISTS public CASCADE で作り直して SQL を入れ直す
・最後に java $JAVA_OPTS -jar /app/app.jar で Spring Boot を起動する
つまり、docker/aws-entrypoint.sh は「RDS 待機」「差分判定」「full dataset 反映」「アプリ起動」をまとめた実行制御ファイルです。
・src/main/resources/application-aws-postgres.yml
このファイルは Spring Boot を AWS 用設定で動かすためのものです。ここに、RDS 接続や schema、cookie、Flyway の扱いがまとまっています。
実際に入っている主な設定は次のようなものです。
spring:
config:
activate:
on-profile: aws-postgres
datasource:
url: jdbc:postgresql://${POSTGRESQL_HOST:localhost}:${POSTGRESQL_PORT:5432}/${POSTGRESQL_DATABASE:dvdrental}
username: ${POSTGRESQL_USER:postgres}
driver-class-name: org.postgresql.Driver
flyway:
enabled: ${SPRING_FLYWAY_ENABLED:true}
server:
forward-headers-strategy: framework
servlet:
session:
cookie:
secure: ${SERVER_SERVLET_SESSION_COOKIE_SECURE:true}
このファイルで明確に決めているのは次の内容です。
・有効 profile は aws-postgres であること
・RDS 接続先は POSTGRESQL_HOST、POSTGRESQL_PORT、POSTGRESQL_DATABASE から受けること
・JDBC driver は org.postgresql.Driver を使うこと
・HikariCP は maximum-pool-size: 10、minimum-idle: 5、connection-timeout: 20000 で動かすこと
・schema は APP_DB_SCHEMA から受け、既定値は public にすること
・Flyway は環境変数で有効化・無効化を切り替えること
・ALB 配下でも正しく動くように forward-headers-strategy: framework を使うこと
・AWS 前提で session cookie を secure にできること
つまり、application-aws-postgres.yml は「コンテナ起動後に Spring Boot がどの接続先と設定で動くか」を固定するファイルです。
・infra/cdk/lib/dvd-rental-admin-stack.ts
このファイルは cdk synth と cdk deploy の対象になる本体です。AWS 側に何をどの設定で作るかは、ほぼここに書いてあります。
ファイルの先頭では、context から実行値を受けています。
const appName = this.node.tryGetContext('appName') ?? 'dvd-rental-admin';
const databaseName = this.node.tryGetContext('databaseName') ?? 'dvdrental';
const desiredCount = Number(this.node.tryGetContext('desiredCount') ?? 1);
const cpu = Number(this.node.tryGetContext('cpu') ?? 512);
const memoryMiB = Number(this.node.tryGetContext('memoryMiB') ?? 1024);
そのうえで、実際には次のような AWS リソース定義が入っています。
・VPC を作ること
・public、application、database の 3 種類の subnet を作ること
・ECS Cluster 名を ${appName}-cluster にすること
・RDS PostgreSQL 17.4 を t4g.micro で作ること
・Application Load Balanced Fargate Service を使うこと
・container port を 8080 にすること
・health check path を /login にすること
・Secrets Manager から DB 接続情報を ECS へ渡すこと
ECS タスクへ渡している環境変数も、このファイルの中に直接あります。
environment: {
SERVER_PORT: '8080',
SPRING_PROFILES_ACTIVE: 'aws-postgres',
SPRING_FLYWAY_ENABLED: 'false',
SERVER_SERVLET_SESSION_COOKIE_SECURE: sessionCookieSecure,
APP_INIT_FULL_DATASET: 'true',
}
Secrets として渡している値も決め打ちです。
secrets: {
POSTGRESQL_HOST: ecs.Secret.fromSecretsManager(connectionSecret, 'host'),
POSTGRESQL_PORT: ecs.Secret.fromSecretsManager(connectionSecret, 'port'),
POSTGRESQL_USER: ecs.Secret.fromSecretsManager(connectionSecret, 'username'),
POSTGRESQL_PASSWORD: ecs.Secret.fromSecretsManager(connectionSecret, 'password'),
POSTGRESQL_DATABASE: ecs.Secret.fromSecretsManager(connectionSecret, 'dbname'),
APP_DB_SCHEMA: ecs.Secret.fromSecretsManager(connectionSecret, 'schema'),
}
つまり、dvd-rental-admin-stack.ts は「VPC の切り方」「ECS と RDS の置き方」「ECS タスクに渡す env と secret」「ALB の health check」まで含めて、AWS 側の実行条件を決めているファイルです。
実際にコマンドを打つときは、これら 4 つがそのまま連動します。Dockerfile でコンテナを作り、infra/cdk/lib/dvd-rental-admin-stack.ts を使って AWS へ配置し、起動時に docker/aws-entrypoint.sh が動き、最後に application-aws-postgres.yml の設定で Spring Boot が RDS へ接続します。
流れを大きくすると、次の 4 段階です。
- Docker でアプリのコンテナを作る
- CDK をコマンドラインから実行して AWS リソースを作る
- コンテナ起動時に entrypoint で DB 初期化を自動実行する
- CloudWatch Logs とブラウザで起動確認する
実際のイメージは次のようになります。
docker build -t dvd-rental-admin:latest .
cd infra/cdk
npm.cmd run synth
npm.cmd run deploy
Windows + AWS SSO では、さらに先に資格情報を PowerShell へ読み込んでから実行していました。
$credentialEnv = aws configure export-credentials --profile AdministratorAccess-812607971800 --format powershell | Out-String
Invoke-Expression $credentialEnv
$env:AWS_REGION='ap-northeast-1'
$env:AWS_DEFAULT_REGION='ap-northeast-1'
$env:AWS_ACCOUNT_ID='812607971800'
このあと cdk synth で manifest のリージョンを確認し、問題なければ cdk deploy へ進む形です。
重要なのは、コンテナを作って ECS に置くだけで終わりにしなかったことです。docker/aws-entrypoint.sh では、RDS が使えるようになるまで待ってから 01-dvdrental-full.sql と 02-convert-currency-to-jpy.sql を適用し、その後にアプリを起動するようにしました。これにより、AWS 上でもローカルと近い初期データで確認できます。
つまり、今回の AWS 対応で本当に重要だったのは、Docker を使ってアプリをコンテナ化したこと、CDK を使ってインフラをコード化したこと、そして起動時の DB 初期化まで含めて、実行に必要なファイルをコマンドライン前提で揃えたことです。
AWS ECS/Fargate + RDS for PostgreSQL にどう配置したか
今回 AWS 上に配置した構成は、役割ごとに分けると次のようになります。
つまり、Spring Boot + PostgreSQL の管理画面アプリを AWS ECS/Fargate と RDS for PostgreSQL に配置する際の基本構成を、そのまま追えるようにしています。
・利用者からのアクセスを受ける入口 ・実際に管理画面を動かすアプリケーション本体 ・業務データを保存するデータベース ・アプリケーションが外部と通信するためのネットワーク経路
構成イメージは次のとおりです。
インターネット ↓ Application Load Balancer ↓ ECS Fargate 上のアプリケーション ↓ RDS PostgreSQL
アプリケーションから外部へ出る通信 ↓ NAT Gateway ↓ インターネット
入口に ALB を置き、その後ろに ECS Fargate のアプリケーションを置き、さらに内側に PostgreSQL を置く構成です。
この構成にした理由は次の 3 点です。
・アプリ本体をインターネットへ直接さらさないため ・データベースをさらに奥に配置して保護するため ・コンテナでアプリを動かし、再デプロイしやすくするため
管理画面アプリは、一般公開サービスとは異なり、扱う情報や操作権限が強くなりやすいため、入口、アプリケーション本体、データベースを段階的に分離する構成を優先しました。

RDS for PostgreSQL へのデータベース初期化の考え方
今回のアプリでは、AWS 上のデータベースを単に空で用意するのではなく、ローカル開発環境に近いデータを反映できるようにしました。
具体的には、アプリケーションコンテナ起動前の entrypoint スクリプトで、次の処理を行う構成です。
- PostgreSQL が使える状態になるまで待機する
- 開発環境用の full dataset SQL を適用する
- 円換算用 SQL を適用する
- その後に Spring Boot アプリケーションを起動する
この方式にしたことで、AWS 上でもログイン確認や一覧画面確認を、ローカルに近い条件で行えるようになりました。
AWS ECS/Fargate + RDS デプロイで実際にハマった点
AWS に載せる過程では、いくつか明確なハマりどころがありました。
1. Spring profile の食い違い
ECS 側では aws-postgres で起動しているのに、設定ファイル側が aws と postgres に分かれていたため、意図しない設定へ落ちる問題がありました。
これに対して、AWS 用の設定を application-aws-postgres.yml にまとめ、プロファイル名と設定内容を一致させる形に修正しました。
2. 起動完了判定が難しい
CloudFormation が完了しても、実際には新しい task が完全に起動していなかったり、ブラウザでは古い状態を見てしまったりすることがありました。
このため、完了判定は次の順に確認するようにしました。
・CloudFormation の完了 ・ECS Service の steady state ・CloudWatch Logs での profile、JDBC URL、dataset 初期化完了、アプリ起動ログ確認 ・最後にブラウザでのログイン成功確認
3. モバイル表示の確認不足
ログイン画面だけを見て完了扱いにすると、共通レイアウトを使っているログイン後画面で崩れていることがあります。
そのため、モバイル修正時はログイン画面だけでなく、ダッシュボードや各管理画面まで確認対象に含めるようにしました。
構成と運用の観点で重視したこと
今回の構築で最も重視したのは、AWS 上で動かすこと自体よりも、その状態を継続的に扱える構成にすることでした。
具体的には、次の観点を優先しています。
1. アプリケーション本体を直接公開しないこと
ECS タスクへ直接アクセスさせるのではなく、入口として ALB を置くことで、公開範囲を明確に分離しました。
2. データベースをさらに内側へ置くこと
RDS はアプリケーションからのみ接続できるようにし、インターネットから直接到達できない構成にしました。
3. 構築後の確認手順を運用に組み込むこと
CloudFormation の完了だけではなく、ECS、CloudWatch Logs、ブラウザ確認まで含めて完了判定を行うようにしました。
4. 問題発生時に同じミスを繰り返さないこと
発生した問題、改善点、運用ルールをドキュメントとして残し、次回の再構築時にも使えるようにしました。
課金を見て気づいたこと
動作確認まで進めた後、構成判断の副次的な論点として AWS の課金も無視できないことが分かりました。
実際に費用に効きやすいのは次の要素でした。
・NAT Gateway ・Application Load Balancer ・ECS Fargate ・RDS
特に NAT Gateway と ALB は、利用量が少なくても固定費に近い形で積み上がりやすく、検証用環境としては想像より重い構成でした。
ここで分かったのは、ECS や RDS を止めるだけでは十分ではないことです。 ALB や NAT Gateway が残っていると、課金は継続します。
そのため、使わない期間は「停止」ではなく「スタック削除」まで行うべき、という運用判断に変わりました。
今回やってよかったこと
今回の作業でよかったのは、実装だけで終わらせず、あとから再利用できる形に整理したことです。
具体的には次の 4 種類の資料を作成しました。
・一般向けの構成概要 ・実際の構築と障害対応の詳細記録 ・課題、問題点、改善点、結果を継続更新するための運用メモ
実際に構築して終わりだと、次回また同じところで迷いやすいですが、構成、問題点、改善点、結果を記録しておくと、再構築や記事化がかなりしやすくなります。
まとめ
Spring Boot + Thymeleaf の管理画面アプリを AWS ECS/Fargate 上で動かすところまで進めてみると、単なるデプロイ作業以上に、構成設計、運用手順、セキュリティ、確認方法まで含めて整理が必要だと分かりました。
今回特に学びになったのは次の点です。
・管理画面アプリは、動けばよいのではなく、どこまで公開するかを先に決める必要がある ・サーバー描画型の管理画面では、状態保持の設計が重要 ・AWS の完了判定は CloudFormation 完了だけでは足りない ・データベースやアプリケーション本体は段階分離して配置した方が扱いやすい ・検証用環境でも ALB と NAT Gateway は費用に効きやすい ・停止より削除の方が適切な場面がある ・作業記録と構成資料を残しておくと、次回以降がかなり楽になる
同じように、Spring Boot の業務アプリを AWS に載せたい人や、構成とコストのバランスで迷っている人の参考になれば幸いです。