Practice 4 - Cloud Functions
In this lab, we will use Azure Cloud Functions. Azure offers Function as a Service (FaaS) as a Function App service.
In the FaaS Cloud Computing model, your application consists of functions or microservices. Cloud Functions are not constantly running in the background. They are executed only when specific user-specified events' are triggered (e.g. when a new file is uploaded, a new entry is added to the database, a new message arrives, etc. ).
You will use the existing account in Azure Cloud and set up multiple Python Cloud Functions which are automatically triggered on either web requests or database events. You will also use the Azure Cosmos DB NoSQL service as the database for your Cloud Functions.
Your task will be to create three Serverless functions:
- htmlForm - This Function will display the message board page, enable adding new messages, and display current messages in the database
- handleMessage - This will act as a backend function for entering new messages into the Cosmos DB once the user clicks the submit button on the web page.
Additional materials, tutorials, and references
- Azure Cosmos DB - https://learn.microsoft.com/en-us/azure/cosmos-db/
- Azure Functions Python reference - https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python
- Azure Functions Python examples - https://learn.microsoft.com/en-us/samples/browse/?products=azure-functions&languages=python
- Introduction to JSON - https://www.digitalocean.com/community/tutorials/an-introduction-to-json
Exercise 4.1. Creating your first Cloud Function
Let's first create a Functions App, which acts as a group or a logical container for your Cloud Functions. The rest of the functions in this lab will be created under this Function App.
- From the Azure Portal (https://portal.azure.com/), search for "Function App" or go to the Cloud Functions console directly
- Create a new Function App
- Choose Azure for Students subscription
- Choose function App name: should include your last name
- It should NOT include any special characters.
- Choose
Python
for the Runtime stack, andPython 3.9
as theVersion
- Choose North EU for the region.
- NB! If for some reason it fails to load availability zones, it may be that your ad blocker is blocking some javascript on the page. Try to disable ad blocker for Azure, or try a different browser.
- Plan type should stay Consumption (Serverless)
- Click Review + Create --> Create
- As a result, a container for functions will be created for you.
Let's now create our first function
- Under the Functions App, move to the Functions page and create new Function.
- In Development Environment , choose
Development in Portal
- In Programming Model , choose
v2 Programming Model
- Choose: HTTP trigger
- Scroll down and name it: htmlForm
- Authorization level MUST be Anonymous
- This allows anyone from the internet to access the address of the function without credentials.
- Click on Create.
- PS! It takes approximately 1 to 2 minutes for the functions to be created.
- Once the function is created, go to its page and use the Get Function Url feature to copy the link to the web function.
- Open this link in a browser to test that the generated function is working.
- Add ?name=User to the function URL to have it print out the provided user name.
Let's modify the Function code to generate the HTML document for a message board. Similar to how the previous lab's Flask-based message board application worked. This function implements the front end of our application. We can edit and test the function through our browser. We do not need to use source code repositories, but it would be best practice to use them in real-life scenarios for version control and automation of Cloud Functions.
- Open Code + Test page of the new function
- You can remove most of the code inside the
def htmlForm(...):
method. But keep onefunc.HttpResponse()
method call as an example and also thelogging.info()
method. Feel free to add additional logging statements for debugging. - Let's create an HTML page string object in Python at the start of the
def htmlForm(...):
method:
html_data = """ <html> <title>Message board</title> <body> <h4> Welcome to the message board. </h4> <h4> Enter a new message</h4> <form action="/api/handleMessage"> <label> Your message: </label><br> <input type="text" name="msg"><br> <input type="submit" value="Submit"> </form> </body> </html> """
- It contains the HTML form for submitting messages to our message board.
- Modify the function to return the HTML document content using the
func.HttpResponse()
method at the end of the htmlForm() method:
return func.HttpResponse( html_data, status_code=200, mimetype="text/html" )
- Save the function code.
- You should note that the HTML form links to an address of:
/api/handleMessage
- This is an address for another Serverless function we create in the next task.
- Be very careful with Python's code Indentation.
- Always use the same number of spaces to separate the code lines at the same level.
- Guide: https://peps.python.org/pep-0008/#indentation)
- It is easy to make mistakes when editing raw code in the web interface instead of IDE.
Let's now test that the function works
- Click on Test/Run (top row)
- Change the HTTP method to GET
- Click Run.
- In some later tasks, you may need to provide additional parameters, then you can edit the request Body or query parameters.
- The result should look something like this:
- Go back to Function Overview, get the function Url address, and open it in the browser to test that the HTML page works properly.
- The result should look something like this:
- If you click on the Submit button, it will not work properly because the HTML form links to a different function (
handleMessage
) in the SAME Function app that we have not yet created.- The second function will put the message into a Cloud database, and later we will update the currently created
htmlForm
function to also list a set of messages from the database.
- The second function will put the message into a Cloud database, and later we will update the currently created
- Go back and modify the function to also display your name somewhere inside the Web page.
Deliverable: Take a screenshot of the application's web page. The web page address should be visible.
Exercise 4.2. Setting up Cosmos DB cloud database
Before we create the second Serverless function (handleMessage
), we need to set up a Cloud database. In this task, we will use the Azure Cosmos DB - NoSQL database.
- In the top Azure portal search bar, search for Azure Cosmos DB and create a new DB.
- Choose: Azure Cosmos DB for NoSQL
- Choose the same Resource group as you used for the Function App.
- Also, choose the same Location (ex: North Europe)
- Freely choose the name for the account, but do not use any special characters.
- Keep Provisioned throughput as the Capacity mode
- After creating it, you need to wait for a few minutes until it finishes Deployment.
- We are using it as a NoSQL document database. Entries in the database are JSON documents.
- Go to the database resource and IGNORE the initial tutorial.
- Instead, go to Overview and click on the "+ Add Container" to create a new database
- Database id:
messagesdb
- Container id:
messages
- Partition key: keep
/id
- Click OK.
- Database id:
- You can browse the created database and messages table, but it will be empty for now.
- Once we create the second serverless function in the next exercise, it will start inserting messages into this table.
- Note down the connection string to connect to the Cosmos DB which is used in the functions.
- Move on to Settings-->Keys and copy the value of PRIMARY CONNECTION STRING.
Deliverable: Take a screenshot of the overview of your created cosmos DB.
Exercise 4.3. Creating a Cloud Function for posting documents to the database
In this exercise, we will create a Cloud Function for entering messages into the Cosmos database.
Adding connection string in App Settings:
- Go to your function app and let us add application settings to store the Cosmos DB connection string.
- Click on Settings-->Environment Variables-->App Settings and add the key and values. PS! In some accounts it might instead be located at Configuration-->Application settings
- Key should be:
CosmosDB
- Value should be your 'PRIMARY CONNECTION STRING value that you copied from CosmosDB in the previous task.
- Click on Apply and Confirm.
- Make sure to Save the changes in the Configuration.
- Key should be:
Azure Function App's Programming Model allows adding new functions inside the same single function code file. This means that we can append handleMessage function code next to htmlForm code inside the same Python file.
Now, let us create a handleMessage function.
- Create a new HTTP-triggered python function.
- Choose Job type to
Append to app
- Function name MUST be: handleMessage
- Make sure it is of Anonymous type (no API key or authentication needed)
- Click on Code+Test
- The code of the new function is at the bottom of the file. Find the
handleMessage
function.
We will first define a new CosmosDb output type integration for our function. We will store the messages that user provides through the HTML form in the CosmosDB database. We add a new cosmos_db_output
output binding for our function.
- We will do this by defining a new Python Decorator (
@app.cosmos_db_output()
) for integration with the Cosmos DB database we created in the previous exercise.- This decorator should be added just before the existing
@app.route
type decorator:@app.route(route="handleMessage"..)
.- Add
@app.cosmos_db_output(arg_name="outputDocument", database_name="messagesdb", container_name = "messages", create_if_not_exists=False, connection="CosmosDB")
there before the@app.route(...)
line.
- Add
- This specifies that we are creating a function output integration (binding) to Cosmos DB
- Inside the code, we should access the integration through a variable named
outputDocument
.
- Inside the code, we should access the integration through a variable named
app.cosmos_db_output
decorator is used to bind the output integration to add a document to the Cosmos Db database. It accepts the following parameters:- arg_name="outputDocument" - Specifies the Python variable name for the output stream.
- database_name indicates the name of the database inside Cosmos DB.
- container_name is the name/id of the container.
- connection is the connection string, which we added in the Application Settings of the Function App.
- This decorator should be added just before the existing
Let's now modify the def handleMessage(...)
method code to process the entered message (submitted through the HTML form) and send it to the Cosmos DB. The overall function logic is to fetch the form parameters (msg
), calculate the current time, create a JSON object, and write it to the output stream of the outputDocument
object that we just set up in the Function integration output CosmosDB object.
- First, let's modify the arguments of the
def handleMessage(req: func.HttpRequest)
method. - The arguments of
def handleMessage(...)
should include a variable for EVERY input and output stream of the function. - Initially, it only has one argument:
req: func.HttpRequest
, which is the input HTTP stream (related to the HTTP request that triggered this function). - To support the CosmosDB integration, we need to add an outputStream object with the name outputDocument (Name of this
arg_name
variable was configured in the Integration decorator) into the method arguments,- Lets add
outputDocument: func.Out[func.Document]
into the argument of the function.
- Lets add
- After modification, the
handleMessage
function arguments should look like this:def handleMessage(req: func.HttpRequest, outputDocument: func.Out[func.Document]) -> func.HttpResponse:
req
object is the input- function returns an object of
func.HttpResponse
type - But now there is also an output object called
outputDocument
(of typefunc.Document
)- Our function can use this object to submit JSON documents to the CosmosDB database.
Let's now modify the content of the function.
- You can delete all the initial code inside the function block.
- Let's first access the message entered through the HTML form:
msg = req.params.get('msg')
- Before we can enter the message as a JSON document into ConsmosDB, we also need to generate a new ID for the document:
rowKey = str(uuid.uuid4())
- Also, make sure to import the uuid library at the start of the program:
import uuid
- Also, make sure to import the uuid library at the start of the program:
- Then, we will generate a new JSON message to be entered into the DB. It should contain message content, the current timestamp, and document id.
- make sure to also import the JSON library:
import json
- make sure to also import the datetime library:
from datetime import datetime
- make sure to also import the JSON library:
- Let's create the json document to be entered into database:
json_message = { 'content': msg, 'id': rowKey, 'message_time': datetime.now().isoformat(" ", "seconds") }
- To enter the JSON document into the database, we should set it as a value of the outputDocument object
- Because it is func.Document type, we should use the
from_dict(...)
method, like this:outputDocument.set(func.Document.from_dict(json_message))
- Because it is func.Document type, we should use the
- As the function will be called through HTTP request, we should also return a response that includes a message to the user that submitting the message board message succeeded.
return func.HttpResponse( f"Entered message was: {msg}", status_code=200, mimetype="text/html" )
- To be nice, You can (but it is not required) add an HTML link back to the main page of the application.
- Link it to the "/api/htmlForm" address of the first function.
- Save and test the function.
Debugging and Monitoring Function executions
If you test manually, you must add extra parameters (msg) and test it using POST request. Or you can try to use the submit function from the address of the first function.
- In case of errors:
- If you get a 404 error while testing the function, it likely means a Python error is inside the script you are modifying.
- Check the execution logs on the Test/Run page (Limited error logs).
- Open the Monitor (Left side of the panel) page of a function, and check both the:
- Invocations page - it may take some time (5 min) until function invocation results are shown
- This page will usually show the most technical, code-level debugging messages and errors (if any)
- Logs - You will need to keep this page running for a while to see any new logs of recent function invocations.
- Invocations page - it may take some time (5 min) until function invocation results are shown
Deliverable: Take a screenshot of the application's web page. The web page address should be visible.
Exercise 4.4. Modifying the first htmlForm function to list previous messages from the database
Let's now modify the first htmlForm
function to also display the latest messages on the Web page. We need to modify two things:
- Add Cosmos DB input integration to fetch data from the database messages table/container
- Modify the code to generate a list of the messages in the resulting HTML.
Add Cosmos DB input integration to fetch data from the database messages table/container
- Go to
htmlForm
function Overview and move to Code+Test. - Add
@app.cosmos_db_input(..)
decorator just before the@app.route(route="html_form",..
line.- This should specify that now the
arg_name=inputDocument
, but other parameters are the same as we used before.@app.cosmos_db_input(arg_name="inputDocument", database_name="messagesdb", container_name = "messages", connection="CosmosDB")
- This should specify that now the
Let's now modify the def htmlForm(..)
code to add the message information into the resulting HTML.
- Add a new input argument inputDocument of type
func.DocumentList
to the definition of thehtmlForm
function:def htmlForm(req: func.HttpRequest, inputDocument: func.DocumentList) -> func.HttpResponse:
- inputDocument is (actually) a list of documents, and can be used as a list of Python dictionaries, where each JSON-like dictionary object is one message from the database
- For example:
for doc in inputDocument:
would allow us to loop over all the documents in the database (limited to 50).- Accessing the individual document parameters (e.g., content and message_time) can be done like this:
doc['content']
- Modify how the HTML is defined inside the function to generate an HTML document something like this (where the messages are dynamically fetched from the database) :
- You'll need to change how the
html_data
string object is created, as part of it needs to be generated based on the documents from the database- You can divide it into three parts:
- start of the HTML document, which contains the initial page header and title. And also the first list starting tag
<ul>
- Dynamic content, which is generated based on each messages from the database
- You can append additional string content into the
html_data
variable by using something like this:html_data += "<li> ... <li>"
for every message.
- You can append additional string content into the
- End of the HTML document, which contains the closing HTML
</ul>
tag, the HTML form, and the closing HTML tags
- start of the HTML document, which contains the initial page header and title. And also the first list starting tag
- You can divide it into three parts:
Deliverable: Take a screenshot of the application's web page. The web page address should be visible.
Deliverables
- Source code (Python file(s)) of your Cloud Functions.
- You can also find the textual source code files of your functions inside the Storage Account (That you created or selected when creating the Function App)
- Go to: Storage account -> Storage Browser -> File Shares -> name_of_the_function_app -> site -> wwwroot -> name_of_the_function
- Provide the URL to the Cloud Function web endpoint, which displays the HTML form.
- You can also find the textual source code files of your functions inside the Storage Account (That you created or selected when creating the Function App)
- Screenshots from exercises 4.2, 4.3 and 4.4.
- Additional Screenshot of your Cosmos DB database view, which should display one of the open documents and its content (with created_time visible).
- Take a LARGE screenshot, so it also displays your username/email at the top right corner of the page.
In case of issues, check these potential solutions to common issues:
- If the functions "dissapear" from the list of functions under Function App.
- It is likely there is a python error in the code, that stops the platform loading the file.
- As it takes the the functions from the python file, if it can not properly load the file, this can cause the functions not to be loaded.
- You can open the Python file through other means to fix it. Afterwars, the functions should be able to load again.
- Two ways to open the Python files:
- From the App files feature of the Function app.
- From Storage account folders
- If you get a 500 error about "index at 0", check that the CosmosDB Function app configuration string you set up contains Primary connection string (not primary key).
- CosmosDB configuration property should start with "AccountEndpoint=http..."
- If some of the changes made in integrations or inside the code are not saved properly, or get overwritten for unknown reason:
- Make sure you do not have multiple browser windows open for editing the same function code/integrations.
- If you get an error about 404 "Not Found" when trying to execute your function
- Check that there are not Python errors, missing parnethes, etc inside the code.
- Check Function integrations, if you have accidentally created two outputs or removed input.