Day 17 - Serverless Deploy
Deploy a serverless function to AWS Lambda with API Gateway, learning modern serverless architecture patterns.
Description
You need to deploy an API that scales automatically, has zero idle costs, and requires minimal infrastructure management. Enter serverless with AWS Lambda. Today you'll deploy a function-based API.
Task
Deploy a serverless API using AWS Lambda and API Gateway.
Requirements:
- Create a Lambda function
- Set up API Gateway endpoint
- Deploy using Infrastructure as Code
- Add environment variables
- Test the deployed API
Target
- ✅ Lambda function deployed
- ✅ API Gateway endpoint working
- ✅ Function triggered via HTTP
- ✅ Logs visible in CloudWatch
- ✅ Deployed via IaC
Sample App
Serverless Function
handler.js
'use strict';
module.exports.hello = async (event) => {
const name = event.queryStringParameters?.name || 'World';
console.log('Received request:', { name });
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
requestId: event.requestContext.requestId
})
};
};
module.exports.getUser = async (event) => {
const userId = event.pathParameters?.id;
// Simulate database lookup
const user = {
id: userId,
name: 'John Doe',
email: '[email protected]'
};
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
};
};
module.exports.createUser = async (event) => {
const data = JSON.parse(event.body);
console.log('Creating user:', data);
// Validate input
if (!data.name || !data.email) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'Name and email are required'
})
};
}
// Simulate user creation
const user = {
id: Date.now().toString(),
...data,
createdAt: new Date().toISOString()
};
return {
statusCode: 201,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
};
};
View Solution
Solution
1. Serverless Framework Approach
serverless.yml
service: advent-serverless-api
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs20.x
region: us-east-1
stage: ${opt:stage, 'dev'}
memorySize: 256
timeout: 10
environment:
STAGE: ${self:provider.stage}
TABLE_NAME: ${self:custom.tableName}
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- Fn::GetAtt: [UsersTable, Arn]
logs:
restApi: true
functions:
hello:
handler: handler.hello
events:
- httpApi:
path: /hello
method: get
getUser:
handler: handler.getUser
events:
- httpApi:
path: /users/{id}
method: get
createUser:
handler: handler.createUser
events:
- httpApi:
path: /users
method: post
listUsers:
handler: handler.listUsers
events:
- httpApi:
path: /users
method: get
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:custom.tableName}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
custom:
tableName: users-${self:provider.stage}
plugins:
- serverless-offline # For local testing
package.json
{
"name": "advent-serverless-api",
"version": "1.0.0",
"description": "Serverless API example",
"main": "handler.js",
"scripts": {
"deploy": "serverless deploy",
"remove": "serverless remove",
"logs": "serverless logs -f hello -t",
"local": "serverless offline"
},
"dependencies": {
"aws-sdk": "^2.1500.0"
},
"devDependencies": {
"serverless": "^3.38.0",
"serverless-offline": "^13.3.0"
}
}
2. AWS SAM Approach
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless API with SAM
Globals:
Function:
Timeout: 10
MemorySize: 256
Runtime: nodejs20.x
Environment:
Variables:
STAGE: !Ref Stage
Parameters:
Stage:
Type: String
Default: dev
AllowedValues:
- dev
- staging
- prod
Resources:
# API Gateway
ApiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Stage
Cors:
AllowMethods: "'GET, POST, PUT, DELETE, OPTIONS'"
AllowHeaders: "'Content-Type,Authorization'"
AllowOrigin: "'*'"
# Lambda Functions
HelloFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./
Handler: handler.hello
Events:
HelloApi:
Type: Api
Properties:
RestApiId: !Ref ApiGateway
Path: /hello
Method: GET
GetUserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./
Handler: handler.getUser
Policies:
- DynamoDBReadPolicy:
TableName: !Ref UsersTable
Events:
GetUserApi:
Type: Api
Properties:
RestApiId: !Ref ApiGateway
Path: /users/{id}
Method: GET
CreateUserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./
Handler: handler.createUser
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UsersTable
Events:
CreateUserApi:
Type: Api
Properties:
RestApiId: !Ref ApiGateway
Path: /users
Method: POST
# DynamoDB Table
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub users-${Stage}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
Outputs:
ApiUrl:
Description: "API Gateway endpoint URL"
Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/${Stage}"
HelloFunction:
Description: "Hello Lambda Function ARN"
Value: !GetAtt HelloFunction.Arn
UsersTableName:
Description: "DynamoDB table name"
Value: !Ref UsersTable
samconfig.toml
version = 0.1
[default.deploy.parameters]
stack_name = "advent-serverless-api"
region = "us-east-1"
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Stage=dev"
3. Terraform Approach
main.tf
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# Lambda function
resource "aws_lambda_function" "hello" {
filename = "function.zip"
function_name = "hello-${var.environment}"
role = aws_iam_role.lambda_role.arn
handler = "handler.hello"
runtime = "nodejs20.x"
timeout = 10
memory_size = 256
environment {
variables = {
STAGE = var.environment
}
}
source_code_hash = filebase64sha256("function.zip")
}
# IAM role for Lambda
resource "aws_iam_role" "lambda_role" {
name = "lambda-role-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
# Attach CloudWatch Logs policy
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# API Gateway
resource "aws_apigatewayv2_api" "api" {
name = "serverless-api-${var.environment}"
protocol_type = "HTTP"
cors_configuration {
allow_origins = ["*"]
allow_methods = ["GET", "POST", "PUT", "DELETE"]
allow_headers = ["content-type", "authorization"]
}
}
# API Gateway integration
resource "aws_apigatewayv2_integration" "hello" {
api_id = aws_apigatewayv2_api.api.id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.hello.invoke_arn
}
# API Gateway route
resource "aws_apigatewayv2_route" "hello" {
api_id = aws_apigatewayv2_api.api.id
route_key = "GET /hello"
target = "integrations/${aws_apigatewayv2_integration.hello.id}"
}
# API Gateway stage
resource "aws_apigatewayv2_stage" "default" {
api_id = aws_apigatewayv2_api.api.id
name = var.environment
auto_deploy = true
}
# Lambda permission for API Gateway
resource "aws_lambda_permission" "api_gateway" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.api.execution_arn}/*/*"
}
# Outputs
output "api_url" {
value = aws_apigatewayv2_stage.default.invoke_url
}
output "function_name" {
value = aws_lambda_function.hello.function_name
}
Explanation
Serverless Concepts
1. AWS Lambda
Event-driven compute:
HTTP Request → API Gateway → Lambda Function → Response
Pricing: Pay per request + compute time
Benefits:
- No servers to manage
- Auto-scaling
- Pay-per-use
- High availability built-in
2. API Gateway
Managed API service:
- Handles HTTP requests
- Routes to Lambda
- Authentication/authorization
- Rate limiting
- CORS support
3. Cold Starts
First invocation slow:
- Container initialization
- Code loading
- Connection setup
Mitigations:
- Provisioned concurrency
- Keep functions warm
- Optimize package size
Lambda Event Structure
{
"requestContext": {
"requestId": "abc-123",
"http": {
"method": "GET",
"path": "/hello"
}
},
"headers": {
"content-type": "application/json"
},
"queryStringParameters": {
"name": "World"
},
"body": null
}
Response Format
{
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": JSON.stringify({ message: "Success" })
}
Try to solve the challenge yourself first!
Click "Reveal Solution" when you're ready to see the answer.
Result
Deploy with Serverless Framework
# Install Serverless Framework
npm install -g serverless
# Install dependencies
npm install
# Deploy to AWS
serverless deploy --stage dev
# Output:
# Deploying advent-serverless-api to stage dev (us-east-1)
#
# ✔ Service deployed to stack advent-serverless-api-dev
#
# endpoints:
# GET - https://abc123.execute-api.us-east-1.amazonaws.com/hello
# GET - https://abc123.execute-api.us-east-1.amazonaws.com/users/{id}
# POST - https://abc123.execute-api.us-east-1.amazonaws.com/users
#
# functions:
# hello: advent-serverless-api-dev-hello
# getUser: advent-serverless-api-dev-getUser
# createUser: advent-serverless-api-dev-createUser
Deploy with AWS SAM
# Install AWS SAM CLI
brew install aws-sam-cli
# Build
sam build
# Deploy
sam deploy --guided
# Output:
# Successfully created/updated stack - advent-serverless-api
#
# CloudFormation outputs:
# ApiUrl: https://abc123.execute-api.us-east-1.amazonaws.com/dev
# HelloFunction: arn:aws:lambda:us-east-1:123456789:function:HelloFunction
Test the API
# Get API URL
API_URL=$(serverless info --verbose | grep "GET" | head -1 | awk '{print $NF}')
# Test hello endpoint
curl "$API_URL"
# {"message":"Hello, World!","timestamp":"2025-12-17T..."}
# Test with query parameter
curl "$API_URL?name=DevOps"
# {"message":"Hello, DevOps!","timestamp":"2025-12-17T..."}
# Test getUser endpoint
curl "https://abc123.execute-api.us-east-1.amazonaws.com/users/123"
# {"id":"123","name":"John Doe","email":"[email protected]"}
# Test createUser endpoint
curl -X POST \
-H "Content-Type: application/json" \
-d '{"name":"Jane","email":"[email protected]"}' \
"https://abc123.execute-api.us-east-1.amazonaws.com/users"
# {"id":"1702820000000","name":"Jane","email":"[email protected]","createdAt":"2025-12-17T..."}
View Logs
# Serverless Framework
serverless logs -f hello -t
# AWS CLI
aws logs tail /aws/lambda/hello-dev --follow
# CloudWatch Logs Insights query
aws logs start-query \
--log-group-name /aws/lambda/hello-dev \
--start-time $(date -u -d '1 hour ago' +%s) \
--end-time $(date +%s) \
--query-string 'fields @timestamp, @message | filter @message like /ERROR/ | sort @timestamp desc'
Validation
Testing Checklist
# 1. Function deployed
aws lambda get-function --function-name hello-dev
# Should return function configuration
# 2. API Gateway exists
aws apigatewayv2 get-apis
# Should list your API
# 3. Endpoint responds
curl -I https://abc123.execute-api.us-east-1.amazonaws.com/hello
# Should return 200 OK
# 4. Logs visible
aws logs describe-log-groups --log-group-name-prefix /aws/lambda/hello
# Should show log group
# 5. Function can be invoked
aws lambda invoke \
--function-name hello-dev \
--payload '{"queryStringParameters":{"name":"Test"}}' \
response.json
cat response.json
# Should show response
# 6. Metrics available
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Invocations \
--dimensions Name=FunctionName,Value=hello-dev \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 3600 \
--statistics Sum
Best Practices
✅ Do's
- Use Infrastructure as Code: Serverless Framework, SAM, or Terraform
- Set appropriate timeouts: Default may be too long
- Monitor cold starts: Track performance
- Use environment variables: Configuration
- Enable logging: CloudWatch Logs
- Set memory appropriately: Affects CPU too
❌ Don'ts
- Don't put secrets in code: Use Parameter Store/Secrets Manager
- Don't make functions too large: Keep them focused
- Don't ignore costs: Monitor usage
- Don't skip error handling: Return proper status codes
- Don't forget CORS: Enable for web apps
Links
Share Your Success
Deployed serverless? Share it!
Tag @thedevopsdaily on X with:
- API endpoint (if public)
- Response time
- Cost estimate
- What you built
Use hashtags: #AdventOfDevOps #Serverless #AWS #Lambda #Day17
Ready to complete this challenge?
Mark this challenge as complete once you've finished the task. We'll track your progress!
Completed this challenge? Share your success!
Tag @thedevopsdaily on X (Twitter) and share your learning journey with the community!
These amazing companies help us create free, high-quality DevOps content for the community
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorFound an issue?