A production-style Kubernetes resilience and platform engineering project deployed on Amazon EKS, using Helm, Kubernetes StatefulSets, persistent EBS storage, HPA, ALB Ingress, Redis, PostgreSQL, and containerized application workloads.

The project focuses on more than simply deploying an application. It demonstrates how to identify, troubleshoot, recover from, and harden common Kubernetes failure scenarios, particularly around persistent storage, application health checks, autoscaling, and Helm-based deployment.

Project Overview

This project implements a resilient multi-component application on Amazon EKS with:

  • Frontend application
  • Backend application
  • PostgreSQL database
  • Redis cache
  • Persistent EBS-backed PostgreSQL storage
  • Kubernetes Services
  • AWS Application Load Balancer (ALB) Ingress
  • Horizontal Pod Autoscaler (HPA)
  • Helm-based application packaging
  • Environment-specific Helm values
  • Kubernetes health probes
  • PersistentVolume (PV) and PersistentVolumeClaim (PVC) management
  • PostgreSQL storage recovery
  • Storage retention testing
  • AWS EBS volume optimization

The project also includes hands-on troubleshooting of several realistic Kubernetes issues encountered during deployment.

Architecture

 Internet │ ▼ AWS Application Load Balancer │ ▼ Kubernetes Ingress resilient-app-ingress ┌──────┴──────┐ │ │ / /api │ │ ▼ ▼ Frontend Service Backend Service │ │ ▼ ▼ Frontend Pods Backend Pods 2 replicas 2+ replicas │ │ HPA: 2–5 │ ▼ Application API ┌─────────────────────────┐ │ resilient-app │ │ namespace │ └────────────┬────────────┘ │ ┌────────────┴────────────┐ │ │ ▼ ▼ PostgreSQL StatefulSet Redis Deployment │ │ ▼ ▼ PostgreSQL Redis Service │ ▼ PVC │ ▼ PV │ ▼ AWS EBS Volume 

Technology Stack

Technology Purpose
Amazon EKS Managed Kubernetes cluster
Kubernetes Container orchestration
Helm Application packaging and configuration
AWS ALB External application ingress
PostgreSQL 15 Persistent relational database
Redis Application cache
AWS EBS Persistent block storage
HPA Backend autoscaling
Docker/container images Application workloads
YAML Kubernetes and Helm configuration
kubectl Cluster administration
AWS CLI EBS and AWS resource management

Repository Structure

. ├── charts/ ├── docs/ │ └── screenshots/ └── manifests/ ├── backend-hpa.yaml ├── backend.yaml ├── cache.yaml ├── configmap-secret.yaml ├── database.yml ├── frontend.yaml ├── helm/ │ ├── resilient-app/ │ │ ├── Chart.yaml │ │ ├── postgres-recovery-pvc.yaml │ │ ├── templates/ │ │ │ ├── backend-hpa.yaml │ │ │ ├── backend.yaml │ │ │ ├── cache.yaml │ │ │ ├── configmap-secret.yaml │ │ │ ├── database.yaml │ │ │ ├── frontend.yaml │ │ │ └── ingress.yaml │ │ ├── values-dev.yaml │ │ ├── values-staging.yaml │ │ ├── values-prod.yaml │ │ └── values.yaml │ └── values-prod.yaml ├── ingress.yaml ├── namespace.yml └── storageclass.yaml 

The repository contains both the original Kubernetes manifests and the Helm-based implementation used to package the application.

Kubernetes Components

Frontend

The frontend is deployed as a Kubernetes Deployment and exposed internally through:


frontend-service:80 

The production configuration supports multiple replicas to improve availability.

The application was validated through Kubernetes port forwarding and returned:

Kubernetes Resilience Demo - Frontend Active 

Backend

The backend is deployed as a Kubernetes Deployment and exposed through:

backend-service:8080 

The backend health endpoint returns:


{ "status": "healthy", "database": "connected", "redis": "connected" } 

The backend is configured with an HPA allowing it to scale between:

Minimum replicas: 2 Maximum replicas: 5 CPU target: 50% 

PostgreSQL

PostgreSQL runs as a Kubernetes StatefulSet:

