AWS Velocity Series: Serverless app

Michael Wittig – 24 Apr 2017

The API Gateway provides an HTTPS endpoint that invokes a Lambda function when a request arrives.

Serverless request flow

The diagram was created with Cloudcraft - Visualize your cloud architecture like a pro.

As you can see, there is not much infrastructure to set up. To makes things even simpler, you will use the AWS Serverless Application Model (AWS SAM) to reduce the lines of your CloudFormation template to a minimum. All CloudFormation resource types that start with AWS::Serverless:: are transformed by SAM.

AWS Velocity Series

Most of our clients use AWS to reduce time-to-market following an agile approach. But AWS is only one part of the solution. In this article series, I show you how we help our clients to improve velocity: the time from idea to production. Discover all posts!

Let’s start to describe the needed infrastructure.

Serverless app infrastructure

The serverless app infrastructure for the factorial app consists of two parts:

  • API Gateway: Provides a configurable HTTPS REST Endpoint that can trigger integrations such as Lambda when a request arrives.
  • Lambda function: Lambda provides a fully managed (aka Serverless) runtime for Node.js, Java, Python, and C# code. You upload your code and Lambda runs the code for you.

I will start with the Lambda function.

Lambda function

You can follow step by step or get the full source code here: https://github.com/widdix/aws-velocity

Create a file infrastructure/serverless.yml and describe the Lambda function that is invoked on GET /{number} requests.

infrastructure/serverless.ymlGitHub
---
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31' # this line activates the SAM transformations!
Description: 'Serverless'
Parameters:
# S3Bucket and S3Key where the zipped code is located. This will be created with CodeBuild
S3Bucket:
Type: String
S3Key:
Type: String
Resources:
GetFactorialLambda:
Type: 'AWS::Serverless::Function'
Properties:
Handler: 'app/handler.factorial'
Runtime: 'nodejs6.10'
CodeUri:
Bucket: !Ref S3Bucket
Key: !Ref S3Key
Events:
Http:
Type: Api
Properties:
Path: /{n}
Method: get
RestApiId: !Ref ApiGateway

Lambda dictates an interface that you have to follow. So far, the factorial app is based on express and comes with its own web server. This is no longer needed. Instead, we can have a simpler entry point into the application. Create a file app/handler.js with the following content.

app/handler.jsGitHub
'use strict';

var factorial = require('./lib/factorial.js');

// Lambda dictates an interface: a function with 3 arguments
exports.factorial = function(event, context, cb) {
var n = parseInt(event.pathParameters.n, 10);
if (n < 0 || n > 14) {
cb(null, {
statusCode: 400
});
} else {
cb(null, {
statusCode: 200,
headers: {
'Content-Type': 'text/plain'
},
body: factorial(n).toString()
});
}
};

But how do you get notified if something goes wrong? Let’s add a parameter to the Parameters section to make the receiver configurable:

infrastructure/serverless.ymlGitHub
AdminEmail:
Description: 'The email address of the admin who receives alerts.'
Type: String

Alerts are triggered by a CloudWatch Alarm which can send an alert to an SNS topic. You can subscribe to this topic via an email address to receive the alerts. Let’s create an SNS topic and two alarms in the Resources section:

infrastructure/serverless.ymlGitHub
# A SNS topic is used to send alerts via Email to the value of the AdminEmail parameter 
Alerts:
Type: 'AWS::SNS::Topic'
Properties:
Subscription:
- Endpoint: !Ref AdminEmail
Protocol: email
# This alarm is triggered, if the Node.js function returns or throws an Error
GetFactorialLambdaLambdaErrorsAlarm:
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'GET /{n} lambda errors'
Namespace: 'AWS/Lambda'
MetricName: Errors
Dimensions:
- Name: FunctionName
Value: !Ref GetFactorialLambda
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
AlarmActions:
- !Ref Alerts
# This alarm is triggered, if the there are too many function invocations
GetFactorialLambdaLambdaThrottlesAlarm:
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'GET /{n} lambda throttles'
Namespace: 'AWS/Lambda'
MetricName: Throttles
Dimensions:
- Name: FunctionName
Value: !Ref GetFactorialLambda
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
AlarmActions:
- !Ref Alerts

