Deploying Production Ready Application on Kubernetes

Deploying Production Ready Application on Kubernetes

Introduction

Greetings everyone! Today, we embark on a journey to deploy a Python application securely using HTTPS on Kubernetes. Our application allows users to set and manage goals, showcasing a sleek interface crafted with HTML and powered by Flask—a lightweight web framework. This guide will walk you through configuring certificates, setting up a PostgreSQL database using CloudNativePG for Kubernetes, and ensuring seamless scalability with Horizontal Pod Autoscaling. By the end, you'll have a robust setup ready to handle real-world demands efficiently.

Quick Overview of the Application

Under Templates directory, we have index.html

<!DOCTYPE html>
<html>
<head>
    <title>Kubernetes Hindi Bootcamp Application 2024!</title>
    <style>
        body {
            font-family: 'Arial', sans-serif;
            background-color: #f4f4f4;
            color: #333;
        }
        .container {
            width: 80%;
            margin: 50px auto;
            background: #fff;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }
        h2 {
            text-align: center;
            color: #4CAF50;
            margin-bottom: 20px;
            font-family: 'Trebuchet MS', sans-serif;
        }
        form {
            text-align: center;
            margin-bottom: 30px;
        }
        input[type="text"] {
            padding: 10px;
            font-size: 16px;
            border: 1px solid #ccc;
            border-radius: 5px;
            width: 70%;
        }
        button {
            padding: 10px 15px;
            font-size: 16px;
            border: none;
            border-radius: 5px;
            background-color: #4CAF50;
            color: white;
            cursor: pointer;
        }
        button:hover {
            background-color: #45a049;
        }
        .goal-list {
            list-style-type: none;
            padding: 0;
        }
        .goal-item {
            margin: 10px 0;
            padding: 10px;
            border-bottom: 1px solid #ddd;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .delete-button {
            background-color: #f44336;
            color: white;
            border: none;
            padding: 5px 10px;
            border-radius: 5px;
            cursor: pointer;
        }
        .delete-button:hover {
            background-color: #e53935;
        }
        .logo {
            display: block;
            margin: 0 auto 20px;
        }
        footer {
            text-align: center;
            margin-top: 30px;
            font-size: 14px;
            color: #777;
        }
    </style>
</head>
<body>
    <div class="container">
        <img src="https://avatars.githubusercontent.com/u/101195255?s=200&v=4" alt="Kubernetes Logo" class="logo">
        <h2>Kubernetes 12th stream live viewers are awesome!  </h2>
        <form action="/add_goal" method="post">
            <input type="text" name="goal_name" placeholder="Enter your goal">
            <button type="submit">Add Goal</button>
        </form>
        {% if goals %}
        <ul class="goal-list">
            {% for goal in goals %}
                <li class="goal-item">
                    {{ goal[1] }}
                    <form action="/remove_goal" method="post" style="display: inline;">
                        <input type="hidden" name="goal_id" value="{{ goal[0] }}">
                        <button type="submit" class="delete-button">Delete</button>
                    </form>
                </li>
            {% endfor %}
        </ul>
        {% endif %}
    </div>
    <script>
        document.querySelectorAll('form').forEach(form => {
            form.onsubmit = () => setTimeout(() => location.reload(), 500); // Reload page after a short delay
        });
    </script>
</body>
</html>

And we also have app.py

from flask import Flask, render_template, request, redirect, url_for
import psycopg2
from psycopg2 import sql, Error
import os

app = Flask(__name__)

def create_connection():
    try:
        connection = psycopg2.connect(
            user=os.getenv('DB_USERNAME'),
            password=os.getenv('DB_PASSWORD'),
            host=os.getenv('DB_HOST'),
            port=os.getenv('DB_PORT'),
            database=os.getenv('DB_NAME')

        )
        return connection
    except Error as e:
        print("Error while connecting to PostgreSQL", e)
        return None

@app.route('/', methods=['GET'])
def index():
    connection = create_connection()
    if connection:
        cursor = connection.cursor()
        cursor.execute("SELECT * FROM goals")
        goals = cursor.fetchall()
        cursor.close()
        connection.close()
        return render_template('index.html', goals=goals)
    else:
        return "Error connecting to the PostgreSQL database", 500

@app.route('/add_goal', methods=['POST'])
def add_goal():
    goal_name = request.form.get('goal_name')
    if goal_name:
        connection = create_connection()
        if connection:
            cursor = connection.cursor()
            cursor.execute("INSERT INTO goals (goal_name) VALUES (%s)", (goal_name,))
            connection.commit()
            cursor.close()
            connection.close()
    return redirect(url_for('index'))

@app.route('/remove_goal', methods=['POST'])
def remove_goal():
    goal_id = request.form.get('goal_id')
    if goal_id:
        connection = create_connection()
        if connection:
            cursor = connection.cursor()
            cursor.execute("DELETE FROM goals WHERE id = %s", (goal_id,))
            connection.commit()
            cursor.close()
            connection.close()
    return redirect(url_for('index'))

@app.route('/health', methods=['GET'])
def health_check():
    return "OK", 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

And we also have Requirement.txt

Flask
psycopg2-binary
gunicorn

Creation of the Docker file

# Use an official Python runtime as a parent image
FROM python:3.9-slim

# Set the working directory in the container
WORKDIR /app

# Copy the requirements file into the container at /app
COPY requirements.txt /app/

# Install any dependencies specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code into the container at /app
COPY . /app

# Make port 8080 available to the world outside this container
EXPOSE 8080

# Define environment variable for Flask
ENV FLASK_APP=app.py

# Run the application using Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]

Run the docker build -t <image-name> . to build the image and push it to any registry . In my case i have pushed it to DockerHub.

Prerequisites before Deploying Application

  • Kubernetes cluster with worker node of 2cpu and 8Gi of Memory it can be either AKS or EKS .

  • If your are using Student subscription account in Azure then make sure to disable public Ip per node while configuring the NodePool because you can't have more than 3 Public Ip at once under this Subscription and you'll not get External Ip of the Loadbalancer.

Start Deploying Application

Installing Cloudnative-pg database

Instead of deploying Database as statefulset, we have modern solution to run database in Kubernetes. CloudNativePG is an open source operator designed to manage PostgreSQL workloads on any supported Kubernetes cluster running in private, public, hybrid, or multi-cloud environments.

kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.23/releases/cnpg-1.23.1.yaml

Create the secret.yaml

kubectl create secret generic my-postgresql-credentials --from-literal=password='new_password'  --from-literal=username='goals_user'  --dry-run=client -o yaml | kubectl apply -f -

Create the instance of Database:

This configuration sets up a PostgreSQL cluster named my-postgresql in the default namespace with 3 instances, each having 1Gi of storage. It initializes with a database goals_database owned by goals_user. The owner’s credentials are stored in the my-postgresql-credentials secret.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: my-postgresql
  namespace: default
spec:
  instances: 3
  storage:
    size: 1Gi
  bootstrap:
    initdb:
      database: goals_database
      owner: goals_user
      secret:
        name: my-postgresql-credentials

This will create the 3 instance of postgresql database and uses the secret my-postgresql-credentials which we just created.

Exec into the pod to create Table

kubectl exec -it my-postgresql-1 -- psql -U postgres -c "ALTER USER goals_user WITH PASSWORD 'new_password';"
kubectl port-forward my-postgresql-1 5432:5432
PGPASSWORD='new_password' psql -h 127.0.0.1 -U goals_user -d goals_database -c "

CREATE TABLE goals (
    id SERIAL PRIMARY KEY,
    goal_name VARCHAR(255) NOT NULL
);
"

Creation of configmap

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DB_USERNAME: "goals_user"
  DB_NAME: "goals_database"
  DB_PORT: "5432"

Creation of the Deployment manifest

This Deployment configuration creates a single replica of a pod named my-app. It selects pods with the label app: my-app and defines a pod template with this label. The container named my-app uses the image anish60/app:latest and always pulls the latest version. Environment variables for database connection (username, password, host, port, and database name) are set, with sensitive information retrieved from the my-postgresql-credentials secret. The container exposes port 8080 and includes readiness and liveness probes to check the /health endpoint. Resource requests and limits are specified for memory and CPU to ensure appropriate resource allocation.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: anish60/app:latest
        imagePullPolicy: Always
        env:
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: my-postgresql-credentials
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: my-postgresql-credentials
              key: password
        - name: DB_HOST
          value: my-postgresql-rw.default.svc.cluster.local
        - name: DB_PORT
          value: "5432"
        - name: DB_NAME
          value: goals_database
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
        resources:
          requests:
            memory: "350Mi"
            cpu: "250m"
          limits:
            memory: "500Mi"
            cpu: "500m"

Creation of Service

This Service configuration defines a Kubernetes service named my-app-service that routes traffic to pods with the label app: my-app. It listens on port 80 and forwards incoming traffic to the target port 8080 on the selected pods using the TCP protocol. This setup allows external access to the application running on port 8080 of the pods.

apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

Deploy Nginx ingress Controller

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.4/deploy/static/provider/cloud/deploy.yaml

Apply Ingress manifest

This Ingress configuration defines routing rules for the domain demo.anishbista.xyz, directing traffic to the my-app-service on port 80. It uses the nginx ingress class and references a TLS certificate stored in the app secret to enable HTTPS. The cert-manager.io/cluster-issuer annotation specifies that the production-app ClusterIssuer should be used by cert-manager to manage the certificate for this Ingress.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    cert-manager.io/cluster-issuer: production-app

spec:
  ingressClassName: nginx
  rules:
  - host: demo.anishbista.xyz
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-service
            port:
              number: 80
  tls:
  - hosts:
    - demo.anishbista.xyz
    secretName: app

Configure DNS Record

In the value section, insert the Loadbalancer External-IP.

Now, if you hit demo.anishbista.xyz you will access application but without https. for https you have to configure the certificate.

Configure for HTTPS

Install cert-Manger

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.5/cert-manager.yaml

Create the cluster_issuer.yaml and apply it.

This ClusterIssuer configuration for cert-manager sets up an ACME issuer using Let's Encrypt's production server. It registers with the email demo@v1.com and stores the ACME account's private key in a secret named app. The ClusterIssuer enables the HTTP-01 challenge provider using the nginx ingress class to prove domain ownership for certificate issuance.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: production-app
spec:
  acme:
    # The ACME server URL
    server: https://acme-v02.api.letsencrypt.org/directory
    # Email address used for ACME registration
    email: demo@v1.com
    # Name of a secret used to store the ACME account private key
    privateKeySecretRef:
      name: app 
    # Enable the HTTP-01 challenge provider
    solvers:
    - http01:
        ingress:
          class: nginx

Apply the certificate

This Certificate configuration for cert-manager requests a TLS certificate for the domain demo.anishbista.xyz. The certificate will be stored in a Kubernetes secret named app. It uses the ClusterIssuer named production-app to issue the certificate. The dnsNames field ensures the certificate is valid for demo.anishbista.xyz.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: app
spec:
  secretName: app
  issuerRef:
    name: production-app
    kind: ClusterIssuer
  commonName: demo.anishbista.xyz
  dnsNames:
  - demo.anishbista.xyz

Now, when you hit demo.anishbista.xyz you will be able to access it using https

Configure Horizontal pod Scaling

This Horizontal Pod Autoscaler (HPA) configuration automatically scales the my-app deployment based on CPU and memory usage. It maintains between 1 and 10 replicas. For CPU, it targets 20% average utilization, and for memory, it targets an average of 350Mi. The HPA adjusts the number of pods to meet these resource utilization targets.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 20
  - type: Resource
    resource:
      name: memory
      target:
        type: AverageValue
        averageValue: 350Mi

Test the application by increasing the server load

Grafana k6 is an open-source load testing tool that makes performance testing easy and productive for engineering teams. k6 is free, developer-centric, and extensible.

Install k6 cli

Create load.js file and paste the below content. Now, Run k6 run load.js

This k6 script performs a load test on https://demo.anishbista.xyz, simulating 100 virtual users for 30 seconds. The demo function sends a GET request to the URL and checks if the response status is 200, logging "PASS!" if successful or "FAIL!" with the status code if not. The default function runs the demo function, executing the load test.

import http from "k6/http";
import { check } from "k6";


export const options = {
  vus: 100,
  duration: '30s',
};

const BASE_URL = 'https://demo.anishbista.xyz'

function demo() {
  const url = `${BASE_URL}`;


  let resp = http.get(url);

  check(resp, {
    'endpoint was successful': (resp) => {
      if (resp.status === 200) {
        console.log(`PASS! url`)
        return true
      } else {
        console.error(`FAIL! status-code: ${resp.status}`)
        return false
      }
    }
  });
}


export default function () {
    demo()
}

Once, this script run completely your pods start scaling automatically.

Would you really want to learn Kubernetes in depth?

if yes, please follow the below link.
Best Kubernetes bootcamp

Conclusion

In conclusion, we've successfully orchestrated a comprehensive deployment of our Python application on Kubernetes, fortified with HTTPS for secure communication. Leveraging tools like CloudNativePG for database management and cert-manager for certificate automation ensures reliability and scalability. With Horizontal Pod Autoscaling in place, our application can dynamically adjust to varying workloads. As you continue to refine and expand your Kubernetes deployments, remember these foundational steps to maintain a robust and secure application infrastructure. Happy deploying!

These sections provide a clear overview and wrap-up of your deployment process, ensuring your readers understand the scope and achievements of your setup.