# AWS Lambda — Deploy Your First Serverless Function

URL: https://www.devopsness.com/blog/aws-lambda-deploy-first-serverless-function
Author: Kiril Urbonas
Published: 2026-04-29
Updated: 2026-04-29
Categories: Cloud
Tags: AWS, Lambda, Serverless, Cloud, Tutorial, Beginner

Summary: Write, package, and deploy a Lambda function using only the AWS CLI. Trigger it via a public URL. Understand what serverless actually means.

---
# AWS Lambda: Deploy Your First Serverless Function

By the end of this post you'll have a Python function deployed to AWS Lambda, reachable from the public internet via a Function URL, and you'll understand the basic cost model. About 25 minutes. Stays in the AWS free tier.

You'll need an AWS account and the AWS CLI installed and configured (`aws configure`).

## What "serverless" actually means

Serverless does not mean "no servers." There are servers; AWS just doesn't make you provision, patch, or scale them. You upload code, AWS runs it on demand, and you pay per invocation + per millisecond of execution.

The trade: you give up control over the runtime environment (max execution time, memory limits, no persistent disk between invocations) in exchange for never thinking about capacity again. Good fit for event-driven workloads (S3 triggers, scheduled jobs, webhooks, light APIs). Bad fit for sustained high-throughput services or anything with persistent state.

## The Lambda mental model

Three things to know:

- **Function code** lives in a `.zip` (or container image). AWS unpacks it onto an "execution environment" — a tiny container — and runs it.
- **A handler** is the entrypoint function AWS calls when an event fires. For Python: `lambda_function.lambda_handler(event, context)`.
- **An event** is a JSON object describing why the function was invoked. Could be an HTTP request, an S3 upload notification, a scheduled time, etc. The function returns a response (also JSON).

That's the whole programming model. Everything else is wiring.

## Step 1: Write the function

```bash
mkdir lambda-tutorial && cd lambda-tutorial
```

Create `lambda_function.py`:

```python
import json
import datetime

def lambda_handler(event, context):
    name = event.get("queryStringParameters", {}).get("name", "world") if event else "world"
    body = {
        "message": f"Hello, {name}!",
        "time_utc": datetime.datetime.utcnow().isoformat() + "Z",
        "request_id": context.aws_request_id if context else "local",
    }
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps(body),
    }


if __name__ == "__main__":
    print(lambda_handler({}, None))
```

The `if __name__ == "__main__"` block lets you test locally:

```bash
python lambda_function.py
```

You should see a dict with `Hello, world!`, current time, and `request_id: local`. Locally `context` is `None`; on Lambda it'll be a real object with the actual request ID.

## Step 2: Package it

Lambda wants a `.zip` containing your code:

```bash
zip function.zip lambda_function.py
```

For a function with no extra dependencies, that's the whole package. With dependencies (e.g. `requests`), you'd `pip install requests --target .` first, then zip the directory.

## Step 3: Create an execution role

Lambda runs as an IAM role. The simplest viable role lets the function write logs to CloudWatch:

```bash
cat > 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 lambda-tutorial-role \
  --assume-role-policy-document file://trust-policy.json

aws iam attach-role-policy --role-name lambda-tutorial-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
```

The first command creates a role Lambda is allowed to assume. The second attaches a managed policy that allows writing CloudWatch logs. Wait ~10 seconds before the next step — IAM is eventually consistent and a freshly-created role isn't immediately usable.

Get your AWS account ID for the next step:

```bash
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
echo $ACCOUNT
```

## Step 4: Create the Lambda function

```bash
aws lambda create-function \
  --function-name hello-tutorial \
  --runtime python3.12 \
  --role arn:aws:iam::${ACCOUNT}:role/lambda-tutorial-role \
  --handler lambda_function.lambda_handler \
  --zip-file fileb://function.zip
```

You should get back JSON describing the function. The important field is `FunctionArn`.

Test it:

```bash
aws lambda invoke --function-name hello-tutorial --payload '{}' /tmp/out.json
cat /tmp/out.json
```

You'll see your response. Lambda is running.

## Step 5: Expose it as a URL

```bash
aws lambda create-function-url-config \
  --function-name hello-tutorial \
  --auth-type NONE
```

This gives you a public URL — no API Gateway needed for this simple case. Copy the `FunctionUrl` from the output.

Lambda also needs an explicit permission to be invoked anonymously:

```bash
aws lambda add-permission \
  --function-name hello-tutorial \
  --statement-id public-invoke \
  --action lambda:InvokeFunctionUrl \
  --principal "*" \
  --function-url-auth-type NONE
```

Now hit it:

```bash
curl "<your-function-url>?name=ada"
```

You should see:

```json
{"message": "Hello, ada!", "time_utc": "...", "request_id": "..."}
```

That's a serverless function reachable from the public internet, in five steps.

## Step 6: Update the function

Change something in `lambda_function.py` (e.g. add `"version": 2` to the body), then:

```bash
zip function.zip lambda_function.py
aws lambda update-function-code --function-name hello-tutorial --zip-file fileb://function.zip
```

The next request hits the new version. No restart, no provisioning. That's the part teams fall in love with.

## Step 7: Cleanup

```bash
aws lambda delete-function-url-config --function-name hello-tutorial
aws lambda delete-function --function-name hello-tutorial
aws iam detach-role-policy --role-name lambda-tutorial-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name lambda-tutorial-role
```

## Common mistakes

**Cold starts ignored.** The first request after the function has been idle takes longer because Lambda is spinning up a new execution environment. For a tiny Python function it's ~200ms; for a large Java function with heavy startup it's seconds. If user-facing latency matters, use Provisioned Concurrency or a different platform.

**Storing state on disk.** `/tmp` exists in Lambda but is per-execution-environment and ephemeral. State that needs to survive across invocations goes to S3, DynamoDB, or another store.

**Long-running tasks.** Lambda has a 15-minute hard limit. Workloads near that should split into smaller invocations or move to ECS / EKS.

**Loose IAM permissions.** It's tempting to grant the function `*` permissions to skip the IAM dance. Don't — the role is your blast radius if the function is ever compromised. Grant exactly what it needs.

## What to read next

You've shipped your first function. The next levels:

- [AWS Lambda optimization: cost and performance](/blog/aws-lambda-optimization-reducing-costs-improving-performance) — the patterns that move the bill once you're running ~200 functions
- [Serverless architecture patterns](/blog/serverless-architecture-patterns-building-scalable-applications) — when Lambda is the right tool, when it isn't
- [AWS ECS vs EKS: choosing the right container platform](/blog/aws-ecs-vs-eks-choosing-right-container-platform) — what to reach for when Lambda's constraints get in the way

Lambda is one of AWS's best products when used for event-driven work and short-lived tasks. The hardest part of getting started is the IAM dance — once your first function is running, every subsequent one is faster.

