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.
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:
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!