I built ShopSwift, a Node.js/Express e-commerce API, and wrapped it in a production-grade local validation, CI, , and takes a different approach. Two environments run in parallel. One is live. The other is where the new release lands. Traffic switches only after validation. Rollback is a routing change, not a redeployment.
The question I wanted to answer with this project was practical:
Can I build a blue-green pipeline that delivers genuinely zero failed requests through a traffic switch, a rollback, and a simulated broken release - locally and in the cloud?
The answer is yes. But the path had a real failure in it. That failure made the project better.
Repository: and , ,
Local Kubernetes
Container Registry
CI/CD
Manifest Validation
, ,
Architecture: The Final Routing Model
After testing and refinement (including one important failure), the traffic routing model settled into this:
CODE
User request
|
AWS Load Balancer (cloud) or kubectl port-forward (local)
|
NGINX Ingress Controller
|
shopswift-ingress
|
shopswift-active-service
|
selector: environment=blue OR environment=green
|
Blue pods Green pods
The key design principle: NGINX Ingress never changes. The only thing that changes during a switch is the label selector on shopswift-active-service.
This distinction matters - and it came from a real failure. More on that below.
Phase 1: The Application
I started with the Express API and immediately wrote tests using before touching Docker or Kubernetes.
CODE
Test Suites: 1 passed
Tests: 7 passed
This was deliberate. Kubernetes deployment should not begin with an untested application. The health, readiness, and version endpoints needed to be correct before any of the deployment logic could trust them.
Phase 2: Dockerizing ShopSwift
The Docker image was designed to support both Blue and Green from the same codebase using environment variables:
CODE
# Blue container
docker run -eAPP_VERSION=v1.0.0 -eAPP_ENV=blue shopswift:v1.0.0
# Green container
docker run -eAPP_VERSION=v2.0.0 -eAPP_ENV=green shopswift:v2.0.0
No separate codebases. No duplicated Dockerfiles. One image, configured at runtime.
I also wrote smoke test scripts to validate all endpoints quickly after each build - a habit that paid dividends throughout the project.
Challenge: npm ci Caught a Lockfile Mismatch
The Docker build failed at:
CODE
RUN npm ci --omit=dev
The cause: package-lock.json was out of sync with package.json.
Running Minikube with the Docker driver inside WSL meant that accessing shopswift.local directly was unreliable - a known networking limitation of this environment. The app and Service were fine; the issue was local DNS and networking.
The solution was to port-forward the NGINX Ingress Controller and pass the correct Host header:
This still exercised the full NGINX Ingress routing path - just without relying on local DNS resolution. It was the right tradeoff for a local validation environment.
Before switching traffic, I tested Green internally through its own Service. This is non-negotiable in a proper blue-green workflow - Green pods running does not mean Green is ready to serve users.
Both environments were now running. Time to switch traffic.
The Failure That Made This Project Better
My first switching approach was to patch the Ingress backend directly:
CODE
# Before backend: service: name:shopswift-blue-service
# After patching backend: service: name:shopswift-green-service
Logical. Clean-looking. But during a continuous zero-downtime test:
CODE
FAILED request: status=503
Failed requests: 1 of 26
A single 503 during a traffic switch means the design cannot honestly be called zero-downtime. I did not hide this result. I used it to understand what was happening.
The likely cause: when the Ingress backend is patched, NGINX reloads its configuration. During that reload - even briefly - upstream connections can fail. One request landed in that gap.
The Fix: The Stable Active Service Pattern
Instead of touching the Ingress, I introduced a stable intermediary:
CODE
# shopswift-active-service - this never changes in Ingress apiVersion:v1 kind:Service metadata: name:shopswift-active-service spec: selector: app:shopswift environment:blue# <-- only this changes during a switch
The Ingress always points to shopswift-active-service. To switch traffic, I only patch the selector:
CODE
# Switch to Green
kubectl patch service shopswift-active-service \ -n ecommerce-bluegreen \ --type='merge'\ -p'{"spec":{"selector":{"app":"shopswift","environment":"green"}}}'
# Roll back to Blue
kubectl patch service shopswift-active-service \ -n ecommerce-bluegreen \ --type='merge'\ -p'{"spec":{"selector":{"app":"shopswift","environment":"blue"}}}'
detected this and never admitted the broken pods to the Service's endpoint pool. The rollout timed out. Traffic never left Blue.
Live /version response during the broken Green simulation:
The live smoke test still passed. Users were never affected.
Readiness probes are not decoration. They are an operational safety control. A pod that fails its readiness probe never receives production traffic - regardless of whether it is running.
Phase 8: GitHub Actions CI Pipeline
The and pin the Trivy binary version to match.
Final CI result:
CODE
ShopSwift CI: PASSED
This phase closed an important gap. The project now had automated validation, not just manual testing.
Phase 9: AWS EKS Cloud Deployment
Local validation proved the architecture. AWS EKS proved it at scale.
Cloud stack:
Component
Service
Kubernetes
Ingress
NGINX Ingress Controller via
Cluster configuration:
CODE
Cluster:shopswift-bluegreen-eks Region:us-east-1 Nodes:2 x t3.small
SOCIAL SHARE CARD GENERATOR