1. What you will build
Customer / Browser
View properties, submit inquiries
AWS Application Load Balancer
Kubernetes Ingress managed by AWS Load Balancer Controller
Frontend Deployment
Nginx + React
frontend-service
Backend Deployment
FastAPI + SQL
backend-service
Amazon RDS PostgreSQL
Main application database — outside Kubernetes
Separate PostgreSQL StatefulSet
PVC → PV → AWS EBS volume
Persistence experiment only
Fluent Bit DaemonSet
Collects node container logs → CloudWatch Logs
What each component is responsible for
| Component | Responsibility |
|---|---|
| Frontend Deployment | Runs website Pods and maintains desired replicas |
| Backend Deployment | Runs API Pods that process property and inquiry requests |
| ReplicaSet | Maintains Pods created by a Deployment |
| ClusterIP Service | Provides a stable internal network endpoint |
| Ingress / ALB | Routes incoming HTTP traffic to Services |
| Amazon RDS | Manages PostgreSQL database infrastructure |
| StatefulSet | Maintains stable Pod identities for our separate database experiment |
| PVC / PV | Connects the StatefulSet to persistent storage |
| StorageClass | Defines how storage is dynamically provisioned |
| EBS CSI Driver | Integrates Kubernetes storage operations with AWS EBS |
| DaemonSet | Ensures a log collection agent runs on eligible nodes |
| kubelet | Starts and monitors Pod containers on a worker node |
| IAM | Controls AWS permissions for controllers and workloads |
| Kubernetes Secret | Provides application credentials to Pods |
Important: We will not place RDS inside a StatefulSet. These are two separate database deployment models.
2. Lab phases
| Phase | Activity |
|---|---|
| 1 | Inspect existing repository and EKS cluster |
| 2 | Verify frontend/backend application and Docker images |
| 3 | Deploy frontend/backend using Deployments |
| 4 | Create internal ClusterIP Services |
| 5 | Configure ALB Ingress |
| 6 | Provision and connect Amazon RDS PostgreSQL |
| 7 | Deploy separate PostgreSQL StatefulSet |
| 8 | Configure EBS CSI, StorageClass and PVC |
| 9 | Deploy Fluent Bit DaemonSet with CloudWatch |
| 10 | Test, troubleshoot, document and clean up |
This is a multi-session project, not a 30-minute exercise. Complete one phase successfully before proceeding.
3. Phase 1 — Inspect the existing project
Why: A DevOps Engineer should understand the existing infrastructure and application before modifying it.
On the EC2 instance:
cd ~/real-estate-company pwd git status --short git remote -v ls -la ls -la backend ls -la frontend ls -la k8s find backend frontend k8s \ -maxdepth 2 -type f | sort | head -100 Inspect Docker and Kubernetes files:
find . -name Dockerfile -o -name '*.yaml' -o -name '*.yml' Check the AWS identity and cluster:
aws sts get-caller-identity aws configure get region kubectl config current-context kubectl get nodes -o wide kubectl get namespaces kubectl get deployments,services,ingresses -A Student checkpoint: Identify the frontend technology, backend technology, existing container ports, current Kubernetes namespace, ECR repositories and EKS cluster name.
Do not print .env files, passwords, tokens or database connection strings into shared classroom screenshots.
4. Phase 2 — Prepare the application
What the application must do
Your Real Estate Company will have two parts.
Frontend: Display available properties, prices, locations and an inquiry form.
Backend: Return property listings, receive customer inquiries, validate requests and save them in PostgreSQL.
The backend is responsible for SQL operations. Kubernetes is responsible for running and connecting the application, not for implementing its business logic.
4.1 Reference backend — FastAPI
If your existing backend already implements these features, retain it and adapt the Kubernetes configuration. Otherwise, create this reference backend in a separate directory so that you do not overwrite your classmates' work.
cd ~/real-estate-company mkdir -p lab-reference/backend cd lab-reference/backend Create requirements.txt:
fastapi==0.115.12 uvicorn==0.34.2 psycopg[binary]==3.2.6 Create main.py:
import osfrom contextlib import asynccontextmanagerimport psycopgfrom fastapi import FastAPI, HTTPExceptionfrom pydantic import BaseModel, FieldDATABASE_URL = os.environ["DATABASE_URL"]def connect(): return psycopg.connect(DATABASE_URL, connect_timeout=5)@asynccontextmanagerasync def lifespan(app: FastAPI): with connect() as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS properties ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title TEXT NOT NULL, city TEXT NOT NULL, price INTEGER NOT NULL ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS inquiries ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, property_id INTEGER NOT NULL REFERENCES properties(id), customer_name TEXT NOT NULL, email TEXT NOT NULL, message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """) count = conn.execute( This is teaching code, not a production-ready customer-data platform. It omits authentication, authorization, rate limiting, full email validation and a proper migration workflow. Use fictitious customer information only.
Create Dockerfile:
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . RUN useradd --uid 10001 --create-home appuser USER 10001 EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] Why Docker? Kubernetes deploys container images. It does not directly deploy your Python source folder.
4.2 Reference frontend — React
For students who already have a working React frontend, retain the existing code and adapt its API calls to /api/properties and /api/inquiries.
For a new reference frontend:
cd ~/real-estate-company/lab-reference npm create vite@latest frontend -- --template react cd frontend npm install Replace src/App.jsx:
import { useEffect, useState } from "react"; export default function App() { const [properties, setProperties] = useState([]); const [selected, setSelected] = useState(""); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [message, setMessage] = useState(""); const [result, setResult] = useState(""); useEffect(() => { fetch("/api/properties") .then(async r => { if (!r.ok) throw new Error("API unavailable"); return r.json(); }) .then(data => { setProperties(data); if (data.length) setSelected(String(data[0].id)); }) .catch(() => setResult("Unable to load properties")); }, []); async function submit(e) { e.preventDefault(); try { const response = await fetch("/api/inquiries", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ property_id: Number(selected), customer_name: name, email, message }) }); if (!response.ok) throw new Error("Request failed"); const data = await response.json(); setResult(`Inquiry saved! Reference #${data.id}`); setMessage(""); } catch { setResult("Unable to save inquiry"); } } return ( <main style={{maxWidth: 900, margin: "40px auto", fontFamily: "Arial"}}> <h1>JumpToTech Real Estate</h1> <p>Explore properties and request more information.</p> <section style={{display: "grid", gap: 16}}> {properties.map(p => ( <article key={p.id} style={{border: "1px solid #ddd", padding: 16}}> <h2>{p.title}</h2> <p>{p.city}</p> <strong>${p.price.toLocaleString()}</strong> </article> ))} </section> <h2>Contact an agent</h2> <form onSubmit={submit} style={{display: "grid", gap: 12}}> <select value={selected} onChange={e => setSelected(e.target.value)}> {properties.map(p => <option key={p.id} value={p.id}>{p.title}</option> )} </select> <input placeholder="Your name" value={name} onChange={e => setName(e.target.value)} required /> <input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required /> <textarea placeholder="Your question" value={message} onChange={e => setMessage(e.target.value)} required /> <button type="submit">Send inquiry</button> </form> <p role="status">{result}</p> </main> ); } Create nginx.conf:
server { listen 8080; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } } Create Dockerfile:
FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginxinc/nginx-unprivileged:stable-alpine COPY nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 8080 Why use /api? The browser can access the frontend and backend through the same ALB hostname. The Ingress sends /api/* requests to the backend, avoiding the need for browser-side access to a private ClusterIP.
5. Phase 3 — Build images and push to Amazon ECR
First define your AWS environment.
export AWS_REGION=us-east-1 export AWS_ACCOUNT_ID=$(aws sts get-caller-identity \ --query Account --output text) export EKS_CLUSTER="YOUR_EKS_CLUSTER" export NAMESPACE="real-estate-lab" export FRONTEND_REPO="real-estate-frontend" export BACKEND_REPO="real-estate-backend" export IMAGE_TAG="lab-v1" Replace YOUR_EKS_CLUSTER with the actual cluster name.
Create ECR repositories
aws ecr describe-repositories \ --repository-names "$FRONTEND_REPO" \ --region "$AWS_REGION" >/dev/null 2>&1 || aws ecr create-repository \ --repository-name "$FRONTEND_REPO" \ --region "$AWS_REGION" Repeat for the backend:
aws ecr describe-repositories \ --repository-names "$BACKEND_REPO" \ --region "$AWS_REGION" >/dev/null 2>&1 || aws ecr create-repository \ --repository-name "$BACKEND_REPO" \ --region "$AWS_REGION" Authenticate Docker:
aws ecr get-login-password --region "$AWS_REGION" | docker login \ --username AWS \ --password-stdin \ "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com" Build and push from the reference implementation:
cd ~/real-estate-company/lab-reference docker build --platform linux/amd64 \ -t "$BACKEND_REPO:$IMAGE_TAG" ./backend docker tag "$BACKEND_REPO:$IMAGE_TAG" \ "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$BACKEND_REPO:$IMAGE_TAG" docker push \ "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$BACKEND_REPO:$IMAGE_TAG" Frontend:
docker build --platform linux/amd64 \ -t "$FRONTEND_REPO:$IMAGE_TAG" ./frontend docker tag "$FRONTEND_REPO:$IMAGE_TAG" \ "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$FRONTEND_REPO:$IMAGE_TAG" docker push \ "$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$FRONTEND_REPO:$IMAGE_TAG" These commands assume x86-64 EKS nodes. If your EKS nodes are ARM64, use linux/arm64 instead.
Verify
aws ecr list-images \ --repository-name "$BACKEND_REPO" \ --region "$AWS_REGION" aws ecr list-images \ --repository-name "$FRONTEND_REPO" \ --region "$AWS_REGION" Student question: Why does Kubernetes need the ECR image URI rather than the source-code directory?
Answer: The container runtime pulls a packaged image containing the application and its dependencies.
6. Phase 4 — Amazon RDS PostgreSQL
Why RDS?
In this architecture, RDS manages the database service outside the Kubernetes cluster.
Kubernetes does not schedule or restart the RDS database process. AWS manages the database instance infrastructure, while your team remains responsible for database configuration, access control, schema, queries and appropriate backup settings.
6.1 Inspect networking before provisioning
aws eks describe-cluster \ --name "$EKS_CLUSTER" \ --region "$AWS_REGION" \ --query 'cluster.resourcesVpcConfig' Find the VPC ID and subnet IDs.
For this lab, RDS should be in private subnets reachable from the EKS worker nodes. Do not make PostgreSQL publicly accessible.
You will need:
- A DB subnet group covering suitable subnets in at least two Availability Zones.
- An RDS security group.
- A rule allowing TCP 5432 from the EKS node security group, or another narrowly scoped security group used by the workloads.
- A PostgreSQL database and database user.
- A secure way to provide the credentials to the application.
6.2 Create RDS
For a shared class, the instructor should provision RDS once. Students should not each create expensive AWS infrastructure without checking costs and quotas.
In the AWS Console:
- Open RDS → Databases → Create database.
- Select PostgreSQL.
- Choose an appropriately small lab instance, such as
db.t4g.microwhere supported and available. - Set database name to
realestate. - Use RDS-managed master credentials in AWS Secrets Manager where available.
- Choose the same VPC as EKS.
- Select a private DB subnet group.
- Set Public access to No.
- Attach the restricted RDS security group.
- Configure backups and deletion protection according to the lab's cleanup plan.
Do not store the master password in Git.
After RDS becomes available, record the endpoint:
YOUR_RDS_ENDPOINT.rds.amazonaws.com 6.3 Create an application database user
Use a database administrator connection from an approved private network location.
Create a dedicated application user with the permissions required for the lab rather than using the RDS master account in the backend.
For a simplified classroom environment, an instructor can provision the database and grant the application user ownership of its schema. Production systems should use separate migration and runtime roles with narrower privileges.
6.4 Create the Kubernetes Secret
The simplest teaching method is a Kubernetes Secret. In production, prefer AWS Secrets Manager with a supported integration such as the Secrets Store CSI Driver or External Secrets Operator.
Create the namespace:
kubectl create namespace "$NAMESPACE" On your private terminal, read the application password without echoing it:
read -rs -p "RDS application password: " DB_PASSWORD echo Construct the connection string using the actual endpoint, database username and URL-encoded password. Special characters in passwords must be percent-encoded in a URI.
For this lab, a simpler and safer option is to create individual Secret keys:
kubectl create secret generic realestate-db \ -n "$NAMESPACE" \ --from-literal=DB_HOST="YOUR_RDS_ENDPOINT" \ --from-literal=DB_PORT="5432" \ --from-literal=DB_NAME="realestate" \ --from-literal=DB_USER="realestate_app" \ --from-literal=DB_PASSWORD="$DB_PASSWORD" Then remove the shell variable:
unset DB_PASSWORD Do not paste passwords into a shared Zoom chat or commit them to Git.
For the reference FastAPI application, we will use a small code change to construct the PostgreSQL connection from these environment variables instead of a single URL.
Replace:
DATABASE_URL = os.environ["DATABASE_URL"]def connect(): return psycopg.connect(DATABASE_URL, connect_timeout=5) with:
def connect(): return psycopg.connect( host=os.environ["DB_HOST"], port=int(os.environ.get("DB_PORT", "5432")), dbname=os.environ["DB_NAME"], user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"], connect_timeout=5, sslmode="require" ) Rebuild and push the backend image with a new tag, for example lab-v2, and use that tag in the Deployment below.
Production note: sslmode=require encrypts the connection but does not provide full server identity verification. Production should use verify-full with an appropriate RDS CA bundle.
7. Phase 5 — Kubernetes Deployments and Services
Create a directory for the new lab manifests:
cd ~/real-estate-company mkdir -p lab-reference/k8s cd lab-reference/k8s 7.1 Backend Deployment
Create backend.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: realestate-backend namespace: real-estate-lab spec: replicas: 2 selector: matchLabels: app: realestate-backend template: metadata: labels: app: realestate-backend spec: containers: - name: backend image: BACKEND_IMAGE_URI ports: - containerPort: 8000 envFrom: - secretRef: name: realestate-db resources: requests: cpu: 100m memory: 128Mi limits: memory: 512Mi startupProbe: httpGet: path: /healthz port: 8000 failureThreshold: 30 periodSeconds: 5 readinessProbe: httpGet: path: /readyz port: 8000 periodSeconds: 10 livenessProbe: httpGet: path: /healthz port: 8000 periodSecondsSource:https://dev.to/jumptotech/jumptotech-devops-bootcamp-real-estate-company-complete-aws-eks-integration-lab-19h9 Related posts
Request, Review, Accept, Repeat: The Life Inside the Loop
Claude Code Hooks: Automating Your AI Coding Workflow

Canva Alternatives 2026: 5 Tools Compared, Prices Checked Nov 4
