Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Patient API

Stateless REST API for patient record management — JWT authentication, Firestore persistence, graceful shutdown, multi-stage Docker build, and full GCP Cloud Run deployment via Terraform IaC.

be2-backend/   Spring Boot 4 REST API (Java 21, Gradle)
terraform/     GCP infrastructure (Cloud Run + Firestore + Artifact Registry)

What's technically interesting

Graceful shutdown with live drain count ActiveRequestCounter is a plain servlet Filter that wraps every request with an AtomicInteger increment/decrement. On SIGTERM, ShutdownLogger marks the health endpoint 503 (so the load balancer stops routing), logs the live in-flight count, and @PreDestroy closes the Firestore client after Spring's 15s drain window. server.shutdown: graceful handles the rest.

PID 1 / SIGTERM forwarding in Docker ENTRYPOINT ["sh", "-c", "java ..."] makes sh PID 1. Cloud Run sends SIGTERM to PID 1 — but sh -c doesn't forward signals to its child, so Spring's graceful shutdown never fires and Cloud Run falls back to SIGKILL. Fix: prefix with exec so the shell replaces itself with the Java process, making Java PID 1.

Build-only-on-change via source hash A null_resource in Terraform computes a SHA-1 of be2-backend/src/** as its trigger. Cloud Build only runs when source actually changes — not on every terraform apply.

Alpine vs Noble base image The Firestore SDK uses gRPC + netty-tcnative which requires glibc. Alpine uses musl — this causes a JVM SIGSEGV on startup. Base image: eclipse-temurin:21-jre-noble (Debian-based).

Architecture

graph TB
    Client -->|HTTPS| CR[Cloud Run<br/>patient-api]
    CR -->|gRPC / ADC| FS[(Firestore<br/>Native Mode)]
    CB[Cloud Build] -->|push| AR[Artifact Registry]
    AR -->|deploy| CR

    subgraph GCP Project
        CR
        FS
        AR
        CB
        SA[Service Account<br/>roles/datastore.user]
    end

    SA -.->|identity| CR
    TF[Terraform] -->|provisions| CR
    TF -->|provisions| FS
    TF -->|provisions| AR
    TF -->|provisions| SA
Loading

Request flow: HTTPS → Cloud Run → Firestore (Native Mode)

Security: JwtAuthFilter extends OncePerRequestFilter, validates the Bearer token, and sets SecurityContextHolder. Expired → 401; invalid → 403. SecurityConfig is STATELESS; /auth/login and /health are public; /patients/** requires JWT.

Quick Start (local)

Option A — Firestore emulator (no GCP credentials needed)

# Terminal 1: start emulator
gcloud beta emulators firestore start --host-port=127.0.0.1:8090

# Terminal 2: run app
cd be2-backend
FIRESTORE_EMULATOR_HOST=127.0.0.1:8090 \
JWT_SECRET=this-is-a-32-byte-secret-key-here \
./gradlew bootRun

Option B — Real Firestore

cd be2-backend
GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json \
JWT_SECRET=this-is-a-32-byte-secret-key-here \
GCP_PROJECT_ID=your-gcp-project-id \
./gradlew bootRun

API Reference

Method Path Auth Description
POST /auth/login No Authenticate, receive JWT
GET /patients JWT List all patients
GET /patients/{id} JWT Get patient by ID (404 if missing)
POST /patients JWT Create patient (server-generated UUID)
GET /health No 200 healthy / 503 shutting down or DB disconnected
GET /slow?delay=N No Sleeps N seconds (max 30) — graceful shutdown demo

Demo credentials: admin / password123, doctor / secret456

Infrastructure as Code (Terraform)

terraform apply provisions everything in one command:

Resource Purpose
Artifact Registry Docker image storage
Firestore (Native Mode) Document database
Service Account Cloud Run identity (roles/datastore.user)
Cloud Build Builds and pushes container image
Cloud Run Serverless compute, public HTTPS
cd terraform/
cp terraform.tfvars.example terraform.tfvars   # fill in your values
terraform init
terraform apply
terraform destroy   # tear down everything cleanly

Uses a named Firestore database (not (default)) so terraform destroy can cleanly delete it.

Docker

# Build
docker build -t patient-api:local ./be2-backend

# Run (with Firestore emulator)
docker run -p 8080:8080 \
  -e JWT_SECRET=this-is-a-32-byte-secret-key-here \
  -e GCP_PROJECT_ID=local-dev \
  -e FIRESTORE_EMULATOR_HOST=host.docker.internal:8090 \
  patient-api:local
Stage Base Image Purpose
Build eclipse-temurin:21-jdk-noble Gradle build, produces fat JAR
Run eclipse-temurin:21-jre-noble JRE only, non-root user

Built with AI

This project was built using Claude Code (Anthropic) as the primary development tool throughout — code generation, debugging, architecture decisions, and documentation.

My role was directing the overall approach, reviewing and approving AI-generated output, and redirecting when something was wrong or incomplete. The key decisions — GCP over AWS, Firestore over Cloud SQL, Terraform from scratch after learning it via Google Labs, the exec fix for PID 1 signal forwarding, the source-hash Cloud Build trigger — were made by me after understanding the tradeoffs.

AI-assisted development is increasingly the norm. I'm being upfront about it here because the interesting part isn't who typed the code — it's the engineering judgment behind the decisions.

Key Environment Variables

Variable Required Description
JWT_SECRET Yes HMAC-SHA256 signing key (min 32 chars)
GCP_PROJECT_ID Yes (prod) GCP project ID. Defaults to local-dev
FIRESTORE_DB No Firestore database name. Defaults to (default)
GOOGLE_APPLICATION_CREDENTIALS Local only Path to service account JSON key
FIRESTORE_EMULATOR_HOST Local only Firestore emulator address

See be2-backend/README.md for full API docs, 12-Factor compliance details, and test coverage.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages