<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Backend & DevOps Developer]]></title><description><![CDATA[Sharing real-world projects on Kubernetes, AWS, and system design.]]></description><link>https://blog.omblogs.tech</link><image><url>https://cdn.hashnode.com/uploads/logos/6986f9f0eaede2fbacc9d2ea/2c8c51a1-b433-428c-b66f-bc6d0b63a9e7.webp</url><title>Backend &amp; DevOps Developer</title><link>https://blog.omblogs.tech</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 27 May 2026 11:25:59 GMT</lastBuildDate><atom:link href="https://blog.omblogs.tech/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Deploying 3-Tier Application on Kubernetes]]></title><description><![CDATA[In this blog, I’ll walk you through how I deployed a 3-tier full-stack application on Kubernetes using kubeadm on AWS EC2. This includes setting up storage using the EBS CSI driver and exposing the ap]]></description><link>https://blog.omblogs.tech/deploying-3-tier-application-on-kubernetes</link><guid isPermaLink="true">https://blog.omblogs.tech/deploying-3-tier-application-on-kubernetes</guid><dc:creator><![CDATA[OM GANAPURE]]></dc:creator><pubDate>Sat, 21 Mar 2026 12:01:38 GMT</pubDate><content:encoded><![CDATA[<p>In this blog, I’ll walk you through how I deployed a 3-tier full-stack application on Kubernetes using kubeadm on AWS EC2. This includes setting up storage using the EBS CSI driver and exposing the application using an Ingress controller.</p>
<h2>1. Architecture</h2>
<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088583/iw4xyiagpbidklu3su7f.png" alt="3-tier app architecture diagram" style="display:block;margin:0 auto" />

<p>The application is deployed on a Kubernetes cluster set up using kubeadm on AWS EC2 infrastructure. The cluster consists of two EC2 instances: one acting as the control plane (master node) and the other as a worker node. The control plane manages the cluster state, scheduling, and API communication, while the worker node is responsible for running the application workloads in the form of pods.</p>
<p>Inside the Kubernetes cluster, the application follows a typical 3-tier architecture. The frontend (React application), backend (Node.js API), and database (MongoDB) are each deployed as separate pods. These pods are exposed internally using Kubernetes Services, which enable communication between different components. The frontend communicates with the backend service, and the backend interacts with the MongoDB database service.</p>
<p>To expose the application externally, an NGINX Ingress controller is configured. The Ingress resource routes incoming user traffic to the appropriate service within the cluster, allowing users to access the application through a single entry point instead of directly exposing NodePorts.</p>
<p>For persistent storage, especially for MongoDB, the AWS EBS CSI (Container Storage Interface) driver is used. This enables dynamic provisioning of EBS volumes, which are attached to the worker node and mounted inside the MongoDB pod using Persistent Volumes (PV) and Persistent Volume Claims (PVC). This ensures that database data is retained even if the pod restarts.</p>
<p>Overall, the request flow starts from the user, which is routed through the Ingress controller to the frontend service. The frontend then communicates with the backend API, which processes the request and interacts with the MongoDB database stored on an EBS-backed persistent volume.</p>
<h2>2. Infrastructure Setup</h2>
<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088963/pevimxhin7goasd5bzaf.png" alt="EC2 setup" style="display:block;margin:0 auto" />

<p>Created 2 EC2 instance (t2.medium) for control-plane and worker node in the default vpc.</p>
<p>Security Group for control-plane node</p>
<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088963/qhrzquu8kucy5vnrehvk.png" alt="control-plane-sg" style="display:block;margin:0 auto" />

<p>Security Group for worker node</p>
<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088963/lmefbbmkonicqc0kpwfb.png" alt="worker-sg" style="display:block;margin:0 auto" />

