Serverless computing has revolutionized how we deploy and scale applications. AWS Lambda, as a leading serverless platform, offers multiple ways to deploy your applications. In this post, we’ll explore three different Scenarioes to deploy a simple webapp to AWS Lambda using Serverless Framework:

  1. Python with ZIP deployment
  2. Java with JAR deployment
  3. Go with Docker container

Prerequisites Link to heading

Before we begin, ensure you have:

  • AWS CLI installed and configured
  • Appropriate AWS permissions
  • Python 3.11+ (for Python examples)
  • Java 11+ (for Java example)
  • Go 1.21+ (for Go example)
  • Docker (for container scenario)
  • Node.js and npm (for Serverless Framework)
  • Serverless Framework installed (npm install -g serverless@3.39.0)
  • AWS credentials for serverless
# Configure AWS credentials for serverless using environment variables
serverless config credentials --provider aws --key $AWS_ACCESS_KEY_ID --secret $AWS_SECRET_ACCESS_KEY
✔ Profile "default" has been configured

Scenario 1: Python with ZIP Deployment Link to heading

This is the simplest scenario, perfect for small Python applications.

Project Structure Link to heading

python-zip/
├── app.py
├── requirements.txt
└── serverless.yml

app.py Link to heading

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'application/json'
        },
        'body': json.dumps({
            'message': 'Hello from Python Lambda!'
        })
    }

requirements.txt Link to heading

boto3==1.26.137

serverless.yml Link to heading

service: python-zip-app

provider:
  name: aws
  runtime: python3.11
  region: us-east-1

package:
  individually: true

functions:
  hello:
    handler: app.lambda_handler
    package:
      artifact: deployment.zip
    events:
      - http:
          path: /hello
          method: get

Deployment Steps Link to heading

# Create deployment package
pip install -r requirements.txt -t ./package
cp app.py ./package/
cd package
zip -r ../deployment.zip .
cd ..

# Deploy
serverless deploy --stage dev
Deploying python-zip-app to stage dev (us-east-1)

✔ Service deployed to stack python-zip-app-dev (79s)

endpoint: GET - https://jzlki1fb8e.execute-api.us-east-1.amazonaws.com/dev/hello
functions:
  hello: python-zip-app-dev-hello (31 MB)

# Visit the deployed endpoint
curl https://jzlki1fb8e.execute-api.us-east-1.amazonaws.com/dev/hello
{"message": "Hello from Python Lambda!"}

# Tear down
serverless remove --stage dev

Notes: If you don’t want to pre build the deployment ZIP, you can use the serverless-python-requirements plugin to handle dependencies automatically. This approach is particularly useful when:

  • You have complex dependencies
  • You need to compile native extensions
  • You want to ensure consistent builds across environments

Here’s how to configure it:

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: true    # Use Docker to build dependencies
    slim: true           # Remove unnecessary files
    noDeploy: []         # Exclude packages from deployment
    useStaticCache: true # Cache dependencies between builds

functions:
  hello:
    handler: app.lambda_handler
    events:
      - http:
          path: /hello
          method: get

Scenario 2: Java with JAR Deployment Link to heading

This scenario deploys a Java application that needs the full power of the JVM.

Project Structure Link to heading

java-jar/
├── src/
│   └── main/
│       └── java/
│           └── com/
│               └── example/
│                   └── App.java
├── pom.xml
└── serverless.yaml

App.java Link to heading

package com.example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;

public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
    public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
        APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
        response.setStatusCode(200);
        response.setBody("{\"message\": \"Hello from Java Lambda!\"}");
        return response;
    }
}

pom.xml Link to heading

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>lambda-java</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.2.1</version>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>3.11.0</version>
        </dependency>
    </dependencies>

    <build>
    <plugins>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

serverless.yaml Link to heading

service: java-jar-app

provider:
  name: aws
  runtime: java11
  region: us-east-1

package:
  individually: true

functions:
  hello:
    handler: com.example.App::handleRequest
    package:
      artifact: target/lambda-java-1.0-SNAPSHOT-jar-with-dependencies.jar
    events:
      - http:
          path: /hello
          method: get

Deployment Steps Link to heading

# Build the JAR
mvn clean package

# Deploy
serverless deploy --stage dev
Deploying java-jar-app to stage dev (us-east-1)

✔ Service deployed to stack java-jar-app-dev (29s)

endpoint: GET - https://rjk8p44n91.execute-api.us-east-1.amazonaws.com/dev/hello
functions:
  hello: java-jar-app-dev-hello (1 MB)

