8 Simple CloudFormation Lambda Examples

Prerequisites:

GitHub Repository:

Languages & Frameworks:

  • Each of these CloudFormation examples is written in YAML.
  • We also use Node.js for our function code, but the examples can be adapted to any AWS Lambda-supported language, such as Python, Ruby, Java, C#, Go and even PHP.

Using CloudFormation to Deploy to Lambda

A simple "Hello World" CloudFormation Lambda example:

minimal-example.yml:

Resources:
  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: AwsLambdaMinimalExample
      Handler: index.handler
      Runtime: nodejs14.x
      Role: !GetAtt LambdaFunctionRole.Arn
      MemorySize: 1024
      Code:
        ZipFile: |
          exports.handler = async (event) => {
            return "Hello World!";
          }
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: AppendToLogsPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: "*"

To deploy the stack:

aws cloudformation create-stack \
  --stack-name minimal-example \
  --capabilities CAPABILITY_NAMED_IAM \
  --template-body file://minimal-example.yml

To monitor the deployment:

aws cloudformation describe-stack-events \
  --stack-name minimal-example

To invoke the function:

aws lambda invoke \
  --function-name AwsLambdaMinimalExample \
  function-result.json

To print the function's output:

cat function-result.json

This should read:

"Hello World"

Note:

  • All AWS Lambda functions return JSON.
  • As such, string responses will be wrapped in double quotes.

CloudFormation Lambda Function Code: Inline vs ZIP File

To deploy a Lambda function with inline code:

Code:
  ZipFile: |
    exports.handler = async (event) => {
      console.log("Hello World!")
    }

To deploy a Lambda function from a bundled/zipped codebase is a little harder, as it requires S3:

Code: 
  S3Bucket: my-lambda-function-code
  S3Key: MyBundledLambdaFunctionCode.zip

To bundle your code – and to use AWS CloudFormation to deploy the ZIP file to Lambda – do the following:

  1. ZIP your codebase. (The ZIP file must contain an index.js at the root, with your handler function as a named export.)
  2. Upload the ZIP file to S3.
  3. Reference the ZIP file from your CloudFormation template, like in the example above.

Note:  Handler: index.handler in the CloudFormation template means "use index.js as the entry point and handler as the function".

How to Invoke your Lambda Function

There are 3 ways to invoke your function:

  • Using the AWS SDK (and by extension: the AWS CLI).
  • Using AWS Lambda Triggers (to respond to SQS events, S3 events, etc.).
  • Using API Gateway (to respond to HTTP requests).

Here's some examples to demonstrate each option:

Quick Recap: Invoke using the AWS CLI

To invoke your function via the AWS CLI:

aws lambda invoke \
  --function-name AwsLambdaMinimalExample \
  function-result.json

Quick Recap: Invoke using the AWS SDK

To invoke your function programmatically, first install the AWS SDK:

npm install @aws-sdk/client-lambda@3

To invoke using the AWS SDK:

import { Lambda } from "@aws-sdk/client-lambda";

const functionName = "AwsLambdaMinimalExample" // CHANGE ME

const lambda = new Lambda({});

async function executeLambda() {
  const result = await lambda.invoke({ FunctionName: functionName });

  if (result.FunctionError !== undefined) {
    throw new Error(result.FunctionError);
  }
  
  if (result.Payload !== undefined) {
    const jsonString = Buffer.from(result.Payload).toString('utf8')
    return JSON.parse(jsonString)  
  }
  
  return undefined
}

AWS CloudFormation API Gateway Examples

To invoke your function over HTTP, you'll need use API Gateway.

