AWS Lambda Deployment Learning Guide

Deployment is often the biggest pain in software development. Hence, I’m here to share my learning strategies, steps to deploy, and working github templates for people who want to deploy their Python Flask and node express app on AWS Lambda functions.

Step 1: Download sam cli (command line for aws)

Step 2: Add credentials

  • I recommend using “root user” rather than setting up the IAM role in the beginning. Permission problems can be annoying when while root user has all the permission.
  • To add root users, go to security credentials, and add an access key.

Step 3: Start with a simple sample template from aws

Run on your terminal

  • sam init

Build your function and test if it is correct

  • sam build

Test your lint

  • cfn-lint template.yaml This command is very helpful because we usually want to modify our template.yaml file, and it gives us the exact line on our error.

Test your function

  • sam validate

Try to run it locally

  • sam local invoke: You can invoke your local function. To specify a function when you have multiple functions, use sam local invoke {{your_function_name}}
  • sam local start-api: You will receive an output API that you can test on.

Deploy on aws

  • sam deploy: You will be able to see your function on cloud formation.

Step 4: Add your own functions

Python: Flask

Basic folder structure

├── README.md
├── __init__.py
├── events
│ └── event.json
├── hello_world
│ ├── __init__.py
│ ├── app.py
│ └── requirements.txt
├── samconfig.toml
├── template.yaml
└── tests
├── __init__.py
├── integration
│ ├── __init__.py
│ └── test_api_gateway.py
├── requirements.txt
└── unit
├── __init__.py
└── test_handler.py

The only thing we need to modify is the app.py file to add flask, and template.yaml to configure aws lambda.

My app.py

from flask import Flask
import json
app = Flask(__name__)
@app.route('/')
def hello(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message": "Success"
})
}
if __name__ == '__main__':
app.run(debug=True)

Corresponding template.yaml file

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
FlaskAppLambda:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.hello
Runtime: python3.9
Events:
FlaskAppApi:
Type: Api
Properties:
Path: /
Method: get

Node: Express

While I was using zip as a package type of Python deployment, when you use Node, the aws template is naturally tied with the Docker container, but you can also choose to not use it.

Basic folder structure

.
├── README.md
├── events
├── hello-world
│ ├── Dockerfile
│ ├── app.mjs
│ ├── handler.mjs
│ ├── index.mjs
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ └── tests
├── samconfig.toml
└── template.yaml

Some useful command lines to see if your container is correctly connected with your Node.

  • To see all containers: docker ps -a
  • To see only the running ones: docker ps
  • To run a specific container: docker run --platform linux/amd64 -it helloworldfunction:nodejs18.x-v1

Here, you will need to modify several files: index.mjs, template.yaml. I bolded all the code that I added apart from the template.

The aws template comes with app.mjs.

/**
*
* Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
* @param {Object} event - API Gateway Lambda Proxy Input Format
*
* Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
* @param {Object} context
*
* Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
* @returns {Object} object - API Gateway Lambda Proxy Output Format
*
*/
export const lambdaHandler = async (event, context) => {
try {
// Check if the HTTP method is POST
if (event.httpMethod !== "POST") {
return {
statusCode: 405, // Method Not Allowed
body: JSON.stringify({
error: "Method not allowed. Use POST method.",
}),
};
}
// Parse the incoming request body
const requestBody = JSON.parse(event.body);
// Your POST request handling logic here return {
statusCode: 200,
body: JSON.stringify({
message: "hello world",
}),
};
} catch (err) {
console.log(err);
return {
statusCode: 500,
body: JSON.stringify({
error: "Internal Server Error",
}),
};
}
};

Create a new file index.mj

import express from "express";
const app_test = express();
app_test.get("/myfunction/get", (req, res) => {
console.log("Received a GET request to /myfunction");
res.json({ message: "Hello, World!" });
});
// Handler for POST requests to /hello
app_test.post("/myfunction/post", (req, res) => {
console.log("Received a POST request to /myfunction/hell");
res.json({ message: "Received a POST request to /myfunction/hell" });
});
const port = 3002;
app_test.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
export default app_test;

Modify template.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
sam-node-template
Sample SAM Template for sam-node-template
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api
Properties:
Path: /
Method: post
Metadata:
DockerTag: nodejs18.x-v1
DockerContext: ./hello-world
Dockerfile: Dockerfile
# Define the functions section here
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello-world/ #path to the function code
Handler: handler.hello
Runtime: nodejs18.x
Events:
MyGetFunction:
Type: Api
Properties:
Path: /myfunction/get
Method: GET
MyPostFunction:
Type: Api
Properties:
Path: /myfunction/post
Method: POST
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
HelloWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${HelloWorldFunction}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn
MyFunctionArn:
Description: "ARN of MyFunction"
Value: !GetAtt MyFunction.Arn
MyFunctionApi:
Description: "API of MyFunction"
Value: !Sub "https://${MyFunction}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
MyFunctionIamRole:
Description: "Implicit IAM Role created for My function"
Value: !GetAtt MyFunctionRole.Arn

Learning Strategies

Always remember to run it locally first.

  1. Run without lambda: You can run with your Python through python3 app.py or node index.js.
  2. Run with lambda locally: You can build with sam build, test with sam validate, and test your local API through sam local start-api.
  3. Run lambda deployment: After sam deploy, look around your aws cloud console to gain bug reports. You can also directly add an API test on the console to prevent authentication or other potential problems.

Test the deployment environment first before integrating it with your code

This is the biggest learning I gained. Usually, I rush to integrate the deployment code with my existing codebase, but the bug becomes very complex because of too many moving parts involved. This time, I played around the code with Python and Node first to make sure I understood how to tweak it. I only integrate my Nodejs deployment code with my codebase when I’m confident that my get and post request both work on my deployment testing code.

Working templates on my Github

  • Node template: here
  • Node template with express: here
  • Python template: here
  • Python Flask template: here

--

--