├── .gitignore
├── .idea
├── .gitignore
├── inspectionProfiles
│ └── profiles_settings.xml
├── modules.xml
├── misc.xml
├── kubernetes-api.iml
└── aws.xml
├── requirements.txt
├── flaskapi-secrets.yml
├── Dockerfile
├── mysql-pv.yml
├── flaskapp-deployment.yml
├── mysql-deployment.yml
├── README.md
└── flaskapi.py
/.gitignore:
--------------------------------------------------------------------------------
1 | /venv
2 | .idea
3 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==1.0.3
2 | Flask-MySQL==1.4.0
3 | PyMySQL==0.9.3
4 | uWSGI==2.0.17.1
5 | mysql-connector-python
6 | cryptography
--------------------------------------------------------------------------------
/flaskapi-secrets.yml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Secret
4 | metadata:
5 | name: flaskapi-secrets
6 | type: Opaque
7 | data:
8 | db_root_password: YWRtaW4=
9 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/kubernetes-api.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6-slim
2 |
3 | RUN apt-get clean \
4 | && apt-get -y update
5 |
6 | RUN apt-get -y install \
7 | nginx \
8 | python3-dev \
9 | build-essential
10 |
11 | WORKDIR /app
12 |
13 | COPY requirements.txt /app/requirements.txt
14 | RUN pip install -r requirements.txt --src /usr/local/src
15 |
16 | COPY . .
17 |
18 | EXPOSE 5000
19 | CMD [ "python", "flaskapi.py" ]
20 |
--------------------------------------------------------------------------------
/mysql-pv.yml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolume
3 | metadata:
4 | name: mysql-pv-volume
5 | labels:
6 | type: local
7 | spec:
8 | storageClassName: manual
9 | capacity:
10 | storage: 2Gi
11 | accessModes:
12 | - ReadWriteOnce
13 | hostPath:
14 | path: "/mnt/data"
15 | ---
16 | apiVersion: v1
17 | kind: PersistentVolumeClaim
18 | metadata:
19 | name: mysql-pv-claim
20 | spec:
21 | storageClassName: manual
22 | accessModes:
23 | - ReadWriteOnce
24 | resources:
25 | requests:
26 | storage: 2Gi
--------------------------------------------------------------------------------
/.idea/aws.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/flaskapp-deployment.yml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apps/v1
3 | kind: Deployment
4 | metadata:
5 | name: flaskapi-deployment
6 | labels:
7 | app: flaskapi
8 | spec:
9 | replicas: 3
10 | selector:
11 | matchLabels:
12 | app: flaskapi
13 | template:
14 | metadata:
15 | labels:
16 | app: flaskapi
17 | spec:
18 | containers:
19 | - name: flaskapi
20 | image: flask-api
21 | imagePullPolicy: Never
22 | ports:
23 | - containerPort: 5000
24 | env:
25 | - name: db_root_password
26 | valueFrom:
27 | secretKeyRef:
28 | name: flaskapi-secrets
29 | key: db_root_password
30 | - name: db_name
31 | value: flaskapi
32 |
33 | ---
34 | apiVersion: v1
35 | kind: Service
36 | metadata:
37 | name: flask-service
38 | spec:
39 | ports:
40 | - port: 5000
41 | protocol: TCP
42 | targetPort: 5000
43 | selector:
44 | app: flaskapi
45 | type: LoadBalancer
--------------------------------------------------------------------------------
/mysql-deployment.yml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apps/v1
3 | kind: Deployment
4 | metadata:
5 | name: mysql
6 | labels:
7 | app: db
8 | spec:
9 | replicas: 1
10 | selector:
11 | matchLabels:
12 | app: db
13 | template:
14 | metadata:
15 | labels:
16 | app: db
17 | spec:
18 | containers:
19 | - name: mysql
20 | image: mysql
21 | imagePullPolicy: Never
22 | env:
23 | - name: MYSQL_ROOT_PASSWORD
24 | valueFrom:
25 | secretKeyRef:
26 | name: flaskapi-secrets
27 | key: db_root_password
28 | ports:
29 | - containerPort: 3306
30 | name: db-container
31 | volumeMounts:
32 | - name: mysql-persistent-storage
33 | mountPath: /var/lib/mysql
34 | volumes:
35 | - name: mysql-persistent-storage
36 | persistentVolumeClaim:
37 | claimName: mysql-pv-claim
38 |
39 |
40 | ---
41 | apiVersion: v1
42 | kind: Service
43 | metadata:
44 | name: mysql
45 | labels:
46 | app: db
47 | spec:
48 | ports:
49 | - port: 3306
50 | protocol: TCP
51 | name: mysql
52 | selector:
53 | app: db
54 | type: NodePort
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deploying a Flask API and MySQL server on Kubernetes
2 |
3 | This repo contains code that
4 | 1) Deploys a MySQL server on a Kubernetes cluster
5 | 2) Attaches a persistent volume to it, so the data remains contained if pods are restarting
6 | 3) Deploys a Flask API to add, delete and modify users in the MySQL database
7 |
8 | ## Prerequisites
9 | 1. Have `Docker` and the `Kubernetes CLI` (`kubectl`) installed together with `Minikube` (https://kubernetes.io/docs/tasks/tools/)
10 |
11 | ## Getting started
12 | 1. Clone the repository
13 | 2. Configure `Docker` to use the `Docker daemon` in your kubernetes cluster via your terminal: `eval $(minikube docker-env)`
14 | 3. Pull the latest mysql image from `Dockerhub`: `Docker pull mysql`
15 | 4. Build a kubernetes-api image with the Dockerfile in this repo: `Docker build . -t flask-api`
16 |
17 | ## Secrets
18 | `Kubernetes Secrets` can store and manage sensitive information. For this example we will define a password for the
19 | `root` user of the `MySQL` server using the `Opaque` secret type. For more info: https://kubernetes.io/docs/concepts/configuration/secret/
20 |
21 | 1. Encode your password in your terminal: `echo -n super-secret-passwod | base64`
22 | 2. Add the output to the `flakapi-secrets.yml` file at the `db_root_password` field
23 |
24 | ## Deployments
25 | Get the secrets, persistent volume in place and apply the deployments for the `MySQL` database and `Flask API`
26 |
27 | 1. Add the secrets to your `kubernetes cluster`: `kubectl apply -f flaskapi-secrets.yml`
28 | 2. Create the `persistent volume` and `persistent volume claim` for the database: `kubectl apply -f mysql-pv.yml`
29 | 3. Create the `MySQL` deployment: `kubectl apply -f mysql-deployment.yml`
30 | 4. Create the `Flask API` deployment: `kubectl apply -f flaskapp-deployment.yml`
31 |
32 | You can check the status of the pods, services and deployments.
33 |
34 | ## Creating database and schema
35 | The API can only be used if the proper database and schemas are set. This can be done via the terminal.
36 | 1. Connect to your `MySQL database` by setting up a temporary pod as a `mysql-client`:
37 | `kubectl run -it --rm --image=mysql --restart=Never mysql-client -- mysql --host mysql --password=`
38 | make sure to enter the (decoded) password specified in the `flaskapi-secrets.yml`
39 | 2. Create the database and table
40 | 1. `CREATE DATABASE flaskapi;`
41 | 2. `USE flaskapi;`
42 | 3. `CREATE TABLE users(user_id INT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(255), user_email VARCHAR(255), user_password VARCHAR(255));`
43 |
44 | ## Expose the API
45 | The API can be accessed by exposing it using minikube: `minikube service flask-service`. This will return a `URL`. If you paste this to your browser you will see the `hello world` message. You can use this `service_URL` to make requests to the `API`
46 |
47 | ## Start making requests
48 | Now you can use the `API` to `CRUD` your database
49 | 1. add a user: `curl -H "Content-Type: application/json" -d '{"name": "", "email": "", "pwd": ""}' /create`
50 | 2. get all users: `curl /users`
51 | 3. get information of a specific user: `curl /user/`
52 | 4. delete a user by user_id: `curl -H "Content-Type: application/json" /delete/`
53 | 5. update a user's information: `curl -H "Content-Type: application/json" -d {"name": "", "email": "", "pwd": "", "user_id": } /update`
54 |
--------------------------------------------------------------------------------
/flaskapi.py:
--------------------------------------------------------------------------------
1 | """Code for a flask API to Create, Read, Update, Delete users"""
2 | import os
3 | from flask import jsonify, request, Flask
4 | from flaskext.mysql import MySQL
5 |
6 | app = Flask(__name__)
7 |
8 | mysql = MySQL()
9 |
10 | # MySQL configurations
11 | app.config["MYSQL_DATABASE_USER"] = "root"
12 | app.config["MYSQL_DATABASE_PASSWORD"] = os.getenv("db_root_password")
13 | app.config["MYSQL_DATABASE_DB"] = os.getenv("db_name")
14 | app.config["MYSQL_DATABASE_HOST"] = os.getenv("MYSQL_SERVICE_HOST")
15 | app.config["MYSQL_DATABASE_PORT"] = int(os.getenv("MYSQL_SERVICE_PORT"))
16 | mysql.init_app(app)
17 |
18 |
19 | @app.route("/")
20 | def index():
21 | """Function to test the functionality of the API"""
22 | return "Hello, world!"
23 |
24 |
25 | @app.route("/create", methods=["POST"])
26 | def add_user():
27 | """Function to create a user to the MySQL database"""
28 | json = request.json
29 | name = json["name"]
30 | email = json["email"]
31 | pwd = json["pwd"]
32 | if name and email and pwd and request.method == "POST":
33 | sql = "INSERT INTO users(user_name, user_email, user_password) " \
34 | "VALUES(%s, %s, %s)"
35 | data = (name, email, pwd)
36 | try:
37 | conn = mysql.connect()
38 | cursor = conn.cursor()
39 | cursor.execute(sql, data)
40 | conn.commit()
41 | cursor.close()
42 | conn.close()
43 | resp = jsonify("User created successfully!")
44 | resp.status_code = 200
45 | return resp
46 | except Exception as exception:
47 | return jsonify(str(exception))
48 | else:
49 | return jsonify("Please provide name, email and pwd")
50 |
51 |
52 | @app.route("/users", methods=["GET"])
53 | def users():
54 | """Function to retrieve all users from the MySQL database"""
55 | try:
56 | conn = mysql.connect()
57 | cursor = conn.cursor()
58 | cursor.execute("SELECT * FROM users")
59 | rows = cursor.fetchall()
60 | cursor.close()
61 | conn.close()
62 | resp = jsonify(rows)
63 | resp.status_code = 200
64 | return resp
65 | except Exception as exception:
66 | return jsonify(str(exception))
67 |
68 |
69 | @app.route("/user/", methods=["GET"])
70 | def user(user_id):
71 | """Function to get information of a specific user in the MSQL database"""
72 | try:
73 | conn = mysql.connect()
74 | cursor = conn.cursor()
75 | cursor.execute("SELECT * FROM users WHERE user_id=%s", user_id)
76 | row = cursor.fetchone()
77 | cursor.close()
78 | conn.close()
79 | resp = jsonify(row)
80 | resp.status_code = 200
81 | return resp
82 | except Exception as exception:
83 | return jsonify(str(exception))
84 |
85 |
86 | @app.route("/update", methods=["POST"])
87 | def update_user():
88 | """Function to update a user in the MYSQL database"""
89 | json = request.json
90 | name = json["name"]
91 | email = json["email"]
92 | pwd = json["pwd"]
93 | user_id = json["user_id"]
94 | if name and email and pwd and user_id and request.method == "POST":
95 | # save edits
96 | sql = "UPDATE users SET user_name=%s, user_email=%s, " \
97 | "user_password=%s WHERE user_id=%s"
98 | data = (name, email, pwd, user_id)
99 | try:
100 | conn = mysql.connect()
101 | cursor = conn.cursor()
102 | cursor.execute(sql, data)
103 | conn.commit()
104 | resp = jsonify("User updated successfully!")
105 | resp.status_code = 200
106 | cursor.close()
107 | conn.close()
108 | return resp
109 | except Exception as exception:
110 | return jsonify(str(exception))
111 | else:
112 | return jsonify("Please provide id, name, email and pwd")
113 |
114 |
115 | @app.route("/delete/")
116 | def delete_user(user_id):
117 | """Function to delete a user from the MySQL database"""
118 | try:
119 | conn = mysql.connect()
120 | cursor = conn.cursor()
121 | cursor.execute("DELETE FROM users WHERE user_id=%s", user_id)
122 | conn.commit()
123 | cursor.close()
124 | conn.close()
125 | resp = jsonify("User deleted successfully!")
126 | resp.status_code = 200
127 | return resp
128 | except Exception as exception:
129 | return jsonify(str(exception))
130 |
131 |
132 | if __name__ == "__main__":
133 | app.run(host="0.0.0.0", port=5000)
134 |
--------------------------------------------------------------------------------