Deploy & Debug
Deploy, invoke, and debug Lambda, execution roles, triggers, concurrency, versions, and observability.
Prerequisites
Deploy + Invoke + Logs
Goal
Deploy a Lambda function from scratch using only the CLI. Invoke it synchronously and asynchronously. Read its logs. Understand the cold start lifecycle.
Estimated time: 1–2 hours
Create the Lambda execution role
What's happening here
Lambda doesn't run with your IAM credentials, it needs its own role it can assume. This is the execution role: it's what Lambda uses to write logs to CloudWatch, pull secrets, call other AWS services, etc. We start with only AWSLambdaBasicExecutionRole (CloudWatch Logs write access), nothing else. The trust policy restricts role assumption to lambda.amazonaws.com only. You'll add more permissions in later sessions as needed, so you always know exactly why each permission is there.
cat > lambda-trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
EOF
aws iam create-role \
--role-name lambdaLabRole \
--assume-role-policy-document file://lambda-trust-policy.json
aws iam attach-role-policy \
--role-name lambdaLabRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
export LAMBDA_ROLE_ARN=$(aws iam get-role \
--role-name lambdaLabRole \
--query 'Role.Arn' --output text)
echo "Role ARN: $LAMBDA_ROLE_ARN"Checkpoint: role exists and has exactly one attached policy.
aws iam list-attached-role-policies --role-name lambdaLabRole \
--query 'AttachedPolicies[*].PolicyName'Write and deploy your first function
What's happening here
Lambda runs your code as a handler function: a known entry point (lambda_handler(event, context)) that the runtime calls on each invocation. The function receives two objects: event (the input payload, different shape depending on who invoked it) and context (metadata: function name, memory limit, remaining time in ms, request ID). We're starting with a simple echo so you can see the raw event shape for every trigger type in later sessions. The zip + create-function pattern is the lowest-level deploy method (no SAM, no CDK) so you see exactly what's happening.
mkdir lambda-lab && cd lambda-lab
cat > handler.py << 'EOF'
import json
import os
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info("Event received: %s", json.dumps(event))
response = {
"version": "1",
"function": context.function_name,
"request_id": context.aws_request_id,
"remaining_ms": context.get_remaining_time_in_millis(),
"env": os.getenv("APP_ENV", "not-set"),
"echo": event
}
logger.info("Returning response: %s", json.dumps(response))
return response
EOF
zip function.zip handler.py
aws lambda create-function \
--function-name lambda-lab \
--runtime python3.11 \
--role $LAMBDA_ROLE_ARN \
--handler handler.lambda_handler \
--zip-file fileb://function.zip \
--timeout 10 \
--memory-size 128 \
--environment Variables={APP_ENV=lab} \
--region $AWS_REGIONVerify:
aws lambda get-function-configuration \
--function-name lambda-lab \
--region $AWS_REGION \
--query '{name:FunctionName,runtime:Runtime,state:State,handler:Handler,memory:MemorySize,timeout:Timeout}'Checkpoint: State is Active.
Invoke synchronously
What's happening here
InvocationType=RequestResponse (the default) means your CLI call blocks until Lambda returns. You get the response payload directly. This is the model for API Gateway, direct SDK calls, and any case where the caller needs a result. The response payload is base64-encoded in the CLI output, that's just the CLI's encoding; the actual payload is plain JSON. The StatusCode in the CLI output is the HTTP status of the Lambda invocation, not anything your function returned. Your function's return value lands in the output file.
aws lambda invoke \
--function-name lambda-lab \
--payload '{"message": "hello", "source": "cli"}' \
--cli-binary-format raw-in-base64-out \
--region $AWS_REGION \
output.json
cat output.json | jq .Observe:
version,function,request_id,remaining_ms: from thecontextobjectenv: lab: from the environment variable you set at deploy timeecho: the exact payload you sent
Checkpoint: remaining_ms should be close to 10000 (your 10s timeout minus execution time).
Invoke asynchronously and observe the difference
What's happening here
InvocationType=Event tells Lambda to accept the event and return immediately: the CLI gets a 202 Accepted: not the function's response. Lambda queues the event internally and invokes your function in the background. This is how SNS, S3 event notifications, and EventBridge invoke Lambda. You never get the return value. If the function fails, Lambda retries up to 2 more times (configurable) before discarding the event, or routing it to a Dead Letter Queue. The only way to observe the execution is via CloudWatch Logs.