Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
12 changes: 7 additions & 5 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Ignore cdk folder
cdk.out
.history
.tox
.git
*
!src/
!infrastructure/aws/lambda/*.py
!.python-version
!pyproject.toml
!README.md
!uv.lock
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"]
54 changes: 52 additions & 2 deletions infrastructure/aws/lambda/handler.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,62 @@
"""AWS Lambda handler."""
"""AWS Lambda handler optimized for container runtime."""

import logging
import os
import warnings
from typing import Any, Dict

from mangum import Mangum

from titiler.multidim.main import app

# Configure logging for Lambda CloudWatch integration
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")
# Suppress warnings for cleaner CloudWatch logs
warnings.filterwarnings("ignore", category=UserWarning)
warnings.filterwarnings("ignore", category=FutureWarning)

# Lambda container optimizations
os.environ.setdefault("PYTHONPATH", "/var/runtime")
os.environ.setdefault("AWS_DEFAULT_REGION", os.environ.get("AWS_REGION", "us-west-2"))

# 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

# Initialize Mangum with optimizations for Lambda containers
handler = Mangum(
app,
lifespan="off", # Disable lifespan for Lambda
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."""
# Handle the request
response = handler(event, context)

# Optional: Force garbage collection for memory management
# Uncomment if experiencing memory issues
# gc.collect()

return response


# Alias for backward compatibility and direct Mangum usage
handler.lambda_handler = lambda_handler
Loading
Loading