Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Ignore cdk folder
cdk.out
.history
.tox
.git
infrastructure/aws/cdk.out
.venv
.mypy_cache
.pytest_cache
.ruff_cache
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## unreleased

* Convert Lambda to a containerized function

## 0.3.1

* Upgrade to `titiler>=0.21,<0.22`
Expand Down
14 changes: 5 additions & 9 deletions infrastructure/aws/cdk/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from aws_cdk import aws_sns as sns
from aws_cdk import aws_sns_subscriptions as subscriptions
from aws_cdk.aws_apigatewayv2_integrations import HttpLambdaIntegration
from aws_cdk.aws_ecr_assets import Platform
from config import AppSettings, StackSettings
from constructs import Construct

Expand Down Expand Up @@ -121,19 +122,14 @@ def __init__(
role_arn=app_settings.reader_role_arn,
)

lambda_function = aws_lambda.Function(
lambda_function = aws_lambda.DockerImageFunction(
self,
f"{id}-lambda",
runtime=runtime,
code=aws_lambda.Code.from_docker_build(
path=os.path.abspath(context_dir),
code=aws_lambda.DockerImageCode.from_image_asset(
directory=os.path.abspath(context_dir),
file="infrastructure/aws/lambda/Dockerfile",
platform="linux/amd64",
build_args={
"PYTHON_VERSION": runtime.to_string().replace("python", ""),
},
platform=Platform.LINUX_AMD64,
),
handler="handler.handler",
memory_size=memory,
reserved_concurrent_executions=concurrent,
timeout=Duration.seconds(timeout),
Expand Down
88 changes: 65 additions & 23 deletions infrastructure/aws/lambda/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,37 +1,79 @@
ARG PYTHON_VERSION=3.12

FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION}
# Build stage - includes all build tools and dependencies
FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} AS builder

# Copy uv for faster dependency management
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /tmp
# Install system dependencies needed for compilation
RUN dnf install -y gcc-c++ && dnf clean all

# Install system dependencies to compile (numexpr)
RUN dnf install -y gcc-c++
# Set working directory for build
WORKDIR /build

COPY uv.lock .python-version pyproject.toml LICENSE README.md ./
# Copy dependency files first for better caching
COPY README.md uv.lock .python-version pyproject.toml ./
COPY src/titiler/ ./src/titiler/

# Install dependencies to temporary directory with Lambda-specific optimizations
RUN uv export --locked --no-editable --no-dev --extra lambda --format requirements.txt -o requirements.txt && \
uv pip install --compile-bytecode --no-binary pydantic --target /asset -r requirements.txt
uv pip install \
--compile-bytecode \
--no-binary pydantic \
--target /deps \
--no-cache-dir \
--disable-pip-version-check \
-r requirements.txt

# Aggressive cleanup to minimize size and optimize for Lambda container
WORKDIR /deps
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN <<EOF
# Convert .pyc files and remove source .py files for faster cold starts
find . -type f -name '*.pyc' | while read -r f; do n="$(echo "$f" | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//')"; cp "$f" "$n"; done
find . -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf
find . -type f -a -name '*.py' -print0 | xargs -0 rm -f
# Remove unnecessary files for Lambda runtime
find . -type d -a -name 'tests' -print0 | xargs -0 rm -rf
find . -type d -a -name 'test' -print0 | xargs -0 rm -rf
rm -rf numpy/doc/ bin/ geos_license Misc/
# Remove unnecessary locale and documentation files
find . -name '*.mo' -delete
find . -name '*.po' -delete
find . -name 'LICENSE*' -delete
find . -name 'README*' -delete
find . -name '*.md' -delete
# Strip debug symbols from shared libraries (preserve numpy.libs)
find . -type f -name '*.so*' -not -path "*/numpy.libs/*" -exec strip --strip-unneeded {} \; 2>/dev/null || true
# Create a manifest file for debugging
du -sh . > /tmp/package_size.txt
EOF

# Final runtime stage - minimal Lambda image optimized for container runtime
FROM public.ecr.aws/lambda/python:${PYTHON_VERSION}

# copy libexpat.so.1 into /asset which is included in LD_LIBRARY_PATH
RUN cp /usr/lib64/libexpat.so.1 /asset/
# Set Lambda-specific environment variables for optimal performance
ENV PYTHONPATH=${LAMBDA_RUNTIME_DIR} \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
AWS_LWA_ENABLE_COMPRESSION=true

# Reduce package size and remove useless files
RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done;
RUN cd /asset && find . -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf
RUN cd /asset && find . -type f -a -name '*.py' -print0 | xargs -0 rm -f
RUN find /asset -type d -a -name 'tests' -print0 | xargs -0 rm -rf
RUN rm -rdf /asset/numpy/doc/ /asset/bin /asset/geos_license /asset/Misc
RUN rm -rdf /asset/boto3*
RUN rm -rdf /asset/botocore*
# Copy only the cleaned dependencies from builder stage
# Copy required system library
COPY --from=builder /deps /usr/lib64/libexpat.so.1 ${LAMBDA_RUNTIME_DIR}/

# Strip debug symbols from compiled C/C++ code (except for numpy.libs!)
RUN cd /asset && \
find . -type f -name '*.so*' \
-not -path "./numpy.libs/*" \
-exec strip --strip-unneeded {} \;
# Copy application handler
COPY infrastructure/aws/lambda/handler.py ${LAMBDA_RUNTIME_DIR}/

COPY infrastructure/aws/lambda/handler.py /asset/handler.py
# Ensure handler is executable and optimize permissions
RUN <<EOF
chmod 644 "${LAMBDA_RUNTIME_DIR}"/handler.py
# Pre-compile the handler for faster cold starts
python -c "import py_compile; py_compile.compile('${LAMBDA_RUNTIME_DIR}/handler.py', doraise=True)"
# Create cache directories with proper permissions
mkdir -p /tmp/.cache && chmod 777 /tmp/.cache
EOF

CMD ["echo", "hello world"]
# Set the Lambda handler
CMD ["handler.lambda_handler"]
41 changes: 39 additions & 2 deletions infrastructure/aws/lambda/handler.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
"""AWS Lambda handler."""
"""AWS Lambda handler optimized for container runtime."""

import logging
import warnings
from typing import Any, Dict

from mangum import Mangum

from titiler.multidim.main import app

logging.getLogger("mangum.lifespan").setLevel(logging.ERROR)
logging.getLogger("mangum.http").setLevel(logging.ERROR)
logging.getLogger("botocore").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)

handler = Mangum(app, lifespan="off")
warnings.filterwarnings("ignore", category=UserWarning)
warnings.filterwarnings("ignore", category=FutureWarning)


# Pre-import commonly used modules for faster cold starts
try:
import numpy # noqa: F401
import pandas # noqa: F401
import rioxarray # noqa: F401
import xarray # noqa: F401
except ImportError:
pass

handler = Mangum(
app,
lifespan="off",
api_gateway_base_path=None,
text_mime_types=[
"application/json",
"application/javascript",
"application/xml",
"application/vnd.api+json",
],
)


def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""Lambda handler with container-specific optimizations."""
response = handler(event, context)

return response


handler.lambda_handler = lambda_handler
37 changes: 20 additions & 17 deletions infrastructure/aws/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading