Dynamic HTML with Python, AWS Lambda, and Containers

This article is an extension of my previous article describing a similar deployment process using native AWS Lambda tools. However, Amazon since started supporting container images and updated it’s pricing policy to 1ms granularity. Both are major developments improving tooling and making small deployments cost effective.

Deploying AWS Lambda using a container

My previous article focused on the logic of the code and didn’t address how to actually deploy the function because that was well covered by AWS in its many tutorials. Here I explore the new the container deployment options while keeping all business logic untouched. Please review the AWS tutorial on deploying a generic Python Lambda code using containers which I leveraged below.

1. Dockerfile

FROM public.ecr.aws/lambda/python:3.8
RUN mkdir -p /mnt/app
ADD app.py /mnt/app
ADD index.html /mnt/app
WORKDIR /mnt/app
RUN pip install --upgrade pip
RUN pip install Jinja2==2.11.*
CMD ["/mnt/app/app.handler"]

I am using the AWS base image because it is packaged with a very nice mini server that simulates function responses when developing locally. This is extremely useful because we can call the function with 100s of arguments and verify that it behaves as expected before deployed.

App code

From the Dockerfile, we can see that all application code is contained in two files:

1) app.py:

import os
from jinja2 import Environment, FileSystemLoader
def lambda_handler(event, context):
    env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "."), encoding="utf8"))
    my_name_from_query = False
    if event["queryStringParameters"] and "my_name" in event["queryStringParameters"]:
        my_name_from_query = event["queryStringParameters"]["my_name"]
    template = env.get_template("index.html")
    html = template.render(
        my_name=my_name_from_query
    )
    return {
        "statusCode": 200,
        "body": html,
        "headers": {
            "Content-Type": "text/html",
        }
    }

2) index.html:

index.html

app.py simply parses one argument named “my_name” from the Lambda query string and passes it to the html template as variable named “my_name”. Jinja2 then parses the variable and returns the final template.

Calling and testing the app locally

Testing the app locally is very simple thanks to the new container packaging. Simply run docker-compose -f docker-compose.yml up, where docker-compose.yml file is defined as:

version: '3'
services:
  cont_name:
    container_name: cont_name
    image: cont_name_img
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/mnt/app
    ports:
      - "9000:8080"
    stdin_open: true
    tty: true
    restart: always

This stands up the function locally on a simple AWS-provided server. We can send requests and monitor responses using Python code such as:

import requests
r = requests.get(
    "http://localhost:9000/2015-03-31/functions/function/invocations", 
    data=open("event.json", "rb")
)
print(r.json())

where “event.json” is any .json file we wish to send to the lambda function as arguments. In the example case above, we would send something like:

{
  "queryStringParameters": {
    "my_name": "Adam"
  }
}

Cost

The simple AWS base server returns responses such as the one below. This is where we can see the significant impact of the new 1ms pricing update. The cost of running this example code is about 9ms which is very small considering that we are returning a full html template to browsers. However, previously AWS would charge for the full 100ms because that was the minimum charge defined. Now, this function could cost nearly 90% less!

Lambda duration