Deploying a full three-tier web application — a React frontend, a Node.js / Express API, and a MongoDB database — live on Amazon EKS, exposed to the internet through a single AWS Application Load Balancer (ALB) Ingress.
Each tier runs as its own Deployment + Service in a dedicated three-tier namespace. Container images are stored in Amazon ECR, the cluster is provisioned with eksctl, and the public entry point is created by the AWS Load Balancer Controller.
An internet-facing ALB routes / to the React frontend and /api to the Node.js API. The API reaches MongoDB by its in-cluster Service name; MongoDB persists to a PersistentVolume and reads its credentials from a Kubernetes Secret. All Services are ClusterIP — only the ALB is public.
flowchart TD
U["🌐 Browser"] --> ALB["AWS ALB Ingress · mainlb<br/>internet-facing"]
subgraph NS["namespace: three-tier (Amazon EKS)"]
ALB -->|"/"| FE["frontend svc · ClusterIP :3000<br/>React pods"]
ALB -->|"/api"| API["api svc · ClusterIP :3500<br/>Node.js API × 2"]
API --> MO["mongodb-svc · ClusterIP :27017<br/>MongoDB pod"]
MO -->|"PVC"| PV[("PersistentVolume<br/>hostPath — /data/db")]
SEC["Secret: mongo-sec<br/>DB user + password"] -.->|"env vars"| MO
SEC -.-> API
end
LBC["AWS Load Balancer Controller<br/>(Helm, in kube-system)"] -.->|"provisions the ALB<br/>from the Ingress"| ALB
| Tier | Component | Service type | Port |
|---|---|---|---|
| Presentation | React SPA | ClusterIP (via Ingress /) |
3000 |
| Application | Node.js / Express API (replicas: 2, liveness+readiness on /ok) |
ClusterIP (via Ingress /api) |
3500 |
| Data | MongoDB (Secret + PV/PVC) |
ClusterIP | 27017 |
.
├── Kubernetes-Manifests-file/ # ← the EKS deployment (core of this project)
│ ├── Database/
│ │ ├── secrets.yaml # mongo-sec (base64 — set your own values)
│ │ ├── pv.yaml / pvc.yaml # persistent storage for MongoDB
│ │ ├── deployment.yaml # MongoDB Deployment (reads creds from Secret)
│ │ └── service.yaml # mongodb-svc (ClusterIP)
│ ├── Backend/
│ │ ├── deployment.yaml # Node API × 2 + health probes + ECR image
│ │ └── service.yaml # api (ClusterIP :3500)
│ ├── Frontend/
│ │ ├── deployment.yaml # React + ECR image + REACT_APP_BACKEND_URL
│ │ └── service.yaml # frontend (ClusterIP :3000)
│ └── ingress.yaml # mainlb — the ALB Ingress (/ and /api)
└── Application-Code/
├── backend/ # Node.js / Express API + Dockerfile
└── frontend/ # React app + Dockerfile
Placeholders to fill in before deploying:
<your-aws-account-id>(ECR image URLs + IAM policy ARN),<your-domain>(Ingress host +REACT_APP_BACKEND_URL), and the base64 values insecrets.yaml.
- An AWS account with permissions to use EKS, EC2, ECR, IAM, and Elastic Load Balancing.
- A small Ubuntu EC2 "control machine" (e.g.
t2.medium) from which you run everything below — it is not part of the cluster. - Basic familiarity with Docker and the AWS console.
💸 Heads-up on cost: EKS is not free — the control plane is ~$0.10/hour, plus the EC2 worker nodes and the ALB. Follow the Cleanup step the moment you're done so nothing keeps billing.
Create an IAM user for this project and generate programmatic credentials:
- Create a user named
eks-adminand attach theAdministratorAccesspolicy (fine for a learning project; scope it down for real work). - Under Security credentials, generate an Access key + Secret access key — you'll enter these in Step 3.
- Launch an Ubuntu EC2 instance (e.g.
t2.medium) in your chosen region (e.g.us-west-2). - SSH into it — every command below runs here.
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
sudo apt install unzip
unzip awscliv2.zip
sudo ./aws/install -i /usr/local/aws-cli -b /usr/local/bin --update
aws configure # paste the Access key, Secret key, region (e.g. us-west-2), output = jsonaws configure stores the IAM user's keys so every later aws and eksctl command can act on your account.
sudo apt-get update
sudo apt install docker.io
sudo chown $USER /var/run/docker.sock # talk to Docker without sudo each time
docker pscurl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.19.6/2021-01-05/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin
kubectl version --short --clientThe URL above pins an older kubectl — feel free to grab a current release instead. The pattern is always: download the binary,
chmod +x, andmvit onto yourPATH.
curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
sudo mv /tmp/eksctl /usr/local/bin
eksctl versioneksctl create cluster --name three-tier-cluster --region us-west-2 \
--node-type t2.medium --nodes-min 2 --nodes-max 2
aws eks update-kubeconfig --region us-west-2 --name three-tier-cluster
kubectl get nodeseksctl provisions the managed control plane, a VPC, and a node group of two EC2 workers via CloudFormation — this takes ~15 minutes. update-kubeconfig writes the cluster's connection details into ~/.kube/config so kubectl talks to it. When kubectl get nodes shows two nodes in STATUS: Ready, the cluster is live.
Kubernetes runs images, not source code — and the Deployments pull from Amazon ECR (a private registry), so build and push each tier first.
# From Application-Code/backend and Application-Code/frontend respectively:
docker build -t backend .
docker build -t frontend .
# Log Docker in to your ECR registry
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin <your-aws-account-id>.dkr.ecr.us-east-1.amazonaws.com
# Tag and push each image (create the two ECR repos in the console first, or via `aws ecr create-repository`)
docker tag backend:latest <your-aws-account-id>.dkr.ecr.us-east-1.amazonaws.com/backend:latest
docker tag frontend:latest <your-aws-account-id>.dkr.ecr.us-east-1.amazonaws.com/frontend:latest
docker push <your-aws-account-id>.dkr.ecr.us-east-1.amazonaws.com/backend:latest
docker push <your-aws-account-id>.dkr.ecr.us-east-1.amazonaws.com/frontend:latestThe image tags above must match the
image:fields inKubernetes-Manifests-file/Backend/deployment.yamlandFrontend/deployment.yaml— update the<your-aws-account-id>placeholder in both.
Now create the namespace and a credential so the cluster can pull the private ECR images:
kubectl create namespace three-tier
kubectl create secret docker-registry ecr-registry-secret \
--docker-server=<your-aws-account-id>.dkr.ecr.us-east-1.amazonaws.com \
--docker-username=AWS \
--docker-password=$(aws ecr get-login-password --region us-east-1) \
-n three-tierApply the manifests, in order (data tier first, then backend, then frontend):
kubectl apply -f Kubernetes-Manifests-file/Database/
kubectl apply -f Kubernetes-Manifests-file/Backend/
kubectl apply -f Kubernetes-Manifests-file/Frontend/
kubectl get pods -n three-tier # watch all three tiers come up
⚠️ Every manifest hard-codesnamespace: three-tier, so create exactly that namespace (notworkshop). Also notekubectl apply -f .alone does not recurse into subfolders — apply each tier's folder as shown (or use-R). Give MongoDB a few minutes before the backend connects cleanly.
The Ingress becomes a real ALB only once the controller is installed. It needs an IAM policy, an OIDC-backed service account, and the controller itself.
# 1) IAM policy the controller needs
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.5.4/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
# 2) Enable IAM Roles for Service Accounts (OIDC) on the cluster
eksctl utils associate-iam-oidc-provider --region=us-west-2 --cluster=three-tier-cluster --approve
# 3) Create the service account bound to that policy
eksctl create iamserviceaccount --cluster=three-tier-cluster --namespace=kube-system \
--name=aws-load-balancer-controller --role-name AmazonEKSLoadBalancerControllerRole \
--attach-policy-arn=arn:aws:iam::<your-aws-account-id>:policy/AWSLoadBalancerControllerIAMPolicy \
--approve --region=us-west-2sudo snap install helm --classic
helm repo add eks https://aws.github.io/eks-charts
helm repo update eks
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system \
--set clusterName=three-tier-cluster \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller
kubectl get deployment -n kube-system aws-load-balancer-controller # should be Available
# Apply the Ingress — the controller now provisions a real ALB
kubectl apply -f Kubernetes-Manifests-file/ingress.yaml
kubectl get ingress -n three-tier # wait for the ADDRESS (…elb.amazonaws.com) to appearSet
clusterNameto your real cluster name (three-tier-cluster) — the chart's examplemy-clusteris a placeholder. Whenkubectl get ingressshows anADDRESS, open that URL in your browser:/serves the React frontend and/apireaches the Node API. (Point the Ingresshost:and the frontend'sREACT_APP_BACKEND_URLat your own<your-domain>.)
EKS bills by the hour — tear everything down when you're done:
eksctl delete cluster --name three-tier-cluster --region us-west-2Then, in the AWS console, double-check that:
- the ALB created by the Ingress is gone (the controller makes it, so
eksctlmay not remove it), - any leftover security groups from the steps above are deleted, and
- the control-machine EC2 from Step 2 is stopped or terminated.
- Provisioning a managed Amazon EKS cluster end-to-end with
eksctl(control plane, VPC, managed node group) - Multi-tier deployment on Kubernetes: Deployments, Services, namespaces, and cross-tier service discovery by Service name
- Private image registry workflow with Amazon ECR and
imagePullSecrets - Persistent storage (PV/PVC) and Secrets for database credentials
- Health checks — liveness & readiness probes for reliable rolling updates and self-healing
- Ingress + ALB: exposing multiple tiers through one load balancer, routed by path (
/and/api), via the AWS Load Balancer Controller (Helm + IAM/OIDC) - Cost hygiene: full teardown with
eksctlto avoid ongoing AWS charges
The Kubernetes manifests use <...> placeholders for account- and domain-specific values — replace them with your own before deploying. Kubernetes Secrets are only base64-encoded (not encrypted), so never commit real credentials; generate your own values and, for production, use a sealed-secrets tool or an external secrets manager.