A blueprint, set square, and mechanical pencil. The blueprint depicts an app window with two cogs.

Deploying a Lambda with CloudFormation

Matthew Casperson

Matthew Casperson

June 8, 2022 • 3 mins

Lambda is a serverless Function as a Service (FaaS) offering from AWS. Lambdas provide scaling, high availability, and the ability to scale to zero keeping costs down for infrequently used deployments.

Like most AWS resources, Lambdas can access VPCs to interact with other resources like databases or EC2 instances.

In this post, you deploy a simple Lambda, and then build on the VPC with private and public subnets example provided in a previous post to deploy a Lambda in a VPC with internet access using CloudFormation.

A simple Lambda CloudFormation template

Deploying a Lambda can be as simple as creating a log group to capture the Lambda's output, granting the Lambda access to the log group through a IAM role, and then defining the Lambda itself. An example template deploying these resources is shown below:

Parameters:
  Tag:
    Type: String
  LambdaS3Bucket:
    Type: String
  LambdaS3Key:
    Type: String
  LambdaName:
    Type: String

Resources: 
  AppLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaName}"

  IamRoleLambdaExecution:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/"
      RoleName: !Sub "${LambdaName}-role"  
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: "Allow"
          Principal:
            Service:
            - "lambda.amazonaws.com"
          Action: "sts:AssumeRole"
      ManagedPolicyArns: 
      - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
      Policies:
      - PolicyName: !Sub "${LambdaName}-policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action:
            - "logs:CreateLogStream"
            - "logs:CreateLogGroup"
            - "logs:PutLogEvents"
            Resource:
            - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaName}*:*"

  MyLambda:
    Type: "AWS::Lambda::Function"
    Properties:
        Code:
          S3Bucket: !Ref "LambdaS3Bucket"
          S3Key: !Ref "LambdaS3Key"
        Description: "My Lambda"
        FunctionName: !Ref "LambdaName"
        Handler: "not.used.in.provided.runtime"
        MemorySize: 256
        PackageType: "Zip"
        Role: !GetAtt "IamRoleLambdaExecution.Arn"
        Runtime: "provided"
        Timeout: 30

The logs generated by the Lambda are placed in a new log group, represented by the AWSLogsLogGroup resource:

  AppLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaName}"

To grant the Lambda permission to write to the log group above, you must create a new IAM role able to be assumed by a Lambda and include the permissions to write to the log group:

  IamRoleLambdaExecution:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/"
      RoleName: !Sub "${LambdaName}-role"  
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: "Allow"
          Principal:
            Service:
            - "lambda.amazonaws.com"
          Action: "sts:AssumeRole"
      ManagedPolicyArns: 
      - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
      Policies:
      - PolicyName: !Sub "${LambdaName}-policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action:
            - "logs:CreateLogStream"
            - "logs:CreateLogGroup"
            - "logs:PutLogEvents"
            Resource:
            - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaName}*:*"

The final step is creating the Lambda itself, represented by the AWSLambdaFunction resource.

Lambdas deploy code that has been uploaded to an S3 bucket. Uploading files is not performed by CloudFormation, so this must be performed as a separate step prior to deploying the template.

The example Lambda below is configured to deploy a natively compiled binary, usually written in a language like Go or using a compiler like GraalVM.

Other languages, like Java, DotNET Core, Python, PHP, and Node.js, require their own unique runtime, and this affects the Runtime and Handler properties in the CloudFormation template:

MyLambda:
  Type: "AWS::Lambda::Function"
  Properties:
      Code:
        S3Bucket: !Ref "LambdaS3Bucket"
        S3Key: !Ref "LambdaS3Key"
      Description: "My Lambda"
      FunctionName: !Ref "LambdaName"
      Handler: "not.used.in.provided.runtime"
      MemorySize: 256
      PackageType: "Zip"
      Role: !GetAtt "IamRoleLambdaExecution.Arn"
      Runtime: "provided"
      Timeout: 30

Placing a Lambda in a VPC

In a more complex scenario, your Lambda will be granted access to a VPC in order to access shared resources like a database or an EC2 instance.

The template below builds on a previous example demonstrating a VPC with a mix of public and private subnets, and then deploys a Lambda with VPC access:

Parameters:
  Tag:
    Type: String
  LambdaS3Bucket:
    Type: String
  LambdaS3Key:
    Type: String
  LambdaName:
    Type: String