<h2>4. Kubernetes Setup using kubeadm</h2>
<p>Install dependencies</p>
<pre><code class="language-plaintext">sudo apt update
sudo apt install -y docker.io
</code></pre>
<p>Install kubeadm, kubelet, kubectl in the same way , i'll mention how to do that in another blog</p>
<p>Initialize cluster (control-plane) <code>kubeadm init</code></p>
<p>Join worker node <code>kubeadm join ...</code></p>
<p>Install CNI (Calico) <code>kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml</code></p>
<h2>5. Setup Persistent Storage (EBS CSI Driver)</h2>
<p>To make sure MongoDB data is not lost when pods restart, persistent storage is required. For this, the AWS EBS CSI driver was installed, which allows Kubernetes to create and attach EBS volumes dynamically to the pods. An IAM role with proper permissions was attached to the EC2 instances so that Kubernetes can interact with AWS services. Once configured, the CSI driver runs as pods inside the cluster and handles volume creation, attachment, and management automatically, ensuring reliable data persistence for the database.</p>
<p>Create IAM policy: AmazonEBSCSIDriverPolicy and attach it to both the ec2 nodes</p>
<p>Install EBS CSI Driver - kubectl apply -k "github.com/kubernetes-sigs/aws-ebs-csi-driver/deploy/kubernetes/overlays/stable/?ref=release-1.29"</p>
<p>Create a StorageClass</p>
<pre><code class="language-plaintext">apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  type: gp3
</code></pre>
<p>Create a secret to store the mongo creds</p>
<pre><code class="language-plaintext">apiVersion: v1
kind: Secret
metadata:
  name: mongodb-secret
type: Opaque
data:
  MONGO_INITDB_ROOT_USERNAME: bW9uZ29hZG1pbg==   # mongoadmin
  MONGO_INITDB_ROOT_PASSWORD: cGFzc3dvcmQ=       # password
</code></pre>
<p>Since it is not the recommended way to store the secret. Use something called secret storage csi driver using AWS secret manager or Hashicorp vault and mount it into nodes to fetch the secrets.</p>
<p>Create a service for Mongodb</p>
<pre><code class="language-plaintext">apiVersion: v1
kind: Service
metadata:
  name: mongodb
spec:
  clusterIP: None
  selector:
    app: mongodb
  ports:
    - port: 27017
</code></pre>
<p>Below is the manifest to create mongodb inside the nodes</p>
<pre><code class="language-plaintext">apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  serviceName: mongodb
  replicas: 1
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      containers:
      - name: mongodb
        image: mongo:6
        ports:
        - containerPort: 27017

        env:
        - name: MONGO_INITDB_ROOT_USERNAME
          valueFrom:
            secretKeyRef:
              name: mongodb-secret
              key: MONGO_INITDB_ROOT_USERNAME
        - name: MONGO_INITDB_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mongodb-secret
              key: MONGO_INITDB_ROOT_PASSWORD

        volumeMounts:
        - name: mongo-storage
          mountPath: /data/db

        resources:
          requests:
            cpu: "250m"
            memory: "512Mi"
          limits:
            cpu: "500m"
            memory: "1Gi"

        livenessProbe:
          exec:
            command:
              - mongosh
              - --eval
              - "db.adminCommand('ping')"
          initialDelaySeconds: 40
          timeoutSeconds: 5

        readinessProbe:
          exec:
            command:
              - mongosh
              - --eval
              - "db.adminCommand('ping')"
          initialDelaySeconds: 20
          timeoutSeconds: 5

  volumeClaimTemplates:
  - metadata:
      name: mongo-storage
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: ebs-sc
      resources:
        requests:
          storage: 5Gi
</code></pre>
<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088970/mzdutdd6hb23trjrgixr.png" alt="CSI Driver output" style="display:block;margin:0 auto" />

<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088964/iawdkb0neyftvzuki86s.png" alt="pvc and pv" style="display:block;margin:0 auto" />

<p>CSI Driver created volume automatically on PVC request</p>
<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088965/ifhprpspjn2horbcoj5c.png" alt="aws volume" style="display:block;margin:0 auto" />

<h2>6. Backend &amp; Frontend Deployment</h2>
<p>Manifest for backend deployment</p>
<pre><code class="language-plaintext">apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      tolerations:
      - key: "node.kubernetes.io/disk-pressure"
        operator: "Exists"
        effect: "NoSchedule"

      containers:
      - name: backend
        image: om6214/task-new-backend:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 5000

        env:
        - name: MONGO_URI
          value: mongodb://mongoadmin:password@mongodb:27017/mydb?authSource=admin

        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "300m"
            memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: backend-service
