Introduction
In this article we will dive into creating a simple voting app using Python, deploying it in a k8s cluster with helm, and create one important bash script to create the automation process. To create the python app we will use Flask + HTML + CSS for the frontend and mongodb for the backend, we will use kind for our k8s environment. Kind demonstrates us a full k8s cluster environment. So let’s get started.
Prerequisites
- Python (Check out Python installation guide)
- Docker (Check out Docker installation guide)
- Python and Bash scripting knowladge
- Basic Kubernetes and Helm knowladge
Note: everything in this article is suitable for Linux machines, especially Red Hat.
Python Voting App
For this demo, our app will be a simple voting app. The app will contain three pages: home, questions and results.
In the home page the voting takes place, along with the user’s name, the the user answers the questions about the two presidents, and then see the results. We will use MongoDB for our backend. The db will store the voting results, and the answers of the users.
Setting up a virtual environment
Before diving into our source code, it is best practice to develop the app inside a virtual environment, so it will be easy for us to deal with our app’s dependicies later.
# To create a new environment run the command:
$ python3 -m venv .venv
# To activate and enter the environment run:
$ source .venv/bin/activate
The requirements for the app are:
# For developing the app
$ pip install Flask
# For the connection between the app and MongoDB
$ pip install pymongo
Source Code
Our source code will contain two parts. First we will run a script that connects to the db and creates the necessary tables, for voting and user’s answers. Add the HTML+CSS as you like.
import os
from pymongo import MongoClient
def initialize_database():
# Connection to MongoDB
username = os.getenv('MONGO_ROOT_USERNAME')
password = os.getenv('MONGO_ROOT_PASSWORD')
database = os.getenv('MONGO_DATABASE')
ip = os.getenv('MONGO_IP')
mongo_uri = f'mongodb://{username}:{password}@{ip}:27017/'
client = MongoClient(mongo_uri)
db = client[database]
# Check if the collections already exist
if 'votes' not in db.list_collection_names():
# Create a collection for votes
votes_collection = db['votes']
# Insert initial data for Trump and Biden
initial_data = [
{'candidate': 'Trump', 'count': 0},
{'candidate': 'Biden', 'count': 0}
]
votes_collection.insert_many(initial_data)
if 'answers' not in db.list_collection_names():
# Create a collection for answers
answers_collection = db['answers']
# Insert initial data for test_user
initial_answers_data = [
{'username': 'test_user', 'right_answers': 0, 'wrong_answers': 0}
]
answers_collection.insert_many(initial_answers_data)
print("Database initialized successfully.")
if __name__ == "__main__":
initialize_database()
And for our main app code:
import os
from flask import Flask, render_template, request, redirect, url_for, abort
from pymongo import MongoClient
# Getting the environment variables from the secrets file
mongo_username = os.getenv('MONGO_ROOT_USERNAME')
mongo_password = os.getenv('MONGO_ROOT_PASSWORD')
mongo_ip = os.getenv('MONGO_IP')
mongo_db = os.getenv('MONGO_DATABASE')
# Establishing connection to mongo database
mongo_uri = f'mongodb://{mongo_username}:{mongo_password}@{mongo_ip}:27017/{mongo_db}?authSource=admin'
client = MongoClient(mongo_uri)
db = client[mongo_db]
app = Flask(__name__)
# Home page
@app.route('/')
def home():
return render_template('home.html')
# Collecting the username and the vote and adding the vote to the database
@app.route('/vote', methods=['POST'])
def vote():
candidate = request.json['candidate']
data = request.get_json()
username = data['username']
if candidate not in ['Trump', 'Biden']:
abort(400)
db.votes.update_one({'candidate': candidate}, {'$inc': {'votes': 1}}, upsert=True)
return redirect(url_for('questions', username=username))
# Displaying the questions page according to the username
@app.route('/questions', methods=['GET'])
def questions():
username = request.args.get('username')
return render_template('questions.html', username=username)
# Submitting the answers and adding the answers and the username to the database
@app.route('/submit-answers', methods=['POST'])
def submit_answers():
# actual correct answers
correct_answers = ['Trump', 'Trump', 'Trump', 'Trump', 'Biden', 'Trump', 'Biden', 'Trump', 'Biden', 'Biden']
user_answers = request.form.to_dict()
username = user_answers.pop('username') # retrieve and remove the username from the answers
right_answers = sum(user_answer == correct_answer for user_answer, correct_answer in zip(user_answers.values(), correct_answers))
wrong_answers = len(user_answers) - right_answers
db.answers.insert_one({'username': username, 'right_answers': right_answers, 'wrong_answers': wrong_answers})
return redirect(url_for('results'))
# Getting the results of the votes and the answers
@app.route('/results', methods=['GET'])
def results():
trump_doc = db.votes.find_one({'candidate': 'Trump'})
biden_doc = db.votes.find_one({'candidate': 'Biden'})
# Getting the votes for each candidate or setting it to 0 if it is the first vote
trump_votes = trump_doc['votes'] if trump_doc else 0
biden_votes = biden_doc['votes'] if biden_doc else 0
users_data = list(db.answers.find({'username': {'$ne': 'test_user'}}))
return render_template('results.html', trump_votes=trump_votes, biden_votes=biden_votes, users_data=users_data)
if __name__ == '__main__':
app.run(debug=True)
Docker
Docker Engine is an open source containerization technology for building and containerizing your applications.
In our project we will use docker to containerazie our app and make use of it later in our k8s environment. We will use docker hub to save our image.
Creating Dockerfile
FROM python:slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV MONGO_ROOT_USERNAME=# your username
ENV MONGO_ROOT_PASSWORD=# your password
ENV MONGO_IP=# your mongodb ip
ENV MONGO_DATABASE=# your db name
EXPOSE 5000
CMD ["python", "./app.py"]
Note: Entering the env in the dockerfile isn’t best practice, but to simplify the demonstration we used it here.
Now we need to build the image.
# Run the command:
$ docker build -t vote-app-image .
# To check the new image run:
$ docker images
To push the image, run:
$ docker tag vote-app-image:latest dshwartzman5/vote-app
$ docker push dshwartzman5/vote-app
Configuring K8S and Helm
Helm is a package manager for k8s. This is our way to package all the yaml files we need to run our app, insert everything to one place, and install it. Our helm chart will contain both the mongo’s config files and the app’s as well. First of all let’s start with the files for our mongodb.
MongoDB Config Files
Namespace
This file creates as a namespace. Namespace is a logical environment that we can use to grant rules and centerlize our k8s services that are related.
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.db.namespace }}
Storage
For our db we will need to use PV (Persistant Volume) and PVC (Persistant Volume Claim).
PVs are volume plugins like Volumes but have a lifecycle independent of any individual Pod that uses the PV.
A PVC is a request for storage by a user.
# Creating a PersistentVolume
apiVersion: v1
kind: PersistentVolume
metadata:
name: mongodb-pv
namespace: {{ .Values.db.namespace }}
labels:
type: local
spec:
capacity:
storage: {{ .Values.storage.capacity }} # Storage capacity
accessModes:
- {{ .Values.storage.accessMode }} # This specifies that the volume can be mounted as read-write by a single node
hostPath:
path: "/data/db" # specifies a directory on the host node's filesystem to use for the storage (inside the container)
persistentVolumeReclaimPolicy: Retain # This specifies that the volume should not be deleted when the claim is deleted
storageClassName: {{ .Values.storage.storageClassName }}
volumeMode: Filesystem # specifies that the volume should be mounted to the Pod as a filesystem
---
# Creating a PersistentVolumeClaim
# The info should match the PersistentVolume
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc
namespace: {{ .Values.db.namespace }}
spec:
storageClassName: {{ .Values.storage.storageClassName }}
accessModes:
- {{ .Values.storage.accessMode }}
resources:
requests:
storage: {{ .Values.storage.capacity }}
ConfigMap
ConfigMaps enable you to change your application’s configuration without having to rebuild your application’s image and without exposing sensitive data in your application code. In our case our configmap creates us a new user so we will be able to login to the db from the app.
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.cm.name }}
namespace: {{ .Values.db.namespace }}
# The section to mention the key:value pare, in this file it is the name of a js script and the content of the file
data:
init.js: |
db = db.getSiblingDB('admin');
db.createUser({
user: 'root',
pwd: 'root',
roles: [{ role: 'readWriteAnyDatabase', db: 'admin' }],
});
Secrets
Secrets in Kubernetes are a way to store sensitive information like passwords, but, they are not entirely secured, because the data should be stored in a base64 encode. In our case, we use secrets so our app can get the values for our db from there.
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.secrets.name }}
namespace: {{ .Values.db.namespace }}
type: Opaque
data:
MONGO_ROOT_USERNAME: # your base64 encoded username
MONGO_ROOT_PASSWORD: # your base64 encoded password
MONGO_DATABASE: # your base64 encoded db name
MONGO_IP: # your base64 encoded db ip address
Service
A service in Kubernetes is enabling network communication, holds one static IP address to all the pods with the same label. Our service type is load balancer so it can get the IP from metallb. MetalLB assigns IP addresses to services (applications) within Kubernetes, providing load balancing functionality similar to what cloud providers offer.
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.db.service.name }}
namespace: {{ .Values.db.namespace }}
labels:
app: {{ .Values.db.label }}
spec:
type: LoadBalancer
ports:
- port: {{ .Values.db.service.port }}
name: {{ .Values.db.label }}
selector:
app: {{ .Values.db.label }}
Statesfulset
There are different deployment workloads in k8s, for example: Deployment, Statefulset, DaemonSet. In our case we are using Statefulset as the kind of deployment. The StatefulSet ensures that each Pod gets a unique and stable hostname. If we delete a pod, it will return to the same place with the same id.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Values.db.label }}
namespace: {{ .Values.db.namespace }}
spec:
serviceName: {{ .Values.db.service.name }}
replicas: {{ .Values.db.replicas }}
selector:
matchLabels:
app: {{ .Values.db.label }}
template:
metadata:
labels:
app: {{ .Values.db.label }}
spec:
containers:
- name: {{ .Values.db.label }}
image: {{ .Values.db.image }}
ports:
- containerPort: {{ .Values.db.service.port }}
name: {{ .Values.db.label }}
env: # This section is defining environment variables for the containers in the pods(Taken from secrets.yaml)
- name: {{ .Values.secrets.username }}
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: {{ .Values.secrets.username }}
- name: {{ .Values.secrets.password }}
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: {{ .Values.secrets.password }}
- name: {{ .Values.secrets.database }}
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: {{ .Values.secrets.database }}
- name: {{ .Values.secrets.ip }}
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: {{ .Values.secrets.ip }}
volumeMounts: # This section is defining where volumes (persistent storage) should be mounted within the container's filesystem
- name: mongodb-data
mountPath: /data/db
- name: {{ .Values.cm.name }}
mountPath: /docker-entrypoint-initdb.d # This is a special directory that Docker checks at startup for scripts
volumes: # This section is defining the volumes that can be mounted by containers in the pod
- name: {{ .Values.cm.name }}
configMap:
name: {{ .Values.cm.name }}
volumeClaimTemplates: # This section is used to define the pvc for each pod
- metadata:
name: mongodb-data
spec:
accessModes: [ "{{ .Values.storage.accessMode }}" ]
storageClassName: {{ .Values.storage.storageClassName }}
resources:
requests:
storage: {{ .Values.db.storage.capacity }}
Application Config Files
Namespace
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.app.namespace }}
Service
apiVersion: v1 # Setting the version of the API
kind: Service # The kind of resource we are creating
metadata:
namespace: {{ .Values.app.namespace }} # The namespace where the service will be created (from the values.yaml file)
name: {{ .Values.app.service.name }} # The name of the service
labels:
app: {{ .Values.app.label }} # The label of the service (used to match the service with the deployment)
spec:
ports:
- port: {{ .Values.app.service.port }} # The port that the service will listen on
targetPort: {{ .Values.app.service.targetPort }} # The port that the service will forward requests to
selector:
app: {{ .Values.app.label }} # The label of the service (used to match the service with the deployment)
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: vote-app-deployment
namespace: {{ .Values.app.namespace }}
labels:
app: {{ .Values.app.label }}
spec:
replicas: {{ .Values.app.replicas }}
selector:
matchLabels:
app: {{ .Values.app.label }}
template:
metadata:
labels:
app: {{ .Values.app.label }}
spec:
containers:
- name: {{ .Values.app.label }}
image: "{{ .Values.app.image }}:{{ .Values.app.tag }}"
ports:
- containerPort: {{ .Values.app.service.targetPort }}
env: # Define environment variables
- name: {{ .Values.secrets.username }}
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: {{ .Values.secrets.username }}
optional: true
- name: {{ .Values.secrets.password }}
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: {{ .Values.secrets.password }}
optional: true
- name: {{ .Values.secrets.ip }}
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: {{ .Values.secrets.ip }}
optional: true
- name: {{ .Values.secrets.database }}
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: {{ .Values.secrets.database }}
optional: true
Creating Ingress
This file helps us to define the rules for the ingress. For example: the accessable domain, the port the traffic will be sent to and etc.
apiVersion: networking.k8s.io/v1 # Setting the API version
kind: Ingress # Setting the kind
metadata:
namespace: {{ .Values.app.namespace }} # Setting the namespace
name: vote-app-ingress # Setting the name of the ingress
annotations:
kubernetes.io/ingress.class: "nginx" # Setting the ingress class
spec:
rules:
- host: ds-vote-app.octopus.lab # Setting the domain (taken from /etc/hosts)
http:
paths:
- path: / # Setting the path(homepage)
pathType: Prefix
backend:
service:
name: {{ .Values.app.service.name }} # Ensuring the service matches the service name
port:
number: {{ .Values.app.service.port }} # Setting the port number traffic will be sent to
Values.yaml
This file helps us to store repeated values along the files and we can access those values from one central place.
You can modify this file as needed.
storage:
capacity: 5Gi
storageClassName: standard
accessMode: ReadWriteOnce
cm:
name: mongo-init-db
secrets:
name: mongodb-credentials
username: MONGO_ROOT_USERNAME
password: MONGO_ROOT_PASSWORD
database: MONGO_DATABASE
ip: MONGO_IP
db:
namespace: mongo
image: mongo:4.4.6
label: mongodb
replicas: 1
storage:
capacity: 5Gi
service:
name: mongodb-service
port: 27017
app:
namespace: vote-app
image: dshwartzman5/vote-app
tag: latest
label: vote-app
replicas: 1
service:
name: vote-app-service
port: 80
targetPort: 5000
Bash Script
Bash is a specific type of shell that is widely used in Unix/Linux systems, this is where the automation part begins. To deploy our app, and make use of our helm chart, we need an a few things to be set and installed first. We need to make sure we have kubectl and kind for our cluster environment. Our script will make all of that happen, so let’s dive into the script and explain each part.
Installation Functions
In this part, first we create a log file so we will send every error into there. Next, we move into the functions that install our dependencies. We start with a function that updates and upgrades the system, then we move to install kubectl, kind, helm, mail and the yq command that we will use later on. In each function we first check if the package exists and if not we perform an installation.
#!/bin/bash
# Creating log file
log_file="errors.log"
# System Update && Upgrade
system_update() {
echo "Updating system..."
sudo dnf -y update > /dev/null 2> $log_file
echo "Upgrading system..."
sudo dnf -y upgrade > /dev/null 2> $log_file
}
# Kubectl installation
kubectl_install() {
# Check if kubectl is installed and if the user wants to install it
if ! which kubectl &> /dev/null; then
read -r -p "kubectl is not installed. Do you want to install it? [y/n] " answer
if [ "$answer" == "y" ]; then
echo "Installing kubectl..."
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" > /dev/null 2> $log_file
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl.sha256" > /dev/null 2> $log_file
# Checking for successful installation
if echo "$(cat kubectl.sha256) kubectl" | sha256sum --check --quiet; then
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
if ! which kubectl &> /dev/null; then
echo "kubectl installation failed." | tee -a "$log_file"
exit 1
else
echo "kubectl installed successfully."
fi
else
echo "sha256sum check failed for kubectl. Not installing." | tee -a "$log_file"
exit 1
fi
else
echo "Exiting..."
exit 1
fi
fi
}
# Kind installation
kind_install() {
# Check if kind is installed and if the user wants to install it
if ! which kind &> /dev/null; then
read -r -p "kind is not installed. Do you want to install it? [y/n] " answer
if [ "$answer" == "y" ]; then
echo "Installing kind..."
[ "$(uname -m)" = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64 > /dev/null 2> $log_file
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
# Checking for successful installation
if ! which kind &> /dev/null; then
echo "Kind installation failed." | tee -a "$log_file"
exit 1
else
echo "Kind installed successfully."
fi
else
echo "Exiting..."
exit 1
fi
fi
}
# Helm installation
helm_install() {
# Check if helm is installed and if the user wants to install it
if ! which helm &> /dev/null; then
read -r -p "helm is not installed. Do you want to install it? [y/n] " answer
if [ "$answer" == "y" ]; then
echo "Installing helm..."
sudo dnf -y install helm > /dev/null 2> $log_file
# Checking for successful installation
if ! which helm &> /dev/null; then
echo "Helm installation failed." | tee -a "$log_file"
exit 1
else
echo "Helm installed successfully."
fi
else
echo "Exiting..."
exit 1
fi
fi
}
# Yq command installation
yq_install() {
# Check if yq is installed
if ! which yq &> /dev/null; then
pip install yq > /dev/null 2> $log_file
# Checking for successful installation
if ! which yq &> /dev/null; then
echo "Yq installation failed." >> "$log_file"
exit 1
fi
fi
}
# Mail installation
mail_install() {
# Check if mail is installed
if ! which mail &> /dev/null; then
sudo dnf -y install mailx sendmail > /dev/null 2> $log_file
# Checking for successful installation
if ! which mail &> /dev/null; then
echo "Mail installation failed." >> "$log_file"
exit 1
fi
fi
}
Creating the cluster
In this part we will create the k8s cluster, which contains one master node and one worker node. Then, we install MetalLB and Nginx ingress controller. An ingress controller is the thing that enforces ingress rules and helps to manage the traffic, ensuring traffic get smoothly to the right destination.
# Creating Cluster
create_cluster() {
echo "Creating cluster..."
# Check if the cluster already exists
if kind get clusters 2>/dev/null | grep -q 'cluster1'; then
echo "Cluster 'cluster1' already exists."
return
fi
# Create the kind config file
cat <<EOF > ~/kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: cluster1
nodes:
- role: control-plane
image: kindest/node:v1.25.3@sha256:f52781bc0d7a19fb6c405c2af83abfeb311f130707a0e219175677e366cc45d1
- role: worker
image: kindest/node:v1.25.3@sha256:f52781bc0d7a19fb6c405c2af83abfeb311f130707a0e219175677e366cc45d1
EOF
# Creating the cluster
kind create cluster --config ~/kind-config.yaml > /dev/null 2> $log_file
sleep 5
# Checking for successful creation
if ! kubectl get nodes &> /dev/null; then
echo "Cluster creation failed." | tee -a "$log_file"
exit 1
else
echo "Cluster created successfully."
fi
}
# Installing MetalLB
metalLB_install() {
echo "Installing MetalLB..."
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.7/config/manifests/metallb-native.yaml > /dev/null 2> $log_file
sleep 90
# Getting the subnet of the kind network for the MetalLB config (ipv4)
subnet=$(docker network inspect -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}' kind | grep -oP '(\d{1,3}\.){3}\d{1,3}/\d{1,2}')
# Extract the network base (First two octates) and define the IP range for MetalLB
network_base=$(echo "$subnet" | cut -d'.' -f1-2)
ip_range="$network_base.255.200-$network_base.255.250"
# Create the MetalLB config file
cat <<EOF > metallb-config.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: example
namespace: metallb-system
spec:
addresses:
- $ip_range
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: empty
namespace: metallb-system
EOF
# Apply the MetalLB config
kubectl apply -f metallb-config.yaml > /dev/null 2> $log_file
sleep 5
# Checking for successful installation
if ! kubectl get pods -n metallb-system &> /dev/null; then
echo "MetalLB installation failed." | tee -a "$log_file"
exit 1
else
echo "MetalLB installed successfully."
fi
}
# Installing Nginx Ingress Controller
nginx_ingress_install() {
echo "Installing Nginx Ingress Controller..."
helm pull oci://ghcr.io/nginxinc/charts/nginx-ingress --untar --version 1.1.3 &> /dev/null
cd nginx-ingress || exit 1
kubectl apply -f crds/ > /dev/null 2> $log_file
helm install my-release oci://ghcr.io/nginxinc/charts/nginx-ingress --version 1.1.3 > /dev/null 2> $log_file
sleep 20
# Checking for successful installation
if ! helm list -f my-release &> /dev/null; then
echo "Nginx Ingress Controller installation failed." | tee -a "$log_file"
exit 1
else
echo "Nginx Ingress Controller installed successfully."
fi
}
System check
This is the function that calles and activates all the installation process above. In addition, it checks if the scipt is running by root user or not.
# Prequsites check
system_check() {
# Check if the script is running as root
if [ "$USER" != "root" ]; then
echo "Please run the script as root."
exit 1
fi
# Preparing For Installations
system_update
# Checking for kubectl
kubectl_install
# Checking for kind
kind_install
# Checking for yq
yq_install
# Checking for helm
helm_install
# Creating cluster
create_cluster
# Installing MetalLB
metalLB_install
# Installing Nginx Ingress Controller
nginx_ingress_install
# Installing mail
mail_install
}
Dealing with replicas
In our script, we are giving the user the opportunity to choose how many replicas of the app and the db he wants. To make the necessary changes, we are using two functions, one for each replica, the app and the db.
Here we are making use of the yq command, which enables us to modify the values file in the helm chart, and change the number of replicas accordingly.
# Changing mongo replicas
change_mongo() {
# Check if the app is already installed
# grep -q '^app\s' is a regex that checks if the output of helm list starts with "app"
if helm list | grep -q '^app\s'; then
echo "App is already installed. Cannot install the app with the same name."
exit 1
fi
# Checking if the passong arg isn't another flag or empty
file="/home/ortuser19/Desktop/ex5/k8s/app/values.yaml"
answer=$1
if [[ -z "$answer" || "$answer" == -* ]]; then
echo "Option -m requires an argument."
exit 1
# Changing the number of replicas
else
echo "Changing mongo replicas to $answer..."
sleep 2
yq -Y -i ".db.replicas = $answer" "$file"
fi
}
# Changing app replicas
change_app() {
# Check if the app is already installed
if helm list | grep -q '^app\s'; then
echo "App is already installed. Cannot install the app with the same name."
exit 1
fi
# Checking if the passong arg isn't another flag or empty
file="/home/ortuser19/Desktop/ex5/k8s/app/values.yaml"
answer=$1
if [[ -z "$answer" || "$answer" == -* ]]; then
echo "Option -a requires an argument."
exit 1
# Changing the number of replicas
else
echo "Changing app replicas to $answer..."
sleep 2
yq -Y -i ".app.replicas = $answer" "$file"
fi
}
First check on the running pods
During the process of creating the app we want to make sure the app pods and the mongo pods are up, and the application in total is ready for first usage.
# Checking for running app and mongo pods
check_namespace() {
namespace=$1
pods=$(kubectl get pods -n "$namespace")
# Checking if the pods are running
if echo "$pods" | grep -q "Running"; then
if [ "$namespace" == "mongo" ]; then
echo "MongoDB is initialized."
elif [ "$namespace" == "vote-app" ]; then
echo "App is initialized."
fi
else
if [ "$namespace" == "mongo" ]; then
echo "MongoDB is not initialized." >> "$log_file"
elif [ "$namespace" == "vote-app" ]; then
echo "App is not initialized." >> "$log_file"
fi
fi
}
Creating the app
In this function we bring it all together and deploy our application. In this function we are using the functions above to deploy our app according to the user’s choice and make sure everything was ok.
# Creating app
create_app() {
# Check if the app is already installed
if helm list | grep -q '^app\s'; then
echo "App is already installed. Cannot install the app with the same name."
exit 1
fi
helm_chart="/home/ortuser19/Desktop/ex5/k8s/app/"
echo "Creating app..."
helm install app "$helm_chart" > /dev/null 2> $log_file
sleep 20
# Checking mongo
check_namespace mongo
sleep 20
#Checking app
check_namespace vote-app
sleep 3
ingress_ip=$(kubectl get ing -n vote-app | awk '{print $4}' | tail -n1)
echo "$ingress_ip ds-vote-app.octopus.lab" | sudo tee -a /etc/hosts > /dev/null
echo "App created successfully."; echo "You can access the app at http://ds-vote-app.octopus.lab."
}
Monitoring
We need to have a functions, that runs once in a while, in our case every hour, and check the logs within the pods to see if there are any errors accured. In addition, we want to be notified if we see any errors, for that we are using a mail funcion that sends an email if the error file isn’t empty.
# Sending Mail With The Erros
send_mail() {
address="dshwartzman5@gmail.com"
# Sends mail only if the log file is not empty
if [ -s "$log_file" ]; then
echo "Sending mail with the errors..."
mail -s "Errors" "$address" < "$log_file"
fi
}
# Creating monitoring function
monitor() {
while true; do
# Check if the application is running
if ! kubectl get deployment vote-app -n vote-app > /dev/null 2>&1; then
echo "The application is not running. Stopping the monitoring function."
break
fi
# Get the names of the pods
pods=$(kubectl get pods -n vote-app -o jsonpath='{.items[*].metadata.name}')
# Loop over the pods and get their logs
for pod in $pods; do
kubectl logs "$pod" -n vote-app --since=1h 2>> "$log_file"
done
# Check the log file for new entries
send_mail
# Clear the log file
echo "" > "$log_file"
sleep 3600 # wait for 1 hour
done
}
Help
It is very important to have in any bash script an help function that expalin the user how to use the script, what are the flags that can be used and general usage.
# Help
help() {
echo "Usage: $0 [ -m <mongo_replicas> ] [ -a <app_replicas> ] [ -d ]"
echo "Options:"
echo " -m <mongo_replicas> Change the number of mongo replicas."
echo " -a <app_replicas> Change the number of app replicas."
echo " -d Delete the app."
exit 1
}
Cleanup
Let’s say that after a while that we used the app, we want to delete it. For that case, we have the cleanup function that deletes the cluster and everything in it.
# Cleanup
cleanup() {
# Delete the cluster
kind delete cluster --name cluster1
sleep 5
echo "All components uninstalled successfully."
exit 0
}
Command line args and flags
This is the part in the script that will deal with the user’s input. We check the input he delivers as with falgs and arguments. To deal with flags we are using the getopts method. This a very convenient way to deal with flags in bash scripts. We have a while loop, that runs as long the input is flags, and uses a switch case methodology to activate different commands or functions according to the given flag. In addition, in this part we are calling the create_app, monitor and system_check functions.
# main
# Checking for help flag or empty input
if [ "$1" == "--help" ] || [ -z "$1" ]; then
help
fi
# Check if the first argument is -d for cleanup
if [ "$1" == "-d" ]; then
cleanup
fi
# Running system check
system_check
# Parsing the flags for other options
while getopts ":m:a:" opt; do
case ${opt} in
m)
change_mongo "$OPTARG"
;;
a)
change_app "$OPTARG"
;;
*)
echo "Invalid option: -$OPTARG"
help
;;
esac
done
# Running app
create_app
monitor &
To run the script, enter:
# Give permmisions
$ sudo chmod 700 ./app-creation
# Run the script
$ sudo ./app-creation
Expected Output
$ sudo ./app-creation.sh -m 1 -a 1
Updating system...
Upgrading system...
Creating cluster...
Cluster created successfully.
Installing MetalLB...
MetalLB installed successfully.
Installing Nginx Ingress Controller...
Nginx Ingress Controller installed successfully.
Changing mongo replicas to 1...
Changing app replicas to 1...
Creating app...
App is initialized.
App created successfully.
You can access the app at http://ds-vote-app.octopus.lab.
Summary
In this article, you learned how to take an app, deploy it with kubernetes and helm, and automate the whole process using a bash script. I hope this article was very helpful to you, I wish you as less bugs as possible and happy coding 🙂