Resources: 
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
      InstanceTenancy: "default"
      Tags:
      - Key: "Name"
        Value: !Ref "Tag"

  InternetGateway:
    Type: "AWS::EC2::InternetGateway"

  VPCGatewayAttachment:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref "VPC"
      InternetGatewayId: !Ref "InternetGateway"

  SubnetA:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select 
        - 0
        - !GetAZs 
          Ref: 'AWS::Region'
      VpcId: !Ref "VPC"
      CidrBlock: "10.0.0.0/24"

  SubnetB:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select 
        - 1
        - !GetAZs 
          Ref: 'AWS::Region'
      VpcId: !Ref "VPC"
      CidrBlock: "10.0.1.0/24"

  SubnetC:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select 
        - 1
        - !GetAZs 
          Ref: 'AWS::Region'
      VpcId: !Ref "VPC"
      CidrBlock: "10.0.2.0/24"

  RouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref "VPC"

  InternetRoute:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref RouteTable

  SubnetARouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref SubnetA

  EIP:
    Type: "AWS::EC2::EIP"
    Properties:
      Domain: "vpc"

  Nat:
    Type: "AWS::EC2::NatGateway"
    Properties:
      AllocationId: !GetAtt "EIP.AllocationId"
      SubnetId: !Ref "SubnetA"

  NatRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref "VPC"

  NatRoute:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref "Nat"
      RouteTableId: !Ref "NatRouteTable"

  SubnetBRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref NatRouteTable
      SubnetId: !Ref SubnetB

  SubnetCRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref NatRouteTable
      SubnetId: !Ref SubnetC

  InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupName: "Example Security Group"
      GroupDescription: "Lambda Traffic"
      VpcId: !Ref "VPC"
      SecurityGroupEgress:
      - IpProtocol: "-1"
        CidrIp: "0.0.0.0/0"

  InstanceSecurityGroupIngress:
    Type: "AWS::EC2::SecurityGroupIngress"
    DependsOn: "InstanceSecurityGroup"
    Properties:
      GroupId: !Ref "InstanceSecurityGroup"
      IpProtocol: "tcp"
      FromPort: "0"
      ToPort: "65535"
      SourceSecurityGroupId: !Ref "InstanceSecurityGroup"

  AppLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaName}"

  IamRoleLambdaExecution:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/"
      RoleName: !Sub "${LambdaName}-role"  
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: "Allow"
          Principal:
            Service:
            - "lambda.amazonaws.com"
          Action: "sts:AssumeRole"
      ManagedPolicyArns: 
      - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
      Policies:
      - PolicyName: !Sub "${LambdaName}-policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action:
            - "logs:CreateLogStream"
            - "logs:CreateLogGroup"
            - "logs:PutLogEvents"
            Resource:
            - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaName}*:*"

  MyLambda:
    Type: "AWS::Lambda::Function"
    Properties:
        Code:
          S3Bucket: !Ref "LambdaS3Bucket"
          S3Key: !Ref "LambdaS3Key"
        Description: "My Lambda"
        FunctionName: !Ref "LambdaName"
        Handler: "not.used.in.provided.runtime"
        MemorySize: 256
        PackageType: "Zip"
        Role: !GetAtt "IamRoleLambdaExecution.Arn"
        Runtime: "provided"
        Timeout: 30
        VpcConfig:
            SecurityGroupIds:
            - !Ref "InstanceSecurityGroup"
            SubnetIds:
            - !Ref "SubnetB"
            - !Ref "SubnetC"

Outputs:
  VpcId:
    Description: The VPC ID
    Value: !Ref VPC

The majority of this template defines the resources required to build a VPC with both a private and public subnet, and building the network infrastructure like internet gateways and NAT gateways to provide internet access to any other resources placed in the VPC subnets. These resources are covered in detail in our post about creating a mixed AWS VPC with CloudFormation.

You then create a security group, represented by the AWSEC2SecurityGroup resource, to define the networking rules applied to resources in this VPC. This example includes rules that allow all outbound traffic:

  InstanceSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupName: "Example Security Group"
      GroupDescription: "Lambda Traffic"
      VpcId: !Ref "VPC"
      SecurityGroupEgress:
      - IpProtocol: "-1"
        CidrIp: "0.0.0.0/0"

Resources that share the security group are allowed to communicate with each other using the following security group ingress rule, represented by the AWSEC2SecurityGroupIngress resource. This allows related resources access to each other without requiring them to have known IP addresses or be placed in special CIDR blocks:

  InstanceSecurityGroupIngress:
    Type: "AWS::EC2::SecurityGroupIngress"
    DependsOn: "InstanceSecurityGroup"
    Properties:
      GroupId: !Ref "InstanceSecurityGroup"
      IpProtocol: "tcp"
      FromPort: "0"
      ToPort: "65535"
      SourceSecurityGroupId: !Ref "InstanceSecurityGroup"

The log group and IAM role are the same as the simple example described at the start of this post.

The Lambda is changed slightly to include a new VPCConfig property granting the Lambda access to resources inside the VPC.

It's important to note that the Lambda is granted access to the private subnets SubnetB and SubnetC, which are those subnets without an internet gateway attached:

  MyLambda:
    Type: "AWS::Lambda::Function"
    Properties:
        Code:
          S3Bucket: !Ref "LambdaS3Bucket"
          S3Key: !Ref "LambdaS3Key"
        Description: "My Lambda"
        FunctionName: !Ref "LambdaName"
        Handler: "not.used.in.provided.runtime"
        MemorySize: 256
        PackageType: "Zip"
        Role: !GetAtt "IamRoleLambdaExecution.Arn"
        Runtime: "provided"
        Timeout: 30
        VpcConfig:
            SecurityGroupIds:
            - !Ref "InstanceSecurityGroup"
            SubnetIds:
            - !Ref "SubnetB"
            - !Ref "SubnetC"

Conclusion

Lambdas can be simple to deploy, requiring a small number of supporting resources like log groups and IAM roles to allow the Lambda to be monitored and debugged.

For more complex scenarios where Lambdas must have access to other resources like databases or EC2 instances inside a VPC, Lambdas can be configured with network access to specified subnets and have network traffic controlled using security groups.

In this post, you learned how to perform a simple Lambda deployment, and then saw a more complex example that built a VPC alongside the Lambda.

We have other posts about CloudFormation templates you might find helpful too.

Follow our series about Runbooks

We're focusing on Runbooks for a few months this year. We’ll include a summary of the blog posts in our monthly newsletter. Sign up to follow along.

Happy deployments!

Loading...