A Gentle Intro to CloudFormation and SAM

A Gentle Intro to CloudFormation and SAM

AWS is pretty amazing; we no longer need to self manage and provision our own servers. For pretty much any web service you can think of, AWS has a managed solution for it. You might think that using a self-managed service means that you can sit back and relax and AWS will take care of all the heavy lifting for you. While this is true to a certain extent, you still need to manage and coordinate how each AWS service is configured and how it plays with the other AWS services.

I remember building my first AWS Gateway/Lambda application. Every code change required me to manually re-upload the source code. I also had to refresh the gateway endpoint at certain times. Any name change required me to login to the AWS Console and manually make the necessary changes. This process proved to be slow and counterproductive.

Infrastructure as Code (IaC)

Infrastructure as code solves the problem of manually provisioning and configuring AWS services by automating the process with code. There are several offerings of IaC: Serverless, Terraform, Ansible and CloudFormation, to name a few . The great thing about sticking with CloudFormation is that it has amazing integration with the entire AWS platform - meaning that a configuration setting for every service is most likely available.

Getting Started

Install and configure aws-cli:

$ pip install awscli
$ aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-west-2
Default output format [None]: json

Now that you’re all set up with aws-cli, you can deploy your first CloudFormation template. All CloudFormation configuration lives in YAML or JSON template files. My recommendation is to use JSON, since any inconsistency with whitespaces in the YAML version will cause hard to locate errors.

At first, CloudFormations can seem a bit overwhelming, but they're really quite simple. For each service you need to configure, simply look up the required properties in the documentation: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html. Add a section for your desired AWS service with the appropriate properties, and you're good to go.

Create an S3 Bucket That Notifies an SNS Topic Upon Any Change

Here’s a simple template that creates a CloudFormation stack with an S3 bucket configured to send any updates to an SNS Topic. The SNS recipient email and bucket name are configured via parameters.

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "Simple S3 Configuration That Emails Updates to a Given Recipient",

  "Parameters": {
    "S3UpdatesEmailRecipient": { "Type": "String" },
    "BucketName": { "Type": "String" }
  },

  "Resources": {
    "S3UpdatesTopic" : {
      "Type" : "AWS::SNS::Topic",
      "Properties" : {
        "Subscription" : [
          {
            "Endpoint" : {"Ref": "S3UpdatesEmailRecipient"},
            "Protocol" : "email"
          }
        ]
      }
    },
    "SNSTopicPolicy": {
      "Type": "AWS::SNS::TopicPolicy",
      "Properties": {
        "Topics": [{"Ref": "S3UpdatesTopic"}],
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [{
            "Effect": "Allow",
            "Principal": "*",
            "Action": "sns:Publish",
            "Resource": {"Ref": "S3UpdatesTopic"},
            "Condition": {
              "ArnLike": {
                "aws:SourceArn": { "Fn::Join": [ "", [ "arn:aws:s3:::", {"Ref": "BucketName"} ]]}
              }
            }
          }]
        }
      }
    },
    "OutputBucket": {
      "Type": "AWS::S3::Bucket",
      "DependsOn": ["SNSTopicPolicy"],
      "Properties": {
        "AccessControl": "BucketOwnerFullControl",
        "BucketName": {"Ref": "BucketName"},
        "NotificationConfiguration": {
          "TopicConfigurations": [
            {
              "Topic": {"Ref": "S3UpdatesTopic"},
              "Event": "s3:ObjectCreated:*"
            }
          ]
        }
      }
    }
  }
}

You can deploy this template with the following command:

aws cloudformation deploy \
--template template.json \
--stack-name <name-of-cloudformation-stack> \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
S3UpdatesEmailRecipient="<your-email-address>" \
BucketName="<bucket-name-to-use>"

Be sure to replace all the strings enclosed by <...> with appropriate values. The --parameter-overrides option allows you to modify your stack during deployment without requiring you to hardcode strings directly into the template. Upon success, you should see an email such as this:

You have chosen to subscribe to the topic:
arn:aws:sns:us-east-1:083435881685:ramin-tutorial-S3UpdatesTopic-8PWPD04U85IC

To confirm this subscription, click or visit the link below (If this was in error no action is necessary):
Confirm subscription

Please do not reply directly to this email. If you wish to remove yourself from receiving all future SNS subscription confirmation requests please send an email to sns-opt-out

You should also see an S3 bucket created with the given name in the console. In the event that you see errors with your stack deployment, you can use the following command to see what went wrong.

$ aws cloudformation describe-stack-events --stack-name <your_stack_name>

To delete your stack, use:

$ aws cloudformation delete-stack --stack-name <your_stack_name> NOTE: Use caution when deleting stacks as this step is irreversible.

Go ahead an upload something to this newly created bucket. Upon doing so, you should see an email sent to your provided email address with something along the lines of:

{"Records":[{"eventVersion":"2.1","eventSource":"aws:s3","awsRegion":"us-east-1","eventTime":"...","eventName":"ObjectCreated:Put","userIdentity":{"principalId":"..."},"requestParameters":{"sourceIPAddress":"..."},"responseElements":{"x-amz-request-id":"...","x-amz-id-2":"..."},"s3":{"s3SchemaVersion":"1.0","configurationId":".....","bucket":{"name":"ramin-tutorial-bucket","ownerIdentity":{"principalId":"..."},"arn":"arn:aws:s3:::ramin-tutorial-bucket"},"object":{"key":"HelloWorld.txt","size":12,"eTag":"...","sequencer":"..."}}}]}

Once you get the hang of it, adding or configuring a new resource to an existing template is simply a matter of looking up the resource in the docs and adding in the necessary properties.

Create a REST API with SAM

REST APIs have become the standard method for providing a decoupled backend to any sort of interface. You can easily create a REST API with SAM by wiring up an API Gateway endpoint backed by a Lambda function. SAM adds a layer on top of CloudFormation to help with building serverless applications with less template code.

We’ll use a YAML template this time:

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM tutorial

Globals:
  Api:
    Cors:
        AllowMethods: "'*'"
        AllowHeaders: "'*'"
        AllowOrigin:  "'*'"
  Function:
    Timeout: 3
    Runtime: nodejs10.x
    CodeUri: src/


Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: helloWorld.handler
      Events:
        HttpGet:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Outputs:
  FullStackExerciseApi:
    Description: API Gateway endpoint URL
    Value:
      Fn::Sub: https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello

This template configures a Lambda handler located in the src/ directory and makes it available behind the GET /hello API Gateway endpoint.

And let’s create the most basic lambda function ever:

src/helloWorld.js

exports.handler = async (event) => {
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

Ensure that you are returning a response in the correct format: https://aws.amazon.com/premiumsupport/knowledge-center/malformed-502-api-gateway/.

Create a package.json file in src:

$ cd src
$ npm init -y

Install SAM:

$ pip install --user aws-sam-cli

Prepare your serverless application for packaging and deployment:

$ sam build

Package and upload your assets to an S3 bucket:

$ aws s3 mb s3://<bucket-name> # first create a bucket to store you assets
$ sam package --output-template packaged.yaml --s3-bucket <bucket-name>

Deploy your app and pray that everything goes well:

$ sam deploy --template-file packaged.yaml \
--capabilities CAPABILITY_IAM \
--stack-name <stack-name>

Print out the HTTP endpoint using describe-stacks via the Outputs section provided at the bottom of template.yaml:

$ aws cloudformation describe-stacks --stack-name <stack-name> --query "Stacks[].Outputs[].OutputValue"
[
https://<some random string>.execute-api.us-east-1.amazonaws.com/Prod/hello
]

Finally test your shiny REST API:

$ curl https://<some random string>.execute-api.us-east-1.amazonaws.com/Prod/hello
"Hello from Lambda!"

There you have it! An entire REST API workflow configured from scratch with SAM.

Introducing the JBS Quick Launch Lab!

FREE 1/2 Day Assessment

Quantify what it will take to implement your next big idea! Our intensive 1/2 day session will deliver tangible timelines, costs, high-level requirements, and recommend architectures that will work best, and all for FREE. Let JBS show you why over 20 years of experience matters.
Yes, I'd Like A FREE Assessment