Table of contents
- Introduction
- Quick Overview of the Application
- Creation of the Docker file
- Prerequisites before Deploying Application
- Start Deploying Application
- Configure for HTTPS
- Configure Horizontal pod Scaling
- Test the application by increasing the server load
- Would you really want to learn Kubernetes in depth?
- Conclusion
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.
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.