Build A Fitness Machine Learning API and Deploy to Azure using GitHub Actions CI/CD
Tags: azuredevstories, azure, python, machinelearning
Published: 2021-04-07 18:28:01


Lets Build A Fitness `Machine Learning` App and Deploy to `Azure`.

Today I am going to share how to deploy simple `Machine Learning` Model in `Azure` and will also share the challenges i faced while deploying this `python flask` app in `Azure`.

**What you will learn ?**

 You can upskill in below skills:

- Create Basic `REST API's` using `Python`.
- Basics of `Machine Learning`.
- Deploy Python `REST API's` to `Azure`.
- Setup `CI/CD` using `Github Actions`.
- Use `PostMan` to test Your ML `API`.

Lets get started....

**Prerequisite**

 - [Visual Studio Code](https://code.visualstudio.com/)

 - [Install Python 3.7](https://www.python.org/downloads/release/python-370/)

 - [Install Post Man](https://www.postman.com/downloads/)


**Install Required Python Packages**

Open your `command prompt` and execute below commands:

     1. py -m pip install flask

     2. py -m pip install -U flask-cors

     3. py -m pip install -U scikit-learn

Here we are using 3 `packages`.

- `flask` is used to create `REST API's`, which means our API can be consumed by web app , mobile app , desktop app, IOT etc. As `REST API's` return `Json` data, so it becomes easy to Deserialize to objects of any programming language.

- `flask-cors` is used to allow request from another domian, this will be usefull if your front end is deployed in different domain.

- `scikit-learn` is used for Machine Learning.

**Machine Learning Fitness DataSet:**

For training our `ML model` we are using `Dataset` from [here](https://in.pinterest.com/pin/618330223837926135/).

Basically we are going to find ideal weight to be fit based on your height and current weight. We have trained our model to predict from 5 feet to 6 feet of male dataset, you can train for other height ranges if required.

**Code Walkthrough:**

Open `VS code` inside your project folder, Create `app.py` file in `VS Code` and paste below code as required. 

First we are importing all required modules required in our `flask` code.


    from flask import Flask, jsonify, abort, request
    from werkzeug.exceptions import HTTPException
    from flask_cors import CORS
    from sklearn import tree


Second we are creating objects, constructors and configuration necessary for `app` to run.

    app = Flask(__name__)
    CORS(app)
    app.config["DEBUG"] = True


Finally we are implementing `REST API` functions, which will  be called when a `URL` is requested by client.

Below is the function , which helps us to handle errors globally in our `REST API`.

    @app.errorhandler(Exception)
    def handle_error(e):
        code = 500
        if isinstance(e, HTTPException):
            code = e.code
        return jsonify(error=str(e)), code


Next we have our simple function named as `get-message` to return `Hello World!` `Json` data, when a client initiates `GET` requests to `/api/get-message` endpoint.

    @app.route('/api/get-message', methods=['GET'])
    def getMessage():
        data = {'message': 'Hello World!'}
        return jsonify(data)

Then we have our `Machine Learning` endpoint which predicts fitness and returns `Json` data, indicating you are fit or not. :) The code is explained using comments.

    @app.route('/api/predict-fitness', methods = ['POST'])
    def predict_fitness():
        if not request.json or not 'userHeight' in request.json:
            abort(400)
    
        # Data Cleaning, remvoing dot from userHeight value
    
        userHeight = request.json.get('userHeight', "")
        userHeight = int(str(userHeight).replace('.', ''))
        userWeight = int(request.json.get('userWeight', ""))
    
        if userHeight == 51: # Just a fix because python is removing trailing zeros while coverting to str or int
           userHeight = 510
    
        # Set up training data
    
        # Expected User Inputs to classifier
    
        # Example, For a person with 5 feet height, the expected weight is 43 to 53 kg
        # So for this we mention in features like : features = [[5, 43], [5, 99],....]] 
        # which means for 5 feet height, user can enter values from 43 to 99 
        # but the expected weight will be 4353 as mentioned in labels like: labels = [4353, 4353,.....]
    
        features = [[5, 43], [5, 99], [51, 45], [51, 99], [52, 48], [52, 99], [53, 50], [53, 99], 
                    [5.4, 53], [54, 99], [55, 55], [55, 99], [56, 58], [56, 99], [57, 60], [57, 99], 
                    [58, 63], [58, 99], [59, 65], [59, 99], [510, 67], [510, 99], [511, 70], [511, 99], 
                    [6, 72], [6, 99], [6, 72], [6, 99]]
    
        # Expected output values based on user inputs or expected weight ranges based on person height
    
        labels = [4353, 4353, 4555, 4555, 4859, 4859, 5061, 5061, 5365, 5365 ,5558 ,5558, 5870,
                5870, 6074,6074 , 6376, 6376 ,6580 , 6580 ,6783 , 6783, 7085, 7085, 7289, 7289, 7289, 7289]
        
        # Train classifier
    
        classifier = tree.DecisionTreeClassifier()  # Decision tree classifier is used
        classifier = classifier.fit(features, labels)  # Find common patterns in training data
    
        # Make predictions using the trained model
    
        expectedWeight = classifier.predict([[userHeight,userWeight]])
    
        # Get first two numbers from expected Weight
    
        expectedWeight = int(expectedWeight)
        fromEpectedWeight = int(str(expectedWeight)[:2])
    
        # Get last two numbers from expected Weight
    
        toExpectedWeight = int(str(expectedWeight)[2:4])
    
        # Check if weight is in between the range of expected weight
    
        is_Weight_In_between = userWeight >= fromEpectedWeight and userWeight <= toExpectedWeight
    
        if is_Weight_In_between:
         message = f'Congratulations!, Your expected weight is in between {fromEpectedWeight} kg and {toExpectedWeight} kg.'
        else:
         message = f'Your expected weight should be in between {fromEpectedWeight} kg and {toExpectedWeight} kg.'
    
    
        fitData = {
            'isFit': is_Weight_In_between,
            'message': message
        }
    
        return jsonify( { 'fitInfo': fitData } ), 201



Finally we have below line to run the app.

       app.run(debug=True)


**Deploying Python Flask REST API to Azure:**

Next we are going to use a service in Azure known as `Azure App Service` to host our `Python Flask` App.

Login to [Azure portal](https://portal.azure.com/). Search for `Azure App Service` In the Search box and select it.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n303yxt4e5digz8qe04w.png)

Next Click **Create**, and fill details in **Basic** tab, Enter Resource Group name or create new and give a name, enter your app name.

In **Run Time Stack select Python 3.7** , Then Click **Review and Create.**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u01ii0bwmprtb9wfimal.png)

Just review your details and click finally **Create.**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/69m29lg5ciqnr5b7vgnl.png)

Then it will show you that your  **Deployment is Progress**, it may take few minutes.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t9yhq3kr9y4dg223hmat.png)

Once your Deployment is done, click  **Go to resource** which will take you to App Management blade.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6bfk3emdw7h0b5lkxps.png)

