DEV Community

Aisalkyn Aidarova
Aisalkyn Aidarova

Posted on

JumpToTech DevOps Bootcamp Real Estate Company — Complete AWS EKS Integration Lab

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
Enter fullscreen mode Exit fullscreen mode

Inspect Docker and Kubernetes files:

find . -name Dockerfile -o -name '*.yaml' -o -name '*.yml'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create requirements.txt:

fastapi==0.115.12
uvicorn==0.34.2
psycopg[binary]==3.2.6
Enter fullscreen mode Exit fullscreen mode

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(
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Create nginx.conf:

server {
    listen 8080;
    server_name _;

    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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:

  1. Open RDS → Databases → Create database.
  2. Select PostgreSQL.
  3. Choose an appropriately small lab instance, such as db.t4g.micro where supported and available.
  4. Set database name to realestate.
  5. Use RDS-managed master credentials in AWS Secrets Manager where available.
  6. Choose the same VPC as EKS.
  7. Select a private DB subnet group.
  8. Set Public access to No.
  9. Attach the restricted RDS security group.
  10. 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

On your private terminal, read the application password without echoing it:

read -rs -p "RDS application password: " DB_PASSWORD
echo
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Then remove the shell variable:

unset DB_PASSWORD
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"    )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
            periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
  name: backend-service
  namespace: real-estate-lab
spec:
  type: ClusterIP
  selector:
    app: realestate-backend
  ports:
    - port: 80
      targetPort: 8000
Enter fullscreen mode Exit fullscreen mode

Replace BACKEND_IMAGE_URI with the full ECR image URI, including its tag.

Why two replicas? We want to demonstrate how a Deployment maintains multiple backend Pods and how a Service routes requests to healthy endpoints.

Why separate readiness and liveness? If RDS is unavailable, readiness should fail so the Pod stops receiving traffic. Liveness should not depend on RDS, or a temporary database outage could cause unnecessary container restarts.

7.2 Frontend Deployment

Create frontend.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: realestate-frontend
  namespace: real-estate-lab
spec:
  replicas: 2
  selector:
    matchLabels:
      app: realestate-frontend
  template:
    metadata:
      labels:
        app: realestate-frontend
    spec:
      containers:
        - name: frontend
          image: FRONTEND_IMAGE_URI
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              memory: 256Mi
          readinessProbe:
            httpGet:
              path: /
              port: 8080
          livenessProbe:
            httpGet:
              path: /
              port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
  namespace: real-estate-lab
spec:
  type: ClusterIP
  selector:
    app: realestate-frontend
  ports:
    - port: 80
      targetPort: 8080
Enter fullscreen mode Exit fullscreen mode

Replace FRONTEND_IMAGE_URI with the full ECR URI.

7.3 Apply and verify

kubectl apply -f backend.yaml
kubectl apply -f frontend.yaml

kubectl rollout status deployment/realestate-backend \
  -n real-estate-lab --timeout=180s

kubectl rollout status deployment/realestate-frontend \
  -n real-estate-lab --timeout=180s
Enter fullscreen mode Exit fullscreen mode

Check:

kubectl get deployments -n real-estate-lab
kubectl get replicasets -n real-estate-lab
kubectl get pods -n real-estate-lab -o wide
kubectl get services -n real-estate-lab
kubectl get endpointslices -n real-estate-lab
Enter fullscreen mode Exit fullscreen mode

Expected result: Two frontend Pods and two backend Pods should be Ready, provided the images can be pulled and the backend can reach RDS.

If backend Pods are not Ready, investigate before moving on:

kubectl describe pod -n real-estate-lab \
  -l app=realestate-backend

kubectl logs deployment/realestate-backend \
  -n real-estate-lab --tail=50
Enter fullscreen mode Exit fullscreen mode

7.4 Test the backend without ALB

Use port forwarding from a machine with kubectl access:

kubectl port-forward \
  -n real-estate-lab svc/backend-service 8081:80
Enter fullscreen mode Exit fullscreen mode

In a second terminal:

curl -i http://localhost:8081/healthz
curl -i http://localhost:8081/readyz
curl -i http://localhost:8081/api/properties
Enter fullscreen mode Exit fullscreen mode

Create a sample inquiry:

curl -i -X POST http://localhost:8081/api/inquiries \
  -H 'Content-Type: application/json' \
  -d '{
    "property_id": 1,
    "customer_name": "Test Student",
    "email": "student@example.com",
    "message": "I would like a property tour."
  }'
Enter fullscreen mode Exit fullscreen mode

Verify:

curl http://localhost:8081/api/inquiries/count
Enter fullscreen mode Exit fullscreen mode

Student checkpoint: Explain the complete path from the HTTP request through Service, Backend Pod and RDS PostgreSQL.

8. Phase 6 — AWS ALB Ingress

Why Ingress?

Our ClusterIP Services are reachable only through Kubernetes networking. Customers need an external HTTP entry point.

AWS Load Balancer Controller can create and manage an Application Load Balancer from a Kubernetes Ingress.

Before proceeding, verify that the AWS Load Balancer Controller is installed, has appropriate IAM permissions and that the VPC subnets are correctly tagged for load balancer discovery.

kubectl get deployment aws-load-balancer-controller \
  -n kube-system

kubectl get ingressclass
Enter fullscreen mode Exit fullscreen mode

If the controller is missing, do not assume that creating an Ingress will automatically provision an ALB. The instructor must first install the controller using the supported AWS installation procedure and IAM configuration.

8.1 Create the Ingress

Create ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: realestate-ingress
  namespace: real-estate-lab
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/healthcheck-path: /
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}]'
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: backend-service
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80
Enter fullscreen mode Exit fullscreen mode