# Visit the deployed endpoint
curl https://rjk8p44n91.execute-api.us-east-1.amazonaws.com/dev/hello
{"message": "Hello from Java Lambda!"}

# Tear down
serverless remove --stage dev

Scenario 3: Go with Docker Container Link to heading

This scenario offers the most flexibility and control over the runtime environment, with the added benefit of Go’s performance and small binary size.

Project Structure Link to heading

go-docker/
├── main.go
├── go.mod
├── Dockerfile
└── serverless.yaml

main.go Link to heading

package main

import (
	"context"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

type Response events.APIGatewayProxyResponse

func HandleRequest(ctx context.Context) (Response, error) {
	response := Response{
		StatusCode: 200,
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		Body: `{"message": "Hello from Go Lambda in Docker!"}`,
	}
	return response, nil
}

func main() {
	lambda.Start(HandleRequest)
}

go.mod Link to heading

module go-lambda

go 1.21

require (
	github.com/aws/aws-lambda-go v1.41.0
)

Dockerfile Link to heading

FROM public.ecr.aws/lambda/provided:al2

RUN yum install -y golang

WORKDIR /app

COPY go.mod go.sum main.go ./

RUN go mod download

RUN go build -o main

ENTRYPOINT [ "/app/main" ]

serverless.yaml Link to heading

service: go-docker-app

provider:
  name: aws
  region: us-east-1
  ecr:
    images:
      appimage:
        uri: ${env:AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/go-lambda:latest

functions:
  hello:
    image:
      name: appimage
    architecture: arm64
    events:
      - http:
          path: /hello
          method: get

Deployment Steps Link to heading

# 
go mod tidy

# Create ECR and login
export AWS_ACCOUNT_ID='<your-account-id>'
aws ecr create-repository --repository-name go-lambda --region us-east-1
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com

# Build and push the Docker image
docker build -t go-lambda .
docker tag go-lambda:latest $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/go-lambda:latest
docker push $AWS_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/go-lambda:latest

# Deploy
serverless deploy --stage dev
Deploying go-docker-app to stage dev (us-east-1)

✔ Service deployed to stack go-docker-app-dev (109s)

endpoint: GET - https://dm54x42wrk.execute-api.us-east-1.amazonaws.com/dev/hello
functions:
  hello: go-docker-app-dev-hello

# Visit the deployed endpoint
curl https://dm54x42wrk.execute-api.us-east-1.amazonaws.com/dev/hello
{"message": "Hello from Go Lambda in Docker!"}

# Tear down
serverless remove --stage dev

# Remove ECR repository and images
aws ecr delete-repository --repository-name go-lambda --region us-east-1 --force

Comparison Link to heading

ScenarioProsConsBest For
Python ZIP- Simple deployment
- Fast cold starts
- Small package size
- Limited dependencies
- No custom runtime
Small to medium applications
Quick prototypes
Script-like functions
Java JAR- Full JVM capabilities
- Strong typing
- Enterprise features
- Larger package size
- Slower cold starts
Enterprise applications
Complex business logic
Existing Java codebases
Go Docker- Full control over runtime
- Any dependencies
- Consistent environment
- Excellent performance
- More complex setup
- Larger image size
Performance-critical applications
Custom runtime needs
Microservices

Cold starts are a crucial consideration when working with AWS Lambda. They occur when:

  • A function is invoked for the first time
  • After a period of inactivity (typically 5-15 minutes)
  • When Lambda needs to scale to handle increased load

What Happens During a Cold Start Link to heading

  1. Container Initialization: Lambda creates a new container
  2. Runtime Setup: The runtime environment (Python, Java, Go) is initialized
  3. Code Loading: Your function code and dependencies are loaded
  4. Initialization: Any initialization code in your function runs
  5. Request Handling: Finally, your function can process the actual request

Strategies to Minimize Cold Starts Link to heading

  1. Provisioned Concurrency

    functions:
      hello:
        provisionedConcurrency: 5  # Keep 5 instances warm
    
  2. Optimize Package Size

    • Minimize dependencies
    • Use slim packages
    • Remove unused code
  3. Warm-up Strategies

    • Regular pings to keep functions warm
    • CloudWatch Events to trigger functions periodically
    • Use provisioned concurrency for critical paths
  4. Architecture Considerations

    • Use appropriate memory settings
    • Consider function timeout settings
    • Implement proper error handling