Dissecting Serverless Stacks (IV)

Thumbnail

Dissecting Serverless Stacks (IV)

After we figured out how to implement a sls command line option to switch between the usual behaviour and a way to conditionally omit IAM in our deployments, we will get deeper into it and build a small hack on how we could hand over all artefacts of our project to somebody who does not even know SLS at all.

If you looked around a bit inside your development directory, you probably already know that there is a command sls package which creates the CloudFormation document which the whole stack is based on. This is basically a step right before deployment and we have that JSON file as .serverless/cloudformation-template-update-stack.json in the project directory. Next to it we also find the ZIP file of our Lambda.

So we are almost at a point where we could hand over a iam.yaml, the cloudformation-template-update-stack.json and the ZIP file to someone who deploys it manually.

But wait, one thing is not quite right: By default, Serverless will create an S3 bucket for our project, upload the ZIP there and the Stack only references this for the deployment. This won’t work for a manual deployment where we need to have some Parameters to hand over Bucket and S3 key.

For this, I built a small Ruby script which will - read the resulting JSON - add Parameters of type String for ServerlessDeploymentBucket and ServerlessDeploymentArtifact - replace the Code block of the Lambda CloudFormation Resource - write the result back as YAML

As I implemented this as a task within Rake, my Rakefile contains this:

desc "Export separately"
task :export do
  sh <<~EOS, { verbose: false }
    sls package --deployment no_includes
    cp .serverless/*.zip pkg/
    cp cloudformation-resources/* pkg/
  EOS

  require 'json'
  require 'yaml'

  project_base = Rake.application.find_rakefile_location[1]

  filename = File.join(project_base, '.serverless/cloudformation-template-update-stack.json')
  json = JSON.parse(File.read(filename))

  # Replace S3 resource by Parameter
  json['Resources'].delete('ServerlessDeploymentBucket')
  json['Parameters'] = {}
  json['Parameters']['ServerlessDeploymentBucket'] = {
    "Type" => 'String'
  }

  zip_file = Dir.glob(File.join(project_base, '.serverless/*.zip')).first
  json['Parameters']['ServerlessDeploymentArtifact'] = {
    'Type' => 'String',
    'Default' => File.basename(zip_file)
  }

  functions = json['Resources'].select { |k,v| v['Type'] == 'AWS::Lambda::Function' }
  functions.each do |function|
    source = File.join(project_base, function[1]['Properties']['Handler'].gsub(/\.[a-z_]+$/, '.rb'))

    function[1]['Properties']['Code'] = {
      'S3Bucket' => {
        'Ref' => "ServerlessDeploymentBucket"
      },
      'S3Key' => {
        'Ref' => "ServerlessDeploymentArtifact"
      }
    }
  end

  fp = File.open(File.join(project_base, 'pkg/stack.yaml'), 'w')
  fp.write json.to_yaml
  fp.close
end

At the end of this journey, we have three ways of deploying our SLS project - sls deploy as the standard way, which will deploy all parts automatically - sls deploy --deployment no_includes for the “IAM first, Serverless second” approach - rake export for handing over artefacts to a third party and manual deployment.

Summary

Of course, these approaches might not work well for you. I created it especially for smaller, 1-Lambda projects and not for complicated microservices architectures. But if you work with customers and want to recycle your small Lambda-based helpers across different types of organizations, you might find it handy to follow the patterns.

And even if you don’t want to use these deployment styles, I hope I could show you some lesser known ways of structuring your serverless.yml (externalized sub-stacks, references) or how to use custom CLI options (with mappings and double references).