Aws
AwsIntermediate

How to Deploy AWS Lambda Functions Using Container Images

15 min read
AWSLambdaContainersServerlessDocker

TL;DR

A step-by-step guide to creating, deploying, and managing AWS Lambda functions using container images, including best practices and optimization techniques.

Leveraging AWS Lambda Container Image Support for Enhanced Serverless Deployments

$1

AWS Lambda's container image support allows packaging functions as OCI-compliant container images up to 10GB, enabling greater flexibility in dependency management and runtime customization. This guide provides a comprehensive overview for experienced engineers on setting up, building, deploying, and optimizing containerized Lambda functions, detailing practical examples, trade-offs, and best practices for security, cost, and performance.

---

$1

AWS Lambda's container image support is a significant advancement, bridging the gap between traditional containerized applications and the serverless paradigm. By allowing deployment of functions as container images up to 10GB, it offers greater control over runtime environments, dependencies, and custom tooling, while retaining the core benefits of serverless: automatic scaling, pay-per-execution pricing, and reduced operational overhead. This article delves into the practical aspects of adopting this feature, focusing on considerations crucial for seasoned engineers.

$1

* Core Concepts: Understanding the architecture and benefits of Lambda container images versus traditional ZIP deployments.

* Environment Setup: Configuring the necessary tools and IAM permissions.

* Image Development: Strategies for building efficient, optimized container images.

* Deployment Strategies: Comparing different methods for deploying containerized Lambda functions.

* Optimization: Techniques for enhancing performance and reducing costs.

* Observability: Implementing robust monitoring and troubleshooting.

* Security: Best practices for securing your containerized Lambda functions.

$1

To effectively follow this guide, ensure you have the following:

| Tool/Resource | Minimum Version | Purpose | Notes |

| :-------------------- | :-------------- | :------------------------------------------ | :------------------------------------------------------------------------------------------------------------ |

| AWS Account | N/A | Deployment and resource management | Access to create IAM roles, ECR repositories, and Lambda functions. |

| Docker | 20.10+ | Building and managing container images | Essential for local development and image creation. |

| AWS CLI | 2.x | Interacting with AWS services | Required for ECR operations, Lambda deployment, and resource management. |

| IDE | Any | Code development and editing | VS Code, JetBrains IDEs, etc., with appropriate Docker and AWS extensions recommended. |

| Python (or chosen runtime) | 3.11+ (for examples) | Application development and testing | This guide uses Python for examples; adapt for your preferred runtime. |

| AWS SDK (e.g., Boto3) | Latest stable | Programmatic AWS interactions (optional) | Useful for managing resources or interacting with other AWS services within your Lambda function. |

---

$1

$1

| Aspect | ZIP Deployment | Container Image | Implications |

| :-------------- | :------------------------------------------------ | :------------------------------------------------- | :---------------------------------------------------------------------------------------------------------- |

| Size Limit | 250MB (unzipped, including layers) | 10GB | Accommodates larger dependencies, ML models, or custom binaries. |

| Cold Start | Generally faster | Potentially slower (image pull and initialization) | Optimize base image and dependencies. Consider Provisioned Concurrency for latency-sensitive applications. |

| Build Tools | Limited to Lambda's supported runtimes/layers | Full Docker ecosystem and tooling | Use any language, libraries, or binaries compatible with a container runtime. |

| Dependencies| Managed via Lambda Layers or bundled in ZIP | Managed via Docker layers and image structure | Leverages Docker's layer caching for efficient builds; allows for more granular dependency control. |

| Portability | Lambda-specific packaging | OCI-compliant; portable across container platforms | Images can be tested locally with Docker, promoting greater development-production parity. |

| Runtime | AWS-managed runtime (e.g., Python 3.11) | Customizable runtime environment | Full control over the OS, system libraries, and language versions. |

$1

1. Expanded Runtime Flexibility: Deploy functions with any language, custom binaries, or specific versions of system libraries not supported by standard Lambda runtimes. This is particularly useful for machine learning workloads with large model files or complex C++ dependencies.

2. Consistent Development Experience: Utilize familiar Docker tooling and workflows for building, testing, and debugging images locally. This promotes better parity between local development and the cloud environment.

3. Simplified Dependency Management: Package all dependencies within the container image. This eliminates the need for Lambda Layers for many use cases, simplifying deployment artifacts, though it can increase image size.

