The serverless kingslayer? - Migration from serverless to CDK
This content is more than 4 years old and the cloud moves fast so some information may be slightly out of date.
The serverless kingslayer? - Migration from serverless to CDK
Last day I have been at a customer and suggested using the CDK for Infrastructure as code. He responded with a huge yes. He has worked his way through the CDK Intro Workshop aws-cdk-examples/typescript at master · aws-samples/aws-cdk-examples · GitHub and wanted to use it.
Then we discussed a lambda function. The customer was using the serverless framework - like most of the people. But we decided to use CDK instead. Here is why.
How Dare you? - Why choose a new framework over an existing one?
Pros:
- Serverless defines state, CDK is a program
- If you have many AWS infrastructure ressources, you have CDK constructs to define it more easily
- If you want to access ressources generated by Serverless its sometimes tricky, with CDK you just reference them
- Modularisation of ressources in serverless means CloudFormation include files, in CDK its real modularisation
Cons:
- Learning a new framework
- Serverless most of the time just works
- Serverless has huge library of plugins
- Serverless can serve other clouds too
How Could you? - Migrate a Serverless lambda function
Let`s have a look at a simple lambda function:
Hello kingslayer world
First we will create the function with the serverless framework, which only takes minutes.
Generate lambda with the serverless framework
serverless create --template hello-world
6 serverless create --template hello-world
6
Serverless: Generating boilerplate...
_______ __
| _ .-----.----.--.--.-----.---- | .-----.-----.-----. | | | | | | | | | | |
| -------------------------------- | ----------------------- | ------------------------------------ | ----- | ----- | --- | --- | --- | ----- | ----- | ----- | ----- |
| | ___ | -__ | _ | | | -__ | _ | | -__ | __ -- | __ -- |
| ____ | _____ | __ | \___/ | _____ | __ | | __ | _____ | _____ | _____ | |
| | | The Serverless Application Framework | | | | | | | | | |
| | serverless.com, v1.46.0 | | | | | | | | | | |
-------'
Serverless: Successfully generated boilerplate for template: "hello-world"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name
Now we have:
.
├── handler.js
└── serverless.yml
File | Purpose |
---|---|
handler.js | Lambda function |
serverless.yml | Configuration |
Generate CDK App
With the CDK we need some more minutes, but if you define a template for that its also very fast.
cdk init app --language=typescript
Applying project template app for typescript
Initializing a new git repository...
Executing npm install...```
Now we have some more:
├── README.md
├── bin
├── cdk.json
├── lib
├── node_modules
├── package-lock.json
├── package.json
└── tsconfig.json
File | Purpose |
---|---|
README.md | You should have one |
bin | directory with the main app |
cdk.json | CDK main configuration |
lib | directory with the stack modules |
node_modules | node libraries |
package* | Node modules configuration |
tsconfig.json | typescript configuration |
So with greater power comes … more files.
Add Lambda resource
We edit the main stack:
vi lib/cdk-stack.ts
to:
|
|
So we have to add 6 more lines that the serverless example. Note that we choose the name of the directory lambda
for ourselves.
So it’s easy to have more lambda functions from the start.
Now we create the lambda function directory:
mkdir lambda
Every file in this directory will be zipped to lambda.
Lets create a sample lambda:
vi lambda/hello.js
|
|
With CDK the template starts without an API Gateway:
Compare deploy
Deploy serverless
Deploying with serverless is straightforward:
serverless deploy
gives:
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (404 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.................................
Serverless: Stack update finished...
Service Information
service: serverless-hello-world
stage: dev
region: us-east-1
api keys:
None
endpoints:
GET - https://mmuds1arad.execute-api.us-east-1.amazonaws.com/dev/hello-world
functions:
helloWorld: serverless-hello-world-dev-helloWorld
Please note that the region us-east-1
is predefined with the serverless.yml
.
And destroy:
serverless remove
You get:
Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack removal progress...
.............
Serverless: Stack removal finished...
If you do not specify a deploymentBucket
as shown here, sls will create a new bucket each time you deploy a new function.
A feature wich is missing with CDK is that you only deploy the function code, not the CloudFormation again:
serverless deploy function -f helloWorld
Gives you:
Serverless: Packaging function: helloWorld...
Serverless: Excluding development dependencies...
Serverless: Uploading function: helloWorld (404 B)...
Serverless: Successfully deployed function: helloWorld
You have to change something in handler.js
beforehand, so you have something to deploy!
With CDK in combination with SAM, you may execute your lambda localy in an docker lambda environment, which is even more easy for testing. See the CDK Documentation.
Deploy CDK
The first time you use the CDK with assets you have to create a deployment bucket with the “bootstrap” command.
Deploying with CDK has the ability to add the comparism step:
cdk diff
That shows you the changed resources:
Stack CdkStack
The CdkStack stack uses assets, which are currently not accounted for in the diff output! See https://github.com/awslabs/aws-cdk/issues/395
IAM Statement Changes
┌───┬─────────────────────────────────┬────────┬────────────────┬──────────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼─────────────────────────────────┼────────┼────────────────┼──────────────────────────────────┼───────────┤
│ + │ ${HelloHandler/ServiceRole.Arn} │ Allow │ sts:AssumeRole │ Service:lambda.${AWS::URLSuffix} │ │
└───┴─────────────────────────────────┴────────┴────────────────┴──────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼─────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${HelloHandler/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴─────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)
Parameters
[+] Parameter HelloHandler/Code/S3Bucket HelloHandlerCodeS3Bucket4359A483: {"Type":"String","Description":"S3 bucket for asset \"CdkStack/HelloHandler/Code\""}
[+] Parameter HelloHandler/Code/S3VersionKey HelloHandlerCodeS3VersionKey07D12610: {"Type":"String","Description":"S3 key for asset version \"CdkStack/HelloHandler/Code\""}
[+] Parameter HelloHandler/Code/ArtifactHash HelloHandlerCodeArtifactHash5DF4E4B6: {"Type":"String","Description":"Artifact hash for asset \"CdkStack/HelloHandler/Code\""}
Resources
[+] AWS::IAM::Role HelloHandler/ServiceRole HelloHandlerServiceRole11EF7C63
[+] AWS::Lambda::Function HelloHandler HelloHandler2E4FBA4D
This comes in very handy especially with production environment when you really want to know whether a resource is updated, created or delete and recreated.
Then deploy:
cdk deploy
Gives:
cdk deploy
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
IAM Statement Changes
┌───┬─────────────────────────────────┬────────┬────────────────┬──────────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼─────────────────────────────────┼────────┼────────────────┼──────────────────────────────────┼───────────┤
│ + │ ${HelloHandler/ServiceRole.Arn} │ Allow │ sts:AssumeRole │ Service:lambda.${AWS::URLSuffix} │ │
└───┴─────────────────────────────────┴────────┴────────────────┴──────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼─────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${HelloHandler/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴─────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)
Do you wish to deploy these changes (y/n)?
and
CdkStack: deploying...
Updated: asset.4460f9e1f069325b4df0d02c7ccdfebb4ceb607caf3d28422cf04274907fb15f (zip)
CdkStack: creating CloudFormation changeset...
0/4 | 8:04:50 AM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata
0/4 | 8:04:50 AM | CREATE_IN_PROGRESS | AWS::IAM::Role | HelloHandler/ServiceRole (HelloHandlerServiceRole11EF7C63)
0/4 | 8:04:50 AM | CREATE_IN_PROGRESS | AWS::IAM::Role | HelloHandler/ServiceRole (HelloHandlerServiceRole11EF7C63) Resource creation Initiated
0/4 | 8:04:51 AM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated
1/4 | 8:04:51 AM | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata
2/4 | 8:05:08 AM | CREATE_COMPLETE | AWS::IAM::Role | HelloHandler/ServiceRole (HelloHandlerServiceRole11EF7C63)
2/4 | 8:05:20 AM | CREATE_IN_PROGRESS | AWS::Lambda::Function | HelloHandler (HelloHandler2E4FBA4D)
2/4 | 8:05:21 AM | CREATE_IN_PROGRESS | AWS::Lambda::Function | HelloHandler (HelloHandler2E4FBA4D) Resource creation Initiated
3/4 | 8:05:21 AM | CREATE_COMPLETE | AWS::Lambda::Function | HelloHandler (HelloHandler2E4FBA4D)
4/4 | 8:05:22 AM | CREATE_COMPLETE | AWS::CloudFormation::Stack | CdkStack
✅ CdkStack
Stack ARN:
arn:aws:cloudformation:eu-central-1:669453403305:stack/CdkStack/06554110-a9eb-11e9-b01a-0692890e48f0
With the additional parameter cdk deploy --require-approval never
you will not be asked to confirm.
And destroy:
cdk destroy
Gives you:
Are you sure you want to delete: CdkStack (y/n)? y
CdkStack: destroying...
✅ CdkStack: destroyed
Or
cdk destroy --force
without confirmation. Changes wich are made only to the lambda asset will be noticed and deployed.
Migrate basic configuration
Lets look at the serverless.yaml
and the matching parts in CDK:
Serverless | CDK |
---|---|
service: serverless-hello-world | Stack Name/Function name |
Provider: name: aws | Its always AWS |
runtime: nodejs10.x | lib/cdk-stack.ts: runtime: lambda.Runtime.NODEJS_8_10 |
handler: handler.helloWorld | handler: ‘hello.handler’ |
events | we will come to that later |
So its not so different.
Events: http
If you use a lambda inside aws, you call the aws api directly. From the “outside” you have to put an api gateway in front of the lambda to create a REST API. You could also attach an lambda to an Application Load Balancer. Lets use the Api Gateway here:
Migrate API Gateway Basics
Serverless API
In serverless its very short to define a API Gateway:
events:
- http:
path: hello-world
method: get
cors: true
Gives you an deployed APi endpoint, like shown in serverless deploy
:
endpoints:
GET - https://g72uv1y4e2.execute-api.eu-central-1.amazonaws.com/dev/hello-world
Now we may access the endpoint:
wget https://g72uv1y4e2.execute-api.eu-central-1.amazonaws.com/dev/hello-world
which gives us the output, including:
"message":"Go Serverless v1.0! Your function executed successfully!",
CDK API: choose you path
With the CDK you may use CDK constructs or AWS SAM. But first let`s have a look at the Api Gateway resource.
Defining API Gateway in pure CloudFormation (also in terraform) is not very convinient, so AWS created SAM (serverless application modell) for that.
Why is defining the API in pure CloudFormation complex? This comes from the many moving parts, which have to reference each other.
Looking at the Cloudformation documentation you see the method
and the request
(and many other) resource types. Refer e.g. here to see an example of the referencing .
These are the minimum of resources for creating the APIGateway:
- API:
AWS::ApiGateway::RestApi
- Resource
AWS::ApiGateway::Resource
references API - Method
AWS::ApiGateway::Method
references resource
and some more to deploy the api.
So you would have to have at least:
|
|
Redundant references are in lines 9,14,21,23,29,34
With the AWS Serverless Application Model it has become easy:
Events:
ThumbnailApi:
# Define an API Gateway endpoint that responds to HTTP GET at /thumbnail
Type: Api
Properties:
Path: /thumbnail
Method: GET
Now you may use SAM with CDK. But with the CDK its also easy. So we will create the APIGateway with CDK constructs.
CDK API with basic constructs
CDK constructs are designed to abstract details. So it is no surprise its simpler as in pure CloudFormation:
You add the apigateway library:
npm i import @aws-cdk/aws-apigateway --save
and include the api gateway into cdk-agw/lib/cdk-stack.ts
:
|
|
Let`s compare this to the serverless.yml definition:
events:
- http:
path: hello-world
method: get
cors: true
The main difference is that the static yaml definition is “stupid”. It does not know what it defines.
In the CDK definition, through the cdk api it is defined that this const api
really is an apigateway
.
Because of that it is possible that the editor shows you information about the context apigateway
, which is a huge benefit during development:
While in yaml the editor is lost in trying to show something usefull:
Deploy CDK apigateway construct
Now the concept of the CDK should has become clearer. As serverless tries to make things easy by not telling you whats happens under the hood, CDK tries to make things easy by telling you whats happens in detail or by giving you methodes to access the underlying resources.
So the “http event” in sls becomes an “LambdaRestApi” in CDK.
With cdk diff
we get also information about the CloudFormation resources which will be added for the API Gateway:
[+] AWS::ApiGateway::RestApi HelloAPI HelloAPI15CE0595
[+] AWS::ApiGateway::Deployment HelloAPI/Deployment HelloAPIDeploymentC6DB7C2Cfc9698904ca98f9c6235ab6301368b0c
[+] AWS::ApiGateway::Stage HelloAPI/DeploymentStage.prod HelloAPIDeploymentStageprod9702371B
[+] AWS::ApiGateway::Account HelloAPI/Account HelloAPIAccount55AE3404
[+] AWS::ApiGateway::Resource HelloAPI/Default/{proxy+} HelloAPIproxy0FA343AE
[+] AWS::ApiGateway::Method HelloAPI/Default/{proxy+}/ANY HelloAPIproxyANY3911E02A
[+] AWS::ApiGateway::Method HelloAPI/Default/ANY HelloAPIANY99C05532
Evolution of your function
Now we will add an event supported by sls. After that we will do something which is not directly supported by sls.
Migrate events around the clock: Cron
Serverless CloudWatch events
You have a lambda which should be called at 12 minutes past midnight each day. How do we do that? With a CloudWatch events rule.
With the supported events from serverless, you just add the event to the serverless.yml
:
functions:
handler: handler.helloWorld
events:
- schedule: cron(0 12 * * ? *)
CDK CloudWatch events
In CDK its also simple:
|
|
No we increase the difficulty!
Extending: add an SNS Topic to existing Cfn Rule
Suppose now I want to add an SNS topic to the CloudWatch rule.
Extend the CDK app
With CDK its simple - you add the SNS topic in line 30 and add that topic as a target for the CloudWatch event rule (line 32).
|
|
Were done!
Extend serverless
Serverless is great and simple of you work whithin the given boundaries. Beyond this it can get tricky..
Plugins
With Serverless you are lucky if there is an existing and working plugin. So search at plugin page for a solution.
Some of the plugins just extend Cfn. E.g. a CloudWatch Logs subscription has its own serverless plugin: Log subscription plugin This is just a construct in CDK,see Log Subscription Destination.
But in this UseCase we have no plugin for adding targets to the event rule! So we have to extend the generated CloudFormation or write our own serverless plugin. See variables in serverless documentation.
Because writing a plugin just for this simple task would be overkill, we (try) to extend the serverless generated Cfn.
Access CloudFormation Resource in serverless
So, no luck with plugins, then you may choose to access the generated CloudFormation.
From the working CloudFormation from the CDK we learn a way to add another target to the eventrule:
|
|
We only need an sns topic with policy and the additional lines 12-14 to add the topic as an target to the CloudWatch event rule.
For details read the fine Cfn manual
Let´s do this in serverless. You can add CloudFormation resources in CloudFormation yaml at a resource
section in serverless.yml
.
Here is the point: From now on you only can create a more complex solution in serverless as with the CDK.
Because any Cfn resource, which has no high level construct in the CDK, has an low-level CFN resources. See the CDK API Reference for more details on this.
So if the resource is already a simple to use higher level construct, its simpler to develop in CDK. If the resource only has a low-level Cfn resource, then the effort is nearly the same.
But lets see how much effort this scenario is in sls (serverless).
First see AWS Resources in the serverless documentation.
In the documentation we find Events::Rule
with Schedule: {normalizedFunctionName}EventsRuleSchedule{SequentialID}
as the name or this resource.
You may have a look at the generated CloudFormation from serverless by doing:
- `serverless package``
- See generated `.serverless/cloudformation-template-update-stack.json``
This is the sls generated events rule so far:
"HelloWorldEventsRuleSchedule1": {
"Type": "AWS::Events::Rule",
"Properties": {
"ScheduleExpression": "cron(0 12 * * ? *)",
"State": "ENABLED",
"Targets": [
{
"Arn": {
"Fn::GetAtt": [
"HelloWorldLambdaFunction",
"Arn"
]
},
"Id": "helloWorldSchedule"
}
]
}
So we add a resource
section to the serverless.yml
. In this section you can add CloudFormation to the generated CloudFormation in serverless.
There we create the sns topic as CloudFormation. In serverless you may create an SNS topic as an event bound to the lambda function. But if you want a standalone topic, you have to fall back to CloudFormation:
Here is the configuration added to serverless.yml
to get an sns topic and policy:
|
|
And these were the lines from above which the cdk needs to create an sns topic:
|
|
With serverless you may mix attributes into the generated CloudFormation, so with the right reference to the generated resource you can alter the resource.
The normalizedFunctionName
is HelloWorld
so we add to the resources
section the code which should add the new topic blogtopic
to the rule:
HelloWorldEventsRuleSchedule1:
Properties:
Targets:
- Arn:
Ref: blogtopic
Id: TopicTarget
Serverless should add another target to the events rule. Unfortunately this only works with additional properties.
So this attempt leads to this generated CloudFormation:
|
|
This is wrong, because in line 13 the reference to the topic was addes to the first entry in the Targets
array instead of beeing added in another array item.
The problem is, that all ids from the items are the same: Arn
.
Surrender
So its not possbile to mix a new target into the cfn in a simple way.
One solution would be creating these resources manually in the resource
section of the serverless.yml
:
- AWS::SNS::Topic
- AWS::SNS::TopicPolicy
- AWS::Events::Rule
and reference one ARN in the Events::Rule to the generated Lambda Funktion. This would be a fallback to doing most things manually.
Now Choose your side: declarative with serverless or imparative with CDK
My conclusion - only for AWS projects - is that if you stay within the boundaries of serverless, it can still be a good fit for your project.
But if you extend your Lamdas with additional AWS Services, CDK have some serious advantages over serverless and to be frank over AWS SAM too.
What do you think?