├── .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 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 11 | 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 | --------------------------------------------------------------------------------