Apply:

kubectl apply -f ingress.yaml
kubectl get ingress -n real-estate-lab
kubectl describe ingress realestate-ingress \
  -n real-estate-lab
Enter fullscreen mode Exit fullscreen mode

The ADDRESS field should eventually contain an ALB hostname if provisioning succeeds.

Important production distinction

This is an HTTP-only classroom configuration. Do not send real customer information through it. For a real application, configure HTTPS using ACM, an HTTPS listener, appropriate redirect rules and an approved DNS name.

The single ALB routes /api to Backend and / to Frontend. The frontend uses relative /api requests, so browser requests reach the same ALB hostname.

8.2 Verify

Once the ALB hostname is assigned:

export ALB_DNS=$(kubectl get ingress realestate-ingress \
  -n real-estate-lab \
  -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')

echo "$ALB_DNS"

curl -i "http://$ALB_DNS/"
curl -i "http://$ALB_DNS/api/properties"
Enter fullscreen mode Exit fullscreen mode

If you see 502, 503 or 504, do not immediately restart the Pods. Inspect the ALB target groups, backend health, Service endpoints, application logs and database connectivity.

9. Phase 7 — PostgreSQL StatefulSet with EBS

This phase is a separate experiment from the RDS database.

Our main Real Estate Company uses RDS. Now we will deploy another PostgreSQL instance inside Kubernetes to understand StatefulSet, PVC, PV, StorageClass and the EBS CSI driver.

9.1 Understand the storage path

StatefulSet

Creates and manages postgres-0

Pod

Runs the PostgreSQL database software

PVC

Requests 5 GiB of persistent storage

StorageClass

Selects the EBS storage provisioner

EBS CSI Driver

Coordinates EBS volume provisioning and attachment

PV / EBS Volume

Stores PostgreSQL data files

9.2 Check EBS CSI and StorageClasses

kubectl get csidrivers
kubectl get storageclass -o wide
Enter fullscreen mode Exit fullscreen mode

For a standard EKS installation with the EBS CSI driver, the CSI provisioner is typically:

ebs.csi.aws.com
Enter fullscreen mode Exit fullscreen mode

For EKS Auto Mode, the provisioner is:

ebs.csi.eks.amazonaws.com
Enter fullscreen mode Exit fullscreen mode

Use the driver and StorageClass that actually match your cluster.

If EBS CSI is missing, the instructor must install the appropriate EKS add-on and configure its AWS IAM permissions. The CSI controller needs permissions to provision and manage EBS volumes. Merely creating a PVC will not grant those permissions.

For standard EKS with the standard EBS CSI driver, create storageclass.yaml:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: realestate-gp3
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
Enter fullscreen mode Exit fullscreen mode

For EKS Auto Mode, use its supported provisioner instead and review the applicable configuration before creating the class.

Apply:

kubectl apply -f storageclass.yaml
Enter fullscreen mode Exit fullscreen mode

Why WaitForFirstConsumer? EBS volumes are Availability Zone-specific. Kubernetes should consider the Pod's placement before provisioning the volume.

Why reclaimPolicy: Delete? This is a disposable classroom lab. Deleting the PVC may lead to deletion of its dynamically provisioned EBS volume. Do not use this configuration for valuable data without understanding its consequences.

9.3 Create the PostgreSQL credentials

read -rs -p "Lab PostgreSQL password: " PG_PASSWORD
echo

kubectl create secret generic postgres-lab-secret \
  -n real-estate-lab \
  --from-literal=POSTGRES_USER=labuser \
  --from-literal=POSTGRES_PASSWORD="$PG_PASSWORD" \
  --from-literal=POSTGRES_DB=labdb

unset PG_PASSWORD
Enter fullscreen mode Exit fullscreen mode

9.4 Create PostgreSQL StatefulSet

Create postgres-statefulset.yaml:

apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
  namespace: real-estate-lab
spec:
  clusterIP: None
  selector:
    app: postgres-lab
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: real-estate-lab
spec:
  serviceName: postgres-headless
  replicas: 1
  selector:
    matchLabels:
      app: postgres-lab
  template:
    metadata:
      labels:
        app: postgres-lab
    spec:
      securityContext:
        fsGroup: 999
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
              name: postgres
          envFrom:
            - secretRef:
                name: postgres-lab-secret
          env:
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              memory: 1Gi
          readinessProbe:
            exec:
              command:
                - /bin/sh
                - -c
                - pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
            initialDelaySeconds: 10
            periodSeconds: 10
  volumeClaimTemplates:
    - metadata:
        name: postgres-data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: realestate-gp3
        resources:
          requests:
            storage: 5Gi
Enter fullscreen mode Exit fullscreen mode

Why one replica? Creating three PostgreSQL Pods does not automatically configure PostgreSQL replication. We are demonstrating persistence and stable identity, not building a highly available database cluster.

Apply:

kubectl apply -f postgres-statefulset.yaml

kubectl get statefulset -n real-estate-lab
kubectl get pods -n real-estate-lab -l app=postgres-lab
kubectl get pvc -n real-estate-lab
kubectl get pv
Enter fullscreen mode Exit fullscreen mode

Wait until postgres-0 is Running/Ready and the PVC is Bound.

9.5 Create test data

Open PostgreSQL:

kubectl exec -it postgres-0 \
  -n real-estate-lab -- \
  psql -U labuser -d labdb
Enter fullscreen mode Exit fullscreen mode

Inside psql:

CREATE TABLE property_notes (
    id SERIAL PRIMARY KEY,
    property_name TEXT NOT NULL,
    note TEXT NOT NULL
);

INSERT INTO property_notes (property_name, note)
VALUES ('Chicago Condo', 'Customer requested a tour');

SELECT * FROM property_notes;
Enter fullscreen mode Exit fullscreen mode

Exit:

\q
Enter fullscreen mode Exit fullscreen mode

9.6 Delete the PostgreSQL Pod

kubectl delete pod postgres-0 -n real-estate-lab
Enter fullscreen mode Exit fullscreen mode

Wait:

kubectl rollout status statefulset/postgres \
  -n real-estate-lab --timeout=300s
Enter fullscreen mode Exit fullscreen mode

Check the data again:

kubectl exec postgres-0 -n real-estate-lab -- \
  psql -U labuser -d labdb \
  -c "SELECT * FROM property_notes;"
Enter fullscreen mode Exit fullscreen mode

Expected: The saved record remains because PostgreSQL is using its persistent volume.

Student explanation

The StatefulSet recreated postgres-0 with its stable identity. The PVC remained associated with the StatefulSet ordinal, and Kubernetes remounted the existing persistent storage.

If the underlying EBS volume is deleted, corrupted or unavailable, StatefulSet alone cannot recover the data.

10. Phase 8 — DaemonSet and centralized logging

What is DaemonSet responsible for?

A DaemonSet controller ensures that an eligible worker node runs a Pod for a particular node-level workload.

Typical examples include log collectors, monitoring agents and some networking components.

A DaemonSet does not replace kubelet, and it does not automatically centralize logs. We need a log collector and a destination.

For this project:

Fluent Bit DaemonSet → Amazon CloudWatch Logs

10.1 Understand the architecture

Worker Node 1

Application Pods

Fluent Bit Pod

Worker Node 2

Application Pods

Fluent Bit Pod

Amazon CloudWatch Logs

Centralized application and container logs

10.2 Verify IAM permissions

The Fluent Bit workload needs permission to create log streams and publish log events.

For standard EKS, prefer a dedicated IAM role for the collector using EKS Pod Identity or IRSA, according to your cluster configuration. Do not give every application Pod broad CloudWatch permissions.

A minimum illustrative policy for an instructor-created CloudWatch log group is:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogStreams"
      ],
      "Resource": [
        "arn:aws:logs:us-east-1:ACCOUNT_ID:log-group:/jumptotech/real-estate:*"
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Replace ACCOUNT_ID with the actual AWS account ID. The instructor must configure the trust relationship and attach this policy to the collector's dedicated role.

Create the log group:

aws logs create-log-group \
  --log-group-name /jumptotech/real-estate \
  --region "$AWS_REGION"
Enter fullscreen mode Exit fullscreen mode

If the log group already exists, AWS returns an already-exists error; that does not mean logging is broken.

Set a short retention period for the lab:

aws logs put-retention-policy \
  --log-group-name /jumptotech/real-estate \
  --retention-in-days 7 \
  --region "$AWS_REGION"
Enter fullscreen mode Exit fullscreen mode

10.3 Install Fluent Bit with Helm

For a real centralized-logging lab, use the maintained Fluent Bit Helm chart rather than writing an incomplete logging DaemonSet from scratch.

helm repo add fluent https://fluent.github.io/helm-charts
helm repo update

helm show values fluent/fluent-bit > fluent-default-values.yaml
Enter fullscreen mode Exit fullscreen mode

The Helm chart must be configured for your EKS runtime log paths, CloudWatch output and IAM integration. Do not assume that the default values automatically send logs to CloudWatch.

Create fluent-values.yaml:

kind: DaemonSet

serviceAccount:
  create: true
  name: fluent-bit-realestate

config:
  service: |
    [SERVICE]
        Flush         5
        Daemon        Off
        Log_Level     info
        Parsers_File  /fluent-bit/etc/parsers.conf
        HTTP_Server   On
        HTTP_Listen   0.0.0.0
        HTTP_Port     2020

  inputs: |
    [INPUT]
        Name              tail
        Path              /var/log/containers/*real-estate-lab*.log
        Tag               kube.*
        Mem_Buf_Limit     5MB
        Skip_Long_Lines   On
        Refresh_Interval  10
        DB                /var/log/flb_realestate.db

  filters: |
    [FILTER]
        Name   kubernetes
        Match  kube.*
        Kube_Tag_Prefix kube.var.log.containers.
        Merge_Log On
        Keep_Log Off

  outputs: |
    [OUTPUT]
        Name cloudwatch_logs
        Match kube.*
        region us-east-1
        log_group_name /jumptotech/real-estate
        log_stream_prefix fluentbit-
        auto_create_group false
Enter fullscreen mode Exit fullscreen mode

Install only after the Fluent Bit ServiceAccount has been connected to the appropriate AWS IAM role. With EKS Pod Identity, this requires a Pod Identity association for the ServiceAccount; with IRSA, the ServiceAccount needs the role annotation and a valid OIDC trust policy.

The chart's default hostPath mounts must also be verified against your node runtime. The above values are a reference configuration to be validated, not a guarantee that every EKS node image exposes identical log paths.

helm upgrade --install realestate-logs \
  fluent/fluent-bit \
  -n logging --create-namespace \
  -f fluent-values.yaml
Enter fullscreen mode Exit fullscreen mode

Check:

kubectl get daemonsets -n logging
kubectl get pods -n logging -o wide
kubectl logs -n logging \
  -l app.kubernetes.io/name=fluent-bit \
  --tail=50
Enter fullscreen mode Exit fullscreen mode

10.4 Verify centralized logs

Generate some backend traffic:

curl "http://$ALB_DNS/api/properties"
Enter fullscreen mode Exit fullscreen mode

Then inspect CloudWatch:

aws logs describe-log-streams \
  --log-group-name /jumptotech/real-estate \
  --region "$AWS_REGION"
Enter fullscreen mode Exit fullscreen mode

Read recent logs:

aws logs tail /jumptotech/real-estate \
  --since 10m \
  --region "$AWS_REGION"
Enter fullscreen mode Exit fullscreen mode

If no logs appear, inspect the Fluent Bit Pod logs, IAM permissions, log paths, filter configuration and output plugin errors.

Student question: Why do we use DaemonSet rather than a Deployment with three replicas for a node-level log collector?

Answer: A DaemonSet ties desired Pod placement to eligible nodes. A Deployment controls replica count but does not guarantee one collector on every node.

11. Phase 9 — Terraform and infrastructure ownership

Terraform is an infrastructure-as-code tool. It can provision the AWS infrastructure that Kubernetes applications depend on.

For this lab, divide ownership as follows:

Resource Managed by Responsible team
VPC, subnets, security groups Terraform / AWS Cloud / DevOps
EKS cluster and node groups Terraform / AWS Platform / DevOps
ECR repositories Terraform / AWS DevOps
RDS instance and subnet group Terraform / AWS Cloud / DBA
AWS IAM roles and policies Terraform / AWS Cloud Security / DevOps
Frontend and backend code Git Application developers
Deployments and Services Kubernetes manifests / Helm DevOps
PostgreSQL StatefulSet and PVC Kubernetes DevOps / DBA
EBS volumes EBS CSI Driver / AWS Kubernetes storage integration
Fluent Bit Helm / DaemonSet Platform / SRE
Database schema and migrations SQL migration tooling Backend / DBA

Terraform example: RDS security group

This example demonstrates how to restrict PostgreSQL access to the EKS worker-node security group.

Create terraform/rds-security.tf in a separate Terraform project:

variable "vpc_id" {
  type = string
}

variable "eks_node_security_group_id" {
  type = string
}

resource "aws_security_group" "rds" {
  name_prefix = "realestate-rds-"
  description = "RDS access from EKS worker nodes"
  vpc_id      = var.vpc_id

  tags = {
    Project = "JumpToTech-RealEstate"
  }
}

resource "aws_vpc_security_group_ingress_rule" "postgres" {
  security_group_id            = aws_security_group.rds.id
  referenced_security_group_id = var.eks_node_security_group_id
  ip_protocol                  = "tcp"
  from_port                    = 5432
  to_port                      = 5432
}

resource "aws_vpc_security_group_egress_rule" "rds_egress" {
  security_group_id = aws_security_group.rds.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}
Enter fullscreen mode Exit fullscreen mode

This is only one component of the RDS Terraform configuration, not a complete standalone infrastructure deployment. The VPC, provider, DB subnet group, RDS instance, credential management and Terraform backend still need to be configured.

Important: Students must never run terraform apply against an existing shared EKS infrastructure project without understanding its state and proposed changes.

12. Phase 10 — Production troubleshooting exercises

Each student must investigate these scenarios independently and explain the findings.

Scenario Symptoms First checks
ImagePullBackOff Pod cannot start Image URI, ECR tag, pull permissions
CrashLoopBackOff Container repeatedly exits kubectl logs --previous, events
Backend not Ready Readiness probe fails /readyz, RDS connectivity, credentials
Service unavailable No backend endpoints Selectors, Pod labels, EndpointSlices
ALB 502 ALB cannot get valid backend response Target health, ports, app process
ALB 503 No available targets or other service issue Target groups, endpoints, health
ALB 504 Backend response timeout Application latency, networking, RDS
PVC Pending PostgreSQL cannot obtain storage StorageClass, CSI driver, IAM, scheduling
PostgreSQL Pod Pending Pod cannot schedule Node capacity, AZ constraints, volume events
Fluent Bit no logs CloudWatch log group empty IAM, input path, collector logs, output errors

Troubleshooting commands

kubectl get all -n real-estate-lab

kubectl get pods -n real-estate-lab -o wide

kubectl describe deployment realestate-backend \
  -n real-estate-lab

kubectl logs deployment/realestate-backend \
  -n real-estate-lab --tail=100

kubectl get svc -n real-estate-lab

kubectl get endpointslices -n real-estate-lab

kubectl get events -n real-estate-lab \
  --sort-by=.lastTimestamp

kubectl get pvc -n real-estate-lab

kubectl describe pvc postgres-data-postgres-0 \
  -n real-estate-lab

kubectl get daemonsets -A

kubectl get ingress -n real-estate-lab
Enter fullscreen mode Exit fullscreen mode

Simulated production incident

Your instructor will introduce a problem.

Incident: Customers can open the Real Estate Company website, but submitting an inquiry fails.

Students must:

  1. Verify whether the frontend is available.
  2. Check the backend HTTP response.
  3. Inspect backend Pod readiness.
  4. Check Service selectors and EndpointSlices.
  5. Examine application logs.
  6. Test PostgreSQL connectivity.
  7. Identify the root cause.
  8. Propose a safe correction.
  9. Verify that inquiries can be saved again.
  10. Write a short incident report.

Do not delete a namespace or restart all workloads as your first troubleshooting action.

13. Final student assessment

Student completion checklist

0/12 complete

Frontend and backend container images pushed to ECR

Frontend and backend Deployments running

ClusterIP Services and EndpointSlices verified

ALB routes frontend and backend traffic

RDS PostgreSQL connected to Backend

Customer inquiry saved and retrieved

PostgreSQL StatefulSet created

PVC Bound to an EBS-backed PV

PostgreSQL data survives Pod replacement

Fluent Bit DaemonSet deployed

CloudWatch receives application logs

One incident investigated and documented

Interview questions

Students should be able to answer these questions in English without reading notes.

  1. What is the difference between Deployment and StatefulSet?
  2. Why does the frontend use a Deployment?
  3. Why do we use ClusterIP Services?
  4. What is the difference between Amazon RDS and PostgreSQL in a StatefulSet?
  5. What happens if the PostgreSQL Pod is deleted?
  6. What is the difference between PV, PVC and StorageClass?
  7. What does the EBS CSI Driver do?
  8. Why is a DaemonSet suitable for a log collector?
  9. What is the difference between a DaemonSet controller and kubelet?
  10. How do you troubleshoot a backend Pod that is Running but not Ready?
  11. What is the purpose of AWS IAM in this architecture?
  12. How does a customer HTTP request travel from ALB to PostgreSQL?

14. Cleanup — prevent unnecessary AWS charges

Do not delete resources from a shared or production cluster. Confirm the namespace and ownership before cleanup.

First inspect:

kubectl get all -n real-estate-lab
kubectl get pvc -n real-estate-lab
kubectl get ingress -n real-estate-lab
Enter fullscreen mode Exit fullscreen mode

Remove the ALB Ingress and verify that the AWS Load Balancer Controller has deleted the corresponding ALB:

kubectl delete ingress realestate-ingress \
  -n real-estate-lab
Enter fullscreen mode Exit fullscreen mode

Uninstall Fluent Bit if it was installed specifically for this lab:

helm uninstall realestate-logs -n logging
Enter fullscreen mode Exit fullscreen mode

Remove the StatefulSet:

kubectl delete statefulset postgres \
  -n real-estate-lab
Enter fullscreen mode Exit fullscreen mode

The StatefulSet's PVC is generally retained when the StatefulSet is deleted. Inspect the PVC and PV before deleting storage:

kubectl get pvc -n real-estate-lab
kubectl get pv
Enter fullscreen mode Exit fullscreen mode

If the PostgreSQL data is disposable and the PVC belongs to this lab:

kubectl delete pvc postgres-data-postgres-0 \
  -n real-estate-lab
Enter fullscreen mode Exit fullscreen mode

With the lab's Delete reclaim policy, this can cause the backing EBS volume to be deleted. Verify the actual EBS resource state in AWS.

After verifying that all resources in the namespace belong to this lab:

kubectl delete namespace real-estate-lab
Enter fullscreen mode Exit fullscreen mode

Remove the lab-specific RDS instance, snapshots, CloudWatch log group, ECR images, IAM associations and other AWS resources only after verifying their ownership and backup requirements. Deleting the Kubernetes namespace does not automatically delete Amazon RDS.

Do not delete the EKS cluster if other students or projects use it.

15. What students must submit

Each student must submit a GitHub repository link and a short lab report containing evidence of the deployment, networking, database persistence, logging and troubleshooting.

The report should explain:

  • Why the frontend and backend use Deployments.
  • Why RDS is separate from Kubernetes.
  • How StatefulSet, PVC, PV, StorageClass and EBS CSI work together.
  • Why the log collector uses a DaemonSet.
  • Which Kubernetes controllers are responsible for maintaining the desired state.
  • What failed during troubleshooting and how the student verified recovery.

Students must not commit passwords, .env secrets, kubeconfig credentials or AWS access keys.

Final teaching outcome

By the end of this project, you should be able to explain the architecture of a multi-tier application on AWS EKS and demonstrate how its components work together.

The goal is not simply to create Pods. The goal is to understand application deployment, networking, database persistence, AWS permissions, observability and production troubleshooting.

Instructor implementation note: The application and Kubernetes examples above form a reference lab, but several infrastructure prerequisites are environment-specific: EKS IAM configuration, ALB controller installation, RDS networking and credentials, EBS CSI installation, and Fluent Bit AWS permissions. These must be validated against the actual student cluster before calling the deployment fully operational.

Top comments (0)