Tutorial: Create a FastHTML app that runs on AWS Lambda
If you know Python, and don't want to learn JavaScript, FastHTML might be for you.
FastHTML is a lightweight Python web framework that is designed to help you create web applications in pure Python. It builds upon the principles of htmx, bridging the gap between backend simplicity and frontend interactivity.
5-Second Pitch
Are you a backend dev, data scientist, or newbie programmer who knows Python and wants to build something for the web? FastHTML is here to help you build your dreams without having to learn Javascript. Knowledge of CSS and HTML is required though!
Okay, I'm interested, tell me a little more
This example cribbed from the FastHTML homepage shows a table implemented fully in Python. FastHTML maps various HTML elements to their equivalent component forms in Python, allowing you describe your application in Python. And then using the magic of htmx, it converts these into HTML and CSS that gets sent to the browser. For the pedants in the room, yes, at the end of the day, there's Javascript involved, but that comes from the htmx library, and you don't have to worry about or interact with it. And fear not, if you want to bring in javascript at some point, you can.
So what does a minimal FastHTML app look like? After running pip install python-fasthtml
, all you need is:
# src/main.py
=
,
return
Let's put this on AWS Lambda
We'll use a slightly modified version to account for some lambda idiosyncracies, namely,
that you can't write to any other location than /tmp
and FastHTML will try to create a
.sesskey
file where its launched if we don't apass it a secret key.
= or
= ,
return
We'll build the infrastructure for this using Terraform, specifically the AWS Lambda module from serverless.tf. We'll also leverage AWS Lambda Web Adapter to make our local development story smooth and purely Docker based. This lets us migrate to ECS, EKS or even off AWS if we need to play the startup cloud credit arbitrage game. You can find all the code below and more here.
The Dockerfile
#./Dockerfile
FROM public.ecr.aws/docker/library/python:3.10-bookworm
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT=8000
WORKDIR /var/task
COPY requirements.txt ./
COPY favicon.ico ./
RUN apt-get update && \
apt-get install -y build-essential gcc && \
python -m pip install --upgrade pip setuptools && \
python -m pip install -r requirements.txt
COPY src/ ./src
EXPOSE 8000
CMD ["python", "src/main.py"]
We use the AWS Lambda Web Adapter to run the FastHTML app on Lambda without modifying the code for Lambda. Normally, a Python Lambda function requires a Lambda function handler as an entry point, but with the Web Adapter, you can build your project as usual. Simply add the Lambda Web Adapter extension to your Dockerfile. The adapter listens for incoming events, translates them, and routes them to your HTTP server (FastHTML). This lets you code normally while gaining the flexibility to dynamically switch between AWS Lambda and Fargate using tools like Lambda Flex or migrate to any service that can run containers when its right for your workload.
Setup ECR repository where the lambda docker image will live
# ./infra/main.tf
resource "random_pet" "this" {
length = 2
}
module "ecr" {
source = "terraform-aws-modules/ecr/aws"
repository_name = "${random_pet.this.id}-ecr"
repository_force_delete = true
create_lifecycle_policy = false
repository_lambda_read_access_arns = [module.lambda_function_with_docker_build_from_ecr.lambda_function_arn]
}
We used random here because I didn't particularly care about naming this, but you don't have to. You can name it something that makes sense for the project you're working on.
Notice how we use repository_lambda_read_access_arns
to ensure the lambda can pull the image during setup.
The local docker build using Terraform
# ./infra/main.tf
locals {
source_path = "../"
path_include = ["**"]
path_exclude = ["**/__pycache__/**"]
files_include = setunion([for f in local.path_include : fileset(local.source_path, f)]...)
files_exclude = setunion([for f in local.path_exclude : fileset(local.source_path, f)]...)
files = sort(setsubtract(local.files_include, local.files_exclude))
dir_sha = sha1(join("", [for f in local.files : filesha1("${local.source_path}/${f}")]))
}
module "docker_build_from_ecr" {
source = "terraform-aws-modules/lambda/aws//modules/docker-build"
ecr_repo = module.ecr.repository_name
use_image_tag = true
image_tag = local.dir_sha
source_path = local.source_path # "../"
platform = "linux/amd64"
build_args = {
FOO = "bar"
}
triggers = {
dir_sha = local.dir_sha
}
}
The key thing here is to enable use_image_tag
and set the image_tag
to the sha of the changed files.
The lambda function infra defintion
# ./infra/main.tf
resource "random_password" "session_secret" {
length = 20
special = false
}
module "lambda_function_with_docker_build_from_ecr" {
source = "terraform-aws-modules/lambda/aws"
function_name = "${random_pet.this.id}-lambda-with-docker-build-from-ecr"
description = "My FastHTML lambda function"
create_package = false # This would be true if you wanted to use Zip Package
package_type = "Image"
architectures = ["x86_64"]
image_uri = module.docker_build_from_ecr.image_uri
create_lambda_function_url = true # Use with caution. See note below.
environment_variables = {
"LIVE" = "False"
"SESSION_SECRET" = random_password.session_secret.result
}
reserved_concurrent_executions = 1 # Prevent denial of wallet attack
}
output "url" {
value = module.lambda_function_with_docker_build_from_ecr.lambda_function_url
}
We build our lambda with a lambda function url which lets you expose your AWS Lambda function via a simple dedicated endpoint. No API Gateway or Load Balancer required. While super convenient, please understand that they are security implications when using these and so they may not be appropriate for your use case. If nothing else, set a reserved concurrency you are comfortable with to account for denial of wallet attacks.
In the environment variables, we set LIVE=false
so that we don't serve the application in hot reload mode. And we also set a SESSION_SECRET
which will be used for cookie encryption.
The local development story
As mentioned earlier, we will be packaging this up as a Docker image so we have two options for our local development experience. We can run the application as a Dockerfile if we want to get as close as possible to how it will be running on Lambda. Or we can just run it locally as python script. The vscode debugging definitions below allow you to use either approach.
# .vscode/launch.json - json doesn't support comments so you'll need to delete this line
And the supporting vscode task for the docker debug option ("Docker: Python - FastHTML") is :
# .vscode/tasks.json - json doesn't support comments so you'll need to delete this line
The most important part of this task.json
is where we define the volumes and map our local source
code path to the path in the container, which, combined with our LIVE=True
env var enables hot
reloading.
To run locally, hit F5 and it will run the application without Docker. If you'd like to run it under
Docker, change the target on the debug extension to Docker: Python - FastHTML and hit F5. Make any changes
you like in main.py
and see them reflected instantly.
So back to our original goal, after testing locally, once you're ready to deploy, run terraform plan
, review your plan, and then terraform apply
.
You should get back your newly deployed lambda function's url. Enjoy your new FastHTML app!
Does serving a FastHTML app on AWS Lambda work beyond a HelloWorld app?
I don't know, I haven't got that far. I do plan to build something more interesting on FastHTML to see how it feels and form an actual opinion. If I do get around to doing so, I'll post a follow up. Curious to hear from you as well if you keep working in FastHTML, whether your deployment target ends up being Lambda or not!
Oh, one more thing, here's an example that shows FastHTML running on AWS Lambda with Bedrock and response streaming!
If you like the ideas of FastHTML, you might also like :
htpy pyscript puepy pyodide skulpt flet nicegui django-unicorn
And if you want options with paid tiers, anvil, reflex and solara might interest you