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 a
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:
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:
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
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
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)
I solved this using CI/CD, an ant script and the git revision to create a unique zip name in the S3 bucket for each commit.
The ant script is invoked by the CI/CD to substitute the git revision into the name of the lambda code zip file and cloudformation template. These references are made before copying the code and cloudformation scripts to S3. This is similar to the way SAM works but this works with plain old Cloudformation Stacks and importantly Stack Sets that might need be deployed across a number of accounts. At time of writing SAM was not compatible with CF Stack Sets
There are two files: the ant file and a properties file that tells the ant file which lambda source directories to zip.
First the build.xml ant file:
<project basedir="." name="AWS Lambda Tooling Bucket Zip" default="ziplambdas">
<!-- this ant file is responsible for zipping up lambda source code that needs to be placed on an S3 bucket for deployment.
It reads a file `lambda-zip-build.properties` that contains a list of lambda folders and the corresponding zip names.
This allows a lambda to include source code and any required library packages into a single zip for deployment.
For further information refer to the comments at the top of the zip properties file.
-->
<property name="ant.home" value="${env.ANT_HOME}" />
<taskdef resource="net/sf/antcontrib/antlib.xml">
<classpath path="${ant.home}/lib/ant-contrib-1.0b3.jar" />
</taskdef>
<!-- <available file=".git" type="dir" property="git.present"/> -->
<available file="../../.git" type="dir" property="git.present"/>
<!-- get the git revision to make a unique filename on S3. This allows the zip key to be replaced, forcing an update if CloudFormation is deployed. Clunky,
AWS Support raised but advice was to use SAM, which is not compatible with StackSets ... *sigh* -->
<target name="gitrevision" description="Store git revision in ${repository.version}" if="git.present">
<exec executable="git" outputproperty="git.revision" failifexecutionfails="false" errorproperty="">
<arg value="describe"/>
<arg value="--tags"/>
<arg value="--always"/>
<arg value="HEAD"/>
</exec>
<condition property="repository.version" value="${git.revision}" else="unknown">
<and>
<isset property="git.revision"/>
<length string="${git.revision}" trim="yes" length="0" when="greater"/>
</and>
</condition>
<echo>git revision is ${git.revision} </echo>
</target>
<target name="replace.git.revision.in.files" depends="gitrevision" description="replace the git marker text in cloudformation files and zip properties file">
<replace dir="." token="@git.revision@" value="${git.revision}" summary="yes"/>
</target>
<property file="lambda.zip.build.properties"/>
<!-- zip the lambda code into a unique zip name based on the git revision -->
<target name="ziplambdas" description="Create Zip files based on the property list" depends="replace.git.revision.in.files">
<property file="lambda.zip.build.properties" prefix="zipme." />
<propertyselector property="zip.list" match="^zipme\.(.*)" select="\1"/>
<foreach list="${zip.list}" delimiter="," target="zip" param="folder"/>
</target>
<target name="zip">
<propertycopy property="zip.path" from="${folder}" />
<basename property="zip.file" file="${zip.path}" />
<echo message="${folder} is being zipped to ${zip.path}"/>
<zip destfile="${zip.path}">
<zipfileset dir="${folder}">
<exclude name="**/${zip.file}"/>
</zipfileset>
</zip>
</target>
</project>
The lambda.zip.build.properties
file looks like this:
# This property file contains instructions for CI/CD Build Process to zip directories containing lambda code to place on the S3 bucket.
# Lambda source code when deployed by CloudFormation must be available inside a Zip file in a S3 bucket.
# CI/CD runs an ant task that reads this file to create the appropriate zip files referenced by the CloudFormation scripts.
#
# Each property key value pair below contains a key of the top level directory containing the lambda code (in python, javascript or whatever),
# and a value of the path to the zip file that should be deployed to S3. The @git.revision@ tag is substituted with the actual git revision before copying to S3.
# This allows the lambda S3key to change for each deployment and forces a lambda code update.
#
# for example: myproject/lambda/src=myproject/lambda/my-src-@git.revision@.zip
# ^^ Directory ^^ Zip File
#
###################################################################################################################################################################################
myproject/lambda/src=myproject/lambda/lambda-code-@git.revision@.zip
# place your key value pairs above here...
And then the CloudFormation Template:
Resources:
MyLambda:
Type: AWS::Lambda::Function
Properties:
# git.revision is placed when code is zipped up by CI/CD and placed on S3 bucket. It allows a unique name for each commit and thereby forces
# lambda code to be replaced on cloudformation stackset redeployment.
Code:
S3Bucket: mybucket
S3Key: myproject/lambda/lambda-code-@git.revision@.zip
Handler: autotag-costcentre.lambda_handler
MemorySize: 128
Runtime: python3.7
Timeout: 10
.... etc
The result is a zip file with a unique name lambda-code-0f993c3.zip
and a Cloudformation template with S3Key
referencing the unique name.
S3Key: myproject/lambda/lambda-code-0f993c3.zip
Deploy the template from the S3 location and it will force the existing lambda code to be refreshed every time.
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).
Worked for me the following:
"LambdaAlias": {
"Type": "AWS::Lambda::Alias",
"DeletionPolicy" : "Retain",
"Properties": {
"FunctionName": {
"Ref": "LambdaFunction"
},
"FunctionVersion": {
"Fn::GetAtt": ["LambdaVersion","Version"]
},
"Name": "MyAlias"
}
Looking for a similar thing that works with Lambda functions deployed from S3.
My use case was this:
Not happy with this I looked for an alternative and came across this question. None of the answers exactly worked for me so I have taken some ideas and adapted the answers here and made my own version written in Python.
This code is adapted from the answer from @wjordan so credit to him for the idea and the original answer. The differences are:
You need a nonce parameter. You change the value of this parameter when the code needs to be republished to Lambda. This is to ensure that cloudformation will update your custom resource. When the custom resource is updated, it will run the Python code that ultimately updates your Lambda code.
Hope this helps someone.
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:
S3Bucket: BucketContainingYourLambdaFunction
S3Key: KeyToYourLambdaFunction.zip
Runtime: "python3.6"
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
LambdaDeployCustomResource:
Type: Custom::LambdaVersion
Properties:
ServiceToken: !GetAtt LambdaDeployFunction.Arn
FunctionName: !Ref MyFunction
S3Bucket: BucketContainingYourLambdaFunction
S3Key: KeyToYourLambdaFunction.zip
Nonce: !Ref Nonce
LambdaDeployFunction:
Type: AWS::Lambda::Function
DependsOn: LambdaDeployFunctionExecutionRole
Properties:
Handler: "index.handler"
Role: !GetAtt LambdaDeployFunctionExecutionRole.Arn
Code:
ZipFile: !Sub |
import boto3
import json
import logging
import cfnresponse
import time
from botocore.exceptions import ClientError
def handler(event, context):
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.info (f"Input parameters from cloud formation: {event}")
responseData = {}
if (event["RequestType"] == 'Delete'):
logger.info("Responding to delete event...")
cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
try:
lambdaClient = boto3.client('lambda')
s3Bucket = event['ResourceProperties']['S3Bucket']
s3Key = event['ResourceProperties']['S3Key']
functionName = event['ResourceProperties']['FunctionName']
logger.info("Updating the function code for Lambda function '{}' to use the code stored in S3 bucket '{}' at key location '{}'".format(functionName, s3Bucket, s3Key))
logger.info("Sleeping for 5 seconds to allow IAM permisisons to take effect")
time.sleep(5)
response = lambdaClient.update_function_code(
FunctionName=functionName,
S3Bucket='{}'.format(s3Bucket),
S3Key='{}'.format(s3Key),
Publish=True)
responseValue = "Function: {}, Version: {}, Last Modified: {}".format(response["FunctionName"],response["Version"],response["LastModified"])
responseData['Data'] = responseValue
cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, response["FunctionArn"])
except ClientError as e:
errorMessage = e.response['Error']['Message']
logger.error(errorMessage)
cfnresponse.send(event, context, cfnresponse.FAILED, responseData)
Runtime: "python3.6"
Timeout: "30"
LambdaDeployFunctionExecutionRole:
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: ReadS3BucketContainingLambdaCode
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- s3:GetObject
Resource: ArnOfS3BucketContainingLambdaCode/*
- PolicyName: UpdateCodeAndPublishVersion
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- lambda:UpdateFunctionCode
- lambda:PublishVersion
Resource: '*'
Outputs:
LambdaVersion:
Value: !GetAtt LambdaDeploy.Version
CustomResourceResult:
Value: !GetAtt MyCustomResource.Result