How to create a new version of a Lambda function using CloudFormation?

45,792

Solution 1

AWS::Lambda::Version is not useful. You have to add a new resource for every Lambda version. If you want to publish a new version for every Cloudformation update, you have to hack the system.

I solved this issue creating a Lambda backed custom resource which is triggered for every deployment. Inside this Lambda, I am creating a new version for the Lambda function given in parameter.

For the Lambda's source you can check http://serverless-arch-eu-west-1.s3.amazonaws.com/serverless.zip

Here is the example Cloudformation using this Deployment Lambda function (You might need some modification):

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "DeploymentTime": {
      "Type": "String",
      "Description": "It is a timestamp value which shows the deployment time. Used to rotate sources."
    }
  },
  "Resources": {
    "LambdaFunctionToBeVersioned": {
      "Type": "AWS::Lambda::Function",
       ## HERE DEFINE YOUR LAMBDA AS USUAL ##
    },
    "DeploymentLambdaRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "lambda.amazonaws.com"
                ]
              },
              "Action": [
                "sts:AssumeRole"
              ]
            }
          ]
        },
        "Path": "/",
        "ManagedPolicyArns": [
          "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
        ],
        "Policies": [
          {
            "PolicyName": "LambdaExecutionPolicy",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "lambda:PublishVersion"
                  ],
                  "Resource": [
                    "*"
                  ]
                }
              ]
            }
          }
        ]
      }
    },
    "DeploymentLambda": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "Role": {
          "Fn::GetAtt": [
            "DeploymentLambdaRole",
            "Arn"
          ]
        },
        "Handler": "serverless.handler",
        "Runtime": "nodejs4.3",
        "Code": {
          "S3Bucket": {
            "Fn::Sub": "serverless-arch-${AWS::Region}"
          },
          "S3Key": "serverless.zip"
        }
      }
    },
    "LambdaVersion": {
      "Type": "Custom::LambdaVersion",
      "Properties": {
        "ServiceToken": {
          "Fn::GetAtt": [
            "DeploymentLambda",
            "Arn"
          ]
        },
        "FunctionName": {
          "Ref": "LambdaFunctionToBeVersioned"
        },
        "DeploymentTime": {
          "Ref": "DeploymentTime"
        }
      }
    }
  }
}

(Disclaimer: This code is a part of my book, for more information about Lambda & API Gateway you can check: https://www.amazon.com/Building-Serverless-Architectures-Cagatay-Gurturk/dp/1787129195)

Solution 2

I have a similar use case (needing to use CloudFormation to manage a lambda function to be used @edge in CloudFront, for which a specific lambda function version is always required, not $LATEST) and my searches landed me at this question first, but after a bit more digging I was happy to find there is now native support for automatic lambda versioning with the new AutoPublishAlias feature of the AWS Serverless Application Model (basically an optional extra set of higher-level constructs for your CloudFormation templates).

Announced here: https://github.com/awslabs/serverless-application-model/issues/41#issuecomment-347723981

For details see:

Essentially you include AutoPublishAlias in your AWS::Serverless::Function definition:

MyFunction:
  Type: "AWS::Serverless::Function"
  Properties:
    # ...
    AutoPublishAlias: MyAlias

And then elsewhere in the CloudFormation template you can reference the latest published version as !Ref MyFunction.Version (yaml syntax).

Solution 3

This post is out-of-date. I am updating it here so others can see the correct solution for versioning Lambdas as of 06-09-2020, without the need for extra custom versioning Lambdas.

This:

Description: Lambda Example
Resources:
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Code:
        ZipFile: |
          'Example Code';
      Runtime: nodejs12.x
      Timeout: 5

Becomes this:

Description: Lambda Example
Transform: AWS::Serverless-2016-10-31
Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      AutoPublishAlias: live
      Handler: index.handler
      InlineCode: |
        'Example Code';
      Runtime: nodejs12.x
      Timeout: 5

The Transform: allows AWS::Serverless::Function inside of a CloudFormation template which in turn supports lambda versioning.

Don't let the dated "Best Answer" above - built for that persons book - throw you down a rabbit hole like I did.

You're welcome.

Solution 4

The AWS::Lambda::Version resource only represents a single published Lambda function version- it will not automatically publish new versions on every update of your code. To accomplish this, you have two options:

1. Custom resource

You can implement your own Custom Resource that calls PublishVersion on each update.

For this approach, you'll still need to change at least one Parameter every time you update your stack, in order to trigger an update on the Custom Resource that will trigger the PublishVersion action. (You won't have to actually update the template, though.)

Here's a full, working example:

Launch Stack

Description: Publish a new version of a Lambda function whenever the code is updated.
Parameters:
  Nonce:
    Description: Change this string when code is updated.
    Type: String
    Default: "Test"
Resources:
  MyCustomResource:
    Type: Custom::Resource
    Properties:
      ServiceToken: !GetAtt MyFunction.Arn
      Nonce: !Ref Nonce
  MyFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          var response = require('cfn-response');
          exports.handler = function(event, context) {
            return response.send(event, context, response.SUCCESS, {Result: '${Nonce}'});
          };
      Runtime: nodejs4.3
  LambdaDeploy:
    Type: Custom::LambdaVersion
    Properties:
      ServiceToken: !GetAtt LambdaDeployFunction.Arn
      FunctionName: !Ref MyFunction
      Nonce: !Ref Nonce
  LambdaDeployFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: "index.handler"
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          var AWS = require('aws-sdk');
          var response = require('cfn-response');
          exports.handler = (event, context) => {
            console.log("Request received:\n", JSON.stringify(event));
            if (event.RequestType == 'Delete') {
              return response.send(event, context, response.SUCCESS);
            }
            var lambda = new AWS.Lambda();
            lambda.publishVersion({FunctionName: event.ResourceProperties.FunctionName}).promise().then((data) => {
              return response.send(event, context, response.SUCCESS, {Version: data.Version}, data.FunctionArn);
            }).catch((e) => {
              return response.send(event, context, response.FAILED, e);
            });
          };
      Runtime: nodejs4.3
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal: {Service: [lambda.amazonaws.com]}
          Action: ['sts:AssumeRole']
      Path: /
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
      - PolicyName: PublishVersion
        PolicyDocument:
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Action: ['lambda:PublishVersion']
            Resource: '*'