spec:
  selector:
    app: backend
  ports:
    - port: 5000
      targetPort: 5000
  type: ClusterIP
</code></pre>
<p>manifest for frontend deployment</p>
<pre><code class="language-plaintext">apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: om6214/task-frontend:latest
        ports:
        - containerPort: 80
        env:
        - name: REACT_APP_API_URL
          value: http://backend-service:5000
---
apiVersion: v1
kind: Service
metadata:
  name: frontend-service
spec:
  selector:
    app: frontend
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP
</code></pre>
<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088964/iawdkb0neyftvzuki86s.png" alt="deployments" style="display:block;margin:0 auto" />

<h2>8. Ingress Setup</h2>
<p>Create IAM Policy</p>
<p>use this <code>https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json</code> and name it AWSLoadBalancerControllerIAMPolicy</p>
<p>Attach policy to both EC2 nodes</p>
<p>Install ALB Load balancer controller</p>
<p>helm repo add eks <a href="https://aws.github.io/eks-charts">https://aws.github.io/eks-charts</a>
helm repo update</p>
<pre><code class="language-plaintext">helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --set clusterName=my-cluster \
  --set serviceAccount.create=true \
  --set region=us-east-1 \
  --set vpcId= vpc-id
</code></pre>
<p>next tag the subnets</p>
<p>Public subnets:</p>
<pre><code class="language-plaintext">Key: kubernetes.io/role/elb
Value: 1
</code></pre>
<p>Private subnets:</p>
<pre><code class="language-plaintext">Key: kubernetes.io/role/internal-elb
Value: 1
</code></pre>
<p>Create Ingress resource</p>
<pre><code class="language-plaintext">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
spec:
  ingressClassName: alb
  rules:
  - host: ""
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-service
            port:
              number: 5000
</code></pre>
<p>the alb controller will watch this resource and create the alb load balancer</p>
<h1>Issue Faced</h1>
<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088963/jtdqruqqifpazfjnnuqw.png" alt="Issue" style="display:block;margin:0 auto" />

<p>Webhook error:</p>
<pre><code class="language-plaintext">failed calling webhook targetgroupbinding...
context deadline exceeded
</code></pre>
<p>While setting up Ingress, an error occurred related to a failed webhook call (targetgroupbinding.elbv2.k8s.aws) with a “context deadline exceeded” message. This happened because the AWS Load Balancer Controller components were partially present or expected, even though the cluster was created using kubeadm and not EKS. Since this setup does not require the AWS Load Balancer Controller, the webhook had no proper service to respond, causing the failure. The issue was resolved by ignoring or removing the unnecessary AWS Load Balancer Controller components and continuing with the NGINX Ingress controller, which works correctly in a kubeadm-based cluster.</p>
<h2>9. Installing NGINX Ingress Controller</h2>
<p>To expose the application to external users, the NGINX Ingress controller was installed in the Kubernetes cluster. This controller acts as a reverse proxy and routes incoming traffic to the appropriate services inside the cluster.</p>
<p><code>kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml</code></p>
<p>verify it</p>
<pre><code class="language-plaintext">kubectl get pods -n ingress-nginx
</code></pre>
<p>update ingress resource to use nginx controller instead of alb</p>
<pre><code class="language-plaintext">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-service
            port:
              number: 80
</code></pre>
<p>then access the application at http://:</p>
<p>! if the nginx svc is in LoadBalancer type change it to NodePort type for testing.</p>
<h2>RESULT</h2>
<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088965/uh1z5ncmsbddcodmidkq.png" alt="image1" style="display:block;margin:0 auto" />

<img src="https://res.cloudinary.com/dqkzwt6oe/image/upload/v1774088966/o3y3sfqfx97o5ejqxwfg.png" alt="Image2" style="display:block;margin:0 auto" />]]></content:encoded></item></channel></rss>