Designed and deployed a multi-service architecture on AWS EKS with independent service scaling, zero-downtime rolling updates, automated DNS management via Route53, and transactional email delivery through SES — validating three distinct deployment strategies with full rollback capability.
Internet
│
▼
ALB Ingress (HTTPS, SSL termination, HTTP→HTTPS redirect)
│ ← External DNS auto-creates Route53 A records on deploy
▼
User Management Service (:8095, NodePort)
│ │
│ ClusterIP DNS │ ExternalName DNS alias
▼ ▼
Notification Service RDS MySQL
(:8096, ClusterIP) (ExternalName service)
│
│ ExternalName DNS alias
▼
AWS SES SMTP
Why ClusterIP for the Notification service? It has no legitimate reason to be reachable externally. ClusterIP enforces this at the network layer, reduces attack surface, and avoids unnecessary load balancer cost.
Why ExternalName for SMTP and RDS? Application code references stable in-cluster DNS names (smtp-service, mysql). Swapping endpoints — region failover, DB migration — is a one-line manifest change with no redeployment.
Why one ALB Ingress instead of per-service LoadBalancers? A single ALB means one billing unit, centralised SSL, and HTTP→HTTPS redirect handled at the edge. External DNS automates Route53 record creation on every deploy.
| Layer | Technology |
|---|---|
| Container orchestration | Amazon EKS (Kubernetes 1.28) |
| Networking | ALB Ingress Controller, ClusterIP, NodePort, ExternalName |
| DNS automation | External DNS + Route53 |
| Database | AWS RDS (MySQL 8), bridged via ExternalName service |
| Email delivery | AWS SES SMTP, credentials in Kubernetes Secrets |
| IAM | Scoped SMTP credentials, least-privilege policy |
| Containers | Docker (images on Docker Hub) |
| Validation | kubectl, Postman |
- EKS cluster running with managed node group
- ALB Ingress Controller installed (
aws-load-balancer-controller) - External DNS installed with Route53 write permissions
- AWS RDS instance (MySQL) in the same VPC
- AWS SES SMTP credentials generated, sender email verified
- ACM certificate issued for your domain
Verify controllers are running before deploying:
# ALB Ingress Controller
kubectl get pods -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller
# External DNS
kubectl get pods -n kube-system -l app=external-dns1. Configure secrets
Do not edit 00-smtp-secret.yml directly. Create the secret from the command line instead:
kubectl create secret generic smtp-credentials \
--from-literal=username=YOUR_SES_SMTP_USERNAME \
--from-literal=password=YOUR_SES_SMTP_PASSWORD \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic mysql-db-password \
--from-literal=db_password=YOUR_RDS_PASSWORD \
--dry-run=client -o yaml | kubectl apply -f -2. Update placeholders
| File | Field | Replace with |
|---|---|---|
01-mysql-externalname-service.yml |
externalName |
Your RDS endpoint |
07-alb-ingress.yml |
certificate-arn |
Your ACM cert ARN |
07-alb-ingress.yml |
external-dns.alpha.kubernetes.io/hostname |
Your domain |
05-notification-deployment.yml |
AWS_MAIL_SERVER_FROM_ADDRESS |
Verified SES sender |
3. Deploy
chmod +x scripts/deploy.sh
./scripts/deploy.sh deployOr apply directly:
kubectl apply -f kube-manifests/v1/
kubectl rollout status deployment/usermgmt-microservice
kubectl rollout status deployment/notification-microserviceThree strategies were validated with rollback confirmation. All produce zero downtime (maxUnavailable: 0 in the rolling update policy).
Fastest path for hotfixes. No manifest change needed.
kubectl set image deployment/notification-microservice \
notification-service=stacksimplify/kube-notifications-microservice:2.0.0
kubectl rollout status deployment/notification-microservice
kubectl rollout history deployment/notification-microservice
# Rollback
kubectl rollout undo deployment/notification-microserviceUseful when reviewing the full spec in context before committing.
kubectl edit deployment/notification-microservice
# Update: image: stacksimplify/kube-notifications-microservice:2.0.0
# Save and exit — Kubernetes triggers the rolling update immediately
kubectl rollout status deployment/notification-microservice
# Rollback
kubectl rollout undo deployment/notification-microserviceThe GitOps-aligned approach. Idempotent, auditable, and the natural precursor to ArgoCD or Flux.
# Update image tag in kube-manifests/v2/05-notification-deployment.yml, then:
kubectl apply -f kube-manifests/v2/
kubectl rollout status deployment/notification-microservice
# Rollback
kubectl rollout undo deployment/notification-microservice| Approach | When |
|---|---|
kubectl create secret (current) |
Development and demo environments |
| Kubernetes Secrets + RBAC | Multi-team clusters |
| AWS Secrets Manager + External Secrets Operator | Production |
| IRSA + Secrets Manager + envelope encryption | Production (hardened) |
The manifests in this repo use Kubernetes Secrets sourced from secretKeyRef — credentials never appear as literal values in YAML. The .gitignore blocks any *-secret.yml files from being committed.
# Service health
curl https://services.yourdomain.com/usermgmt/health-status
curl https://services.yourdomain.com/usermgmt/notification-health-status
# Pod logs
kubectl logs -f -l app=usermgmt-restapp
kubectl logs -f -l app=notification-restapp
# Full status
./scripts/deploy.sh statusImport docs/postman-collection.json into Postman to run the full create-user → verify-email flow.
Synchronous notification coupling — User creation calls Notification Service synchronously. If the notification service is unavailable, user creation degrades. Production fix: decouple with SQS. User creation publishes an event; Notification Service consumes and retries asynchronously.
Single replica per service — Manifests run one replica for demo purposes. Production would set replicas: 2+ per service with a PodDisruptionBudget to guarantee availability during node maintenance.
No HPA — Horizontal Pod Autoscaler is not configured. A real workload would define CPU/memory thresholds and min/max replica bounds.
CI/CD — The declarative apply strategy is the natural entry point for GitOps. Next step: GitHub Actions pipeline that lints manifests with kubeval, runs kubectl diff, and applies on merge to main via ArgoCD.
./scripts/deploy.sh teardown
# or
kubectl delete -f kube-manifests/v1/Route53 records created by External DNS are cleaned up automatically when the Ingress resource is deleted.