devuplabs.cloud
PreviewLab7 hours

Deploy & Debug

Deploy, invoke, and debug Lambda, execution roles, triggers, concurrency, versions, and observability.

Prerequisites

0 of 5 checked

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.

bash
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.

bash
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.

bash
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_REGION

Verify:

bash
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.

bash
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 the context object
  • env: lab: from the environment variable you set at deploy time
  • echo: 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.

Unlock all 24 AWS services & 291+ lab sessions (~180 hours)

Pricing