Next go to your [Github](https://github.com/) Account and create a Repository and upload the code using `Github` website or using `git` commands or even simple, just `Fork` my code repository from [here](https://github.com/shaijut/Python-ML-Fitness-API)

**Note:** As we are deploying to `Azure` so along with `app.py` file in our repo we need `requirements.txt` file which contains all required packages to be installed in `Azure App Service` for hosting as shown below: 

    Flask==1.1.2
    
    Flask-Cors==3.0.10
    
    scikit-learn==0.24.1


**Setup CI/CD with Deployment Center and Github Actions:**

Once you have your Repository and the code, then next we have to connect your Repository to `Azure Deployment` Center which provides `CI/CD` outofbox with providers like `Github Actions`, `Bitbucket` etc.

Select `Github` as source control and authorize using your `Github` credentials, then select  you Repository as below:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pxja5iaize2bgvq8p3c1.png)

In **Run Time Stack select Python 3.7**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3l4sbu65hu6aezr4i6j7.png)

Once you click **Save**, the deployment starts and a workflow file is created in your `Github` repo to start `CI/CD`, as you can see below in my `Github` repo, in `GithHub` Actions tab, the build is completed and the final deploy is in progress. Here the deploy `Job` is pushing your code changes automatically to` Azure Web App` as you make any changes to your code and push to your `Github` repo. Its that easy :)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j7p1d0vr8daf1xfqo7pv.png)

