Converting From AWS SAM to CloudFormation
AWS Serverless Application Model (SAM) is a framework for building serverless applications on AWS. One of the components of SAM is a template specification. SAM templates would look and feel familiar to anyone who has used AWS CloudFormation to define their infrastructure as code, however they are not completely interchangeable. There are multiple reasons why you might want to convert from SAM to native CloudFormation:
- You want to deploy the app using CloudFormation
StackSets.
SAM uses the
AWS::Serverless
transform in its templates and transforms are not supported by stack sets. - You want to deploy the app as part of an AWS Landing Zone (ALZ) account baseline. ALZ uses stack sets as the mechanism to deploy baseline resources and so suffers from the same constraint as the point above.
- Your operating system of choice isn't documented in the SAM installation instructions and you're uncertain how to install from source or doubtful it will work at all (I'm looking at fellow OpenBSD and FreeBSD users here).
This post will show you how to take an existing SAM application and convert it to a CloudFormation template (CFT). As a CFT, the challenges listed above can be avoided.
Satisfy pre-requisites⌗
These pre-reqs need to be met before continuing:
- Install the SAM CLI
- Create an Amazon S3 bucket to store the serverless code artifacts that the SAM template generates. At a minimum, you will need permission to put objects into the bucket.
- The permissions applied to your IAM identity must include
iam:ListPolicies
. - You must have AWS credentials
configured
either via the AWS CLI or in your shell's environment via the
AWS_*
environment variables. - Git installed.
- Python 3.x installed.
- (Optional) Install Python's virtualenvwrapper.
Sorry BSD users, yes, this process requires the SAM CLI be installed. Boot a Linux VM/instance?
Do the template conversion⌗
I'm going to use a sample SAM app from the SAM documentation to demonstrate the conversion. In a real scenario, you would already have your own SAM app and would skip the step below where a new project is initialized.
Per the SAM documentation for this example app, use the sam
command to
initialize the project:
sam init \
--location https://github.com/aws-samples/cookiecutter-aws-sam-s3-rekognition-dynamodb-python \
--no-input
Open the template.yaml
file in the app and look for any serverless
functions that have been defined to use the Python runtime. Change the runtime
value to match a version of Python that is installed on your system. For
example, if you have Python 3.8 installed, change the parameter to look like
this:
Runtime: python3.8
I don't know if this step is required for other runtimes. Python is my jam. 🐍
At this point, I recommend you create a Python virtual environment to isolate
the packages that you're about to install. If you happen to have multiple
versions of Python installed, be sure to specify the version that matches
what you configured in the template.yaml
file.
mkvirtualenv -p /usr/local/opt/python@3.8/bin/python3.8 sam2cfn
Jump into the SAM app's directory and run the sam package
command. This will
take the serverless function code from the local file system, zip it up, and
stash it in your Amazon S3 bucket. The template file that gets output from this
command will contain references to the bucket and object that was uploaded for
the function which is how the function's code would be deployed into AWS Lambda
if you were to run the sam deploy
command.
sam package --output-template-file packaged.yaml --s3-bucket <BUCKET_NAME>
If you're using credentials from the AWS CLI and need to specify a profile name,
run sam package --profile <PROFILE_NAME> ...
. Otherwise the default profile
will be used or the AWS_*
environment variables will be consulted (if set).
The packaged.yaml
file could be fed to CloudFormation as a template, but at
this point the template still uses the AWS::Serverless
transform so there is
more work still to do. Fortunately, the SAM source code contains a script for
converting a SAM template to a CFT. Clone the SAM repo and then install the
Python dependencies.
git clone https://github.com/aws/serverless-application-model.git
pip install -r serverless-application-model/requirements/base.txt
pip install pyyaml
The conversion script does not let you specify an AWS credential profile name
as one of its arguments. If you're not already using the AWS_*
environment
variables to pass credentials to these tools, set the AWS_PROFILE
variable
to the name of the credential profile you're using. The conversion script
makes some AWS API calls and will throw an exception if it can't determine
your credentials.
Run the conversion script to convert packaged.yaml
to a CFT:
python serverless-application-model/bin/sam-translate.py \
--template-file=packaged.yaml \
--output-template=cft.json
The resulting CFT will be in JSON format, so if you're at all sane, you'll now convert this JSON file to YAML 😄. The cfn-flip tool was written to do exactly this.
pip install cfn-flip
cfn-flip -i json -o yaml cft.json cft.yaml
rm cft.json
A CloudFormation template that does not use any transforms is now ready as
cft.yaml
.
AWSTemplateFormatVersion: '2010-09-09'
Description: SAM app that uses Rekognition APIs to detect text in S3 Objects and stores
labels in DynamoDB.
Resources:
SourceImageBucket:
Type: AWS::S3::Bucket
DependsOn:
- DetectTextInImageBucketEvent1Permission
Properties:
NotificationConfiguration:
LambdaConfigurations:
- Function: !GetAtt 'DetectTextInImage.Arn'
Event: s3:ObjectCreated:*
DetectTextInImage:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket: BUCKET_NAME
S3Key: 915a69e82ff6d649e804e681769cf9b3
Description: Uses Rekognition APIs to detect text in S3 Objects and stores the
text and labels in DynamoDB.
Handler: src/app.lambda_handler
MemorySize: 512
Role: !GetAtt 'DetectTextInImageRole.Arn'
Runtime: python3.8
Timeout: 30
Environment:
Variables:
TABLE_NAME: !Ref 'ResultsTable'
Tags:
- Key: lambda:createdBy
Value: SAM
DetectTextInImageRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: DetectTextInImageRolePolicy0
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
Resource: arn:aws:s3:::*
- Effect: Allow
Action:
- rekognition:DetectText
- rekognition:DetectLabels
Resource: '*'
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Scan
- dynamodb:UpdateItem
Resource: !Join
- ''
- - 'arn:aws:dynamodb:'
- !Ref 'AWS::Region'
- ':'
- !Ref 'AWS::AccountId'
- :table/
- !Ref 'ResultsTable'
Tags:
- Key: lambda:createdBy
Value: SAM
DetectTextInImageBucketEvent1Permission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref 'DetectTextInImage'
Principal: s3.amazonaws.com
SourceAccount: !Ref 'AWS::AccountId'
ResultsTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
I highly recommend you review the template for accuracy before you deploy it. You may also want to clean up little things such as in the template above, there are references to SAM in some tags and the template's description.
Package code with CloudFormation⌗
Just because SAM is no longer needed to deploy this app, doesn't mean the code
can't be packaged up into S3 like sam package
was doing. The
AWS CLI command
aws cloudformation package
will do the same thing. The template has to be prepared to work with this
command by pointing the template to the local path where the code resides. The
package
command will then zip the local code, push it to S3, and output a new
template with the proper references to the bucket and newly-uploaded object
(precisely how SAM did).
Open cft.yaml
and for each Lambda function, find the Code
stanza and delete
the S3Bucket
and S3Key
properties. Set the Code
property to the local
file system path where the code is. For example, for the YAML template above,
you would find this Lambda function:
DetectTextInImage:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket: BUCKET_NAME
S3Key: 915a69e82ff6d649e804e681769cf9b3
You would change the Code
property to this:
DetectTextInImage:
Type: AWS::Lambda::Function
Properties:
Code: local_directory/
Where local_directory/
is a relative path to the directory that contains the
Lambda code.
You now call cloudformation package
to package the Lambda, push it to S3,
and output a new template file.
aws cloudformation package --template-file cft.yaml \
--output-template-file deploy.yaml \
--s3-bucket <BUCKET_NAME>
You end up with two template files from here on out:
- cft.yaml - This is the working copy of the template. Development of the app should happen on this template and it should be checked into version control.
- deploy.yaml - This is used to deploy the app. Once a deployment is done,
this file can be deleted. It can always be created again with the
cloudformation deploy
command.
Whenever changes are made to the Lambda code or cft.yaml
, cloudformation package
must be run as part of deploying those changes.
Find more information⌗
If you work a lot with CloudFormation, check out Rain and these other CloudFormation tools which make working with CFTs almost fun!
Disclaimer: The opinions and information expressed in this blog article are my own and not necessarily those of Amazon Web Services or Amazon, Inc.