statefulset/postgres 

The database uses persistent storage through:


StatefulSet ↓ PersistentVolumeClaim ↓ PersistentVolume ↓ AWS EBS 

The final state was:

postgres-0 1/1 Running 

PostgreSQL connectivity was verified using:

kubectl exec -n resilient-app postgres-0 -- \ pg_isready -U dbadmin -d resilient_db 

Result:

/var/run/postgresql:5432 - accepting connections 

Redis

Redis is deployed as a Kubernetes Deployment and exposed internally through:

redis-service:6379 

The final cluster state showed Redis healthy with no container restarts.

Helm

The application is packaged as a Helm chart:

manifests/helm/resilient-app/ 

The chart contains:

Chart.yaml values.yaml values-dev.yaml values-staging.yaml values-prod.yaml templates/ 

Environment-specific values allow the application configuration to be adapted for different environments without changing the Kubernetes templates.

Example:

helm template resilient-app . -f values-prod.yaml 

The production configuration was rendered successfully.

Helm Validation

The Helm chart was validated before applying changes to the cluster.

Render the chart

helm template resilient-app . -f values-prod.yaml 

Render to a file

helm template resilient-app . -f values-prod.yaml > rendered-prod.yaml 

Server-side Kubernetes validation

kubectl apply --dry-run=server -f rendered-prod.yaml 

This validates the generated resources against the Kubernetes API server without actually applying them.

This was used throughout the project to catch configuration problems before deployment.

Resilience Problems Identified and Resolved

One of the main goals of this project was to troubleshoot real deployment failures rather than simply deploy a predefined manifest.

Several issues were identified.

1. Conflicting Kubernetes StorageClass

The Helm chart initially attempted to manage a gp2 StorageClass that already existed in the EKS cluster.

The cluster already contained:

gp2 (default) provisioner: kubernetes.io/aws-ebs type: gp2 fsType: ext4 

The Helm-generated StorageClass attempted to define conflicting parameters.

Kubernetes rejected the update because StorageClass parameters are immutable.

The error was:

The StorageClass "gp2" is invalid: parameters: Forbidden: updates to parameters are forbidden. 

Resolution

The application chart was changed so that it does not attempt to recreate or modify the cluster-level StorageClass.

The application should consume the existing StorageClass rather than treating it as an application-owned resource.

This follows a cleaner separation of responsibilities:

Cluster infrastructure │ └── StorageClass │ ▼ Application Helm chart │ └── PVC │ ▼ EBS 

2. PostgreSQL Persistent Volume Recovery

The most significant resilience exercise involved PostgreSQL persistent storage.

Initially, Kubernetes reported no PVC/PV resources even though the PostgreSQL pod was still running with an attached EBS-backed filesystem.

Investigation showed that PostgreSQL was using an EBS volume containing valid PostgreSQL data.

The underlying volume was identified using the Kubernetes CSI volume information.

The EBS volume contained the PostgreSQL data directory, including:

PG_VERSION base/ global/ pg_wal/ pg_xact/ pg_multixact/ pg_logical/ postgresql.conf pg_hba.conf 

This confirmed that the database data itself had survived.

3. Manual PV Recovery

The original EBS volume was associated with:

vol-0ed6eca1c06ba8527 

A PersistentVolume was recreated to reference the existing EBS volume through the AWS EBS CSI driver.

The recovery PV was configured with:

persistentVolumeReclaimPolicy: Retain storageClassName: gp2 volumeMode: Filesystem 

The existing EBS volume was referenced through:

csi: driver: ebs.csi.aws.com 

The recovery PVC was then explicitly bound to the recovered PV.

After recovery:

PVC → Bound PV → Bound PostgreSQL → Running 

The PostgreSQL data directory was verified after the volume was reattached.

4. Persistent Storage Lifecycle Test

A deliberate storage lifecycle test was performed to verify the behavior of the Retain policy.

The PostgreSQL StatefulSet was scaled down:

kubectl scale statefulset postgres --replicas=0 -n resilient-app 

The PVC was deleted.

The retained PV entered the Released state rather than immediately destroying the underlying volume.

