Deployment Guide
View SourceThis guide covers advanced deployment scenarios and best practices for Mayfly Lambda functions.
Build Options
Local Build
Build on your local machine (same architecture as Lambda):
mix lambda.build --zip
Docker Build
Build using Docker for cross-platform compatibility:
mix lambda.build --docker --zip
This ensures your build matches the Lambda execution environment exactly.
Custom Output Directory
Specify where to save the build artifacts:
mix lambda.build --zip --outdir ./deploy
API Gateway Integration
When integrating with API Gateway, structure your responses according to the proxy integration format:
defmodule MyApp.ApiHandler do
def handle(event) do
# Extract request details
path = event["path"]
method = event["httpMethod"]
body = event["body"]
# Process request
response_body = process_request(method, path, body)
# Return API Gateway proxy response
{:ok, %{
statusCode: 200,
headers: %{
"Content-Type" => "application/json",
"Access-Control-Allow-Origin" => "*"
},
body: Jason.encode!(response_body)
}}
end
defp process_request("GET", "/users", _body) do
%{users: ["Alice", "Bob"]}
end
defp process_request("POST", "/users", body) do
user = Jason.decode!(body)
%{created: user}
end
defp process_request(_method, _path, _body) do
%{error: "Not found"}
end
endHandling Different HTTP Methods
def handle(%{"httpMethod" => "GET"} = event) do
handle_get(event)
end
def handle(%{"httpMethod" => "POST"} = event) do
handle_post(event)
end
def handle(%{"httpMethod" => "PUT"} = event) do
handle_put(event)
end
def handle(%{"httpMethod" => "DELETE"} = event) do
handle_delete(event)
endError Handling
Returning Errors
Return errors using the standard tuple format:
def handle(event) do
case validate_input(event) do
:ok ->
{:ok, process(event)}
{:error, message} ->
{:error, message}
end
endMayfly will automatically format errors according to Lambda's error response format:
{
"errorType": "RuntimeError",
"errorMessage": "Invalid input",
"stackTrace": "..."
}Raising Exceptions
You can also raise exceptions, which Mayfly will catch and format:
def handle(event) do
unless Map.has_key?(event, "required_field") do
raise "Missing required field"
end
{:ok, process(event)}
endCustom Error Types
Define custom error structs for better error handling:
defmodule MyApp.ValidationError do
defexception [:message, :field]
end
def handle(event) do
case validate(event) do
:ok ->
{:ok, process(event)}
{:error, field} ->
raise MyApp.ValidationError,
message: "Validation failed",
field: field
end
endEvent Sources
S3 Events
defmodule MyApp.S3Handler do
def handle(%{"Records" => records}) do
results = Enum.map(records, fn record ->
bucket = get_in(record, ["s3", "bucket", "name"])
key = get_in(record, ["s3", "object", "key"])
process_s3_object(bucket, key)
end)
{:ok, %{processed: length(results)}}
end
endEventBridge Events
defmodule MyApp.EventBridgeHandler do
def handle(%{"detail-type" => detail_type, "detail" => detail}) do
case detail_type do
"Order Placed" ->
process_order(detail)
"User Registered" ->
process_registration(detail)
_ ->
{:ok, %{status: "ignored"}}
end
end
endSQS Events
defmodule MyApp.SqsHandler do
def handle(%{"Records" => records}) do
results = Enum.map(records, fn record ->
body = record["body"] |> Jason.decode!()
process_message(body)
end)
{:ok, %{
batchItemFailures: [] # Return failed message IDs for retry
}}
end
endPerformance Optimization
Memory Configuration
Start with 512MB and adjust based on your function's needs:
aws lambda update-function-configuration \
--function-name my-function \
--memory-size 1024
More memory also means more CPU power.
Timeout Configuration
Set appropriate timeouts (default is 3 seconds):
aws lambda update-function-configuration \
--function-name my-function \
--timeout 30
Cold Start Optimization
- Keep dependencies minimal
- Use provisioned concurrency for critical functions
- Consider Lambda SnapStart (when available for custom runtimes)
Environment Variables
Set environment variables for configuration:
aws lambda update-function-configuration \
--function-name my-function \
--environment Variables="{
_HANDLER=Elixir.MyApp.Handler.handle,
DATABASE_URL=postgres://...,
API_KEY=secret123
}"
Access in your code:
def handle(event) do
db_url = System.get_env("DATABASE_URL")
api_key = System.get_env("API_KEY")
# Use configuration
{:ok, process(event, db_url, api_key)}
endMonitoring and Logging
Structured Logging
Use Logger for structured logging:
require Logger
def handle(event) do
Logger.info("Processing event", event_type: event["type"])
result = process(event)
Logger.info("Event processed successfully",
event_type: event["type"],
duration_ms: 123
)
{:ok, result}
endLogs appear in CloudWatch Logs automatically.
Metrics
Track custom metrics using CloudWatch:
def handle(event) do
start_time = System.monotonic_time(:millisecond)
result = process(event)
duration = System.monotonic_time(:millisecond) - start_time
Logger.info("MONITORING|#{duration}|milliseconds|ProcessingTime")
{:ok, result}
endCI/CD Integration
GitHub Actions Example
name: Deploy Lambda
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: erlef/setup-beam@v1
with:
elixir-version: '1.15'
otp-version: '26'
- name: Install dependencies
run: mix deps.get
- name: Build Lambda package
run: mix lambda.build --docker --zip
- name: Deploy to AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws lambda update-function-code \
--function-name my-function \
--zip-file fileb://lambda.zipBest Practices
- Keep handlers simple - Move business logic to separate modules
- Use pattern matching - Handle different event types cleanly
- Return early - Validate input and return errors quickly
- Log appropriately - Use structured logging for better observability
- Test locally - Write unit tests for your handler logic
- Monitor performance - Track cold starts and execution times
- Handle errors gracefully - Always return proper error responses
- Use environment variables - Keep configuration out of code
- Version your functions - Use Lambda versions and aliases
- Set appropriate timeouts - Don't use default 3s for long-running tasks