Let’s recap what you implemented: A Lambda function that is connected to an API Gateway for GET /{number} requests. In the case of errors, you will receive an Email. All Lambda functions automatically save their logs in CloudWatch Logs.

Now, you can improve the API Gateway setup and add input validation.

API Gateway

An implicit API Gateway is created and configured automatically when using SAM. But if you want to validate the input on the API Gateway, you have to define the API Gateway explicitly to add the API specification in more details by using the open standard Swagger / OpenAPI Spec. Let’s do this in the Resources section:

infrastructure/serverless.ymlGitHub
ApiGateway:
Type: 'AWS::Serverless::Api'
Properties:
StageName: Prod
DefinitionBody:
swagger: '2.0'
basePath: '/'
info:
title: Serverless
schemes:
- https
# We want to validate the body and request parameters
x-amazon-apigateway-request-validators:
basic:
validateRequestBody: true
validateRequestParameters: true
paths:
'/{n}':
parameters: # we expect one parameter in the path of type number
- name: 'n'
in: path
description: 'N'
required: true
type: number
get:
produces:
- 'text/plain'
responses:
'200':
description: 'factorial calculated'
schema:
type: number
x-amazon-apigateway-request-validator: basic # enable validation for this resource
x-amazon-apigateway-integration: # this section connect the Lambda function with the API Gateway
httpMethod: POST
type: 'aws_proxy'
uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetFactorialLambda.Arn}/invocations'
passthroughBehavior: when_no_match

If you are familiar with Swagger / OpenApi Spec you will find nothing special besides the x-* parameters which are API Gateway specific. You can also monitor the API Gateway. To do so, append the following section the Resources section of your template:

infrastructure/serverless.ymlGitHub
ApiGateway5XXErrorAlarm:
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Api Gateway server-side errors captured'
Namespace: 'AWS/ApiGateway'
MetricName: 5XXError
Dimensions:
- Name: ApiName
Value: !Ref ApiGateway
- Name: Stage
Value: Prod
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
AlarmActions:
- !Ref Alerts

You will now receive alerts via email if the API Gateway returns an 5XX HTTP status code. API Gateway can also save logs to CloudWatch Logs but SAM lacks support to enable logging at the moment.

Let’s add some outputs to the stack to make it easier to connect with the API Gateway later on.

infrastructure/serverless.ymlGitHub
# A CloudFormation stack can return information that is needed by other stacks or scripts.
Outputs:
DNSName:
Description: 'The DNS name for the API gateway.'
Value: !Sub '${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com'
Export:
Name: !Sub '${AWS::StackName}-DNSName'
# The URL is needed to run the acceptance test against the correct endpoint
URL:
Description: 'URL to the API gateway.'
Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/Prod'
Export:
Name: !Sub '${AWS::StackName}-URL'

Now you have a production ready Serverless infrastructure defined in CloudFormation with the help of SAM. It’s time to deploy the Serverless app.

Serverless app CI/CD pipeline

The pipeline is based on the CI/CD Pipeline as Code part of this series. The pipeline so far stops when the application artifact (Zip file) is created. An acceptance test artifact is also created. What is missing?

  1. Create or update an acceptance environment based on the CloudFormation+SAM template serverless.yml
  2. Run the acceptance tests
  3. Create or update a production environment based on the CloudFormation+SAM template serverless.yml

This is how the pipeline looks from the beginning to the end:

Serverless CI/CD pipeline

In the EC2 and ECS examples, I used CREATE_UPDATE in the CodePipeline to create or update the CloudFormation stack. At this point, SAM works only if you use CloudFormation Change Sets. Therefore I have to switch from a single CREATE_UPDATE step to CreateChangeSet and ApplyChangeSet in this example.

