Skip to content

KeepCodingCloudDevops5/contenedores-kubernetes-YilisRamirez

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 

Repository files navigation

contenedores-kubernetes-YilisRamirez

GCP Project id: automatic-array-337820

Containers practice

This practice is intended to deploy a microservice that is able to read and write in a database. To implement this I built a dockerized flask application running in one container which points out toward the database running in another container.

flask app_v01

Requirements:

  • First of all install Docker engine for your OS as described here
  • You would need to install docker compose to run the application and database. You can find the steps over here
  • Python 3.7-alpine, Flask, MySQL 5.7, Docker and Docker Compose already installed
  • Docker compose installation

    Using the following command to install Docker compose. I am installing it under Linux OS.
    sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

    Applying permissions to make the binary executable

    sudo chmod +x /usr/local/bin/docker-compose

    Finally you can verify the installation by this command:

    docker-compose --version

    It will deploy the following info:

    docker-compose version 1.29.2, build 5becea4c

    Flask MySQL App

    Now I am going to create a directory called "flask-mysql-app" to store there all files needed to dockerize it through docker compose.
    mkdir flask-mysql-app

    First of all, I am going to define the app.py file to configure flask app and the database settings for establishing the communication between them. Also, there has been defined environment variables(hardcoded) for sensitive information such as, MYSQL_USER, MYSQL_PASSWORD, MYSQL_HOST and MYSQL_DB to make it configurable from only one file.

    from flask import Flask
    from flask_mysqldb import MySQL
    import os
    
    app = Flask(__name__)
    
    app.config['MYSQL_USER'] = os.environ['MYSQL_USER']
    app.config['MYSQL_PASSWORD'] = os.environ['MYSQL_PASSWORD']
    app.config['MYSQL_HOST'] = os.environ['MYSQL_HOST']
    app.config['MYSQL_DB'] = os.environ['MYSQL_DB']
    mysql = MySQL(app)
    
    
    @app.route('/create-table')
    def createtable():
        cursor = mysql.connection.cursor()
        cursor.execute(''' CREATE TABLE students(id INT NOT NULL AUTO_INCREMENT,
                                                 name VARCHAR(50) NOT NULL,
                                                 email VARCHAR(100) NOT NULL,
                                                 phone INT NOT NULL,
                                                 address VARCHAR(250) NOT NULL, PRIMARY KEY (`id`)) ''')
        cursor.close()
        return 'Tabla Creada'
    
    
    @app.route('/add-students')
    def addstudents():
        cursor = mysql.connection.cursor()
        cursor.execute(''' INSERT INTO students (id,name,email,phone,address) VALUES(1,'Pedro Romero','[email protected]',657798564,'Sant Joan DEspi');
                           INSERT INTO students (id,name,email,phone,address) VALUES(2,'Nazaret Olivieri','[email protected]',610432987,'Cornella de Llobregat'); commit; ''')
        cursor.close()
        return 'Estudiantes añadidos del primer año'
    
    
    @app.route('/')
    def students():
        s = "<table style='border:1px solid red'>"
    
        cursor = mysql.connection.cursor()
        cursor.execute(''' SELECT * FROM students; ''')
        for row in cursor.fetchall():
            s = s + "<tr>"
            for x in row:
                s = s + "<td>" + str(x) + "</td>"
            s = s + "</tr>"
    
        cursor.close()
        return "<html><body>" + s + "</body></html>"
        
    @app.route('/ping')
    def ping():
        return 'pong

    Now into the requirements file, I included the required libraries for app.py file

    flask
    flask-mysqldb

    For building the app, I have disegned a Dockerfile to add the required dependencies, software or libraries. Notice there were also included the environment variables for MySQL configuration, as it should be synchronized with the application.

    There was also introduced the variable FLASK_ENV=development, which reloads the application when it detects any change.

    dockerfile

    Creating a Multistage Docker build for Flask app

    I have adjusted the Dockerfile to make the image smaller.

    FROM python:3.7-alpine
    
    WORKDIR /app
    
    COPY requirements.txt requirements.txt
    
    RUN apk add --no-cache gcc musl-dev linux-headers curl mysql-client mysql-dev
    RUN pip install -r requirements.txt
    
    COPY . ./
    
    ENV FLASK_APP=app.py
    ENV FLASK_ENV=development
    ENV FLASK_RUN_HOST=0.0.0.0
    ENV MYSQL_USER=usuariodb
    ENV MYSQL_PASSWORD=''
    ENV MYSQL_HOST=db
    ENV MYSQL_DB=studentdb
    
    EXPOSE 5000
    
    CMD flask run
    docker build -t flask-app .
    
    docker image ls
    REPOSITORY                    TAG          IMAGE ID       CREATED          SIZE
    ramirezy/flask-app          latest       74a15c780d02   36 minutes ago     250MB

    Once built the flask image, I have uploaded it in my Docker hub repository to make it available to pull.

    Docker compose

    Now the final part is create the docker compose file, where are specified the services for flask and MySQL app, the container's image, the environments, ports, and volumenes to make it persistents for both database and flask app.

    version: '3.9'
    
    services:
      app:
        build: .
        ports:
         - 5000:5000
        depends_on:
          - db
        volumes:
          - ./app.py:/app/app.py
      db:
        image: mysql:5.7
        command: --default-authentication-plugin=mysql_native_password
        restart: always
        environment:
         MYSQL_ROOT_PASSWORD: "passw"
         MYSQL_DATABASE: "studentdb"
         MYSQL_USER: "usuariodb"
         MYSQL_PASSWORD: ""
        ports:
         - 3306:3306
        expose:
         - 3306
        volumes:
         - /my-db:/var/lib/mysql
    
    volumes:
     my-db:

    Once deployed the dockerfile, docker compose, and any other the files required, now you can run the application through the command below:

    docker-compose up

    The output should be as follows:

    docker

    Now you can see the output by typing the url shown above http://172.18.0.3:5000/ on your Unix shell terminal or your browser. To see the output of table creation, type the command:

    http://172.18.0.3:5000/create-table

    You will see the following output on your Unix terminal/browser:

    create-table

    To see the output of added students, type the command:

    http://172.18.0.3:5000/add-students

    You will see the following output on your Unix terminal/browser:

    add-students

    To see the output of table students, type the command:

    http://172.18.0.3:5000/

    You will see the following output on your Unix terminal/browser: output_db

    Verifying the logs for database and flask app are being sent to standard output (STDOUT/STDERR)

    mysql_logs

    flask_logs

    Deploying the application in Kubernetess

    First of all you will need to create a cluster in Google Cloud console to associate it to Kubernetes environment.

    Creating namespace for each manifest.

    k create ns database --dry-run -oyaml > ns.yaml
    k create ns flask-api --dry-run -oyaml > ns-flask-api.yaml
    
    k create -f ns.yaml
    k create -f ns-flask-api.yaml

    Storing database credentials in Kubernetes secrets

    The vulnerable data in the database should be stored as Base64 encoded strings thorugh the following command:

    echo -n 'rootpassword' | base64

    This command will output a string of characters, as shown below.

     c2VjcmV0MTIU=

    Once the rootpassword is encoded, we deployeed a secret file for each namespace created.

    apiVersion: v1
    kind: Secret
    metadata:
      name: mysql-secret
      namespace: database
    type: Opaque
    data:
      rootpassword: cGFzc3c=
    k create -f mysql-secret.yaml
    apiVersion: v1
    kind: Secret
    metadata:
      name: mysql-secret
      namespace: flask-api
    type: Opaque
    data:
      userpassword: c2VjcmV0MTIzNDU=
      username: dXN1YXJpb2Ri
    k create -f flaskapi-secrets.yaml

    Now let's provide persistence to the database with Persistent Volume Claim. For this is needed to check the storage classes from the cluster.

    According to the output below, it seems to be standard:

    kubectl get storageclasses.storage.k8s.io
    NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
    premium-rwo          pd.csi.storage.gke.io   Delete          WaitForFirstConsumer   true                   8d
    standard (default)   kubernetes.io/gce-pd    Delete          Immediate              true                   8d

    Now we can define the manifest for Persistent Volume Claim:

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: mysql-pv-claim
      namespace: database
    spec:
      storageClassName: standard
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 20Gi
    k create -f mysql-pvc.yaml

    As you can see Persistent Volume Claim has been successfully created and bound.

    kubectl get pvc -n database
    NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
    mysql-pv-claim   Bound    pvc-618b86aa-310f-426a-8fd1-70d650c1bb42   20Gi       RWO            standard       10h

    Additionally, we can see persistent volume is automatically created.

    kubectl get persistentvolume
    NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                     STORAGECLASS   REASON   AGE
    pvc-618b86aa-310f-426a-8fd1-70d650c1bb42   20Gi       RWO            Delete           Bound    database/mysql-pv-claim   standard                11h

    We created a configmap file for the database instance in order to make the environment variable (MYSQL_DATABASE) available in the MySQL deployment.

    apiVersion: v1
    data:
      dbname: studentdb
      host: mysql
    kind: ConfigMap
    metadata:
      name: flaskapi-cm
      namespace: database

    Deploying MySQL in Kubernetess

    Now we can run the mysql instance with a deployment workload.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: mysql
      namespace: database
    spec:
      selector:
        matchLabels:
          app: mysql
      strategy:
        type: Recreate
      template:
        metadata:
          labels:
            app: mysql
        spec:
          containers:
          - image: mysql:5.6
            name: mysql
            env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: rootpassword
            - name: MYSQL_DATABASE
              valueFrom:
                configMapKeyRef:
                  name: flaskapi-cm
                  key: dbname
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: username
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: userpassword
            ports:
            - containerPort: 3306
              name: mysql
            volumeMounts:
            - name: mysql-persistent
              mountPath: /var/lib/mysql
          volumes:
          - name: mysql-persistent
            persistentVolumeClaim:
              claimName: mysql-pv-claim

    We can see the associated pod to mysql instance, which is in status running.

    kubectl get pods -n database
    NAME                     READY   STATUS    RESTARTS   AGE
    mysql-54dccfbfbd-vslwh   1/1     Running   0          10h

    Creating a service to provide MySQL access towards Flask app or any other pod inside the cluster.

    apiVersion: v1
    kind: Service
    metadata:
      name: mysql
      namespace: database
    spec:
      ports:
      - port: 3306
      selector:
        app: mysql
      clusterIP: None
    kubectl create -f service-mysql.yaml

    Storing database credentials in Kubernetes Configmap

    I have specified the variables MYSQL_HOST and MYSQL_DB into the configmap configuration.

    apiVersion: v1
    data:
      dbname: studentdb
      host: mysql
    kind: ConfigMap
    metadata:
      creationTimestamp: null
      name: flaskapi-cm
      namespace: flask-api
    k create -f configmap.yaml

    Deploying Flask app in Kubernetess

    Now I set the environment variales in this deployment, using the values specified in the secret and configmap file. The flask image was built from Dockerfile configuration.
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: flaskapp-deployment
      namespace: flask-api
      labels:
        app: flaskapp
    spec:
      selector:
        matchLabels:
          app: flaskapp
      replicas: 1
      template:
        metadata:
          labels:
            app: flaskapp
        spec:
          containers:
            - name: flaskapp
              image: ramirezy/flask-app
              imagePullPolicy: IfNotPresent
              ports:
                - containerPort: 5000
              env:
                - name: MYSQL_HOST
                  valueFrom:
                    configMapKeyRef:
                      name: flaskapi-cm
                      key: host
                - name: MYSQL_DB
                  valueFrom:
                    configMapKeyRef:
                      name: flaskapi-cm
                      key: dbname
                - name: MYSQL_PASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: mysql-secret
                      key: userpassword
                - name: MYSQL_USER
                  valueFrom:
                    secretKeyRef:
                      name: mysql-secret
                      key: username

    Applying the flask app deployment

    kubectl create -f flaskapp-deployment.yaml
    kubectl get pods -n flask-api
    NAME                                   READY   STATUS    RESTARTS   AGE
    flaskapp-deployment-7bd7ccf9b6-h254l   1/1     Running   0          151m

    Creating a service to expose the deployment ousite of the cluster through a LoadBalancer.

    apiVersion: v1
    kind: Service
    metadata:
      name: flask-service
      namespace: flask-api
      labels:
        app: flaskapp
    spec:
      ports:
      - port: 5000
        protocol: TCP
        targetPort: 5000
      selector:
        app: flaskapp
      type: LoadBalancer
    kubectl create -f service-flask.yaml

    Now we can check all the resources deployed in each instance.

    Database instance

    kubectl get all -n database
    NAME                         READY   STATUS    RESTARTS   AGE
    pod/mysql-54dccfbfbd-vslwh   1/1     Running   0          11h
    
    NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
    service/mysql   ClusterIP   None         <none>        3306/TCP   5h34m
    
    NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/mysql   1/1     1            1           11h
    
    NAME                               DESIRED   CURRENT   READY   AGE
    replicaset.apps/mysql-54dccfbfbd   1         1         1       11h

    Flask instance

    kubectl get all -n flask-api
    NAME                                       READY   STATUS    RESTARTS   AGE
    pod/flaskapp-deployment-5fcb4ddbcf-qxj5x   1/1     Running   0          67m
    
    NAME                    TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
    service/flask-service   LoadBalancer   10.80.11.168   35.240.60.60   5000:30209/TCP   67m
    
    NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/flaskapp-deployment   1/1     1            1           67m
    
    NAME                                             DESIRED   CURRENT   READY   AGE
    replicaset.apps/flaskapp-deployment-5fcb4ddbcf   1         1         1       67m

    Creating an Ingress Resource

    You will need to install the ingress controller as per the info here here

    Now you can check the pods and resources generated by the ingress controller:

    kubectl get all --namespace=ingress-nginx
    NAME                                            READY   STATUS      RESTARTS   AGE
    pod/ingress-nginx-admission-create-wnmd2        0/1     Completed   0          4d1h
    pod/ingress-nginx-admission-patch-5kx9m         0/1     Completed   0          4d1h
    pod/ingress-nginx-controller-54d8b558d4-9xwjl   1/1     Running     0          4d1h
    
    NAME                                         TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
    service/ingress-nginx-controller             LoadBalancer   10.80.9.203    35.241.175.40   80:30588/TCP,443:30823/TCP   4d1h
    service/ingress-nginx-controller-admission   ClusterIP      10.80.14.114   <none>          443/TCP                      4d1h
    
    NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/ingress-nginx-controller   1/1     1            1           4d1h
    
    NAME                                                  DESIRED   CURRENT   READY   AGE
    replicaset.apps/ingress-nginx-controller-54d8b558d4   1         1         1       4d1h
    

    I create an ingress resource with anotation kubernetes.io/ingress.class: nginx to access Flask service

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: flask-ingress
      namespace: flask-api
      annotations:
        kubernetes.io/ingress.class: nginx
    spec:
      rules:
      - host: foo.bar.com
        http:
          paths:
          - path: '/'
            pathType: Prefix
            backend:
              service:
                name: flask-service
                port:
                  number: 5000

    Checking ingress

    k get ingress -n flask-api
    NAME            CLASS    HOSTS         ADDRESS         PORTS   AGE
    flask-ingress   <none>   foo.bar.com   35.241.175.40   80      56m

    We can check the nginx access through this IP address 32.241.175.40

    nginx_access

    As per the log below, we can see the ingress controller is enabled

    kubectl describe svc -n flask-api
    Name:                     flask-service
    Namespace:                flask-api
    Labels:                   app=flaskapp
    Annotations:              cloud.google.com/neg: {"ingress":true}
    Selector:                 app=flaskapp
    Type:                     LoadBalancer
    IP Family Policy:         SingleStack
    IP Families:              IPv4
    IP:                       10.80.1.183
    IPs:                      10.80.1.183
    LoadBalancer Ingress:     35.205.88.13
    Port:                     <unset>  5000/TCP
    TargetPort:               5000/TCP
    NodePort:                 <unset>  31506/TCP
    Endpoints:                10.76.0.17:5000
    Session Affinity:         None
    External Traffic Policy:  Cluster
    Events:                   <none>
    

    Horizontal Autoscaling

    We have implemented the following autoscaling for the application Pod in case it needs more load, then HPA will increase more pods automatically.
    apiVersion: autoscaling/v1
    kind: HorizontalPodAutoscaler
    metadata:
      name: flask-ha
      namespace: flask-api
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: flask-service
      minReplicas: 1
      maxReplicas: 10
      targetCPUUtilizationPercentage: 80

    Helm Charts

    Once we have the application deployed in Kubernetes, now we are going to package the code with Helm Charts.

    You may need to install it before to proceed, for more info you can check this Quickstart guide The working direcory of Helm chart

    └── flaskapp
        ├── charts
        ├── Chart.yaml
        ├── templates
        │   ├── configmap.yaml
        │   ├── flaskapp-deployment.yaml
        │   ├── _helpers.tpl
        │   ├── hpa.yaml
        │   ├── ingress.yaml
        │   ├── mysql-deployment.yaml
        │   ├── mysql-pvc.yaml
        │   ├── secret.yaml
        │   ├── service-flask.yaml
        │   ├── service-mysql.yaml
        │   └── tests
        └── values.yaml

    First of all, you would need to create the project name, where will be deployed the helm configuration:

    helm create flaskapp

    To build the Heml we are going to package these files:

    • configmap.yaml
    • laskapp-deployment.yaml
    • ingress.yaml
    • mysql-deployment.yaml
    • secret.yaml
    • service-flask.yaml
    • service-mysql.yaml
    • The main objects and helpers used to pass our files to metadata were as follows:

      • {{ include "flaskapp.fullname" . }}
      • {{ .Release.Namespace }}
      • {{- include "flaskapp.labels" . | nindent 4 }} --> To match the labels and selector labels
      • {{ .Values.image.name }}
      • {{- if .Values.ingress.enabled -}} --> To enable the ingress controller
      • Please have in mind while we are passing the templates in metada, we should be updating the values file to have it aligned and to make it configurable from a single file.

        # Ingress
        ingress:
          enabled: true
          annotations:
            kubernetes.io/ingress.class: nginx
          hosts:
            - host: foo.bar.com
              paths:
              - "/"
        # Secret
        db:
          rootpassword: passw
          userpassword: secret12345
          username: usuariodb
        
        # Configmap
        dbname: studentdb
        host: mysql
        
        # Deployment
        replicaCount: 1
        image:
          app: "ramirezy/flask-app:latest"
          db: "mysql:5.6"
          pullPolicy: IfNotPresent
        
        # Service
        service:
          type: "LoadBalancer"
          app: 5000
          port: 3306
        
        # Autoscaling
        autoscaling:
          enabled: true
          minReplicas: 1
          maxReplicas: 10
          targetCPUUtilizationPercentage: 80

        Once we have all the files compiled we can test it with the following commands:

        helm template --debug flaskapp

        After we have cleaned up any error we might have found, we can proceed to install the Helm chart with the command below:

        helm install project flaskapp

        You will see the output.

        NAME: project
        LAST DEPLOYED: Fri Mar 11 19:13:35 2022
        NAMESPACE: default
        STATUS: deployed
        REVISION: 1
        TEST SUITE: None

        Now we have to verify all the manifests are running as expected.

        kubectl get pod
        NAME                                    READY   STATUS    RESTARTS   AGE
        project-flaskapp-app-5788b4f85b-s68zt   1/1     Running   3          50s
        project-flaskapp-db-6744b7d55d-4rvbw    1/1     Running   0          50s
        kubectl get service
        NAME                   TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)          AGE
        chart-flaskapp-db      ClusterIP      None           <none>          3306/TCP         6h
        kubernetes             ClusterIP      10.80.0.1      <none>          443/TCP          22h
        project-flaskapp-app   LoadBalancer   10.80.15.217   35.195.175.88   3306:31510/TCP   71s
        project-flaskapp-db    ClusterIP      None           <none>          3306/TCP         71s
        kubectl get ingress
        NAME               CLASS    HOSTS         ADDRESS         PORTS   AGE
        project-flaskapp   <none>   foo.bar.com   35.241.175.40   80      92s
        kubectl get hpa
        NAME               REFERENCE                         TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
        flask-ha           Deployment/flask-service          <unknown>/80%   1         10        0          20m
        project-flaskapp   Deployment/project-flaskapp-app   <unknown>/80%   1         10        1          3m42s

        You can also get all the manifest through this command:

        helm get manifest project
        ---
        # Source: flaskapp/templates/secret.yaml
        apiVersion: v1
        kind: Secret
        metadata:
          name: project-flaskapp-secret
          namespace: default
        type: Opaque
        data:
          rootpassword: "cGFzc3c="
          userpassword: "c2VjcmV0MTIzNDU="
          username: "dXN1YXJpb2Ri"
        ---
        # Source: flaskapp/templates/configmap.yaml
        apiVersion: v1
        data:
          dbname: studentdb
          host: project-flaskapp-db
        kind: ConfigMap
        metadata:
          creationTimestamp: null
          name: project-flaskapp-cm
          namespace: default
        ---
        # Source: flaskapp/templates/mysql-pvc.yaml
        apiVersion: v1
        kind: PersistentVolumeClaim
        metadata:
          name: project-flaskapp-mysql-pv-claim
          namespace: default
        spec:
          storageClassName: standard
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 20Gi
        ---

        After deploying the helm chart, we apply a port-forward towards port 5000 to test the application:

        • http://localhost:5000/create-table
        • http://localhost:5000/add-students
        • http://localhost:5000/
        kubectl port-forward svc/project-flaskapp-app 5000:5000
        Forwarding from 127.0.0.1:5000 -> 5000
        Forwarding from [::1]:5000 -> 5000
        Handling connection for 5000
        Handling connection for 5000

        app_localhost

        This is how my kubernetes cluster looks like once whole the manifests have been deployed:

        kubernetes services kubernetes

About

Práctica contenedores kubernetes

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published