In this guide we will demonstrate how to use the SAM step to deploy a SAM serverless application.
AWS SAM is an extension of CloudFormation and is a popular framework for building serverless applications. Besides making CloudFormation easier to write, it also helps with building our code and punching it to S3.
Attini provides a dedicated step for delivering SAM applications, which we will demonstrate how to use in this guide.
In order to use the “AttiniSam” step, you should have a SAM app in your project. You then need to add the step to your deployment plan. The step has more properties but a simple configuration could look something like this:
- Name: DeploySamProject
Type: AttiniSam
Properties:
Project:
Path: /hello-world-app
StackName: my-lambda-stack
The Project section of the steps properties contains the configuration for your SAM project. Because SAM is an extension of CloudFormation it also needs a stack name. This also means that most features from the AttiniCfn step are supported, such as configuration files. However, cross-account and cross-region deployment are not supported, this is because SAM requires the S3 code source to be located in the same region as the app.
When using Attini to deploy SAM apps, the Attini artifact store S3 bucket is used to store the code. This means that all the code will be subject to Attini’s life cycle. This is a good thing because SAM is known to create a lot of zombie data over time.
The Attini CLI will detect if a deployment plan contains an AttiniSam step. If the SAM app is not already built (sam build
) the Attini CLI will build it when packaging the distribution. If you want to build the app yourself you are free to do so, possibly via the prePackage commands in the Attini configuration file.
At the beginning of the deployment plan execution, Attini will start a container to perform the sam package
command from within the AWS account. This will make the code available on S3 and generate a CloudFormation template that correctly references the uploaded code. When you run a deployment plan containing a SAM step you will see that Attini has added a step called “AttiniSamPackage” at the beginning of your deployment plan.
This step will perform the packaging of all SAM apps in your deployment plan.
Packaging the SAM app is only necessary the first time you deploy a new version of a distribution, therefore Attini will add
a choice step at the beginning of the deployment plan to check if it should perform the package step again.
After the app is packaged Attini will treat the SAM step like a normal “AttiniCfn” step, meaning it will simply deploy the CloudFormation stack with any configuration provided to the step.
For this demo we will first create and deploy a distribution containing a single SAM step. We will then add another step to the deployment plan. The new step will deploy a DynamoDB table that our serverless app will use.
Before you begin, make sure to have the SAM CLI installed.
We can use the Attini CLI to create the example distribution for us.
mkdir attini-sam-demo
cd attini-sam-demo
attini init-project sam-project
The project contains a folder containing a “hello world” SAM app. This is a slightly updated version of the “hello world” app generated by the SAM CLI:s init command. The app is written in Python, but you can use any supported language. The deployment plan should look like this:
SamAppDeploymentPlan:
Type: Attini::Deploy::DeploymentPlan
Properties:
DeploymentPlan:
- Name: DeploySamProject
Type: AttiniSam
Properties:
Project:
Path: /hello-world-app
StackName: !Sub ${AttiniEnvironmentName}-sam-lambda
The “Project.Path” property tells the step where the SAM project is located and the “StackName” property contains the name of the stack. The stack name must be unique within the AWS account and region.
Before we start tinkering with it, let’s deploy it and see that it works. Run the deploy run commands:
attini environment create dev
attini deploy run .
It deployed without any problems and we can see the result in the terminal. If we scroll in the terminal a bit we
can see that the Attini CLI packaged the SAM app for us just after the prePackage commands were executed. If we
want to take control over how to SAM app is packaged we can do it via the prePackage commands in the Attini configuration file.
For example, if we wanted to add the --use-container
option to the build command, we could update the attini-config.yaml file
to look like this:
distributionName: sam-project
initDeployConfig:
template: deployment-plan.yaml
stackName: ${environment}-${distributionName}-deployment-plan
package:
prePackage:
commands:
- attini configure set-dist-id --random
- cd hello-world-app
- sam build --use-container
Don’t use the --use-container
option together with the Attini CLIs --container-build
option because it will create a container
in container scenario 🪆. If you get an “access denied” error when pulling from the public ecr repository, you probably have old docker credentials.
If so, try running docker logout public.ecr.aws
and then try again.
ATM our SAM app is a bit pointless, so let’s add a DynamoDB table to make it a bit more exciting. We could add the table directly in the SAM apps template.yaml file. However, this creates needlessly tight coupling and scales poorly and is considered bad practice. Better to create a new template containing the DynamoDB table and then pass its name to the SAM app via the deployment plans payload.
Let’s create a new CloudFormation template called “database.yaml” for our DynamoDB table.
AWSTemplateFormatVersion: '2010-09-09'
Description: >
Contains a simple database
Resources:
Database:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: value
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: value
KeyType: HASH
Outputs:
TableName:
Description: "Database table name"
Value: !Ref Database
Notice that the template outputs the table name. We then need to update the deployment plan to deploy the table before we deploy the SAM app. We also need to pass the table name to the SAM apps CloudFormation stack as a parameter.
SamAppDeploymentPlan:
Type: Attini::Deploy::DeploymentPlan
Properties:
DeploymentPlan:
- Name: DeployDatabase
Type: AttiniCfn
Properties:
StackName: !Sub ${AttiniEnvironmentName}-lambda-database
Template: /database.yaml
- Name: DeploySamProject
Type: AttiniSam
Properties:
Project:
Path: /hello-world-app
StackName: !Sub ${AttiniEnvironmentName}-sam-lambda
Parameters:
TableName.$: $.output.DeployDatabase.TableName
We also need to update the SAM app template. It needs to read the parameter and set it as an environment variable for our lambda, and give the lambda permission to update the table. Let’s also rename the function resource as “HelloWorldFunction” does not really make sense anymore.
Parameters:
TableName:
Type: String
Resources:
SaveValueFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.9
Architectures:
- x86_64
FunctionUrlConfig:
AuthType: NONE
Environment:
Variables:
TABLE_NAME: !Ref TableName
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref TableName
Finally, lets update our SAM app to do something with the table. In the example below it will simply read the value of a query parameter called “save_me”, if it is present. It will then save the value to DynamoDB and then return all saved values.
import json
import os
import boto3
dynamodb_client = boto3.client("dynamodb")
def lambda_handler(event, context):
if "queryStringParameters" in event and "save_me" in event["queryStringParameters"]:
save_data(event["queryStringParameters"]["save_me"])
return {
"statusCode": 200,
"body": json.dumps({
"saved_vales": get_items(),
}),
}
def get_items():
return dynamodb_client.scan(
TableName=os.environ["TABLE_NAME"]
)["Items"]
def save_data(data):
dynamodb_client.put_item(
TableName=os.environ["TABLE_NAME"],
Item={
'value': {
'S': data
}
}
)
That’s it! Now we can go call the URL printed to the console. To save a value to dynamoDB, just add the “save_me” query parameter to the URL. For example:
curl "https://wb2gdbhlknrgook2zz5tqrpz5y0kixao.lambda-url.eu-west-1.on.aws?save_me=someValue"
And the request will return the value together with any previously saved values (because we used the value as primary key duplicates will be overwritten).
Find the complete example on GitHub.