Tech Blog

Configuration, Operations, และ Security สำหรับ Deploy Spring Boot + Thymeleaf + PostgreSQL Admin App ไปยัง AWS ECS/Fargate + RDS

by Tech Writer
Spring Boot AWS ECS Docker

บทความนี้รวบรวมสิ่งที่ยากเป็นพิเศษเมื่อ 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

ส่งข้อความได้ตามสบาย

กรุณาส่งข้อความ หากมีคำปรึกษาด้านเทคนิค ความคิดเห็น หรือคำถาม