The original EBS volume remained available.

A new dynamically provisioned volume was observed during the lifecycle test, demonstrating the distinction between:

  • Kubernetes PVC lifecycle
  • PV lifecycle
  • dynamic provisioning
  • retained storage
  • underlying AWS EBS lifecycle

The original retained PV was then recovered by removing its stale claim reference and binding a recovery PVC to it.

PostgreSQL was started again and the original database files were confirmed to be present.

This demonstrated that the data could be recovered independently of the original PVC object.

5. PostgreSQL Storage Optimization

After confirming the database data was recovered and PostgreSQL was healthy, the underlying AWS EBS volume was upgraded from:

gp2 

to:

gp3 

The EBS modification was performed using the AWS CLI:

aws ec2 modify-volume \ --volume-id <VOLUME_ID> \ --volume-type gp3 \ --region us-east-2 

The volume modification reached:

Progress: 100 State: completed Target type: gp3 Target IOPS: 3000 Target throughput: 125 

PostgreSQL remained operational throughout the storage optimization.

Important Kubernetes detail

The underlying AWS EBS volume is now gp3, while the Kubernetes PV retains its existing:

storageClassName: gp2 

This is intentional in the recovery context.

The Kubernetes StorageClass reference is metadata on the PV and is separate from the actual EBS volume type.

A future production implementation could introduce a dedicated CSI-based gp3 StorageClass and perform a controlled storage migration rather than changing the bound PV metadata directly.

6. Backend Container Failure

The backend initially entered:

CrashLoopBackOff 

The container runtime reported:

exec: "-listen=:8080": executable file not found in $PATH 

Investigation showed that the Helm chart was using:

httpd 

while passing arguments intended for:

hashicorp/http-echo 

The application manifest expected:

image: hashicorp/http-echo:latest 

The Helm values were corrected accordingly.

After the change, the backend pods became healthy:

1/1 Running 

and the Deployment successfully reached its desired replica count.

7. PostgreSQL Health Probe Failure

PostgreSQL initially generated:

FATAL: database "dbadmin" does not exist 

The investigation revealed an important distinction:

dbadmin 

was the PostgreSQL role/user, while:

resilient_db 

was the actual application database.

The original probes used:

pg_isready -U dbadmin 

PostgreSQL interpreted the connection target as the database named after the user:

dbadmin 

which did not exist.

The probes were corrected to:

command: - pg_isready - -U - dbadmin - -d - resilient_db 

Both liveness and readiness probes were updated.

After deployment, PostgreSQL logs showed:

database system is ready to accept connections 

and:

pg_isready -U dbadmin -d resilient_db 

returned:

/var/run/postgresql:5432 - accepting connections 

Horizontal Pod Autoscaling

The backend uses a Horizontal Pod Autoscaler.

Configuration:


Minimum replicas: 2 Maximum replicas: 5 CPU target: 50% 

The HPA was verified in the cluster:

kubectl get hpa -n resilient-app 

Final observed state:


TARGETS MINPODS MAXPODS REPLICAS cpu: 2%/50% 2 5 2 

The HPA behavior was also observed through Kubernetes events.

During testing, the backend scaled:

2 → 3 replicas 

and later automatically scaled back:

3 → 2 replicas 

with Kubernetes reporting:

reason: All metrics below target 

This confirms that the HPA was actively managing the backend workload rather than merely existing as an unused resource.

Application Networking

The application uses Kubernetes Services for internal communication.

backend-service ClusterIP port 8080 frontend-service ClusterIP port 80 postgres-service Headless Service port 5432 redis-service Headless Service port 6379 

The PostgreSQL and Redis services use headless service configurations to support stateful/internal service discovery.

AWS Application Load Balancer

The application is exposed through a Kubernetes Ingress using the AWS Load Balancer Controller.

Ingress:

resilient-app-ingress 

Class:

alb 

The EKS cluster successfully provisioned an AWS Application Load Balancer.

The final cluster state showed an ALB DNS address associated with the Ingress.

Validation and Testing

The final cluster was validated using several Kubernetes commands.

Pod health

kubectl get pods -n resilient-app 

