Serving a static site from Azure functions

May 14, 2020

If you want to just skip to the code an example project can be found on my gitlab account here.

Azure functions using Docker

Azure functions are part of the Microsoft Azure serverless compute offering.

Serverless is appealing for specific types of applications including event driven or applications which do not require a server to be running 24/7. I built a static file server using Azure functions to serve a Vuepress static site which hosts project documentation. I think for this type of application functions are well suited and cost effective. For example as at the time of writing Microsoft provides users with 1 million function requests per month for free. Another not so obvious benefit is AppServices and Functions within Microsoft Azure can make use of Active Directory authentication. Therefore you can secure your site without writing a single line of code.

The example I have provided makes use of the following project structure. I will spend the rest of this post going over each component of the project.

├── __app__
│   ├── __init__.py
│   ├── host.json
│   ├── local.settings.json
│   ├── poetry.lock
│   ├── proxies.json
│   ├── pyproject.toml
│   └── static_file_server
│       ├── function.json
│       ├── __init__.py
│       └── static
│           ├── 404.html
│           └── index.html
├── docker-compose.yml
├── Dockerfile
├── Makefile
└── README.md

Dockerfile

FROM mcr.microsoft.com/azure-functions/python:3.0-python3.8

ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

WORKDIR /home/site/wwwroot

# <start:Application Dependencies>
RUN pip install --upgrade pip
RUN pip install poetry

COPY ./__app__/pyproject.toml .
COPY ./__app__/poetry.lock .

RUN poetry config virtualenvs.create false
RUN poetry install
# <end:Application Dependencies>

COPY __app__ .

The Dockerfile is built on the mcr.microsoft.com/azure-functions/python:3.0-python3.8 image and makes use of Poetry for managing dependencies. The functions are located within the __app__ directory. This directory name is important and is currently required by Azure functions should you wish to make use of absolute imports. Once your dependencies are installed we just copy the contents of the __app__ directory into the /home/site/wwwroot directory. This directory is also important as it is the directory from which the function runtime executes your code. Local Development

# This compose file is for development use only. To deploy
# your function you can build the Docker image and deploy that on azure.

version: "3"
services:
  functions:
    build:
      context: "."
    ports:
      - "8082:80"
    volumes:
      - "./__app__/:/home/site/wwwroot"
    env_file:
      - ".private.env"
      - ".public.env"

  # Azurite storage account emulator
  storage:
    image: "mcr.microsoft.com/azure-storage/azurite"
    ports:
      - "10000:10000"
      - "10001:10001"
    volumes:
      - "./azurite:/data"
      - "./__app__/:/home/site/wwwroot"
    env_file:
      - ".private.env"
      - ".public.env"
    command: ["azurite", "-s", "-l", "/data", "--blobHost", "0.0.0.0", "--queueHost", "0.0.0.0"]

For local development the example project uses docker compose to run the function container and the Azurite storage emulator. Azure functions make use of a storage account in order to function. The example above exposes emulation for blob storage and queue. Proxies

{
  "proxies": {
    "assets": {
      "matchCondition": {
        "route": "{*path}"
      },
      "backendUri": "https://%APPLICATION_HOST%/api/static_file_server?path={path}"
    }
  }
}

Each Azure function is mounted on the api path. So for example our staticfileserver function is only available on /api/staticfileserver. We would like to access our static site from the root path of the application. To achieve this we can make use of proxies. Proxies within an Azure functions are defined by a proxies.json file. The example above proxies all requests from the root path to the static file server function. The path requested is provided to the static file server function as a query parameter. The static file server makes use of the query parameter to lookup the requested document.

One important thing to note is the APPLICATION_HOST variable within the backendUri value. This variable must be available as an environment variable when developing locally and as part of the application configuration when deployed on Azure. Static File Server Function

from __future__ import annotations

import pathlib
import logging
import mimetypes
import azure.functions as func


def main(request: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    """Serve the requested static file."""

    logging.info("Python HTTP trigger function processed a request.")

    status_code = 200
    path = request.params.get("path", "index.html")
    content_type = mimetypes.guess_type(path)[0]

    # Get the static file
    function_path = pathlib.Path(context.function_directory)
    static_path = function_path.joinpath("static")
    static_file_path = static_path.joinpath(path)

    if static_file_path.exists() is False:
        status_code = 404
        static_file_path = static_path.joinpath("404.html")
        content_type = "text/html"

    with open(static_file_path, "rb") as stream:
        return func.HttpResponse(
            stream.read(),
            status_code=status_code,
            mimetype=content_type,
            headers={"Cache-Control": "max-age-300"},
        )

The static file server function implements the following logic: