Deploy Your First AWS Lambda Function with AWS CLI | CloudWebSchool

In this tutorial you will deploy a Python Lambda function from scratch using the AWS CLI. No console, no framework. By the end you will have a working function in AWS, tested with a real JSON event, logs read from CloudWatch, and the code updated once. You will also know exactly where to look when something breaks.

Simple explanation

AWS Lambda runs your code on demand without you managing any servers. Deploying a Lambda function means:

  1. Writing the code locally.
  2. Packaging it as a zip file.
  3. Creating an IAM execution role so Lambda has permission to write logs.
  4. Telling Lambda which runtime to use, where your code is, and which function to call.
  5. Invoking it with a test event to confirm it works.
  6. Reading the output in CloudWatch Logs.

That is the full cycle. Once you understand it once, everything else (triggers, permissions, environment variables) slots in naturally on top of it.

What you will build

  • A simple Python Lambda function that reads a name from an event and returns a greeting.
  • An IAM execution role with the minimum permissions Lambda needs to run.
  • A zip deployment package created with the CLI.
  • A test invocation using a JSON event file.
  • A log inspection in CloudWatch Logs.
  • An update to the function code after initial deployment.

Before you start

You need:

  • The AWS CLI installed and configured. See Installing the AWS CLI if you have not set it up yet.
  • An IAM user or role with permission to create Lambda functions and IAM roles. If you are unsure about the difference, see IAM users vs roles.
  • Python 3 installed locally to write and test the function code.

How it works

The deployment flow from start to finish:

  1. Write the function. A Python file with a handler that accepts an event and a context object.
  2. Create an execution role. An IAM role Lambda assumes when running your code, granting it permission to write CloudWatch logs.
  3. Zip the code. Lambda accepts code as a zip archive. For simple functions this is just the Python file. Functions with dependencies also include installed packages.
  4. Create the function. Register it with Lambda by specifying the runtime, handler, role ARN, and zip file.
  5. Invoke it. Send a test JSON event and inspect the response.
  6. Check the logs. CloudWatch Logs receives all function output automatically. It is the first place to look when something goes wrong.
  7. Update. Rezip and push updated code, or change configuration independently without touching the code.

Step 1: Write the function code

Create a file called lambda_function.py. The handler receives an event (the data passed by whatever triggered the function) and a context (runtime metadata Lambda provides), then returns a response.

# lambda_function.py

import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handler(event, context):
    """
    Simple Lambda function that greets a user.
    """
    logger.info(f"Received event: {json.dumps(event)}")

    name = event.get('name', 'World')
    message = f"Hello, {name}! Your Lambda function is working."

    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': message,
            'function_name': context.function_name,
            'aws_request_id': context.aws_request_id
        })
    }

A few things to note:

  • The handler string you pass to Lambda is lambda_function.handler: the filename (without .py) dot the function name. Any mismatch causes a “handler not found” error at runtime.
  • Use Python’s logging module rather than print(). Lambda captures both, but the logging module preserves log levels so you can filter output in CloudWatch later.
  • The return value uses statusCode and body. This is the format API Gateway expects. For direct invocations and other triggers you return whatever format the caller expects.

Step 2: Create the execution role

Lambda cannot run without an execution role. This is an IAM role that Lambda assumes when it starts your function. It gives the function an identity and controls what AWS services it can call. At minimum the role needs permission to write logs to CloudWatch Logs. Without it, your function will fail silently with no output anywhere.

Think of it like a visitor badge

Imagine Lambda is a contractor who shows up to work in your building. The execution role is the visitor badge you hand them at reception. Without the badge, security turns them away at every door. With a badge that lists exactly the rooms they are allowed into, they can do their job and nothing else. The badge does not give them access to the whole building — just the parts you approve. That is the execution role.

Create the role with a trust policy that allows the Lambda service to assume it:

aws iam create-role \
  --role-name MyLambdaExecutionRole \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {"Service": "lambda.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }]
  }'

Attach the AWS-managed policy that covers CloudWatch Logs writes:

aws iam attach-role-policy \
  --role-name MyLambdaExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Retrieve the role ARN. You will need it when creating the function:

aws iam get-role \
  --role-name MyLambdaExecutionRole \
  --query "Role.Arn" \
  --output text

This returns something like: arn:aws:iam::123456789012:role/MyLambdaExecutionRole

IAM propagation delay

IAM changes are eventually consistent across AWS. Wait 10–15 seconds after creating the role before running create-function. If you move too quickly, Lambda will return a permissions error even though the role exists. A short wait eliminates this race condition entirely.

Step 3: Package the code

Lambda receives your code as a zip archive. For a single-file function with no external dependencies this is a one-liner:

zip function.zip lambda_function.py

If your function uses external Python packages (anything not in the standard library), you must include them in the zip. Install them into a local directory and zip from inside it:

# Install dependencies alongside the function file
pip install requests -t package/
cp lambda_function.py package/

# Zip from inside the directory so files sit at the root of the archive
cd package
zip -r ../function.zip .
cd ..
Pack from inside the suitcase, not from outside it

Imagine you are packing a suitcase to mail. If you put the suitcase inside a box and mail the box, the recipient opens the box and finds a suitcase — they still have to open the suitcase to get to your clothes. Lambda has the same problem. If you zip the folder itself, Lambda extracts a folder and cannot find your code inside it. You need to pack your files directly into the zip (climb into the suitcase) so Lambda opens the archive and finds the function file immediately.

Zip root matters

Lambda extracts the zip and expects your function file at the archive root, not inside a subdirectory. zip -r function.zip package/ zips the directory itself. cd package && zip -r ../function.zip . zips its contents. Only the second form works.

Step 4: Create the Lambda function

aws lambda create-function \
  --function-name my-first-function \
  --runtime python3.12 \
  --role arn:aws:iam::123456789012:role/MyLambdaExecutionRole \
  --handler lambda_function.handler \
  --zip-file fileb://function.zip \
  --timeout 30 \
  --memory-size 128 \
  --description "My first Lambda function"

Key parameters explained:

  • —handler lambda_function.handler: the dot-separated path to your function. Filename (without .py) dot function name. If this does not match exactly, Lambda returns a “handler not found” error.
  • —runtime python3.12: the execution environment. Use the latest supported Python version for new functions.
  • —timeout 30: maximum seconds before Lambda forcibly terminates the invocation. The default is 3 seconds, which is easy to hit when making any network call. Set it higher than you expect to need.
  • —memory-size 128: MB of RAM allocated. Lambda CPU scales proportionally with memory. 128 MB is the minimum.
  • —zip-file fileb://: the fileb:// prefix reads the file as binary data. Using file:// instead silently corrupts the zip.

Wait for the function to reach active state before invoking it:

aws lambda wait function-active --function-name my-first-function
Always use the wait commands

Both aws lambda wait function-active and aws lambda wait function-updated poll Lambda until the operation finishes. Invoking a function before it is active, or updating code before the previous update completes, results in errors that look like failures but are just timing issues. The wait commands cost you a few seconds and save you from debugging phantom problems.

Step 5: Invoke and test the function

Create a test event file with the JSON payload you want to send:

cat > test-event.json <<'EOF'
{
  "name": "Alice"
}
EOF

Invoke the function synchronously and capture the response:

aws lambda invoke \
  --function-name my-first-function \
  --payload file://test-event.json \
  --cli-binary-format raw-in-base64-out \
  response.json

cat response.json
—cli-binary-format

The —cli-binary-format raw-in-base64-out flag tells AWS CLI v2 to accept the payload as plain JSON text rather than base64. Without it, newer versions of the CLI will error on the —payload argument. Always include it when invoking with a payload file.

A successful response looks like:

{
  "statusCode": 200,
  "body": "{\"message\": \"Hello, Alice! Your Lambda function is working.\", \"function_name\": \"my-first-function\", \"aws_request_id\": \"abc-123-def\"}"
}

