This repository contains an end-to-end DevOps implementation for an Online Examination Platform. It demonstrates how a simple examination application can be developed, containerized, tested, deployed to AWS infrastructure, exposed through Kubernetes, scaled automatically, and monitored with Prometheus and Grafana.
The project is intentionally small at the application layer so the DevOps flow is easy to understand. The main value of this repository is the complete deployment pipeline and infrastructure setup:
GitHub -> GitHub Actions -> Jenkins -> Docker -> Amazon ECR -> Terraform -> AWS EKS -> Kubernetes -> Prometheus/Grafana
Vault is intentionally not included in this version. Secrets are represented with a Kubernetes Secret manifest for simplicity. In a production system, that Secret should be replaced with a proper secret manager such as AWS Secrets Manager, External Secrets Operator, or HashiCorp Vault.
- Project Overview
- What This Project Demonstrates
- Architecture
- Repository Structure
- Application Details
- Frontend Details
- Backend API Details
- Docker Setup
- Terraform Infrastructure
- Kubernetes Deployment
- CI/CD Pipeline
- Monitoring and Observability
- Local Development
- AWS Deployment Guide
- Validation and Troubleshooting
- Security Notes
- Current Limitations
- Future Improvements
The Online Examination Platform contains:
- A sample Node.js/Express backend API.
- A standalone static frontend exam portal.
- Docker configuration for local container testing.
- Terraform code to provision AWS infrastructure.
- Kubernetes manifests to run the backend on EKS.
- Jenkins pipeline for build, push, and deploy automation.
- GitHub Actions workflow for pull request and main branch validation.
- Prometheus and Grafana configuration for monitoring.
The backend currently exposes a small mock API for exams and submissions. The frontend in public/index.html is a browser-based examination portal with login, exam listing, timed quiz flow, question navigation, auto-submit behavior, and score display.
The AWS infrastructure provisions a realistic cloud foundation:
- VPC with public and private subnets.
- Internet Gateway and NAT Gateway.
- EKS cluster with managed worker nodes.
- ECR repository for Docker images.
- RDS PostgreSQL database.
- S3 bucket for static assets or exam files.
- IAM roles and policies required by EKS.
This project is useful as a DevOps end-semester project because it covers the major areas expected in a modern deployment workflow:
- Source control using GitHub.
- Automated CI checks using GitHub Actions.
- CI/CD pipeline using Jenkins.
- Containerization using Docker.
- Local multi-container testing using Docker Compose.
- Infrastructure as Code using Terraform.
- Cloud deployment on AWS.
- Kubernetes workload deployment on Amazon EKS.
- Service exposure using Kubernetes Service and Ingress.
- Horizontal Pod Autoscaling.
- Centralized metrics using Prometheus.
- Visualization using Grafana.
- Basic secret handling using Kubernetes Secrets.
- Health checks using liveness and readiness probes.
At a high level, the system is divided into four layers.
The application layer contains the online exam functionality.
public/index.htmlprovides the browser UI.app/server.jsprovides the backend API.- The backend is written with Express.js.
- Exam data is currently in memory for demonstration.
The backend is packaged as a Docker image.
app/Dockerfilebuilds the Node.js backend image.- The image runs as a non-root user.
- The container exposes port
3000. - A Docker health check calls
/healthz.
Terraform provisions the AWS resources.
terraform/vpc.tfcreates the networking layer.terraform/eks.tfcreates EKS, node groups, IAM roles, and ECR.terraform/rds_s3.tfcreates RDS PostgreSQL, an S3 bucket, and the RDS security group.terraform/outputs.tfprints useful values after deployment.
Kubernetes runs the backend workload.
k8s/deployment.yamlcreates backend pods.k8s/service.yamlexposes the pods inside the cluster.k8s/ingress.yamlexposes the service through an AWS Application Load Balancer.k8s/hpa.yamlenables autoscaling.monitoring/servicemonitor.yamlconfigures Prometheus scraping.
exam-platform/
├── app/
│ ├── Dockerfile
│ ├── package.json
│ └── server.js
├── public/
│ └── index.html
├── docker-compose.yml
├── terraform/
│ ├── main.tf
│ ├── variables.tf
│ ├── vpc.tf
│ ├── eks.tf
│ ├── rds_s3.tf
│ └── outputs.tf
├── k8s/
│ ├── namespace-and-secret.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── hpa.yaml
│ └── iam_policy.json
├── monitoring/
│ ├── monitoring-values.yaml
│ ├── servicemonitor.yaml
│ └── dashboards/
│ └── exam-backend-dashboard.json
├── .github/
│ └── workflows/
│ └── ci.yml
├── Jenkinsfile
└── README.md
| File | Purpose |
|---|---|
app/server.js |
Express backend API for exam listing, health checks, and submission |
public/index.html |
Standalone frontend examination portal |
app/Dockerfile |
Production Docker image definition for the backend |
docker-compose.yml |
Local container setup for backend and PostgreSQL |
terraform/main.tf |
Terraform provider and version configuration |
terraform/vpc.tf |
VPC, subnets, route tables, NAT Gateway, and Internet Gateway |
terraform/eks.tf |
EKS cluster, node group, IAM roles, and ECR repository |
terraform/rds_s3.tf |
RDS PostgreSQL database, S3 bucket, and RDS security group |
k8s/deployment.yaml |
Kubernetes backend Deployment |
k8s/service.yaml |
Internal ClusterIP service for the backend |
k8s/ingress.yaml |
ALB Ingress definition |
k8s/hpa.yaml |
Horizontal Pod Autoscaler |
monitoring/monitoring-values.yaml |
Helm values for kube-prometheus-stack |
monitoring/servicemonitor.yaml |
Prometheus Operator scrape configuration |
.github/workflows/ci.yml |
GitHub Actions CI workflow |
Jenkinsfile |
Jenkins build and deployment pipeline |
The application simulates an online exam platform for students. The frontend provides the exam-taking experience, while the backend provides simple API endpoints that can later be connected to a real database.
Main user flow:
- Student enters name and roll number.
- Student views available examinations.
- Student starts an exam.
- A timer begins.
- Student answers multiple-choice questions.
- Student can move between questions using navigation controls.
- Exam is submitted manually or automatically when time expires.
- Score and answer review are displayed.
The current project is best understood as a DevOps-ready prototype. It is not yet a full production learning management system.
The frontend is located at:
public/index.html
It is a standalone HTML, CSS, and JavaScript application. It does not currently require a frontend build tool such as React, Vite, Angular, or Webpack.
Frontend features:
- Student login form.
- Exam dashboard.
- Multiple exam cards.
- Timed quiz interface.
- Question palette.
- Answer selection.
- Previous and next navigation.
- Automatic submission when the timer reaches zero.
- Result page with score and correct answers.
Because the frontend is static, it can be opened directly in a browser:
open public/index.htmlOn Linux:
xdg-open public/index.htmlOn Windows PowerShell:
start public/index.htmlThe frontend currently uses mock exam data embedded inside the HTML file. It does not yet call the backend API.
The backend is located at:
app/server.js
It uses:
- Node.js
- Express.js
- JSON request parsing
- In-memory exam data
| Method | Path | Description |
|---|---|---|
GET |
/ |
Returns a basic API status message |
GET |
/healthz |
Liveness endpoint for Docker and Kubernetes |
GET |
/readyz |
Readiness endpoint for Kubernetes |
GET |
/api/exams |
Returns the sample list of exams |
POST |
/api/exams/:id/submit |
Accepts submitted answers for an exam |
Start the backend:
cd app
npm install
npm startCheck the API:
curl http://localhost:3000/Expected response:
{
"message": "Online Examination Platform API is running"
}Fetch exams:
curl http://localhost:3000/api/examsSubmit answers:
curl -X POST http://localhost:3000/api/exams/1/submit \
-H "Content-Type: application/json" \
-d '{"answers":{"1":"B","2":"C"}}'The backend Docker image is defined in:
app/Dockerfile
Important Docker features:
- Uses
node:20-alpine. - Installs only production dependencies.
- Creates and runs as a non-root user.
- Exposes port
3000. - Includes a container health check.
- Starts the app with
node server.js.
From the project root:
docker build -t exam-platform-backend:local ./appRun the image:
docker run --rm -p 3000:3000 exam-platform-backend:localTest it:
curl http://localhost:3000/healthzThe docker-compose.yml file runs:
exam-backend: the Express backend.postgres: a local PostgreSQL 16 container.
Start local containers:
docker compose up --buildAccess the backend:
http://localhost:3000
Stop containers:
docker compose downRemove containers and database volume:
docker compose down -vImportant note: PostgreSQL is started by Docker Compose, but the current backend code does not yet connect to PostgreSQL. The database container is included to show how the system will support persistent exam data in a future version.
Terraform files are stored in:
terraform/
main.tf configures:
- Terraform version
>= 1.5.0. - AWS provider
~> 5.0. - AWS region from
var.aws_region.
Default region:
ap-south-1
Key variables are defined in variables.tf.
| Variable | Default | Description |
|---|---|---|
aws_region |
ap-south-1 |
AWS region |
project_name |
exam-platform |
Prefix used for resource names |
vpc_cidr |
10.0.0.0/16 |
VPC CIDR range |
azs |
["ap-south-1a", "ap-south-1b"] |
Availability zones |
private_subnet_cidrs |
["10.0.1.0/24", "10.0.2.0/24"] |
Private subnet CIDRs |
public_subnet_cidrs |
["10.0.101.0/24", "10.0.102.0/24"] |
Public subnet CIDRs |
cluster_version |
1.33 |
Kubernetes version for EKS |
node_instance_type |
t3.micro |
EKS worker node instance type |
db_username |
examadmin |
RDS master username |
db_password |
none | RDS master password, required at apply time |
vpc.tf creates:
- One VPC.
- Two public subnets.
- Two private subnets.
- Internet Gateway.
- Elastic IP for NAT Gateway.
- NAT Gateway.
- Public route table.
- Private route table.
- Route table associations.
The public subnets are tagged for external load balancers:
kubernetes.io/role/elb = 1
The private subnets are tagged for internal load balancers:
kubernetes.io/role/internal-elb = 1
Both public and private subnets are tagged for the EKS cluster.
eks.tf creates:
- IAM role for the EKS control plane.
- EKS cluster.
- IAM role for worker nodes.
- Managed node group.
- ECR repository.
The node group runs in private subnets. This is a good default because worker nodes do not need direct public IP exposure.
rds_s3.tf creates:
- RDS security group.
- DB subnet group using private subnets.
- PostgreSQL RDS instance.
- S3 bucket.
- S3 public access block.
- S3 versioning.
The RDS instance is:
- PostgreSQL.
- Private, not publicly accessible.
- Placed in private subnets.
- Protected by a security group.
Current RDS settings are suitable for a demo or exam project, not for production:
skip_final_snapshot = truebackup_retention_period = 0multi_az = falsedb.t3.micro
After terraform apply, the following outputs are available:
| Output | Purpose |
|---|---|
eks_cluster_name |
Used by aws eks update-kubeconfig |
eks_cluster_endpoint |
EKS API endpoint |
rds_endpoint |
Database endpoint for app configuration |
s3_bucket_name |
Static asset or file bucket |
ecr_repository_url |
Docker image repository |
vpc_id |
VPC identifier |
Kubernetes manifests are stored in:
k8s/
namespace-and-secret.yaml creates:
- Namespace:
exam-platform - Secret:
exam-db-secret
The Deployment reads DB_HOST from this Secret:
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: exam-db-secret
key: hostFor a clean deployment, create or update the Secret using the RDS endpoint from Terraform:
kubectl create namespace exam-platform
kubectl create secret generic exam-db-secret \
--from-literal=host="<RDS_ENDPOINT>:5432" \
-n exam-platformIf the namespace or Secret already exists, use:
kubectl create secret generic exam-db-secret \
--from-literal=host="<RDS_ENDPOINT>:5432" \
-n exam-platform \
--dry-run=client -o yaml | kubectl apply -f -deployment.yaml creates the backend Deployment.
Important settings:
- Deployment name:
exam-backend - Namespace:
exam-platform - Replicas:
2 - Container port:
3000 - Image: ECR backend image
- Liveness probe:
/healthz - Readiness probe:
/readyz - CPU request:
100m - Memory request:
128Mi - CPU limit:
500m - Memory limit:
256Mi
The image is currently:
833082650522.dkr.ecr.ap-south-1.amazonaws.com/exam-platform-backend:latest
Jenkins updates the image tag during deployment using:
kubectl set image deployment/exam-backend exam-backend=<ECR_REPO>:<IMAGE_TAG> -n exam-platformservice.yaml creates a ClusterIP service:
- Service name:
exam-backend-svc - Service port:
80 - Target port:
3000 - Named port:
http
The named http port is important because monitoring/servicemonitor.yaml refers to it.
ingress.yaml creates an ALB-backed Ingress.
Important annotations:
kubernetes.io/ingress.class: "alb"
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ipBefore applying Ingress in a real environment, set a valid host:
rules:
- host: exams.example.comThe current file has an empty host field, so you should replace it with your real domain or remove the host field if you want hostless routing.
hpa.yaml creates a Horizontal Pod Autoscaler:
- Minimum replicas:
2 - Maximum replicas:
10 - CPU target:
70% - Memory target:
80%
The HPA requires metrics-server to be available in the cluster.
The project has two automation layers:
- GitHub Actions for basic CI validation.
- Jenkins for full build, push, and deployment.
Workflow file:
.github/workflows/ci.yml
It runs on:
- Pull requests to
main. - Pushes to
main.
Workflow steps:
- Checkout code.
- Set up Node.js 20.
- Install backend dependencies.
- Run backend tests.
- Build Docker image for validation.
Current test command:
npm testAt the moment, this command only prints:
No tests specified yet
and exits successfully. Real unit or integration tests should be added later.
Pipeline file:
Jenkinsfile
Pipeline stages:
CheckoutInstall & TestBuild Docker ImagePush to ECRDeploy to EKS
The Jenkins pipeline uses these environment values:
AWS_REGION = 'ap-south-1'
ECR_REPO = '833082650522.dkr.ecr.ap-south-1.amazonaws.com/exam-platform-backend'
IMAGE_TAG = "${env.BUILD_NUMBER}"
EKS_CLUSTER = 'exam-platform-eks'
K8S_NAMESPACE = 'exam-platform'The build number becomes the Docker image tag, which makes each deployment traceable.
Example image tags:
exam-platform-backend:14
exam-platform-backend:15
exam-platform-backend:latest
Jenkins pushes both:
- A build-specific tag.
- The
latesttag.
Then Jenkins updates the Kubernetes Deployment to the build-specific tag and waits for rollout completion.
Monitoring files are stored in:
monitoring/
The project uses the kube-prometheus-stack Helm chart, which installs:
- Prometheus
- Grafana
- Alertmanager
- Node exporters
- Kubernetes service monitors
- Default Kubernetes dashboards
monitoring-values.yaml configures:
- Prometheus retention:
15d - Prometheus CPU and memory resources
- Grafana enabled
- Grafana admin password
- Dashboard provider for exam platform dashboards
- Alertmanager enabled
Important security note: do not commit a real Grafana admin password in a production repository. Use Helm --set, an external secret, or a sealed secret.
servicemonitor.yaml tells Prometheus to scrape:
- Namespace:
exam-platform - Service label:
app=exam-backend - Port:
http - Path:
/metrics - Interval:
15s
Important note: the current Express backend does not yet expose /metrics. To make the ServiceMonitor fully useful, add a Prometheus metrics package such as prom-client and expose a /metrics endpoint from server.js.
The dashboard file is:
monitoring/dashboards/exam-backend-dashboard.json
It is intended to show backend-related metrics such as:
- CPU usage.
- Memory usage.
- Pod count.
- Request or service-level metrics once
/metricsis implemented.
Install:
- Node.js 20 or later
- npm
- Docker
- Docker Compose
- Terraform
- AWS CLI
- kubectl
- Helm
cd app
npm install
npm startBackend URL:
http://localhost:3000
Health check:
curl http://localhost:3000/healthzReadiness check:
curl http://localhost:3000/readyzFrom the project root:
docker compose up --buildBackend URL:
http://localhost:3000
PostgreSQL URL inside Docker network:
postgres:5432
The frontend is static:
open public/index.htmlOr serve it with any static server:
cd public
python3 -m http.server 8080Then open:
http://localhost:8080
This section describes the full AWS deployment flow.
aws configureUse an IAM user or role with permissions to manage:
- VPC
- EC2
- EKS
- IAM
- ECR
- RDS
- S3
- CloudWatch
- Elastic Load Balancing
cd terraform
terraform init
terraform fmt
terraform validate
terraform plan -var="db_password=<strong-password>"
terraform apply -var="db_password=<strong-password>"Save these outputs:
terraform outputYou will need:
ecr_repository_urleks_cluster_namerds_endpoint
aws eks update-kubeconfig \
--region ap-south-1 \
--name exam-platform-eksVerify:
kubectl get nodesUse the ECR repository URL from Terraform output.
aws ecr get-login-password --region ap-south-1 | \
docker login --username AWS --password-stdin <ECR_REPO_URL>
docker build -t <ECR_REPO_URL>:latest ./app
docker push <ECR_REPO_URL>:latestkubectl create namespace exam-platform
kubectl create secret generic exam-db-secret \
--from-literal=host="<RDS_ENDPOINT>:5432" \
-n exam-platformkubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/hpa.yamlApply Ingress after installing the AWS Load Balancer Controller and setting a valid host:
kubectl apply -f k8s/ingress.yamlkubectl get pods -n exam-platform
kubectl get svc -n exam-platform
kubectl get deploy -n exam-platform
kubectl get hpa -n exam-platform
kubectl get ingress -n exam-platformCheck rollout:
kubectl rollout status deployment/exam-backend -n exam-platformCheck logs:
kubectl logs -n exam-platform deployment/exam-backendPort-forward locally:
kubectl port-forward -n exam-platform svc/exam-backend-svc 3000:80Then open:
http://localhost:3000
The Ingress uses AWS ALB annotations. For it to work, the AWS Load Balancer Controller must be installed in the EKS cluster.
The file k8s/iam_policy.json contains an IAM policy suitable for the controller. The usual setup flow is:
- Create IAM policy from
k8s/iam_policy.json. - Associate IAM OIDC provider with the EKS cluster.
- Create a Kubernetes service account with the IAM role.
- Install AWS Load Balancer Controller using Helm.
- Apply the Ingress manifest.
Without the controller, k8s/ingress.yaml will not create an AWS Application Load Balancer.
Add the Helm repository:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo updateInstall kube-prometheus-stack:
helm install monitoring prometheus-community/kube-prometheus-stack \
-n monitoring \
--create-namespace \
-f monitoring/monitoring-values.yamlApply ServiceMonitor:
kubectl apply -f monitoring/servicemonitor.yamlAccess Grafana:
kubectl port-forward -n monitoring svc/monitoring-grafana 3001:80Open:
http://localhost:3001
Default username:
admin
The password is currently configured in monitoring/monitoring-values.yaml. For real deployments, override it securely instead of committing it.
ruby -e 'require "yaml"; ARGV.each { |f| YAML.load_stream(File.read(f)); puts "OK #{f}" }' k8s/*.yamlOr use kubectl dry-run after your kubeconfig is valid:
kubectl apply --dry-run=client -f k8s/cd terraform
terraform fmt -checkAuto-format:
terraform fmtValidate Terraform:
terraform validateIf the AWS provider plugin is corrupted or fails to start, reinitialize providers:
terraform init -upgradenode --check app/server.jskubectl get all -n exam-platform
kubectl describe pod -n exam-platform <pod-name>
kubectl logs -n exam-platform <pod-name>
kubectl describe deployment -n exam-platform exam-backend
kubectl describe ingress -n exam-platform exam-backend-ingress| Issue | Likely Cause | Fix |
|---|---|---|
Pods stuck in ImagePullBackOff |
ECR image missing or node cannot pull image | Check image URL, ECR permissions, and pushed tags |
| Pods fail readiness probe | /readyz endpoint unavailable or app crashed |
Check pod logs |
| Ingress does not get address | AWS Load Balancer Controller missing | Install controller and IAM policy |
| HPA shows unknown metrics | metrics-server missing | Install metrics-server |
| Prometheus cannot scrape backend | /metrics endpoint missing |
Add prom-client metrics endpoint |
| Terraform validate fails due provider plugin | Corrupted/incompatible provider cache | Run terraform init -upgrade |
| RDS cannot be reached | Security group or subnet routing issue | Check RDS SG, VPC CIDR, private subnets, and DNS |
This repository is a learning project, but it still includes several good security practices:
- Backend container runs as a non-root user.
- RDS is private and not publicly accessible.
- EKS worker nodes run in private subnets.
- S3 public access is blocked.
- Kubernetes Secret is used instead of plain environment values in Deployment.
- Docker image is built from a small Alpine base image.
Important production improvements:
- Do not commit real passwords or sensitive endpoints.
- Do not hardcode account-specific ECR URLs in shared templates.
- Use AWS Secrets Manager, External Secrets Operator, or Vault for secrets.
- Enable RDS backups.
- Consider Multi-AZ RDS.
- Use least-privilege IAM policies.
- Enable remote Terraform state with locking.
- Add TLS to the Ingress.
- Use a real domain and ACM certificate.
- Add authentication and authorization to the backend.
- Add input validation and request rate limiting.
The project is intentionally simplified. Current limitations include:
- Backend exam data is in memory.
- Backend does not yet connect to RDS.
- Frontend does not yet call backend APIs.
- No real student authentication.
- No admin panel for creating exams.
- No persistent result storage.
- No Prometheus
/metricsendpoint yet. - No real test suite yet.
- Ingress host must be configured before real deployment.
- Grafana password should be externalized.
- Terraform state is local unless backend configuration is enabled.
These limitations are good future enhancement points and can be discussed during a project presentation as planned next steps.
Recommended next improvements:
- Connect backend to PostgreSQL.
- Create database schema for students, exams, questions, attempts, and results.
- Replace frontend mock data with API calls.
- Add JWT-based student authentication.
- Add admin APIs for exam creation and question management.
- Add unit tests and integration tests.
- Add
/metricsendpoint usingprom-client. - Add centralized logging using CloudWatch, Loki, or ELK.
- Configure TLS on ALB Ingress using ACM.
- Store Terraform state in S3 with DynamoDB locking.
- Use External Secrets Operator or AWS Secrets Manager for credentials.
- Add blue-green or canary deployment strategy.
- Add database migrations.
- Add backup and disaster recovery policy.
- Add cost optimization notes for AWS resources.
Run backend locally:
cd app
npm install
npm startRun with Docker Compose:
docker compose up --buildBuild backend Docker image:
docker build -t exam-platform-backend:local ./appProvision AWS:
cd terraform
terraform init
terraform apply -var="db_password=<strong-password>"Connect to EKS:
aws eks update-kubeconfig --region ap-south-1 --name exam-platform-eksDeploy to Kubernetes:
kubectl apply -f k8s/Check backend pods:
kubectl get pods -n exam-platformInstall monitoring:
helm install monitoring prometheus-community/kube-prometheus-stack \
-n monitoring \
--create-namespace \
-f monitoring/monitoring-values.yamlAccess Grafana:
kubectl port-forward -n monitoring svc/monitoring-grafana 3001:80This Online Examination Platform is a complete DevOps project blueprint. It combines a sample exam application with a practical deployment workflow covering source control, CI, containerization, infrastructure provisioning, Kubernetes orchestration, autoscaling, and monitoring.
The current version is suitable for demonstrating DevOps concepts end to end. With database integration, API-backed frontend data, real authentication, metrics instrumentation, and production-grade secret management, it can evolve into a more complete examination platform.