Now we have Azure Function App with a Function (API) running. Lets us know write our API. There is no set rules or patterns to develop APIs on Azure Function App. One can have one-function-one-api or we can use popular framework like Flask or FastApi integrated with Azure Function. For this tutorial we will use Flask as that can help us migrate existing Flask App running on Virtual Machine or any Host to Azure Function.
  1. Let us add required modules in requirements.txt. We may not use all of them in this steps but let just add it as they are required as we move ahead.

    azure-functions
    click
    tabulate
    PyYaml
    jsonschema
    requests
    azure-identity
    azure-keyvault-secrets
    flask
    pyjwt
    flask-cors
    flask-restful
    python-box
    werkzeug
    opencensus-ext-azure
    opencensus-ext-logging
    opencensus-ext-flask
    opencensus-ext-requests
    opencensus-extension-azure-functions
    
  2. Click on Run Menu > Run Without Debugging. This will update venv with new packages.

  3. Create a folder src in root of project. (Not inside function folder functiondemo)

  4. Create a folder api in src.

  5. Under src/api, create a python file __init__.py

    .
    ├── README.md
    ├── host.json
    ├── local.settings.json
    ├── requirements.txt
    ├── functiondemo
    │   ├── __init__.py
    │   ├── function.json
    │   └── sample.dat
    └── src
        └── api
            └── app.py
    
  6. __init__.py contains Flask code. It is very much similar to any flask code that we write.

    import time
    
    import requests
    from flask import Flask, jsonify, request, Response
    from flask import g
    from flask_cors import CORS, cross_origin
    import logging
    
    app = Flask(__name__)
    cors = CORS(origins='*', allow_headers=['Content-Type', 'Authorization'])
    
    logger = logging.getLogger(__name__)
    
    
    @app.before_request
    def before_request():
        g.start = time.time()
    
    
    @app.after_request
    def response_logger(response: Response):
        response_time = round(time.time() - g.start, 3) * 1000
        if request.headers.getlist("X-Forwarded-For"):
            ip = request.headers.getlist("X-Forwarded-For")[0]
        else:
            ip = request.remote_addr
        remote_addr = "-" if ip is None else ip
        remote_user = "-" if request.remote_user is None else request.remote_user
        method = request.method
        scheme = request.scheme
        query_string = "-" if request.query_string is None else request.query_string
        url = request.url
        status = response.status
        referrer = request.referrer
        user_agent = request.user_agent
        logger.info(f"addr={remote_addr} user={remote_user} method={method} scheme={scheme} url={url} "
                    f"query_string={query_string} referrer={referrer} user_agent={user_agent} "
                    f"response_time_ms={response_time}, status={status}")
        return response
    
    
    @app.errorhandler(404)
    def handle_not_found(ex):
        return jsonify(message=f"Path {request.path} not found"), 404
    
    
    @app.errorhandler(405)
    def handle_not_found(ex):
        return jsonify(message=f"Method {request.method} not allowed on {request.path}"), 405
    
    
    @app.route("/health", methods=['GET', 'HEAD'])
    def health():
        logger.info("Checking health of the function.")
        try:
            res = requests.get("https://www.linkedin.com", timeout=10)
            if res.status_code == 200:
                response = "Health Check Ok"
                status_code = res.status_code
            else:
                response = "Health Check Failed"
                status_code = res.status_code
            return jsonify(message=response), status_code
        except Exception as error:
            return jsonify(message=str(error)), 500
    
    
    @app.route("/vault", methods=['GET', 'HEAD'])
    @cross_origin(cors)
    def get_secret():
        args = request.args
        secret = args.get("secret")
        return jsonify(message=f"Vault Integration is currently not implemented to get value for {secret}"), 200
    
    
    if __name__ == '__main__':
        app.run()
    
  7. We now need to tell Azure Function to map incoming request to flask-app as created above. Lets update functiondemo/__init__.py with WsgiMiddleware

    import json
    
    import azure.functions as func
    import logging
    from src.api import app as application
    
    __version__ = "0.0.1"
    logger = logging.getLogger(__name__)
    
    
    def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
        logger.info("Receiving request for main handler.")
        try:
            http_response = func.WsgiMiddleware(application.wsgi_app).handle(req, context)
            http_response.headers["x-function-invocationId"] = context.invocation_id
            http_response.headers["x-function-version"] = __version__
            return http_response
        except Exception as error:
            logger.exception(str(error))
            return func.HttpResponse(
                json.dumps({"message": str(error)}),
                mimetype="application/json",
                status_code=504
            )
    
  8. As we are now not having 1-1 relationship between Azure Function and routes i.e. A Single Function is handling multiple routes as defined in flask app, we need to update Function config to handle this. In functiondemo/function.json, add below route details

    {
      "scriptFile": "__init__.py",
      "bindings": [
        {
          "authLevel": "anonymous",
          "type": "httpTrigger",
          "direction": "in",
          "name": "req",
          "methods": [
            "get",
            "post",
            "head"
          ],
          "route": "{*route}"
        },
        {
          "type": "http",
          "direction": "out",
          "name": "$return"
        }
      ]
    }
    
  9. Run Function locally again. This will now expose api endpoint as http://localhost:7071/api/{*route}

  10. Let us remove /api from URL. To do so, we need to update host.json to remove routePrefix.

    {
      "version": "2.0",
      "logging": {
        "applicationInsights": {
          "samplingSettings": {
            "isEnabled": true,
            "excludedTypes": "Request"
          }
        }
      },
      "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[3.*, 4.0.0)"
      },
      "extensions": {
        "http": {
            "routePrefix": ""
        }
      }
    }
    
  11. Re-Run function locally. Now we will see that Function URL is exposed as http://localhost:7071/{*route} where {*route} is route exposed by flask app. For example, To access /health route of flask-app, we can simple use http://localhost:7071/health

  12. Push this code to origin to deploy in Azure Function App.

  13. Wait for Deployment to complete and then, test Azure Function Endpoint, https://function-demo.azurewebsites.net/health or https://function-demo.azurewebsites.net/vault?secret=demo