DevConda — blog-workspace

Connect Spring Boot JPA to Aurora Serverless on Fargate

Part 5 system architecture at a glance

When should you read this?

You already run Spring Boot on ECS Fargate, put ALB HTTPS in front, and roll deploys from GitHub Actions. Health through https://api.example.com works. The next failure is persistence: POST still dies with connection refused, or the jar still points at H2 / a laptop MySQL that the task cannot reach.

Those Fargate posts ship and expose one API. Here the next step is a PostgreSQL database the task can reach, wired through Spring Data JPA.

Use Amazon Aurora PostgreSQL with Aurora Serverless (Serverless v2 capacity) so capacity scales with load instead of a fixed db.t* size for this lab.

What you are building

Same account / region / VPC / task security group as parts 2-4. Database stays private. Only the task SG may open TCP 5432.

Final system with Aurora (CYD-style layered system architecture – Application JPA and Data highlighted):

Part 5: system architecture (request flow) – Spring Boot / Fargate / Aurora

Before: health only. After: one JPA write path that survives a task restart.

Before health only versus after JPA persists on Aurora
Before health only versus after JPA persists on Aurora

Network and security groups

Reuse the VPC and Fargate task SG from the ALB post. Create a DB SG that accepts 5432 only from that task SG.

REGION=ap-northeast-2
# Same VPC as the Fargate service (parts 2-3)
VPC_ID=vpc-xxxxxxxx
# Fargate task security group from parts 2-3
TASK_SG=sg-yyyyyyyy

# Create DB SG; capture GroupId for later authorize / create-db-cluster
DB_SG=$(aws ec2 create-security-group \
  --group-name api-aurora-sg \
  --description "Aurora PG from Fargate tasks only" \
  --vpc-id $VPC_ID \
  --region $REGION \
  --query GroupId --output text)

# Allow TCP 5432 only from the task SG (not 0.0.0.0/0)
aws ec2 authorize-security-group-ingress \
  --group-id $DB_SG \
  --protocol tcp --port 5432 \
  --source-group $TASK_SG \
  --region $REGION

Put the cluster in two private subnets (different AZs). Publicly accessible stays off.

# Replace subnet-aaa / subnet-bbb with two private subnet IDs in $VPC_ID (different AZs)
aws rds create-db-subnet-group \
  --db-subnet-group-name api-aurora-subnets \
  --db-subnet-group-description "Aurora for api Fargate" \
  --subnet-ids subnet-aaa subnet-bbb \
  --region $REGION
SG path: Fargate task SG to TCP 5432 on Aurora DB SG only
SG path: Fargate task SG to TCP 5432 on Aurora DB SG only

Create Aurora PostgreSQL Serverless

Create the cluster with Serverless v2 scaling, then add a db.serverless writer. Pick a strong master password and keep it out of git.

# Set once in this shell; do not commit the password
DB_PASS='ReplaceWithALongSecret'

# Aurora PostgreSQL cluster with Serverless v2 capacity range
aws rds create-db-cluster \
  --db-cluster-identifier api-aurora \
  --engine aurora-postgresql \
  --engine-version 16.4 \
  --master-username apimaster \
  --master-user-password "$DB_PASS" \
  --database-name api \
  --db-subnet-group-name api-aurora-subnets \
  --vpc-security-group-ids $DB_SG \
  --serverless-v2-scaling-configuration MinCapacity=0.5,MaxCapacity=2 \
  --storage-encrypted \
  --region $REGION

# Writer instance: db.serverless class (required for Aurora Serverless v2)
aws rds create-db-instance \
  --db-instance-identifier api-aurora-writer \
  --db-cluster-identifier api-aurora \
  --engine aurora-postgresql \
  --db-instance-class db.serverless \
  --region $REGION

Wait until the cluster is available, then read the writer endpoint:

# Block until the writer instance reports available
aws rds wait db-instance-available \
  --db-instance-identifier api-aurora-writer \
  --region $REGION