4. Larger Deployment Packages: The 10GB limit is a significant increase from the 250MB ZIP limit, enabling deployment of larger applications, ML models, or datasets directly within the function package.

5. Enhanced Security Posture: Leverage ECR's built-in image scanning for vulnerability detection and integrate with image signing for supply chain security.

$1

The typical workflow for Lambda container images involves:

1. Local Development: Developers build a container image locally using Docker, incorporating their application code and dependencies. The image is typically based on an AWS-provided Lambda base image (e.g., public.ecr.aws/lambda/python:3.11).

2. Build & Push: The container image is built and pushed to Amazon Elastic Container Registry (ECR). This process often involves CI/CD pipelines.

3. Lambda Deployment: AWS Lambda pulls the image from ECR and runs it as a function.

4. Event Sources: Lambda functions are triggered by various AWS services (API Gateway, S3, SQS, EventBridge, etc.).

5. Observability: Logs are sent to CloudWatch Logs, and tracing data can be collected by AWS X-Ray. CloudTrail records API calls.

``mermaid

graph TD

A[Developer Machine] --> B{Docker Build};

B --> C[Amazon ECR];

C --> D[AWS Lambda];

D -- Triggered by --> E[API Gateway];

D -- Triggered by --> F[S3 Event];

D -- Triggered by --> G[SQS Queue];

D -- Triggered by --> H[EventBridge Rule];

D -- Logs --> I[CloudWatch Logs];

D -- Traces --> J[AWS X-Ray];

C -- Audit Trail --> K[AWS CloudTrail];

`

---

$1

$1

To build and push container images to ECR, your IAM user or role requires specific permissions. These are typically granted via a managed policy or a custom policy attached to your execution role.

`json

{

"Version": "2012-10-17",

"Statement": [

{

"Effect": "Allow",

"Action": [

"ecr:GetAuthorizationToken",

"ecr:BatchCheckLayerAvailability",

"ecr:GetDownloadUrlForLayer",

"ecr:BatchGetImage",

"ecr:PutImage",

"ecr:InitiateLayerUpload",

"ecr:UploadLayerPart",

"ecr:CompleteLayerUpload",

"ecr:DescribeRepositories",

"ecr:ListImages"

],

"Resource": "*"

},

{

"Effect": "Allow",

"Action": [

"ecr:CreateRepository"

],

"Resource": "*",

"Condition": {

"StringEquals": {

"ecr:CreateRepository": "lambda-container-repo" // Replace with your desired repo name or pattern

}

}

}

]

}

`

Note: For simplicity, Resource: "*" is used. In production, scope these permissions to specific ECR repositories.

$1

When developing containerized Lambda functions, consider distinct environments:

| Environment | Base Image | Dependencies | Configuration/Secrets | Logging/Monitoring | Notes |

| :--------------- | :------------------------------ | :----------------------------------------- | :------------------------------- | :----------------------------- | :------------------------------------------------------------------------------------------------- |

| Local Dev | Any (e.g., python:3.11-slim) | Development & Production | Mocked or local variables | Console (stdout/stderr) | Focus on rapid iteration and debugging. May not perfectly mirror cloud behavior. |

| Staging/Dev | AWS Lambda Base (public.ecr.aws/lambda/python:3.11) | Production-like | AWS Secrets Manager / SSM | CloudWatch Logs (structured) | Use a separate AWS account or VPC for isolation. Test integrations and performance. |

| Production | AWS Lambda Base (public.ecr.aws/lambda/python:3.11-slim) | Production (minimal) | AWS Secrets Manager / SSM | CloudWatch Logs (structured), X-Ray | Use optimized base images. Fine-tune memory/timeout. Implement robust alerting. |

---

$1

$1

A well-organized project structure is crucial for maintainability and clarity, especially with containerized applications.

`plaintext

lambda-container-project/

├── .dockerignore # Files/directories to exclude from Docker build context

├── Dockerfile # Instructions for building the container image

├── app/ # Your application code

│ ├── __init__.py

│ ├── main.py # Lambda handler and core logic

│ ├── utils/ # Helper modules

│ │ ├── __init__.py

│ │ └── helpers.py

│ └── config/ # Configuration management

│ ├── __init__.py

│ └── settings.py

├── requirements/ # Dependency definitions

│ ├── base.txt # Core dependencies

│ ├── dev.txt # Development-specific dependencies (testing, linting)