Outputs:
  LambdaVersion:
    Value: !GetAtt LambdaDeploy.Version
  CustomResourceResult:
    Value: !GetAtt MyCustomResource.Result

2. Template preprocessor

You can use a template preprocessor like embedded Ruby (or just manually updating your template on each deploy) to publish a new Version on each update of your code by changing the AWS::Lambda::Version resource's Logical ID whenever your code is updated.

Example:

# template.yml
Description: Publish a new version of a Lambda function whenever the code is updated.
<%nonce = rand 10000%>
Resources:
  LambdaVersion<%=nonce%>:
    Type: AWS::Lambda::Version
    Properties:
      FunctionName: !Ref MyFunction
  MyCustomResource:
    Type: Custom::Resource
    Properties:
      ServiceToken: !GetAtt MyFunction.Arn
      Nonce: <%=nonce%>
  MyFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          var response = require('cfn-response');
          exports.handler = function(event, context) {
            return response.send(event, context, response.SUCCESS, {Result: '<%=nonce%>'});
          };
      Runtime: nodejs4.3
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal: {Service: [lambda.amazonaws.com]}
          Action: ['sts:AssumeRole']
      Path: /
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Outputs:
  LambdaVersion:
    Value: !GetAtt LambdaVersion<%=nonce%>.Version
  CustomResourceResult:
    Value: !GetAtt MyCustomResource.Result

To create/update the stack while passing template.yml through the erb template preprocessor, run:

aws cloudformation [create|update]-stack \
  --stack-name [stack_name] \
  --template-body file://<(ruby -rerb -e "puts ERB.new(ARGF.read).result" < template.yml) \
  --capabilities CAPABILITY_IAM

Solution 5

Answer updated for February 2018

You can use AWS SAM (Serverless Application Model), and its sam package and sam deploy commands to update Lambda. They are similar to aws cloudformation package and aws cloudformation deploy commands, but also let you update Lambda versions automatically.

SAM can package your code (or take ZIP package you created otherwise), upload it to S3, and update the $LATEST Version of the Lambda from it. (If this is all you need, this can also be done with aws cloudformation, without SAM; code examples are same as below, but only use CloudFormation's standard declarations). Then, with SAM, if configured accordingly, you can also automatically publish a Version and update an Alias to point to it. It can also, optionally, use AWS CodeDeploy to gradually move traffic from previous Version to new one, and rollback in case of errors. All this is explained in Safe Lambda deployments.


Technically, the idea is that every time you update the stack, you need your AWS::Lambda::Function's Code to point to the new package in S3. This will ensure that when you update the stack, Lambda's $LATEST version will be updated from the new package. Then, you can also automate the publishing of new Version and switch an Alias to it.

For it, create a SAM template, which is similar to (a superset of) CloudFormation template. It may include SAM-specific declarations, like the one for AWS::Serverless::Function below. Point the Code to source code directory (or a prepackaged ZIP), and set the AutoPublishAlias property.

...

MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      ...  # all usual CloudFormation properties are accepted 
      AutoPublishAlias: dev  # will publish a Version and create/update Alias `dev` to point to it
      Code: ./my/lambda/src
...

Run:

$ sam package --template-file template.yaml --output-template-file packaged.yaml --s3-bucket my-bucket

This packages source directory contents as a ZIP (if Code is not a ZIP already), uploads it to S3 under new autogenerated key, and generates final CloudFormation template to packaged.yaml, putting for you the proper Code reference into it; like this:

...
MyFunction:
    Properties:
      Code:
        S3Bucket: my-bucket
        S3Key: ddeeaacc44ddee33ddaaee223344
...

Now you can use generated packaged.yaml with SAM, to create function Version:

sam deploy --template-file packaged.yaml --stack-name my-stack [--capabilities ...]

This will update Lambda's $LATEST version, and, if AutoPublishAlias was defined, publish it as a new Version and update the Alias to point to the newly published Version.

See the examples in SAM GitHub repo for a complete template code.

Share:
45,792
boris
Author by

boris

Updated on September 08, 2021

Comments

  • boris
    boris over 2 years

    I'm trying to create a new version of a Lambda function using CloudFormation.

    I want to have multiple versions of the same Lambda function so that I can (a) point aliases at different versions - like DEV and PROD - and (b) be able to roll back to an earlier version

    This is the definition of my Lambda version:

    LambdaVersion:
      Type: AWS::Lambda::Version
      Properties:
        FunctionName:
          Ref: LambdaFunction
    

    A version gets created when running "aws cloudformation create-stack" but the subsequent "aws cloudformation update-stack" commands don't do anything. There are no new Lambda versions created.

    I'm trying to get a new version of the Lambda function created after I upload new zip file to S3 and then run "update-stack". Can I do it with CloudFormation? Is AWS::Lambda::Version really broken (as mentioned here https://github.com/hashicorp/terraform/issues/6067#issuecomment-211708071) or am I just not getting something?

    Update 1/11/17 Official reply from Amazon support: "...for any new version to be published you need to define an addition (sic) AWS::Lambda::Version resource..."

    AWS CloudFormation/Lambda team, if you're reading this - this is unacceptable. Fix it.