**Check your deployed REST API:**

App Service Application URL: `https://<Your app name>.azurewebsites.net/api/get-message`

If you are getting `Hello World!` as response then your `API` is successfully deployed. :)

**Test ML REST API using Post Man:**

Open `Postman` desktop app and create a new request, give a name. next we select the `Http Verb` as `POST` and paste in our API URL.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/frluwemjy7iuxw5d3khs.png)

In the `Body` we select `Raw` and `Json` and `POST` `Json` data to `API`. Paste Below Payload in Body:

    {
        "userHeight":"6",
        "userWeight":"80"
    }

Next click `Send`. Finally the `API` predicts the result and gives back the `Json` results as below:

    {
        "fitInfo": {
            "isFit": true,
            "message": "Congratulations!, Your expected weight is in between 72 kg and 89 kg."
        }
    }


Wow we are done! :smile:. Congratulations!

**My Initial struggle with Azure App Service Deployment:**

I have deployed many `.NET` apps to `Azure` but this was first time I am deploying a `Python` App to `Azure`.

After following online tutorials to deploy `Python` Apps, the `Azure` Web App was down with `502` errors. After many hours of struggle Finally got a advice in `Microsoft` doc to check `Debug console` using  `Kudu Console or the SCM Dashboard` for your app. The Link looks like below:

       https://<Your app name>.scm.azurewebsites.net/.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fq19fyq6xa81lzrawisb.png)

From the Log Stream I came to know that Azure is using `docker and containers` behind the scene and the `container` is failing to start.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ldlyt7rhxzsnuxld5kal.png)

**Finding and Killing The Root Issue:**

By Digging deeper, I came to know that **some process** is still running in port `8000` so the port is in use and thus `container` is not able to start.

So as advised in [this](https://stackoverflow.com/a/51432068) `SO` post, lets kill the process running in port `8000`, using the below `Bash` command.

      alias kill8000="fuser -k -n tcp 8000"

Finally deployed `Python REST API` to `Azure`. :smile:

**Github Link to Code:**

{% github https://github.com/shaijut/Python-ML-Fitness-API %}


PS: You can even test this API in `VS Code` and `PostMan` in your local machine without deploying to `Azure`.


If you have any doubts let me know in comments. Hope you enjoyed. :smile:.

Have a great day.


**References:**

https://dev.to/educationecosystem/a-simple-machine-learning-algorithm-to-differentiate-between-an-apple-and-an-orange-2bpk

https://dev.to/rtficial/building-your-first-restful-api-with-python-flask-1lmc

https://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask

https://programminghistorian.org/en/lessons/creating-apis-with-python-and-flask

https://stackoverflow.com/questions/25594893/how-to-enable-cors-in-flask

https://stackoverflow.com/questions/29332056/global-error-handler-for-any-exception

https://scikit-learn.org/stable/install.html

https://stackoverflow.com/questions/46008128/flask-webserver-not-reloading-on-code-change/66894350#66894350

https://stackoverflow.com/questions/37690195/azure-error-502-web-server-received-an-invalid-response-while-acting-as-a-gate

https://docs.microsoft.com/en-in/azure/app-service/troubleshoot-http-502-http-503

https://stackoverflow.com/questions/16756624/gunicorn-connection-in-use-0-0-0-0-5000

https://stackoverflow.com/questions/9168392/shell-script-to-kill-the-process-listening-on-port-3000