Now check CloudWatch Logs to see what the function printed internally:

# Lambda creates the log group automatically on first invocation
aws logs describe-log-groups \
  --log-group-name-prefix /aws/lambda/my-first-function

# Get the most recent log stream
LOG_STREAM=$(aws logs describe-log-streams \
  --log-group-name /aws/lambda/my-first-function \
  --order-by LastEventTime \
  --descending \
  --query "logStreams[0].logStreamName" \
  --output text)

# Read the log events
aws logs get-log-events \
  --log-group-name /aws/lambda/my-first-function \
  --log-stream-name "$LOG_STREAM"

Every invocation writes a START, END, and REPORT line automatically, plus anything your function logs. When something fails, the error message and stack trace are here. CloudWatch Logs is the primary debugging surface for Lambda. Always check it first.

Step 6: Update the function code

After modifying lambda_function.py, repackage and push the updated zip:

# Repackage
zip function.zip lambda_function.py

# Deploy the update
aws lambda update-function-code \
  --function-name my-first-function \
  --zip-file fileb://function.zip

Wait for the update to complete before invoking again:

aws lambda wait function-updated --function-name my-first-function

Configuration changes (timeout, memory, environment variables) are separate from code updates and do not require a rezip:

aws lambda update-function-configuration \
  --function-name my-first-function \
  --timeout 60 \
  --memory-size 256 \
  --environment "Variables={LOG_LEVEL=DEBUG,TABLE_NAME=my-table}"

Environment variables

Use environment variables to pass non-sensitive configuration to your function without hardcoding it in the source:

import os

TABLE_NAME = os.environ.get('TABLE_NAME', 'default-table')
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')

def handler(event, context):
    # Use TABLE_NAME and LOG_LEVEL here
    pass
Never put secrets in environment variables

API keys, database passwords, and tokens stored in Lambda environment variables are visible in plaintext to anyone with IAM permission to describe functions. That includes the AWS console, CLI output, and CloudFormation exports. Use AWS Secrets Manager instead and retrieve secrets at runtime. Environment variables are fine for non-sensitive configuration: table names, log levels, feature flags.

Adding permissions to the execution role

If your function needs to call other AWS services (read from S3, write to DynamoDB, publish to SQS), add those permissions to the execution role. Grant only what the function actually needs on the specific resources it accesses. The principle of least privilege applied consistently here limits the blast radius if the function is compromised or misconfigured:

aws iam put-role-policy \
  --role-name MyLambdaExecutionRole \
  --policy-name DynamoDBReadAccess \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:Query",
        "dynamodb:Scan"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/my-table"
    }]
  }'

For a fuller picture of how Lambda’s security model works, covering resource-based policies, VPC placement, and production secrets management, see the Lambda security model guide.

When to use this approach

Good fit

  • First Lambda deployments where you want to understand the mechanics before adding frameworks.
  • Simple event-driven functions with few or no external dependencies.
  • Lightweight Python utilities: data transformation, scheduled notifications, file processing.
  • Proof-of-concept workloads where fast iteration matters more than deployment automation.
  • Low-ops production functions that run infrequently and do not need a managed CI/CD pipeline.

Not a good fit

  • Long-running workloads. Lambda times out at 15 minutes. For longer jobs consider AWS Batch or EC2.
  • Heavy dependency trees. When the unzipped package exceeds 250 MB, zip deployment is no longer viable. Container images support up to 10 GB.
  • Custom runtimes or binary dependencies. If your code requires compiled binaries or a specific OS configuration, a container image gives you full control over the environment.
  • Production at scale with frequent deploys. At scale, infrastructure-as-code tools (SAM, Terraform, CloudFormation) are more reliable and auditable than manual CLI commands.

If you are deciding whether Lambda is the right compute option for your workload, see choosing between EC2, Lambda, and containers.

Lambda zip deployment vs container images

Lambda supports two deployment package types: zip archives (used in this tutorial) and container images. Both run the same way once deployed. Lambda handles scaling and infrastructure regardless of which you use. The difference is how you package your code and dependencies.

Zip deploymentContainer image
Simpler for small functionsBetter for heavy or complex dependencies
Up to 250 MB unzippedUp to 10 GB
No Docker requiredRequires Docker and ECR
Fastest path for first deploymentsFull control over runtime and OS packages

This tutorial uses zip deployment because it is the fastest path for a first Lambda function with no infrastructure overhead. Once your packaging needs grow (more dependencies, custom runtimes, larger artifacts), see container images in AWS for how to build and deploy Lambda container images.

Common mistakes

  1. Forgetting the execution role. Lambda cannot write logs or call other services without one. Skipping this step means the function either fails silently or returns a cryptic permissions error with no CloudWatch output to inspect.

  2. Using the wrong handler string. The format is filename.function_name with no .py extension. If your file is lambda_function.py and the function is handler, the handler string is exactly lambda_function.handler. Any mismatch causes a “handler not found” error on every invocation.

  3. Zipping the wrong directory structure. If you zip the package folder itself rather than its contents, Lambda extracts a subdirectory and cannot find the handler. Always cd into the directory before zipping: cd package && zip -r ../function.zip .

  4. Not including external dependencies. Lambda only includes the Python standard library and a small set of AWS SDKs in the runtime. Everything else must be in the zip. A missing import causes ModuleNotFoundError on every invocation.

  5. Setting timeout too low. The default is 3 seconds, which is easily hit during cold starts or any network call. Set it to a safe margin above your expected execution time.

  6. Using the wrong AWS profile or region. If the CLI is configured for a different account or region than where the function was created, invoke will return a “function not found” error. Check with aws configure list and pass —region explicitly if needed.

  7. Not waiting for IAM propagation. Creating a role and immediately using it can fail because IAM changes take seconds to propagate globally. Wait 10–15 seconds between creating the role and running create-function.

  8. Forgetting —cli-binary-format raw-in-base64-out. On AWS CLI v2, passing a JSON payload without this flag causes an error. Always include it with —payload.

  9. Not checking CloudWatch Logs first. When a function fails, the error message and stack trace are in /aws/lambda/<function-name>. Check there before anything else.

  10. Hardcoding secrets in environment variables. Environment variables are readable by anyone with IAM permission to describe the function. Use AWS Secrets Manager for API keys and passwords.

Frequently asked questions

What do I need before deploying a Lambda function?

You need an AWS account with the CLI installed and configured, an IAM user or role with permission to create Lambda functions and IAM roles, and Python 3 installed locally to write and test the code. See the AWS CLI setup guide if you have not configured the CLI yet.

Why does Lambda need an execution role?

Lambda runs your code in an isolated environment managed by AWS. The execution role is an IAM role that Lambda assumes when starting your function. It gives the function permission to call other AWS services, including at minimum writing logs to CloudWatch Logs. Without it the function cannot log output and cannot access any AWS resource.

How do I include Python dependencies in the deployment package?

Install your dependencies into a local directory using pip install <package> -t package/, copy your function file into that directory, then zip the contents of the directory from inside it: cd package && zip -r ../function.zip . Lambda extracts the zip and expects your function file and any packages at the root level of the archive, not inside a subdirectory.

How do I test a Lambda function after deployment?

Use aws lambda invoke --function-name <name> --payload file://test-event.json --cli-binary-format raw-in-base64-out response.json. This sends a JSON event synchronously and writes the return value to response.json. Check CloudWatch Logs at /aws/lambda/<name> to see what the function printed and whether any errors were thrown.

When should I use a container image instead of a zip package?

Use a container image when your function has many dependencies, needs a custom runtime, or the total unzipped package size exceeds 250 MB. Container images support up to 10 GB and give you full control over the execution environment. Zip packages are simpler and faster for small functions with few dependencies, which describes most first Lambda deployments.

Last verified: 1 April 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.