Configuration, Operations, และ Security สำหรับ Deploy Spring Boot + Thymeleaf + PostgreSQL Admin App ไปยัง AWS ECS/Fargate + RDS
บทความนี้รวบรวมสิ่งที่ยากเป็นพิเศษเมื่อ deploy Spring Boot + Thymeleaf + PostgreSQL admin app ไปยัง AWS ECS/Fargate + RDS:
- Multi-stage Docker builds
- Initialization scripts
- application.yml switching ด้วย Spring profiles
- CDK stack implementation
- NAT/ALB billing awareness
- Spring profile mismatches
บทความนี้ต่อจากบทความที่อธิบาย admin app เอง:
การสร้างแอพจัดการ DVD Rental ด้วย Spring Boot + Thymeleaf บน PostgreSQL dvdrental Sample DB
Configuration โดยรวมของแอพ
Configuration ของแอพที่จะ deploy มีดังนี้:
- Spring Boot 4 + Thymeleaf (server-rendered HTML)
- Spring Security (staff table authentication)
- Spring Data JPA + NamedParameterJdbcTemplate
- PostgreSQL (local Docker / AWS RDS PostgreSQL)
- Flyway (DB migration)
สำหรับ deployment destination:
- Container Registry: Amazon ECR
- Container Execution: Amazon ECS (Fargate)
- DB: Amazon RDS (PostgreSQL)
- Load Balancer: Application Load Balancer (ALB)
- Network: VPC, Public/Private Subnets, NAT Gateway
Infrastructure โดยรวมใช้ CDK (TypeScript)
Dockerfile
ใช้ multi-stage build เพื่อทำให้ image เล็กที่สุดเท่าที่จะทำได้
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY . .
RUN ./mvnw -B package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
COPY docker/aws/aws-entrypoint.sh /aws-entrypoint.sh
RUN chmod +x /aws-entrypoint.sh
ENTRYPOINT ["/aws-entrypoint.sh"]
ทำไมถึงใช้ aws-entrypoint.sh
ถ้า Fargate task เริ่มขณะที่ RDS ยังไม่พร้อม แอพจะ error ทันทีและ task จะ restart เพื่อจัดการอย่างราบรื่น จึงใช้ startup script เพื่อรอจนกว่า DB พร้อมก่อนเริ่มแอพ
#!/bin/sh
set -e
echo "Waiting for database to be ready..."
until pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER"; do
echo "Database not ready. Retrying in 5 seconds..."
sleep 5
done
echo "Database is ready. Starting application..."
exec java -jar /app/app.jar
ใช้ pg_isready เพื่อ poll จนกว่า DB รับ connections ได้ จากนั้นจึงเริ่มแอพ
application.yml Spring Profile Switching
เตรียมสอง profile: local และ aws
# application.yml
spring:
profiles:
active: local
# application-local.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/dvdrental
username: postgres
password: postgres
# application-aws.yml
spring:
datasource:
url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
username: ${DB_USER}
password: ${DB_PASS}
Profile selection ทำงานโดยอ่าน SPRING_PROFILES_ACTIVE environment variable ใน ECS task definitions จะตั้งค่าเป็น aws ผ่าน environment variables
Spring Profile Mismatch Trap
สิ่งที่เจอจริงคือความไม่ตรงกันระหว่าง profile name กับ filename
ถ้าตั้งชื่อ profile ว่า aws-postgres แต่ filename คือ application-aws.yml profile จะไม่ถูกอ่าน filename ต้องตรงกับ application-{profile name}.yml เป๊ะๆ
CDK Stack Configuration
CDK stack ถูก define ใน TypeScript ส่วนประกอบหลักที่ configure:
// dvd-rental-admin-stack.ts (simplified)
export class DvdRentalAdminStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC
const vpc = new ec2.Vpc(this, "DvdRentalVpc", {
maxAzs: 2,
natGateways: 1,
});
// RDS
const db = new rds.DatabaseInstance(this, "DvdRentalDb", {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_15,
}),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T3,
ec2.InstanceSize.MICRO
),
});
// ECS Cluster
const cluster = new ecs.Cluster(this, "DvdRentalCluster", { vpc });
// Task Definition
const taskDef = new ecs.FargateTaskDefinition(this, "DvdRentalTask", {
cpu: 512,
memoryLimitMiB: 1024,
});
const container = taskDef.addContainer("DvdRentalContainer", {
image: ecs.ContainerImage.fromEcrRepository(repo),
environment: {
SPRING_PROFILES_ACTIVE: "aws",
DB_HOST: db.instanceEndpoint.hostname,
DB_PORT: "5432",
DB_NAME: "dvdrental",
},
secrets: {
DB_USER: ecs.Secret.fromSecretsManager(dbSecret, "username"),
DB_PASS: ecs.Secret.fromSecretsManager(dbSecret, "password"),
},
});
// ALB + Fargate Service
const fargateService =
new ecs_patterns.ApplicationLoadBalancedFargateService(
this,
"DvdRentalService",
{
cluster,
taskDefinition: taskDef,
publicLoadBalancer: true,
}
);
}
}
DB passwords ถูกจัดการใน Secrets Manager และ inject เป็น environment variables ตอน task startup โดยไม่ hardcode ใน code
Command Reproducibility
เพื่อทำให้ deployment work reproducible จึงจัดระเบียบ commands ต่อไปนี้:
# ECR login
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin {account}.dkr.ecr.ap-northeast-1.amazonaws.com
# Build and push
docker build -t dvd-rental-admin .
docker tag dvd-rental-admin:latest {account}.dkr.ecr.ap-northeast-1.amazonaws.com/dvd-rental-admin:latest
docker push {account}.dkr.ecr.ap-northeast-1.amazonaws.com/dvd-rental-admin:latest
# CDK deploy
cd infra
cdk deploy
NAT Gateway และ ALB Billing
สิ่งที่ต้องระวังคือ NAT Gateway และ ALB billing ทำงานต่อเนื่องแม้ไม่ได้ใช้งาน:
- NAT Gateway: ประมาณ $32/เดือน
- ALB: ประมาณ $16/เดือน
รวมประมาณ $48/เดือนแค่จาก infrastructure fees ยังไม่รวม RDS และ Fargate
Minimum cost configuration สำหรับ AWS deployment ได้สรุปไว้แยกต่างหาก:
การพิจารณา Minimum Cost Architecture สำหรับ AWS ECS Fargate + RDS Deployment
Security Group Configuration
เนื่องจาก RDS อยู่ใน private subnet จึงไม่สามารถ access จาก internet ได้ อนุญาตเฉพาะ traffic จาก ECS tasks เท่านั้น
db.connections.allowFrom(
fargateService.service,
ec2.Port.tcp(5432)
);
Flyway Migration
Flyway รัน DB migration ตอน app startup ก่อน Spring Boot เริ่ม จะอ้างอิง resources/db/migration/V*.sql files และ apply เฉพาะ migrations ที่ยังไม่ได้ apply
resources/
db/
migration/
V1__create_initial_schema.sql
V2__add_index.sql
สรุป
Deployment ไปยัง AWS ECS/Fargate + RDS ผ่านหลายจุดที่ต้องลองผิดลองถูก:
- Spring profile name และ filename ต้องตรงกันเป๊ะๆ
- DB readiness check ก่อน app startup (aws-entrypoint.sh)
- DB passwords ต้องจัดการใน Secrets Manager
- NAT Gateway และ ALB billing ทำงานแม้ไม่ได้ใช้งาน
สำหรับ CDK การ define infrastructure เป็น TypeScript code ทำให้ reproducible ด้วยการ execute ที่สอดคล้องกัน ซึ่งเป็นข้อได้เปรียบที่สำคัญ
Admin app เองและสิ่งที่สร้างได้สรุปไว้ในบทความต่อไปนี้:
การสร้างแอพจัดการ DVD Rental ด้วย Spring Boot + Thymeleaf บน PostgreSQL dvdrental Sample DB