04: Writing an API
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.
-
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
-
Click on
Run Menu
>Run Without Debugging
. This will update venv with new packages. -
Create a folder
src
in root of project. (Not inside function folderfunctiondemo
) -
Create a folder
api
in src. -
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
-
__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()
-
We now need to tell Azure Function to map incoming request to flask-app as created above. Lets update
functiondemo/__init__.py
withWsgiMiddleware
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 )
-
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 belowroute
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" } ] }
-
Run Function locally again. This will now expose api endpoint as
http://localhost:7071/api/{*route}
-
Let us remove
/api
from URL. To do so, we need to updatehost.json
to removeroutePrefix
.{ "version": "2.0", "logging": { "applicationInsights": { "samplingSettings": { "isEnabled": true, "excludedTypes": "Request" } } }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[3.*, 4.0.0)" }, "extensions": { "http": { "routePrefix": "" } } }
-
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 usehttp://localhost:7071/health
-
Push this code to origin to deploy in Azure Function App.
-
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