Copy the deploy/pipeline.yml file to deploy/pipeline_serverless.yml to get the starting point right. If you don’t have the deploy/pipeline.yml file you can download it from https://github.com/widdix/aws-velocity.

Acceptance stage

The acceptance stage consists of a CloudFormation stack based on infrastructure/serverless.yml and the execution of the acceptance tests. To create the CloudFormation stack, you first have to provide a few parameters. Create a file infrastructure/serverless.json with the following content:

infrastructure/serverless.jsonGitHub
{
"Parameters": {
"S3Bucket": {"Fn::GetArtifactAtt": ["App", "BucketName"]},
"S3Key": {"Fn::GetArtifactAtt": ["App", "ObjectKey"]},
"AdminEmail": "your@email.com"
}
}

Make sure to change the value of the AdminEmail parameter. Look at the S3Bucket and S3Key parameter value. This is the way of getting the artifact location in CodePipeline.

To run the acceptance tests, you need another CodeBuild project, add the following resources to the Resources section of deploy/pipeline_serverless.yml:

deploy/pipeline_serverless.ymlGitHub
RunAcceptanceCodeBuildRole:
DependsOn: CloudFormationRole # make sure that CloudFormationRole is deleted last
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- 'codebuild.amazonaws.com'
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: ServiceRole
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: CloudWatchLogsPolicy
Effect: Allow
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: '*'
- Sid: CodeCommitPolicy
Effect: Allow
Action: 'codecommit:GitPull'
Resource: '*'
- Sid: S3GetObjectPolicy
Effect: Allow
Action:
- 's3:GetObject'
- 's3:GetObjectVersion'
Resource: '*'
- Sid: S3PutObjectPolicy
Effect: 'Allow'
Action: 's3:PutObject'
Resource: '*'
- PolicyName: CloudFormation
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: CloudFormation
Effect: Allow
Action:
- 'cloudformation:DescribeStacks'
Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}-acceptance/*'
RunAcceptanceProject:
DependsOn: CloudFormationRole # make sure that CloudFormationRole is deleted last
Type: 'AWS::CodeBuild::Project'
Properties:
Artifacts:
Type: CODEPIPELINE
Environment:
ComputeType: 'BUILD_GENERAL1_SMALL'
Image: 'aws/codebuild/nodejs:6.3.1'
Type: 'LINUX_CONTAINER'
Name: !Sub '${AWS::StackName}-run-acceptance'
ServiceRole: !GetAtt 'RunAcceptanceCodeBuildRole.Arn'
Source:
Type: CODEPIPELINE
BuildSpec: !Sub |
version: 0.1
phases:
build: # execute acceptance tests against the acceptance stack
commands:
- 'cd acceptance/ && ENDPOINT=`aws cloudformation describe-stacks --stack-name ${AWS::StackName}-acceptance --query "Stacks[0].Outputs[?OutputKey==''URL''].OutputValue" --output text` ./node_modules/jasmine-node/bin/jasmine-node .'
TimeoutInMinutes: 10

Now the CodeBuild project needs to be called in the pipeline, therefore change Pipeline resource in the file deploy/pipeline_serverless.yml to:

  1. deploy the CloudFormation stack suffixed with -acceptance
  2. run the acceptance tests
deploy/pipeline_serverless.ymlGitHub
Pipeline:
Type: 'AWS::CodePipeline::Pipeline'
Properties:
# [...]
- Name: BuildAndTestAcceptance
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
Configuration:
ProjectName: !Ref AcceptanceProject
InputArtifacts:
- Name: Source
OutputArtifacts:
- Name: Acceptance
RunOrder: 1
# NEW STUFF!
- Name: Acceptance
Actions:
- Name: CreateChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: 1
Configuration:
ActionMode: CHANGE_SET_REPLACE
Capabilities: CAPABILITY_IAM
RoleArn: !GetAtt 'CloudFormationRole.Arn'
ChangeSetName: !Sub '${AWS::StackName}-acceptance'
StackName: !Sub '${AWS::StackName}-acceptance'
TemplatePath: 'Source::infrastructure/serverless.yml'
TemplateConfiguration: 'Source::infrastructure/serverless.json'
InputArtifacts:
- Name: Source
- Name: App
RunOrder: 1
- Name: ApplyChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: 1
Configuration:
ActionMode: CHANGE_SET_EXECUTE
Capabilities: CAPABILITY_IAM
ChangeSetName: !Sub '${AWS::StackName}-acceptance'
StackName: !Sub '${AWS::StackName}-acceptance'
RunOrder: 2
- Name: Test
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
Configuration:
ProjectName: !Ref RunAcceptanceProject
InputArtifacts:
- Name: Acceptance
RunOrder: 3

The acceptance stage is now ready.

Production stage

The production stage is pretty simple, just one CloudFormation stack. Change the Pipeline resource in the file deploy/pipeline_serverless.yml to add a new stage that looks familiar to the acceptance stage:

deploy/pipeline_serverless.ymlGitHub
Pipeline:
Type: 'AWS::CodePipeline::Pipeline'
Properties:
# [...]
- Name: Test
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
Configuration:
ProjectName: !Ref RunAcceptanceProject
InputArtifacts:
- Name: Acceptance
RunOrder: 3
# NEW STUFF!
- Name: Production
Actions:
- Name: CreateChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: 1
Configuration:
ActionMode: CHANGE_SET_REPLACE
Capabilities: CAPABILITY_IAM
RoleArn: !GetAtt 'CloudFormationRole.Arn'
ChangeSetName: !Sub '${AWS::StackName}-production'
StackName: !Sub '${AWS::StackName}-production'
TemplatePath: 'Source::infrastructure/serverless.yml'
TemplateConfiguration: 'Source::infrastructure/serverless.json'
InputArtifacts:
- Name: Source
- Name: App
RunOrder: 1
- Name: ApplyChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: 1
Configuration:
ActionMode: CHANGE_SET_EXECUTE
Capabilities: CAPABILITY_IAM
ChangeSetName: !Sub '${AWS::StackName}-production'
StackName: !Sub '${AWS::StackName}-production'
RunOrder: 2

Now the application is deployed to production with confidence and without disturbing the users. Try it and run the pipeline!

Summary

Let’s use my production-ready definition to summarize how each point is implemented:

  • Highly available: API Gateway and Lambda are both highly available services out of the box. So your solution is HA as well.
  • Scalable: API Gateway and Lambda are both scaled automatically by AWS for you. You have not to manage anything here!
  • Frictionless deployment: The new zip file created by CodeBuild is deployed with CloudFormation by changing the value of the parameter which is passed down to the Lambda resource.
  • Secure: HTTPS by default. AWS cares about patching. You only need to care about the IAM permissions of your Lambda function. In this case is has only the default permissions to write to CloudWatch Logs.
  • Operations: All logs are stored in CloudWatch Logs, important metrics are monitored, and alarms are defined.

If you now have the impression that deploying and running a Serverless app is easy you are right.

Series

AWS Velocity Cover

  1. Set the assembly line up
  2. Local development environment
  3. CI/CD Pipeline as Code
  4. Running your application
  5. EC2 based app
    a. Infrastructure
    b. CI/CD Pipeline
  6. Containerized ECS based app
    a. Infrastructure
    b. CI/CD Pipeline
  7. Serverless app (you are here)
  8. Summary

You can find the source code on GitHub.

Michael Wittig

Michael Wittig

I’ve been building on AWS since 2012 together with my brother Andreas. We are sharing our insights into all things AWS on cloudonaut and have written the book AWS in Action. Besides that, we’re currently working on bucketAV, HyperEnv for GitHub Actions, and marbot.

Here are the contact options for feedback and questions.