Before you continue, you'll need to decide which integration type to use:

  • HTTP API is the new lightweight option. If you prefer to handle HTTP request routing yourself (i.e. in your function's code), then this option will serve you best.
  • REST API is the older, more feature-rich option. You'll need to define each individual route/endpoint in your CloudFormation YAML, but you'll benefit from extra features like rate limiting, usage plans, and so forth.

API Gateway Lambda CloudFormation (HTTP API)

To expose your function as an HTTP API:

http-api-example.yml:

Resources:
  HandlerLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          exports.handler = async (event) => {
            return {
              isBase64Encoded: false,
              statusCode:      200,
              headers:         { "content-type": "text/plain"},
              body:            "Hello World!"
            }
          }
      Handler: index.handler
      Role: !GetAtt LambdaFunctionRole.Arn
      Runtime: nodejs14.x
  
  # Creates a single-route (wildcard) HTTP API for our Lambda function.
  HttpApi:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: my-example-api
      ProtocolType: HTTP
  HttpApiIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref HttpApi
      IntegrationType: AWS_PROXY
      IntegrationUri: !GetAtt HandlerLambdaFunction.Arn
      PayloadFormatVersion: '2.0'
  HttpApiRoute:
    Type: AWS::ApiGatewayV2::Route
    DependsOn: HttpApiIntegration
    Properties:
      RouteKey: $default
      ApiId: !Ref HttpApi
      Target:
        !Join
        - /
        - - integrations
          - !Ref HttpApiIntegration
  HttpApiStage:
    Type: AWS::ApiGatewayV2::Stage
    Properties:
      StageName: $default
      ApiId: !Ref HttpApi
      AutoDeploy: true

  # Allows the Lambda function to access CloudWatch (add other services here, e.g. S3)
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: AppendToLogsPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: "*"

  # Allows API Gateway to call our Lambda function.
  HandlerLambdaPermissionHttpApi:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt HandlerLambdaFunction.Arn
      Principal: apigateway.amazonaws.com
      SourceArn:
        !Join
        - ''
        - - 'arn:'
          - !Ref AWS::Partition
          - ':execute-api:'
          - !Ref AWS::Region
          - ':'
          - !Ref AWS::AccountId
          - ':'
          - !Ref HttpApi
          - /*

# Outputs the URL to our Lambda HTTP API.
Outputs:
  ApiUrl:
    Description: URL of the HTTP API.
    Value: 
      !Join
      - ''
      - - !Ref HttpApi
        - .execute-api.
        - !Ref AWS::Region
        - .
        - !Ref AWS::URLSuffix

To deploy the HTTP API stack:

aws cloudformation create-stack \
  --stack-name http-api-example \
  --template-body file://http-api-example.yml \
  --capabilities CAPABILITY_NAMED_IAM

To monitor the HTTP API's deployment:

aws cloudformation describe-stack-events \
  --stack-name http-api-example

After it's deployed, get your Lambda's public URL with:

aws cloudformation describe-stacks \
  --stack-name http-api-example \
  --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" \
  --output text

API Gateway Lambda CloudFormation (REST API)

To expose your function as a REST API (i.e. using API Gateway V1):

rest-api-example.yml:

Resources:

  # Our API: we'll use a separate Lambda function per route.
  RestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      EndpointConfiguration:
        Types:
        - EDGE
      Name: simple-rest-endpoint
  RestApiDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
    - HelloWorldApiGatewayMethod
    Properties:
      RestApiId: !Ref RestApi
      StageName: dev

  # Route 1: GET /hello_world
  HelloWorldApiGatewayResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      ParentId: !GetAtt RestApi.RootResourceId
      PathPart: hello_world
      RestApiId: !Ref RestApi
  HelloWorldApiGatewayMethod:
    Type: AWS::ApiGateway::Method
    DependsOn:
    - HelloWorldFunctionApiGatewayPermission
    Properties:
      ApiKeyRequired: false
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri:
          !Join
          - ''
          - - 'arn:'
            - !Ref AWS::Partition
            - ':apigateway:'
            - !Ref AWS::Region
            - :lambda:path/2015-03-31/functions/
            - !GetAtt HelloWorldFunction.Arn
            - /invocations
      ResourceId: !Ref HelloWorldApiGatewayResource
      RestApiId: !Ref RestApi
  HelloWorldFunctionApiGatewayPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt HelloWorldFunction.Arn
      Principal: apigateway.amazonaws.com
      SourceArn:
        !Join
        - ''
        - - 'arn:'
          - !Ref AWS::Partition
          - ':execute-api:'
          - !Ref AWS::Region
          - ':'
          - !Ref AWS::AccountId
          - ':'
          - !Ref RestApi
          - /*/*
  HelloWorldFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: AppendToLogsPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: "*"
  HelloWorldFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          exports.handler = async (event) => {
            return {
              isBase64Encoded: false,
              statusCode:      200,
              headers:         { "content-type": "text/plain"},
              body:            "Hello World!"
            }
          }
      Handler: index.handler
      Role: !GetAtt HelloWorldFunctionRole.Arn
      Runtime: nodejs14.x

  # Route 2: ... copy the above block to make another! ...

# Outputs the URL to our Lambda REST API.
Outputs:
  ApiUrl:
    Description: URL of the REST API.
    Value:
      Fn::Join:
      - ''
      - - https://
        - Ref: RestApi
        - .execute-api.
        - Ref: AWS::Region
        - .
        - Ref: AWS::URLSuffix
        - /dev

To deploy the REST API stack:

aws cloudformation create-stack \
  --stack-name rest-api-example \
  --template-body file://rest-api-example.yml \
  --capabilities CAPABILITY_NAMED_IAM

To monitor the REST API's deployment:

aws cloudformation describe-stack-events \
  --stack-name rest-api-example

After it's deployed, you can get your Lambda's public URL with:

aws cloudformation describe-stacks \
  --stack-name rest-api-example \
  --query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" \
  --output text

Append /hello_world to the URL, and hit it!

AWS CloudFormation Lambda Trigger Examples

To invoke your function in response to events – i.e. raised by other AWS services – create a trigger from one of these examples:

SQS Lambda Trigger CloudFormation Example

To invoke your function in response to SQS events:

Resources:
  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.handler
      Runtime: nodejs14.x
      Role: !GetAtt LambdaFunctionRole.Arn
      Code:
        ZipFile: |
          exports.handler = async (sqsEvent) => {
            console.log(JSON.stringify(sqsEvent));
          }
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: LambdaFunctionRolePolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: "*"
          - Effect: Allow
            Action:
            - sqs:ReceiveMessage
            - sqs:DeleteMessage
            - sqs:GetQueueAttributes
            Resource: YOUR_SQS_QUEUE_ARN                      # CHANGE THIS
  LambdaFunctionSqsEventSourceMapping:
      Type: AWS::Lambda::EventSourceMapping
      DependsOn:
      - LambdaFunctionRole
      Properties:
        BatchSize: 10
        Enabled: true
        EventSourceArn: YOUR_SQS_QUEUE_ARN                    # CHANGE THIS
        FunctionName: !GetAtt LambdaFunction.Arn

Note: replace YOUR_SQS_QUEUE_ARN with the ARN of your SQS queue.

S3 Lambda Trigger CloudFormation Example

To invoke your function in response to S3 events (i.e. object creations and deletions):

Resources:
  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.handler
      Runtime: nodejs14.x
      Role: !GetAtt LambdaFunctionRole.Arn
      Code:
        ZipFile: |
          exports.handler = async (s3Event) => {
            console.log(JSON.stringify(s3Event));
          }
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: LambdaFunctionRolePolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: "*"

  # Allows S3 to call our Lambda function.
  LambdaFunctionS3Permission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt LambdaFunction.Arn
      Principal: s3.amazonaws.com
      SourceAccount: !Ref AWS::AccountId
      SourceArn:
        !Join
        - ''
        - - 'arn:'
          - !Ref AWS::Partition
          - ':s3:::'
          - my-bucket                         # CHANGE THIS

  # Create our S3 bucket, pre-configured to invoke our Lambda.
  S3Bucket:
    Type: AWS::S3::Bucket
    DependsOn:
    - LambdaFunctionS3Permission
    Properties:
      BucketName: my-bucket                   # CHANGE THIS
      NotificationConfiguration:
        LambdaConfigurations:
        - Event: s3:*
          Function: !GetAtt LambdaFunction.Arn

Note: replace my-bucket with a unique bucket name.

SNS Lambda Trigger CloudFormation Example

To invoke your function in response to SNS topic events:

Resources:
  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.handler
      Runtime: nodejs14.x
      Role: !GetAtt LambdaFunctionRole.Arn
      Code:
        ZipFile: |
          exports.handler = async (snsEvent) => {
            console.log(JSON.stringify(snsEvent));
          }
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: LambdaFunctionRolePolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: "*"

  # Allows SNS to call our Lambda function.
  LambdaFunctionSNSPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt LambdaFunction.Arn
      Principal: sns.amazonaws.com
      SourceArn:
        !Join
        - ''
        - - 'arn:'
          - !Ref AWS::Partition
          - ':sns:'
          - !Ref AWS::Region
          - ':'
          - !Ref AWS::AccountId
          - ':'
          - YOUR_SNS_TOPIC_NAME               # CHANGE THIS

  # Create our SNS topic, pre-configured to invoke our Lambda.
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: YOUR_SNS_TOPIC_NAME          # CHANGE THIS
      Subscription:
      - Endpoint: !GetAtt LambdaFunction.Arn
        Protocol: lambda

Note: replace YOUR_SNS_TOPIC_NAME with a unique topic name.

Kinesis Lambda Trigger CloudFormation Example

To invoke your function as a Kinesis record consumer:

Resources:
  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.handler
      Runtime: nodejs14.x
      Role: !GetAtt LambdaFunctionRole.Arn
      Code:
        ZipFile: |
          exports.handler = async (kinesisEvent) => {
            console.log(JSON.stringify(kinesisEvent));
          }
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: LambdaFunctionRolePolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: "*"
          - Effect: Allow
            Action:
            - kinesis:GetRecords
            - kinesis:GetShardIterator
            - kinesis:DescribeStream
            - kinesis:ListStreams
            Resource: YOUR_KINESIS_STREAM_ARN                 # CHANGE THIS
  LambdaFunctionKinesisEventSourceMapping:
      Type: AWS::Lambda::EventSourceMapping
      DependsOn:
      - LambdaFunctionRole
      Properties:
        BatchSize: 10
        Enabled: true
        StartingPosition: TRIM_HORIZON
        EventSourceArn: YOUR_KINESIS_STREAM_ARN               # CHANGE THIS
        FunctionName: !GetAtt LambdaFunction.Arn

Note: replace YOUR_KINESIS_STREAM_ARN with the ARN of your Kinesis stream.

DynamoDB Lambda Trigger CloudFormation Example

To invoke your function in response to DynamoDB events (i.e. item deletions, updates and creations):

Resources:
  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: index.handler
      Runtime: nodejs14.x
      Role: !GetAtt LambdaFunctionRole.Arn
      Code:
        ZipFile: |
          exports.handler = async (dynamoDbEvent) => {
            console.log(JSON.stringify(dynamoDbEvent));
          }
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: LambdaFunctionRolePolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: "*"
          - Effect: Allow
            Action:
            - dynamodb:GetRecords
            - dynamodb:GetShardIterator
            - dynamodb:DescribeStream
            - dynamodb:ListStreams
            Resource: YOUR_DYNAMODB_STREAM_ARN                 # CHANGE THIS
  LambdaFunctionDynamoDbEventSourceMapping:
      Type: AWS::Lambda::EventSourceMapping
      DependsOn:
      - LambdaFunctionRole
      Properties:
        BatchSize: 10
        Enabled: true
        StartingPosition: TRIM_HORIZON
        EventSourceArn: YOUR_DYNAMODB_STREAM_ARN               # CHANGE THIS
        FunctionName: !GetAtt LambdaFunction.Arn

Note: replace YOUR_DYNAMODB_STREAM_ARN with the "Stream ARN" of your DynamoDB table. You can access this with !GetAtt DynamoDbTable.StreamArn.

Final Thoughts

You can find each of these examples in the following GitHub repository:

https://github.com/upload-io/cloudformation-lambda-examples

While examples are great, nothing beats reading the official CloudFormation documentation – it's possibly some of the clearest, most composable documentation there is.

We recommend starting by taking one of our Lambda CloudFormation examples, and then extending it by searching the "resource type" in Google (which will land you on the official AWS docs), and then exploring all the possible fields for that resource type.

Happy coding!