│ └── prod.txt # Production dependencies (runtime libraries)

├── tests/ # Unit and integration tests

│ ├── __init__.py

│ ├── test_handler.py

│ └── test_utils.py

├── scripts/ # Build, deploy, and utility scripts

│ ├── build-and-push.sh # Script to build image and push to ECR

│ └── deploy.sh # Script to deploy Lambda function (e.g., via AWS CLI or SAM)

└── README.md

`

$1

The Lambda handler needs to be compatible with the Lambda runtime interface client. The AWS base images provide this. Your handler should accept event and context and return a response compatible with the invoker (e.g., API Gateway). Structured logging is highly recommended.

`python

app/main.py

import json

import logging

import os

from typing import Dict, Any

from datetime import datetime

Configure logging (e.g., using Python's built-in logging)

For structured logging, consider libraries like python-json-logger or watchtower.

logger = logging.getLogger()

log_level_str = os.environ.get("LOG_LEVEL", "INFO").upper()

log_level = getattr(logging, log_level_str, logging.INFO)

logger.setLevel(log_level)

Example of structured logging formatter

class JsonFormatter(logging.Formatter):

def format(self, record):

log_entry = {

"timestamp": self.formatTime(record, self.datefmt),

"level": record.levelname,

"message": record.getMessage(),

"module": record.module,

"funcName": record.funcName,

"lineno": record.lineno,

}

if record.exc_info:

log_entry["exc_info"] = self.formatException(record.exc_info)

return json.dumps(log_entry)

Apply the formatter

for handler in logger.handlers:

handler.setFormatter(JsonFormatter())

def process_event(event: Dict[str, Any]) -> Dict[str, Any]:

"""

Processes the incoming event.

Replace with your specific business logic.

"""

logger.info({"message": "Received event", "event_payload": event})

# Simulate some processing

processing_start_time = datetime.utcnow()

# Example: Accessing environment variable

environment = os.environ.get("ENVIRONMENT", "unknown")

# Simulate work

import time

time.sleep(0.1) # Simulate 100ms of work

processing_end_time = datetime.utcnow()

duration_ms = (processing_end_time - processing_start_time).total_seconds() * 1000

result = {

"processed_at": processing_end_time.isoformat(),

"environment": environment,

"event_data_preview": str(event)[:200], # Log a preview

"processing_duration_ms": round(duration_ms, 2)

}

logger.info({"message": "Event processed successfully", "result_preview": str(result)[:200]})

return result

def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:

"""

AWS Lambda handler function.

This function is the entry point for Lambda.

"""

# Log request details, but avoid logging sensitive information in production logs

logger.info({

"message": "Lambda function invoked",

"request_id": context.aws_request_id,

"memory_limit_mb": context.memory_limit_in_mb,

"function_name": context.function_name

})

try:

processed_result = process_event(event)

return {

'statusCode': 200,

'headers': {

'Content-Type': 'application/json'

},

'body': json.dumps({

'status': 'success',

'data': processed_result

})

}

except Exception as e:

logger.error({

"message": "An error occurred during event processing",

"error_type": type(e).__name__,

"error_details": str(e),

"traceback": logging.Formatter().formatException(e.__class__, e, e.__traceback__) # Basic traceback logging

})

return {

'statusCode': 500,

'headers': {

'Content-Type': 'application/json'

},

'body': json.dumps({

'status': 'error',

'message': f"Internal server error: {e}"

})

}

`

$1

Separate dependencies for development, testing, and production to keep production images lean.

`plaintext

requirements/base.txt

boto3==1.34.11

pydantic==2.6.1 # For data validation

requirements/dev.txt

-r base.txt

pytest==7.4.4

pytest-cov==4.1.0

black==24.1.1

mypy==1.8.0

ruff==0.1.7 # Faster linter/formatter

requirements/prod.txt

-r base.txt

Only include runtime dependencies. Avoid dev/test tools.

e.g., watchtower==3.0.1 if using CloudWatch logs via watchtower

e.g., aws-xray-sdk==2.13.0 for X-Ray tracing

`

$1

Utilize multi-stage builds to separate build dependencies from runtime dependencies, resulting in smaller, more secure production images.

`dockerfile

Stage 1: Builder

Use a specific Python version for consistency. slim variants are often preferred for size.

FROM public.ecr.aws/lambda/python:3.11-slim as builder

Set working directory

WORKDIR /app

Copy only production requirements

COPY requirements/prod.txt .

Install production dependencies.

--no-cache-dir reduces image size by not storing the pip cache.

Use --prefix to install into a specific directory that can be copied later.

RUN pip install --no-cache-dir --prefix="${LAMBDA_TASK_ROOT}/site-packages" -r prod.txt

Stage 2: Final Runtime Image

Use the official AWS Lambda runtime base image for your target architecture.

Use the -slim variant if available for smaller image size.

FROM public.ecr.aws/lambda/python:3.11-slim

Set environment variables for the Lambda runtime

ENV PYTHONUSERBASE=${LAMBDA_TASK_ROOT}

ENV PATH="${LAMBDA_TASK_ROOT}/bin:${PATH}"

ENV LOG_LEVEL=INFO

ENV ENVIRONMENT=production # Example: set default environment

Copy installed Python packages from the builder stage

COPY --from=builder "${LAMBDA_TASK_ROOT}/site-packages" "${LAMBDA_TASK_ROOT}/site-packages"

Copy application code

Ensure your app directory is correctly placed relative to LAMBDA_TASK_ROOT

COPY app/ ${LAMBDA_TASK_ROOT}/app/

Set the entry point (handler)

The format is .

CMD [ "app.main.handler" ]

`

---

$1

$1

Choosing the right deployment method depends on your team's maturity, existing CI/CD infrastructure, and desired level of automation.

| Feature | AWS Management Console | AWS CLI (aws lambda create-function, update-function-code) | Infrastructure as Code (IaC)
(CloudFormation, Terraform, CDK) | CI/CD Pipelines (Jenkins, GitLab CI, GitHub Actions, AWS CodePipeline) |

| :----------------- | :-------------------- | :--------------------------------------------------------------- | :------------------------------------------------------------------ | :------------------------------------------------------------------- |

| Automation | Manual | Scripted, semi-automated | Fully automated, declarative | Fully automated, event-driven |

| Version Control| Not directly supported| Can be managed via scripts/commit history | Excellent (templates in Git) | Excellent (pipeline definitions in Git) |

| Reproducibility| Low | Medium (scripts need careful management) | High (declarative state) | High (pipeline definitions + IaC) |

| Environment Mgmt| Manual | Scripted | Declarative (e.g., parameter files, modules) | Dynamic (e.g., pipeline variables, overlays) |

| Rollback | Manual | Scripted (versioning needed) | Supported (via IaC stack updates/rollbacks) | Supported (via pipeline strategies, IaC rollbacks) |

| Complexity | Low | Medium | High | High |

| Best For | Prototyping, one-offs | Simple deployments, scripting | Production environments, managing complex infrastructure | Continuous integration and deployment for applications |

$1

Using Infrastructure as Code (IaC) is the recommended approach for managing production deployments. This example demonstrates a basic CloudFormation template for deploying a containerized Lambda function.

`yaml

AWSTemplateFormatVersion: '2010-09-09'

Description: >

Deploys an AWS Lambda function using a container image from Amazon ECR.

Includes IAM role, Log Group, and function configuration.

Parameters:

FunctionName:

Type: String

Description: Base name for the Lambda function and associated resources.

Default: MyContainerLambda

Environment:

Type: String

Description: Deployment environment (e.g., dev, staging, prod).

Default: dev

AllowedValues: [dev, staging, prod]

ImageUri:

Type: String

Description: The ECR image URI (e.g., ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPO_NAME:TAG)

# Example: 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-lambda-repo:latest

MemorySize:

Type: Number

Description: Memory allocation for the Lambda function in MB.

Default: 512

MinValue: 128

MaxValue: 10240 # Max 10GB for Lambda

Timeout:

Type: Number

Description: Function timeout in seconds.

Default: 30

MinValue: 1

MaxValue: 900

Resources:

# IAM Role for Lambda Execution

LambdaExecutionRole:

Type: AWS::IAM::Role

Properties:

RoleName: !Sub ${FunctionName}-ExecutionRole-${Environment}

AssumeRolePolicyDocument:

Version: '2012-10-17'

Statement:

- Effect: Allow

Principal:

Service: lambda.amazonaws.com

Action: sts:AssumeRole

ManagedPolicyArns:

# Basic Lambda execution permissions (logs, metrics)

- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# Add policies for other AWS services your Lambda needs to access

# - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

# - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole

# Lambda Function resource

LambdaFunction:

Type: AWS::Lambda::Function

Properties:

FunctionName: !Sub ${FunctionName}-${Environment}

PackageType: Image

Code:

ImageUri: !Ref ImageUri

# ImageHash: SHA256 digest of the image (optional, but recommended for immutable deployments)

# You can obtain this via aws ecr describe-images --repository-name --image-ids imageTag= --query 'imageDetails[0].imageDigest'

Role: !GetAtt LambdaExecutionRole.Arn

MemorySize: !Ref MemorySize

Timeout: !Ref Timeout

# VPCConfig: # Uncomment and configure if your Lambda needs VPC access

# SubnetIds:

# - subnet-xxxxxxxxxxxxxxxxx

# - subnet-yyyyyyyyyyyyyyyyy

# SecurityGroupIds:

# - sg-zzzzzzzzzzzzzzzzz

Environment:

Variables:

ENVIRONMENT: !Ref Environment

LOG_LEVEL: INFO # Configure log level as needed

# Add other environment variables your application requires

# CloudWatch Log Group for Lambda function logs

LambdaLogGroup:

Type: AWS::Logs::LogGroup

Properties:

LogGroupName: !Sub /aws/lambda/${LambdaFunction}

# RetentionInDays: 30 # Configure log retention policy

Outputs:

FunctionName:

Description: Name of the created Lambda function.

Value: !Ref LambdaFunction

FunctionArn:

Description: ARN of the created Lambda function.

Value: !GetAtt LambdaFunction.Arn

LambdaExecutionRoleArn:

Description: ARN of the IAM role assumed by the Lambda function.

Value: !GetAtt LambdaExecutionRole.Arn

`

---

$1

$1

Lambda's performance is directly tied to the allocated memory. CPU is allocated proportionally to memory. Understanding this relationship is key to optimizing cost and latency.

| Memory (MB) | CPU Allocation (vCPU) | Typical Cold Start Latency (ms) | Typical Warm Start Latency (ms) | Relative Cost (per 1M invocations, 128MB, 100ms duration) |

| :---------- | :-------------------- | :------------------------------ | :------------------------------ | :-------------------------------------------------------- |

| 128 | 0.125 vCPU | ~800 | ~100 | $0.20 |

| 256 | 0.25 vCPU | ~700 | ~90 | $0.40 |

| 512 | 0.5 vCPU | ~600 | ~80 | $0.80 |

| 1024 | 1 vCPU | ~400 | ~60 | $1.60 |

| 2048 | 2 vCPU | ~300 | ~50 | $3.20 |

| 4096 | 4 vCPU | ~200 | ~40 | $6.40 |

| 10240 | 6 vCPU | ~150 | ~35 | $16.00 |

Note: Latency figures are approximate and vary based on image complexity, dependencies, and network conditions. Cost is illustrative and depends on invocation duration and region.

Key Optimization Strategies:

* Right-size Memory: Benchmark your function with varying memory allocations. Higher memory often means lower latency and, for CPU-bound tasks, lower overall cost if execution time decreases significantly.

* Optimize Base Image: Use slim variants of base images whenever possible.

* Minimize Image Layers: Combine RUN commands in your Dockerfile where logical.

* Efficient Code: Profile your application code to identify and optimize bottlenecks.

* Asynchronous Operations: For I/O-bound tasks, leverage asynchronous programming models.

* Provisioned Concurrency: For extremely latency-sensitive applications, consider Provisioned Concurrency to keep function instances warm, though this incurs a higher cost.

$1

1. Multi-stage Builds (as shown in Dockerfile example): This is the most effective way to reduce image size by separating build tools and intermediate artifacts from the final runtime image.

2. Minimize Layers: Each instruction in a Dockerfile (RUN, COPY, ADD) creates a layer. Combine related commands into a single RUN instruction using &&.

``dockerfile

# Less optimal: multiple RUN commands

RUN apt-

Why This Matters

Understanding the business and technical context helps you make informed decisions rather than blindly following patterns.

Trade-offs to Consider

Every architectural decision involves trade-offs. Consider your specific requirements, team expertise, and scale when evaluating options.

When NOT to Use This

Knowing when a solution doesn't apply is as valuable as knowing when it does. Consider alternatives for your specific situation.

Decision Framework

Use this framework to evaluate whether this approach is right for your use case based on your specific constraints and requirements.