Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ ENV/
# mypy
.mypy_cache/

infrastructure/aws/cdk.out/
cdk.out/
node_modules
cdk.context.json
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 --platform=linux/amd64 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
RUN cd /deps && \
# Convert .pyc files and remove source .py files for faster cold starts
find . -type f -name '*.pyc' | while read 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

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

# Set Lambda-specific environment variables for optimal performance
ENV PYTHONPATH=${LAMBDA_RUNTIME_DIR} \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
AWS_LWA_ENABLE_COMPRESSION=true

# Copy only the cleaned dependencies from builder stage
COPY --from=builder /deps ${LAMBDA_RUNTIME_DIR}/

# copy libexpat.so.1 into /asset which is included in LD_LIBRARY_PATH
RUN cp /usr/lib64/libexpat.so.1 /asset/
# Copy required system library
COPY --from=builder /usr/lib64/libexpat.so.1 ${LAMBDA_RUNTIME_DIR}/

# 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 package size manifest for debugging
COPY --from=builder /tmp/package_size.txt /tmp/

# 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 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

CMD ["echo", "hello world"]
# Set the Lambda handler
CMD ["handler.lambda_handler"]
69 changes: 67 additions & 2 deletions infrastructure/aws/lambda/handler.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,77 @@
"""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",
],
)

# Global variables for connection reuse across invocations
_connections_initialized = False


def _initialize_connections() -> None:
"""Initialize connections that can be reused across Lambda invocations."""
global _connections_initialized
if not _connections_initialized:
# Force initialization of any global connections/pools here
# This helps with subsequent invocation performance
_connections_initialized = True


def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""Lambda handler with container-specific optimizations."""
# Initialize connections on first invocation
_initialize_connections()

# 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