Final observed state:

backend Running frontend Running postgres Running redis Running 

All observed pods had:

RESTARTS: 0 

Deployment health

kubectl get statefulset,deployment -n resilient-app 

Final state:

postgres 1/1 backend 2/2 frontend 2/2 redis 1/1 

Persistent storage

kubectl get pvc,pv -n resilient-app 

Final PostgreSQL storage:

PVC: Bound PV: Bound Access Mode: RWO Capacity: 1Gi Reclaim Policy: Retain 

Services and Ingress

kubectl get svc,ingress -n resilient-app 

Verified:

backend-service frontend-service postgres-service redis-service resilient-app-ingress 

HPA

kubectl get hpa -n resilient-app 

Verified:

MINPODS: 2 MAXPODS: 5 CPU TARGET: 50% 

PostgreSQL health

kubectl exec -n resilient-app postgres-0 -- \ pg_isready -U dbadmin -d resilient_db 

Result:

/var/run/postgresql:5432 - accepting connections 

Frontend application test

The frontend service was tested using:

kubectl port-forward svc/frontend-service 8091:80 -n resilient-app 

The application returned:

Kubernetes Resilience Demo - Frontend Active 

Backend application test

The backend service was tested using:

kubectl port-forward svc/backend-service 8092:8080 -n resilient-app 

The service successfully accepted connections through the port-forward.

Final Cluster State

The final observed application state was:

Namespace: resilient-app Backend: 2/2 Running Frontend: 2/2 Running PostgreSQL: 1/1 Running Redis: 1/1 Running PostgreSQL PVC: Bound PostgreSQL PV: Bound PV Reclaim Policy: Retain Backend HPA: 2–5 replicas 50% CPU target Ingress: AWS ALB Pod Restarts: 0 

Key Resilience Principles Demonstrated

This project demonstrates several important Kubernetes and platform engineering principles.

1. Stateful workloads require deliberate storage management

A PostgreSQL pod can be recreated, but database data must live independently from the pod lifecycle.

Pod lifecycle ≠ Data lifecycle 

2. PVCs, PVs, StorageClasses, and cloud volumes are different layers

The project demonstrates the relationship between:

Application ↓ StatefulSet ↓ PVC ↓ PV ↓ CSI Driver ↓ AWS EBS 

Understanding these layers was essential to recovering the database.

3. Retain policies provide an additional recovery boundary

Using:

persistentVolumeReclaimPolicy: Retain 

helps prevent automatic destruction of the underlying persistent volume when a claim is removed.

4. Health checks must test the real application dependency

A syntactically valid probe can still be logically wrong.

The PostgreSQL probe initially checked the wrong database.

Changing:

pg_isready -U dbadmin 

to:

pg_isready -U dbadmin -d resilient_db 

aligned the health check with the actual application database.

5. Autoscaling should be tested, not assumed

The HPA was observed scaling:

2 → 3 → 2 

based on CPU utilization.

This provided evidence that the autoscaling configuration was functioning.

6. Helm should consume infrastructure rather than unnecessarily own it

Cluster-level resources such as StorageClasses should be managed deliberately.

The application chart should consume an appropriate existing StorageClass instead of attempting to redefine a cluster-wide resource with the same name.

Security Considerations

Sensitive configuration should not be committed to source control.

The repository should use:

  • Kubernetes Secrets
  • External Secrets where appropriate
  • AWS Secrets Manager or Parameter Store for production environments
  • Environment-specific configuration
  • .gitignore for local secrets
  • Secret scanning before pushing to GitHub

Example production architecture:

Application │ ▼ Kubernetes Secret │ ▲ External secret management │ ▼ AWS Secrets Manager 

Never commit real passwords, AWS credentials, tokens, kubeconfig files, or private keys to this repository.

Operational Lessons

This project provided hands-on experience with:

  • Kubernetes troubleshooting
  • EKS operations
  • StatefulSets
  • PersistentVolumes
  • PersistentVolumeClaims
  • StorageClasses
  • AWS EBS
  • AWS EBS CSI
  • Kubernetes scheduling
  • Kubernetes events
  • Health probes
  • Container entrypoints
  • Helm templating
  • Environment-specific values
  • Kubernetes Services
  • AWS ALB Ingress
  • HPA
  • Storage recovery
  • Data persistence
  • Failure investigation
  • Production-style validation

Future Improvements

This project is intentionally structured as a foundation for a larger Platform Engineering capstone.

Planned improvements include:

Infrastructure as Code

  • Terraform for EKS
  • VPC
  • IAM
  • Security Groups
  • EBS CSI
  • AWS Load Balancer Controller
  • Supporting AWS infrastructure

CI/CD

  • GitHub Actions
  • Helm linting
  • YAML validation
  • Security scanning
  • Container image scanning
  • Automated deployment

GitOps

  • Argo CD
  • Declarative application delivery
  • Environment promotion
  • Automated drift detection

Observability

  • Prometheus
  • Grafana
  • Alertmanager
  • Application metrics
  • Kubernetes metrics
  • SLO/SLI monitoring

Security

  • NetworkPolicies
  • Pod Security Standards
  • Non-root containers
  • Read-only root filesystems where applicable
  • RBAC hardening
  • Image scanning
  • Secrets management
  • AWS IAM least privilege

Reliability

  • PodDisruptionBudgets
  • Pod anti-affinity
  • Multi-AZ scheduling
  • Automated backup and restore
  • PostgreSQL backup strategy
  • Disaster recovery testing
  • Recovery Time Objective (RTO)
  • Recovery Point Objective (RPO)

Platform Engineering

The long-term goal is to evolve this project into a reusable internal platform that allows application teams to deploy workloads through standardized:

Infrastructure ↓ Kubernetes ↓ Helm ↓ CI/CD ↓ GitOps ↓ Observability ↓ Security ↓ Reliability 

Useful Commands

Helm

helm lint . 
helm template resilient-app . -f values-prod.yaml 
helm template resilient-app . -f values-prod.yaml > rendered-prod.yaml 

Kubernetes validation

kubectl apply --dry-run=server -f rendered-prod.yaml 
kubectl get pods -n resilient-app 
kubectl get svc -n resilient-app 
kubectl get pvc,pv -n resilient-app 
kubectl get statefulset,deployment -n resilient-app 
kubectl get hpa -n resilient-app 
kubectl get ingress -n resilient-app 
kubectl get events -n resilient-app --sort-by='.lastTimestamp' 

PostgreSQL

kubectl exec -n resilient-app postgres-0 -- \ pg_isready -U dbadmin -d resilient_db 

Service testing

Frontend:

kubectl port-forward svc/frontend-service 8091:80 -n resilient-app 

Backend:

kubectl port-forward svc/backend-service 8092:8080 -n resilient-app 

Project Outcome

The project successfully demonstrated a resilient Kubernetes application running on Amazon EKS.

The final implementation successfully achieved:

  • Helm-based application packaging
  • Multi-component Kubernetes deployment
  • Persistent PostgreSQL storage
  • PostgreSQL PV/PVC recovery
  • Persistent volume retention testing
  • AWS EBS volume recovery
  • AWS EBS gp2 → gp3 optimization
  • Correct PostgreSQL health probes
  • Backend container failure resolution
  • Kubernetes Service validation
  • AWS ALB Ingress provisioning
  • Horizontal Pod Autoscaling
  • Application health validation
  • Zero pod restarts in the final observed state

Most importantly, the project demonstrated the ability to troubleshoot and recover a stateful Kubernetes workload instead of simply redeploying it and losing the underlying data.

Author

Jamiu Bakre
Cloud & DevOps Engineer | Kubernetes | AWS | Terraform | CI/CD

Focus areas:

  • Cloud Engineering
  • DevOps
  • Kubernetes
  • Platform Engineering
  • Infrastructure as Code
  • AWS
  • CI/CD
  • Reliability Engineering

Status

Assignment status: Completed — Design & Deploy a Resilient Kubernetes Application

This repository represents the first stage of a broader Platform Engineering journey and is intended to evolve into a more complete production-style platform with Infrastructure as Code, GitOps, observability, security automation, and automated reliability testing.