How I Deployed a Python Chat App (Complete EKS Deployment - Helm for Load Balancer) - Part 4

This blog is part of a 4-part series on building a real-time chat application:

"Why are we doing this": We Need to Run Our App

Our journey is almost to completion (maybe we will build on this project in the future :D ). In Part 3 (opens in a new tab), we built an automated CI pipeline. Our my-chat-app image is now being built and pushed to an AWS ECR "warehouse" every time we commit code.

But that's where it stops. Our image is just sitting in a warehouse, not running.

In this final(maybe) post, we'll build our "factory", a production-grade Amazon EKS (Kubernetes) Cluster and deploy our application for the world to see.

  1. We'll use eksctl, the official CLI for EKS. It's a simple, powerful tool that can create an entire cluster with a single command. Doing this manually in the AWS console is a 50-step, error-prone nightmare. We should automate it.

eksctl works in the background (using AWS CloudFormation) to build an entire, production-ready stack for us, including:

It saves us hours of work and guarantees our cluster is built correctly.

  1. We'll also use AWS Fargate to run our containers, meaning we don't have to manage any EC2 server nodes. A Kubernetes cluster needs compute power to run our containers. Traditionally, this meant creating a bunch of EC2 (virtual server) instances that we are responsible for.

This means we wouldve had to:

AWS Fargate is the "serverless" alternative. It lets us use EKS without managing any servers at all. You just show up with your app (your container), and AWS instantly gives you a room of the exact size you need (e.g., 1 vCPU, 2GB RAM).

  1. AWS Load Balancer Controller: We will install this add-on, which is the "brain" that lets Kubernetes create modern Network Load Balancers (NLBs).

Before we begin, make sure your AWS CLI is configured with the correct credentials and region, or you wont be able to use eksctl as its a AWS CLI command.

EKS costs roughly $0.10 per hour. Make sure you delete your cluster when you're done.


Step 1: Building the Cluster (Provisioning EKS with eksctl)

eksctl is a single command that creates all the complex AWS resources (VPCs, subnets, IAM roles, and the EKS cluster itself) for us.

I ran this command in my terminal: (change region if you need to)

eksctl create cluster \
  --name python-redis-chat-1 \
  --region eu-west-2 \
  --fargate

--name: Gives our cluster a unique name.
--region: The AWS region to build in.
--fargate: This is the magic flag. It tells eksctl to set up our cluster to be "serverless" and create a default Fargate profile to run Kubernetes system pods.

Be patient and after 15-20 minutes, your eksctl should be finished and has automatically configured your local kubectl to talk to the new cluster. You now have a production-ready Kubernetes cluster running in AWS.

alt text You can go to the AWS console and see the cluster resources created by eksctl in the EKS Service. alt text


Step 2: Install the AWS Load Balancer Controller (The "Brain")

This is a critical step. To create a modern, high-performance Network Load Balancer (NLB) that can talk to our Fargate pods, we must install this add-on controller.

This documentation was my saviour, it helped me figure out why my application wasn't working properly when sharing the link with other users access my application. Always check documentation if stuck. I promise.

AWS Documentation NLB (opens in a new tab)

AWS Documentation ALB HELM (opens in a new tab)

We will be following the second guide to Install AWS Load Balancer Controller with Helm.

1a. Create the IAM role

Lets download the IAM policy for AWS load balancer controller which allows us to make API calls

curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.14.1/docs/install/iam_policy.json

We will use this policy to create a policy in AWS IAM console.

aws iam create-policy \ --policy-name AWSLoadBalancerControllerIAMPolicy \ --policy-document file://iam_policy.json

Now the service account

It allows any pod using the aws-load-balancer-controller service account to automatically and securely assume the IAM role and get the AWS permissions it needs, without you ever having to store or manage aws_access_key_ids in Kubernetes.

Replace the values for cluster name, region code, and account ID.

eksctl create iamserviceaccount \
    --cluster=python-redis-chat-1 \
    --namespace=kube-system \
    --name=aws-load-balancer-controller \
    --attach-policy-arn=arn:aws:iam::${AWS_ACCOUNT_ID}:policy/AWSLoadBalancerControllerIAMPolicy \
    --override-existing-serviceaccounts \
    --region eu-west-2 \
    --approve

alt text

I actually got an error because i need an IAM OIDC and this is like a way to enable secure access between Kubernetes service account and the AWS resources.

Run the below eksctl utils associate-iam-oidc-provider --region=eu-west-2 --cluster=python-redis-chat-1 same again with --approve at the end eksctl utils associate-iam-oidc-provider --region=eu-west-2 --cluster=python-redis-chat-1 --approve

Then rerun the previous command and service account should be created successfully.

alt text

alt text

2a. Installing AWS Load Balancer Controller

We will be using something called Helm (I recommend researching this topic as its a powerful tool)

Add the eks-charts Helm chart repository.
helm repo add eks https://aws.github.io/eks-charts

Update your local repo to make sure that you have the most recent charts.
helm repo update eks

Now lets install the load balancer controller (make sure to change cluster name, VPC and region to your own)

helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --set clusterName=my-cluster \
  --set serviceAccount.create=false \
  --set serviceAccount.name=aws-load-balancer-controller \
  --set region=region-code \
  --set vpcId=vpc-xxxxxxxx \
  --version 1.14.0

to verify its all working it can take up to 2/3 minutes
kubectl get deployment -n kube-system aws-load-balancer-controller

alt text

You should see 2/2

alt text

Step 3: Translating Our App for Kubernetes (The Manifests)

This is probably the biggest step. We need to "translate" our manual docker run commands from Part 2 into a set of declarative YAML files that Kubernetes understands.

These files are called manifests. I created a k8s/ folder in my repo to hold them. This folder will contain all the necessary Kubernetes configuration files for our application.

alt text

3a. The Database: redis-deployment.yml & redis-service.yml

First is the Redis database. We need two files:

k8s/redis-deployment.yml: This tells Kubernetes how to run the Redis container. It defines "I want one copy (replicas: 1) of the official redis:latest image."

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-deployment
  labels:
    app: redis-db  # This label is how the Service will find these pods
spec:
  replicas: 1  # We only need one copy of our database
  selector:
    matchLabels:
      app: redis-db  # Match the pods with the label "app: redis-db"
  template:
    metadata:
      labels:
        app: redis-db  # Apply this label to the pod(s)
    spec:
      containers:
      - name: redis
        image: redis:latest  # The official public image from Docker Hub
        ports:
        - containerPort: 6379 # The default port Redis listens on

Here is a breakdown of what this file is doing:

k8s/redis-service.yml: This is the networking. This is how our app will find the database.

Here is the redis-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: redis-service
spec:
  type: ClusterIP
  selector:
    app: redis-db # This must match the 'app' label in your redis-deployment.yml
  ports:
  - port: 6379
    targetPort: 6379

name: redis-service: This is the magic. Kubernetes has its own internal DNS. By creating this, any other pod in our cluster can now find our database simply by using the hostname redis-service.

type: ClusterIP: This is the most important line. It says, "This service is internal only." It's not accessible from the public internet.

In simple terms, docker run --name redis-db... is equivalent to kubectl -f redis-deployment.yml and docker network create.. is equivalent to kubectl -f redis-service.yml.


3b. The App: chat-app-deployment.yml & chat-app-service.yml

Next, our Python chat app. This also gets a Deployment and a Service.

k8s/chat-app-deployment.yml: This file tells Kubernetes how to run our app. The most important part is the env section:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: chat-app-deployment
spec:
  replicas: 2  # Let's run two copies for high availability!
  selector:
    matchLabels:
      app: my-chat-app
  template:
    metadata:
      labels:
        app: my-chat-app
    spec:
      containers:
      - name: my-chat-app
        image: 123456789.dkr.ecr.eu-west-2.amazonaws.com/my-chat-app:latest # My ECR Image
        ports:
        - containerPort: 8080
        env:
        - name: REDIS_ENDPOINT_URL
          value: "redis-service:6379" # <-- This is the "magic glue"!

Make sure here to replace the image name with your own ECR image URL.

This is the "Ahhhh!" make sense moment(well for me). In Part 2, our docker run command used -e REDIS_ENDPOINT_URL=redis-db:6379. Here, in our production manifest, we are passing the exact same variable, but this time the value points to our new Kubernetes service name, redis-service.

k8s/chat-app-service.yml: This is our "front door." (I had to debug a lot of things here with the service but it works for this project)

apiVersion: v1
kind: Service
metadata:
  name: chat-app-service
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
  type: LoadBalancer
  selector:
    app: my-chat-app
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP

type: LoadBalancer: This is the other crucial setting. It tells Kubernetes to ask AWS to create a real, public Load Balancer and point it at our app.

port: 80, targetPort: 8080: This tells the Load Balancer to accept public traffic on port 80 (standard HTTP) and forward it to our container's internal port 8080 (where Gunicorn is running).


Step 4: The Deployment (kubectl apply)

With our cluster running and our YAML files written, the final deployment is a single command. I told Kubernetes to apply all the configuration files in my k8s/ directory:

Now lets run the command to apply the configuration files, its easier than doing it once at a time:

kubectl apply -f k8s/

alt text Kubernetes now does all the work for me:

Pulls the redis:latest image and starts the database pod.

Pulls my my-chat-app image from my private ECR.

Starts my two app pods (because I asked for replicas: 2).

Provisions a new AWS Load Balancer.

The Final Result: It's Live!

After a few minutes, I ran two commands to check the status.

First, I checked the pods. I can see all three pods are Running: kubectl get pods

Next, I checked my services to find the public URL: kubectl get services

As you can see, the Load Balancer has a public IP address: alt text

I copied the external IP address and pasted it into my browser. The chat app was live! Anyone can connect to it! alt text We have completed a full deployment of a Python chat app using Kubernetes on AWS. We created a Redis database, a Python app, and a Load Balancer to expose the app to the internet. We also learned how to use YAML files to define our Kubernetes resources and how to apply them using kubectl.

Heres a video demo: (if it doesnt play, right click and press video)

When finished dont forget to clean up your resources by deleting the Kubernetes cluster and the AWS Load Balancer. eksctl delete cluster --name python-redis-chat-1


© Tawfiq.RSS