Day 63: Kubernetes Multi-Tier Application - Iron Gallery Deployment ๐จ

Welcome back! ๐ Day 63 of the 100 Days Cloud DevOps Challenge, and today we're deploying a complete multi-tier application on Kubernetes! This is real production architecture - frontend, database, services, and proper resource management. Let's architect! ๐ฏ
๐ฏ The Mission - Deploy Iron Gallery Application
๐ TASK TICKET #DEV-8063 - Multi-Tier App Deployment
Priority: HIGH | Type: Application Architecture
Server: Jump Host | Cluster: Kubernetes
APPLICATION:
- Iron Gallery (Image gallery application)
- Two-tier architecture: Frontend + Database
- Custom namespace deployment
- Service exposure configuration
IMPLEMENTATION:
1. Namespace: iron-namespace-nautilus
2. Frontend Deployment: iron-gallery-deployment-nautilus
3. Database Deployment: iron-db-deployment-nautilus
4. Database Service: iron-db-service-nautilus (ClusterIP)
5. Frontend Service: iron-gallery-service-nautilus (NodePort:32678)
SUCCESS CRITERIA:
- All resources in correct namespace
- Both deployments running
- Services properly configured
- Application accessible via NodePort
- Installation page visible
This is production multi-tier architecture! ๐๏ธ
๐ ๏ธ Complete Implementation
Step 1: Create Complete Deployment Manifests
# SSH to jump host
ssh user@jump_host
# Create all-in-one manifest file
cat > iron-gallery-complete.yaml << 'EOF'
---
# Namespace
apiVersion: v1
kind: Namespace
metadata:
name: iron-namespace-nautilus
---
# Frontend Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: iron-gallery-deployment-nautilus
namespace: iron-namespace-nautilus
labels:
run: iron-gallery
spec:
replicas: 1
selector:
matchLabels:
run: iron-gallery
template:
metadata:
labels:
run: iron-gallery
spec:
containers:
- name: iron-gallery-container-nautilus
image: kodekloud/irongallery:2.0
resources:
limits:
memory: "100Mi"
cpu: "50m"
volumeMounts:
- name: config
mountPath: /usr/share/nginx/html/data
- name: images
mountPath: /usr/share/nginx/html/uploads
volumes:
- name: config
emptyDir: {}
- name: images
emptyDir: {}
---
# Database Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: iron-db-deployment-nautilus
namespace: iron-namespace-nautilus
labels:
db: mariadb
spec:
replicas: 1
selector:
matchLabels:
db: mariadb
template:
metadata:
labels:
db: mariadb
spec:
containers:
- name: iron-db-container-nautilus
image: kodekloud/irondb:2.0
env:
- name: MYSQL_DATABASE
value: "database_host"
- name: MYSQL_ROOT_PASSWORD
value: "R00tP@ssw0rd123!"
- name: MYSQL_PASSWORD
value: "Us3rP@ssw0rd456!"
- name: MYSQL_USER
value: "iron_user"
volumeMounts:
- name: db
mountPath: /var/lib/mysql
volumes:
- name: db
emptyDir: {}
---
# Database Service (ClusterIP)
apiVersion: v1
kind: Service
metadata:
name: iron-db-service-nautilus
namespace: iron-namespace-nautilus
spec:
type: ClusterIP
selector:
db: mariadb
ports:
- protocol: TCP
port: 3306
targetPort: 3306
---
# Frontend Service (NodePort)
apiVersion: v1
kind: Service
metadata:
name: iron-gallery-service-nautilus
namespace: iron-namespace-nautilus
spec:
type: NodePort
selector:
run: iron-gallery
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 32678
EOF
Step 2: Deploy All Resources
# Apply the complete manifest
kubectl apply -f iron-gallery-complete.yaml
Expected output:
namespace/iron-namespace-nautilus created
deployment.apps/iron-gallery-deployment-nautilus created
deployment.apps/iron-db-deployment-nautilus created
service/iron-db-service-nautilus created
service/iron-gallery-service-nautilus created
All resources created! โ
Step 3: Verify Namespace
# Check namespace
kubectl get namespace iron-namespace-nautilus
Expected output:
NAME STATUS AGE
iron-namespace-nautilus Active 10s
Step 4: Verify Deployments
# Check all deployments in namespace
kubectl get deployments -n iron-namespace-nautilus
Expected output:
NAME READY UP-TO-DATE AVAILABLE AGE
iron-gallery-deployment-nautilus 1/1 1 1 30s
iron-db-deployment-nautilus 1/1 1 1 30s
Both deployments ready! โ
# Check pods
kubectl get pods -n iron-namespace-nautilus
Expected output:
NAME READY STATUS RESTARTS AGE
iron-gallery-deployment-nautilus-7d8f9c6b5-abc12 1/1 Running 0 45s
iron-db-deployment-nautilus-5c7d8e9f6-xyz89 1/1 Running 0 45s
Step 5: Verify Services
# Check services
kubectl get services -n iron-namespace-nautilus
Expected output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
iron-db-service-nautilus ClusterIP 10.96.123.45 <none> 3306/TCP 1m
iron-gallery-service-nautilus NodePort 10.96.234.56 <none> 80:32678/TCP 1m
Services configured correctly! โ
Step 6: Verify Resource Limits
# Check frontend container resources
kubectl get deployment iron-gallery-deployment-nautilus -n iron-namespace-nautilus -o jsonpath='{.spec.template.spec.containers[0].resources}'
Expected output:
{"limits":{"cpu":"50m","memory":"100Mi"}}
Step 7: Test Application Access
# Get node IP
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
echo "Access URL: http://$NODE_IP:32678"
# Test frontend
curl -I http://$NODE_IP:32678
Expected output:
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html
Application accessible! ๐
Step 8: Complete Verification
# Comprehensive verification script
cat > verify-iron-gallery.sh << 'EOF'
#!/bin/bash
NS="iron-namespace-nautilus"
echo "=== Iron Gallery Multi-Tier Application Verification ==="
echo ""
echo "1. Namespace Status:"
kubectl get namespace $NS
echo ""
echo "2. Deployments Status:"
kubectl get deployments -n $NS
echo ""
echo "3. Pods Status:"
kubectl get pods -n $NS -o wide
echo ""
echo "4. Services Status:"
kubectl get services -n $NS
echo ""
echo "5. Frontend Deployment Details:"
echo " Labels:"
kubectl get deployment iron-gallery-deployment-nautilus -n $NS -o jsonpath=' run={.metadata.labels.run}'
echo ""
echo " Replicas: $(kubectl get deployment iron-gallery-deployment-nautilus -n $NS -o jsonpath='{.spec.replicas}')"
echo " Image: $(kubectl get deployment iron-gallery-deployment-nautilus -n $NS -o jsonpath='{.spec.template.spec.containers[0].image}')"
echo " Resources: $(kubectl get deployment iron-gallery-deployment-nautilus -n $NS -o jsonpath='{.spec.template.spec.containers[0].resources}')"
echo ""
echo "6. Frontend Volume Mounts:"
kubectl get deployment iron-gallery-deployment-nautilus -n $NS -o jsonpath='{range .spec.template.spec.containers[0].volumeMounts[*]} - {.name}: {.mountPath}{"\n"}{end}'
echo ""
echo "7. Database Deployment Details:"
echo " Labels:"
kubectl get deployment iron-db-deployment-nautilus -n $NS -o jsonpath=' db={.metadata.labels.db}'
echo ""
echo " Image: $(kubectl get deployment iron-db-deployment-nautilus -n $NS -o jsonpath='{.spec.template.spec.containers[0].image}')"
echo " Environment Variables:"
kubectl get deployment iron-db-deployment-nautilus -n $NS -o jsonpath='{range .spec.template.spec.containers[0].env[*]} - {.name}={.value}{"\n"}{end}'
echo ""
echo "8. Database Service Details:"
echo " Type: $(kubectl get service iron-db-service-nautilus -n $NS -o jsonpath='{.spec.type}')"
echo " Port: $(kubectl get service iron-db-service-nautilus -n $NS -o jsonpath='{.spec.ports[0].port}')"
echo " Selector: $(kubectl get service iron-db-service-nautilus -n $NS -o jsonpath='{.spec.selector}')"
echo ""
echo "9. Frontend Service Details:"
echo " Type: $(kubectl get service iron-gallery-service-nautilus -n $NS -o jsonpath='{.spec.type}')"
echo " NodePort: $(kubectl get service iron-gallery-service-nautilus -n $NS -o jsonpath='{.spec.ports[0].nodePort}')"
echo ""
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
echo "10. Access URL: http://$NODE_IP:32678"
echo ""
echo "11. Application Health Check:"
if curl -s -o /dev/null -w "%{http_code}" http://$NODE_IP:32678 | grep -q "200"; then
echo " โ Frontend accessible and responding"
else
echo " โ Frontend not responding"
fi
echo ""
echo "12. Pod Resource Usage:"
kubectl top pods -n $NS 2>/dev/null || echo " Metrics server not available"
echo ""
echo "โ VERIFICATION COMPLETE"
EOF
chmod +x verify-iron-gallery.sh
./verify-iron-gallery.sh
Expected output:
=== Iron Gallery Multi-Tier Application Verification ===
1. Namespace Status:
NAME STATUS AGE
iron-namespace-nautilus Active 5m
2. Deployments Status:
NAME READY UP-TO-DATE AVAILABLE AGE
iron-gallery-deployment-nautilus 1/1 1 1 5m
iron-db-deployment-nautilus 1/1 1 1 5m
3. Pods Status:
NAME READY STATUS RESTARTS AGE IP NODE
iron-gallery-deployment-nautilus-7d8f9c6b5-abc12 1/1 Running 0 5m 10.244.1.5 node01
iron-db-deployment-nautilus-5c7d8e9f6-xyz89 1/1 Running 0 5m 10.244.1.6 node01
4. Services Status:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
iron-db-service-nautilus ClusterIP 10.96.123.45 <none> 3306/TCP 5m
iron-gallery-service-nautilus NodePort 10.96.234.56 <none> 80:32678/TCP 5m
5. Frontend Deployment Details:
Labels:
run=iron-gallery
Replicas: 1
Image: kodekloud/irongallery:2.0
Resources: {"limits":{"cpu":"50m","memory":"100Mi"}}
6. Frontend Volume Mounts:
- config: /usr/share/nginx/html/data
- images: /usr/share/nginx/html/uploads
7. Database Deployment Details:
Labels:
db=mariadb
Image: kodekloud/irondb:2.0
Environment Variables:
- MYSQL_DATABASE=database_host
- MYSQL_ROOT_PASSWORD=R00tP@ssw0rd123!
- MYSQL_PASSWORD=Us3rP@ssw0rd456!
- MYSQL_USER=iron_user
8. Database Service Details:
Type: ClusterIP
Port: 3306
Selector: {"db":"mariadb"}
9. Frontend Service Details:
Type: NodePort
NodePort: 32678
10. Access URL: http://172.17.0.2:32678
11. Application Health Check:
โ Frontend accessible and responding
12. Pod Resource Usage:
NAME CPU(cores) MEMORY(bytes)
iron-gallery-deployment-nautilus-7d8f9c6b5-abc12 5m 45Mi
iron-db-deployment-nautilus-5c7d8e9f6-xyz89 25m 180Mi
โ VERIFICATION COMPLETE
All components verified! ๐
๐ Understanding Multi-Tier Architecture
Application Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ External Users (Browser) โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ http://NodeIP:32678
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NodePort Service (32678) โ
โ iron-gallery-service-nautilus โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Port 80
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend Pod โ
โ iron-gallery-deployment-nautilus โ
โ - Nginx web server โ
โ - Image gallery application โ
โ - Volumes: config, images โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Port 3306
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ClusterIP Service (3306) โ
โ iron-db-service-nautilus โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Internal only
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Database Pod โ
โ iron-db-deployment-nautilus โ
โ - MariaDB database โ
โ - Volume: db โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Component Breakdown
1. Namespace:
apiVersion: v1
kind: Namespace
metadata:
name: iron-namespace-nautilus
Purpose:
- Isolates application resources
- Logical grouping
- Resource quotas possible
- Access control boundary
Benefits:
โ Clean separation
โ Multi-tenancy support
โ Resource organization
โ Security isolation
2. Frontend Deployment:
spec:
replicas: 1
selector:
matchLabels:
run: iron-gallery
template:
spec:
containers:
- name: iron-gallery-container-nautilus
image: kodekloud/irongallery:2.0
resources:
limits:
memory: "100Mi"
cpu: "50m"
Components:
- Web server (Nginx)
- PHP application
- Image gallery frontend
- Static file serving
Resource limits:
- Memory: 100Mi (prevents OOM)
- CPU: 50m (0.05 core)
- Protects cluster resources
3. Database Deployment:
env:
- name: MYSQL_DATABASE
value: "database_host"
- name: MYSQL_ROOT_PASSWORD
value: "R00tP@ssw0rd123!"
- name: MYSQL_PASSWORD
value: "Us3rP@ssw0rd456!"
- name: MYSQL_USER
value: "iron_user"
MariaDB Configuration:
- Database: database_host
- Root password: (complex)
- User: iron_user
- User password: (complex)
Storage:
- emptyDir at /var/lib/mysql
- Temporary (pod lifetime)
- Production: Use PersistentVolume
4. Services:
Database Service (ClusterIP):
- Internal cluster access only
- No external exposure
- Port 3306 (MySQL default)
- Frontend connects via DNS
Frontend Service (NodePort):
- External access
- Port 32678 on all nodes
- Routes to frontend pods
- Users access application
Volume Strategy
emptyDir volumes used:
volumes:
- name: config
emptyDir: {}
- name: images
emptyDir: {}
- name: db
emptyDir: {}
Why emptyDir:
- Quick setup (no PV needed)
- Development/testing
- Installation phase
- Data not critical yet
emptyDir characteristics:
- Created when pod assigned to node
- Deleted when pod removed
- Shared between containers in pod
- Backed by node disk
Production recommendation:
- Use PersistentVolume for db
- Use PersistentVolume for images
- Keep config as emptyDir (generated)
Service Types Explained
ClusterIP (Database):
Access: Internal only
DNS: iron-db-service-nautilus.iron-namespace-nautilus.svc.cluster.local
Port: 3306
Use case:
- Backend services
- Database access
- Internal APIs
- Not exposed externally
Security benefit:
โ No external access
โ Network isolation
โ Protected by firewall
NodePort (Frontend):
Access: External via node IP
URL: http://NodeIP:32678
Port mapping: 32678 (external) โ 80 (internal)
Use case:
- Development environments
- Testing
- Simple external access
- No load balancer available
Limitations:
- Fixed port on all nodes
- Direct node access required
- Not production standard
๐ก Key Takeaways
โจ Namespaces organize and isolate resources
โจ Labels connect deployments to services
โจ Resource limits prevent resource exhaustion
โจ emptyDir provides temporary storage
โจ ClusterIP for internal service communication
โจ NodePort for external application access
โจ Environment variables configure applications
โจ Multi-tier architecture separates concerns
๐ Quick Interview Questions
Q: Why use separate deployments for frontend and database? A: Separation of concerns: independent scaling, different resource requirements, separate lifecycle management, easier updates. Database can scale independently from frontend. Different restart policies and update strategies.
Q: What's the difference between ClusterIP and NodePort services? A: ClusterIP: Internal cluster access only, no external exposure, uses cluster IP. NodePort: External access via node IP and port (30000-32767), includes ClusterIP functionality. NodePort = ClusterIP + external port.
Q: Why use emptyDir instead of PersistentVolume? A: emptyDir: Simpler setup, no external dependencies, good for temporary data, development/testing. PersistentVolume: Data persistence across pod restarts, production workloads, critical data. This task uses emptyDir for initial setup phase.
Q: How do frontend and database pods communicate? A: Via service DNS: iron-db-service-nautilus.iron-namespace-nautilus.svc.cluster.local:3306. Service provides stable endpoint. Even if database pod IP changes, service DNS remains constant. Frontend uses service name, not direct pod IP.
Q: What happens if you don't specify resource limits? A: Pod can consume unlimited node resources, causing: (1) Node resource exhaustion, (2) Other pods starved, (3) Node crashes, (4) Unpredictable performance. Always set limits in production to protect cluster.
Q: Why use labels and selectors? A: Labels: Identify and organize resources. Selectors: Query resources by labels. Services use selectors to find target pods. Deployments use selectors to manage pods. Enables loose coupling - change pod template without changing service.
Q: Can you access NodePort service from inside cluster? A: Yes, three ways: (1) NodeIP:NodePort from any pod, (2) ServiceName:Port using ClusterIP (preferred), (3) ClusterIP:Port directly. NodePort includes ClusterIP, so internal access works normally.
Q: What's matchLabels in deployment spec? A: Selector that defines which pods the deployment manages. Must match template.metadata.labels. Deployment creates/deletes pods based on this matching. If labels don't match, deployment won't manage the pods.
Q: Why specify exact image tags (not latest)? A: Reproducibility: exact version known, predictable behavior, easier rollback. "latest" is unpredictable: changes without warning, makes troubleshooting difficult, inconsistent across nodes. Production standard: always use specific tags.
Q: What's the purpose of environment variables in database deployment? A: Configure MariaDB at startup: create database, set passwords, create users. Alternative to manual configuration. Environment-based config enables: same image, different configs, easy customization, no image rebuild.
๐ Final Thoughts
You've successfully deployed a complete multi-tier application on Kubernetes! This is production architecture.
What you accomplished: โ
Created isolated namespace
โ
Deployed frontend application
โ
Deployed database backend
โ
Configured internal service (ClusterIP)
โ
Exposed external service (NodePort)
โ
Set resource limits
โ
Configured persistent storage
โ
Established service communication
Real-world impact:
Scalable architecture (independent tier scaling)
Resource efficiency (limits prevent waste)
Security (internal services isolated)
Maintainability (clean separation)
Production-ready (proper service exposure)
Skills acquired:
Multi-tier application deployment
Namespace management
Service type selection
Resource limit configuration
Volume mounting
Label-based routing
Environment variable injection
This is production-grade architecture! ๐ช
Day: 63/100
Challenge: KodeKloud Cloud DevOps
Date: January 07, 2026
Topic: Kubernetes Multi-Tier Application
How do you structure your multi-tier apps? Share your architecture patterns! ๐จ




