AWS Velocity Series: Serverless App
If you have the impression that deploying and running a serverless application is easy after reading this — then you are right!
Join the DZone community and get the full member experience.
Join For FreeThe API Gateway provides an HTTPS endpoint that invokes a Lambda function when a request arrives.
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.
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.
Create a file infrastructure/serverless.yml
and describe the Lambda function that is invoked on GET /{number}
requests.
---
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.
'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:
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:
# 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:
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:
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.
# 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?
- Create or update an acceptance environment based on the CloudFormation+SAM template
serverless.yml
- Run the acceptance tests
- 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:
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 here.
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:
{
"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
:
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:
- Deploy the CloudFormation stack suffixed with
-acceptance
- Run the acceptance tests
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:
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.
Published at DZone with permission of Michael Wittig. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments