UC3 SAM Sceptre gem

This gem provides functionality that can be used to help Sceptre trigger a SAM build and/or SAM deploy to create an image and push it to an ECR repository or build the ZIP archive and put it into an S3 bucket and then run the CloudFormation create/update/delete.

You can run the script manually and via a Sceptre hook when you are initialiiy creating your stacks.

Prerequisites

This gem assumes that you have organized your Sceptre config by environment! For example:

my_project
   |
    ------ config
   |         |
   |          ---- dev
   |         |      |
   |         |       ----- s3.yaml
   |         |
   |          ---- prd
   |                |
   |                 ----- s3.yaml
   |
    ------ templates
              |
               ------ s3.yaml

This gem assumes that you have access to run CloudFormation tasks in 2 AWS accounts: dev and prd

If you specify env: prd or env: stg when initializing the Proxy, your resources will be constucted in the prd AWS account otherwise they will be created in the dev account.

Since SAM is not directly connected to Sceptre and cannot therefore access any of Sceptre's resolvers, you will need to ensure access to any SAM template parameters defined in your template.

This gem will retrieve values from either the SSM parameter store or exported CloudFormation stack outputs. The SSM parameters and stack outputs MUST live within the same region that you are running your SAM deploy!

For example, given the following SAM template:

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

Parameters:
  VpcId:
    Type: 'AWS::EC2::VPC::Id'

You would need to make the VpcId available as an SSM parameter or as an exported output from one of your other Sceptre managed CloudFormation stacks like this:

Outputs:
  VpcId:
    Value: !Ref Vpc
    Export:
      Name: VpcId

You should place your SAM template and the Lambda code under the Sceptre project's templates directory. For example:

my_project
    |
     ------ config
    |          |
    |           ----- dev
    |                   |
    |                    ----- api-gateway.yaml                     # Your Sceptre config for the API Gateway that
    |                                                               # includes a hook to run the SAM build + deploy
    |
     ------ templates
               |
                ------- api-gateway.yaml                            # Your SAM template that builds the API Gateway
               |
               |
                ------- lambdas
                         |
                          ----- src
                                 |
                                  ------ my_lambda
                                 |          |
                                 |           ------ file(s)         # Your Lambda source
                                 |
                                  ------- Gemfile                   # Gemfile that includes this gem
                                 |
                                  ------- sam_build_deploy.rb       # The Ruby script that initializes and uses this gem
                                 |
                                  ------- template.yaml             # Your SAM template for the Lambda(s)

Usage

This gem is intended to be invoked from a Ruby script that you create and store in the same directory as your SAM template. The script can be run manually at any time and via a Sceptre hook.

Since CloudFormation requires your Lambda code to exist in an S3 bucket or an ECR when you create your stack, we advise that you at least add this script as a hook to an appropriate Sceptre config. For example as an after_create hook on the Sceptre config for the S3 bucket or ECR that will be used to store your Lambda code.

Build a Ruby script

Create a Gemfile that is adjacent to your SAM template (see the location of the Gemfile file in the directory structure outlined above). This file should look like this:

# frozen_string_literal: true

source 'https://rubygems.org'

gem 'uc3-sam-sceptre'

Create a Ruby script that is adjacent to your SAM template (see the location of the sam_build_deploy.rb file in the directory structure outlined above). (See the exmaples below)

The following arguments can be used in your Ruby script when initializing the SAM proxy:

# REQUIRED arguments:
# ------------------------
service: 'dmp'                                # REQUIRED - The service name
subservice: 'hub'                             # REQUIRED - The subservice name
git_repo: 'https://github.com/MYORG/my-repo'  # REQUIRED - The location of your SAM code
stack_suffix: 'docker-lambdas'                # REQUIRED - Used to generate the CloudFormation stack name
admin_email_key: 'AdminEmail'                 # REQUIRED - An SSM parameter name or a CF stack export name

# Include one of the following:
ecr_uri_key: 'MyEcrRepoUri                    # REQUIRED if Lambda is a Docker Image - An SSM parameter name or a CF stack export name
s3_arn_key: 'S3PrivateBucketArn'              # REQUIRED if Lambda is NOT a Docker Image - An SSM parameter name or a CF stack export name

# An entry for each of your SAM template parameters.
#   - Fetchable will be looked up in either the SSM paramter store or available CloudFormation stack exports
#   - Static will be used as-is
#
fetchable_cf_params: [
  {
    stack_export_name: 'CloudFrontDistroId'   # REQUIRED - An SSM parameter name or a CF stack export name
    template_param_name: 'CFDistributionId'   # (defaults to :stack_export_name) The name of the parameter your SAM template is looking for
  }
],
static_cf_params: [
  {
    template_param_name: 'Version'          # REQUIRED - The name of the parameter your SAM template is looking for
    value: 'v0'                             # REQUIRED - The value of the parameter
  }
]

# OPTIONAL arguments:
# ------------------------
program: 'uc3'                                # The group that owns the service (default: 'uc3')
env: 'stg'                                    # The environment (default: 'dev')
region: 'us-east-1'                           # The AWS region (default: 'us-west-2')
auto: false                                   # Whether or not to run the SAM deploy in `--guided` mode (default: true)

The :program, :service, :subservice, and :env are used to:

  • Create the SAM CloudFormation stack name (along with the :stack_suffix) For example: uc3-dmp-hub-dev-foo-bar
  • Help find the values of parameters in the :fetchable_cf_params by limiting the stacks to those that share your Sceptre project's naming conventions. For example: uc3-dmp-hub-dev
  • Create resource tags along with the :git_repo and the value found for the :admin_email_key to tag all resources created by SAM with tags for Program, Service, Subservice, Environment, CodeRepo and Contact

The entries within the :fetchable_cf_params and :static_cf_params will all be passed to the SAM deploy as --parameter-overrides

This gem will automatically append the :program + :service + :subservice + :env to the names of the SSM parameter keys you provide (unless you provided the fully qulified key name). So for example if you specify /uc3/dmp/hub/dev/AdminEmail, it will be used as-is; if you provide AdminEmail then the gem will prepend /uc3/dmp/hub/dev to the name.

Build the bundle

You will need to run bundle install so that this gem is installed and ready

Add the Sceptre hook

Recommended but optional.

Add a hook to an existing Sceptre config that will run this script. Since CloudFormation requires your Lambda code to already exist in an S3 bucket or an ECR when you are creating your SAM stacks. For example here is a hook that will run a SAM build and deploy immediately after an S3 bucket is created:

# config/dev/s3.yaml
template:
  path: s3.yaml
  type: file

parameters:
  LogBucketObjectLifeSpan: '30'

hooks:
  after_create:
    # Build the Lambda code and store in the bucket so it is available downstream
    - !cmd './templates/lambdas/src/sam_build_deploy.rb true true'

Examples Ruby scripts:

The following is an example of deploying to a Lambda that is a Docker image into an ECR.

# frozen_string_literal: true

require 'uc3-sam-sceptre'

if ARGV.length == 2
  params = {
    service: 'dmp',
    subservice: 'hub',
    git_repo: 'https://github.com/CDLUC3/dmp-hub-cfn',
    stack_suffix: 'docker-lambdas',
    admin_email_key: 'AdminEmail',

    ecr_uri_key: 'EcrRepositoryUri',

    fetchable_cf_params: [
      { stack_export_name: 'VpcId' },
      { stack_export_name: 'SubnetA' },
      { stack_export_name: 'SubnetB' },
      { stack_export_name: 'SubnetC' },
      { stack_export_name: 'DeadLetterQueueArn' },
      { stack_export_name: 'LambdaSecGroupId' },
      { stack_export_name: 'SnsTopicEmailArn', template_param_name: 'SnsEmailTopicArn' },
      { stack_export_name: 'DomainName' },
      { stack_export_name: 'RdsHost' },
      { stack_export_name: 'RdsPort' },
      { stack_export_name: 'RdsDbName' }
    ],
    static_cf_params: [
      { template_param_name: 'Version', value: 'v0' }
    ]
  }
  proxy = Uc3SamSceptre::Proxy.new(params)

  if ARGV[0].to_s.downcase.strip == 'false' && ARGV[1].to_s.downcase.strip == 'false'
    proxy.delete
  else
    proxy.build if ARGV[0].to_s.downcase.strip == 'true'
    proxy.deploy if ARGV[1].to_s.downcase.strip == 'true'
  end
else
  p 'Expected 2 arguments: run build? and run deploy? (For example: `ruby sam_build_deploy.rb true true`).'
  p 'Setting both arguments to false will trigger a `sam delete`.'
end

The following is an example of deploying a Lambda that is NOT a Docker image into a ZIP archive stored in S3

# frozen_string_literal: true

require 'uc3-sam-sceptre'

if ARGV.length == 2
  params = {
    service: 'dmp',
    subservice: 'hub',
    git_repo: 'https://github.com/CDLUC3/dmp-hub-cfn',
    ecr_uri_key: 'EcrRepositoryUri',
    stack_suffix: 'docker-lambdas',
    admin_email_key: 'AdminEmail',

    s3_arn_key: 'S3PrivateBucketArn',

    fetchable_cf_params: [
      { stack_export_name: 'CloudFrontDistroId' },
      { stack_export_name: 'S3PublicBucketArn' },
      { stack_export_name: 'DynamoTableName' },
      { stack_export_name: 'SnsTopicEmailArn', template_param_name: 'SnsEmailTopicArn' },
      { stack_export_name: 'DomainName' }
    ],
    static_cf_params: [
      { template_param_name: 'Version', value: 'v0' }
    ]
  }

  proxy = Uc3SamSceptre::Proxy.new(params)

  if ARGV[0].to_s.downcase.strip == 'false' && ARGV[1].to_s.downcase.strip == 'false'
    proxy.delete
  else
    proxy.build if ARGV[0].to_s.downcase.strip == 'true'
    proxy.deploy if ARGV[1].to_s.downcase.strip == 'true'
  end
else
  p 'Expected 2 arguments: run build? and run deploy? (For example: `ruby sam_build_deploy.rb true true`)'
  p 'Setting both arguments to false will trigger a `sam delete`.'
end