# Cluster writer endpoint -> host part of the JDBC URL
DB_HOST=$(aws rds describe-db-clusters \
  --db-cluster-identifier api-aurora \
  --region $REGION \
  --query 'DBClusters[0].Endpoint' --output text)

# Print the JDBC URL Spring will use (database name api)
echo "jdbc:postgresql://${DB_HOST}:5432/api"

MinCapacity=0.5 keeps a small floor for this lab so the first JDBC connect is not stuck behind a long auto-pause resume. Raise MaxCapacity later if the workload needs it.

Create Aurora: subnet group, cluster, db.serverless writer
Create Aurora: subnet group, cluster, db.serverless writer

Spring Boot: JPA dependencies and config

Add Data JPA and the PostgreSQL driver (Gradle example):

dependencies {
  // JPA + EntityManager; required for @Entity / JpaRepository
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  // PostgreSQL JDBC driver (Aurora speaks the PostgreSQL wire protocol)
  runtimeOnly 'org.postgresql:postgresql'
}

Keep secrets out of the image. Read JDBC settings from the environment (task definition):

# Task definition env SPRING_DATASOURCE_* -> datasource
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
# Lab only: create/update schema from entities on boot
spring.jpa.hibernate.ddl-auto=update
# Avoid open session for the whole HTTP request
spring.jpa.open-in-view=false

Minimal entity + repository + one write API:

@Entity
@Table(name = "posts")
public class Post {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY) // Aurora PG serial / identity
  private Long id;
  @Column(nullable = false)
  private String title;
  // getters/setters
}

// Spring Data: save / findById without boilerplate SQL
public interface PostRepository extends JpaRepository<Post, Long> {}

@RestController
@RequestMapping("/api/posts")
public class PostController {
  private final PostRepository posts;
  public PostController(PostRepository posts) { this.posts = posts; }

  // Persist one row; returns the entity with generated id
  @PostMapping
  public Post create(@RequestBody Post body) {
    return posts.save(body);
  }

  // Load by primary key (404/exception if missing)
  @GetMapping("/{id}")
  public Post get(@PathVariable Long id) {
    return posts.findById(id).orElseThrow();
  }
}

Rebuild the image, push a new ECR tag, and roll the service the same way as part 4 – or register a new task revision by hand for a one-off test.

Request path: Controller to JPA to PostgreSQL
Request path: Controller to JPA to PostgreSQL

Task definition: inject JDBC env

On the api container, set:

NameValue
SPRING_DATASOURCE_URLjdbc:postgresql://$DB_HOST:5432/api
SPRING_DATASOURCE_USERNAMEapimaster
SPRING_DATASOURCE_PASSWORDthe master password you set

Execution role still pulls ECR and writes logs. No extra task role is required for plain JDBC password auth.

Force a new deployment and wait for stability. If the task flaps, open CloudWatch /ecs/api: Connection refused usually means wrong subnet path or SG; password authentication failed means env mismatch; relation "posts" does not exist means the app never reached a successful JPA start with ddl-auto=update.

Verify

# Create one row through ALB -> Fargate -> JPA -> Aurora
curl -sf -X POST "https://api.example.com/api/posts" \
  -H "Content-Type: application/json" \
  -d '{"title":"aurora jpa smoke"}'

# Read the same id back (expect matching title)
curl -sf "https://api.example.com/api/posts/1"

Restart the service once and GET the same id again. If the row is gone, you were still on an in-memory DB or a different schema.

Verify: POST, GET, restart, GET again
Verify: POST, GET, restart, GET again

Minimal checklist

  1. DB SG allows 5432 only from the Fargate task SG; Aurora not public.
  2. Cluster api-aurora + db.serverless writer; note $DB_HOST.
  3. Image has spring-boot-starter-data-jpa + PostgreSQL driver (Gradle) and a small entity/repo API.
  4. Task env sets SPRING_DATASOURCE_* to that cluster (password not in git).
  5. POST then GET through the ALB works after a task restart.

After that, the Fargate API persists through Aurora PostgreSQL Serverless with Spring Data JPA – same VPC path you opened on day one of the series.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *