Functional Servers, Or is that Serverless?

One of the worst named popular technologies is “serverless” technology. Nothing could be further from the truth, clearly, there are servers behind all the magic, it’s just that you aren’t responsible for them.  Two popular technologies in the cloud are Microservices and Webservers. The services are often hosted by the webservers. Then came container technology popularised by Docker a few years ago and the instances with web servers just to host services seemed a bit old hat, after all, why run an instance when you can run multiple containers over a cluster of instances and scale them as needed?

After a while AWS Lambda functions became extremely popular, now we don’t even need containers, we can take little pieces of code have them exist in “lambda land” and not really worry about what it is that’s serving them because that’s all managed for us. To make matters better if you design correctly lambdas are extremely cheap and you only pay for the execution time. Meanwhile, of course, Amazon released Fargate over in ECS and you then didn’t need to worry about provisioning your ECS instances anymore. But which one is right for which.

Let’s take a look at Lambda first, we’ll be using the following:

If you haven’t used any of this before, don’t worry the cloudformation isn’t something we will be writing directly, the Serverless Framework will take care of that for us for now, it will create a “Stack” which will deploy our lambda functions and our API Gateway endpoints, the endpoints will front our lambda functions.

So first things first we need a serverless project, we are going to use the open-source version of the serverless framework, there is a commercial offering but you don’t need to worry about that to get started. The framework runs on NodeJs 6 or higher but we will be using version 10 of node js to remain current as of time of writing so after you have installed NodeJs for your platform you need to install serverless which you can do with the following line:

npm install -g serverless

You can find out what version you are running with

serverless --version

As we are going to deploy to Amazon Webservices we are going to need login credentials for the framework to use, this is a large topic so I am not going to detail that here, however, the guys over at serverless.com have written a fantastic set of instructions which you should follow and then come back.

Following in the tradition of all frameworks these days the serverless cli you installed above has a way of creating projects so you can get started quickly, here we create a service, which is like a project with the following command.

serverless create --template aws-nodejs --path myFirstService

You will get some output that looks like this

Serverless: Generating boilerplate in "/home/krystan/projects/serverless_article/myFirstService"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.52.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"

In the directory “myFirstService” you will have two files, one called serverless.yml and one called handler.js. The most important file is the yml file, this is where you define the application and where you want it deployed. Each serverless service can be thought of as an application, it can have many lambda functions associated with it. Each application is housed in its own directory with its own distinct serverless.yml file. When you execute the deploy stage serverless framework reads the .yml file and take your code and deploys it for you, creating all the infrastructure you need in AWS. It does this by using Amazon Cloudformation under the covers, this is an infrastructure as code (IAC) tool written by Amazon.

At this point it’s worth pointing out that serverless platform can deploy to different clouds, it is not just AWS specific and it does so by making use of different providers, for this article we will stick to the AWS Provider as we are deploying to that cloud.

Let’s take a look at the yml file we shall be using, the one produced by the framework is quite verbose and has a lot of comments in, for this article, we shall use a paired down version that looks like this:

service: myfirstservice
frameworkVersion: "=1.52.0"

provider:
  name: aws
  runtime: nodejs10.x
  stage: dev
  region: eu-west-1
  apiName: ${self:provider.stage}-myfirstservice
  memorySize: 512

functions:
  hello:
    handler: handler.hello
    events:
      - http: GET hello
  tags:
      environment: ${self:provider.stage}
      serverless: true

There is a lot going on here but we have defined an AWS provider so we can deploy to AWS, told the framework we are using nodejs10.x (these types can be found in AWS documentation for languages supported, you don’t have to use NodeJs). The stage can be thought of your environment, so for example, you may have; dev, test, uat and then prod stages. We give the API Gateway deployment a name overriding the default, this is making use of variables, in this case, it refers to itself and the provider and accesses the stage name to produce:

dev-myfirstservice

We then give a starting memory size for the lambda function. We add some tags (because tagging things is good practice) and tell the framework where the code is to build the lambda function. We then associate an HTTP GET event with that handler. The handler code as generated by the serverless framework looks like this:

'use strict';

module.exports.hello = async event => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: 'Go Serverless v1.0! Your function executed successfully!',
        input: event,
      },
      null,
      2
    ),
  };
};

Its really just a hello world function but will do for now. Now lets deploy by typing:

serverless deploy

We will get some output which looks something like this

$ serverless deploy
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 myfirstservice.zip file to S3 (319 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............................
Serverless: Stack update finished...
Service Information
service: myfirstservice
stage: dev
region: eu-west-1
stack: myfirstservice-dev
resources: 10
api keys:
  None
endpoints:
  GET - https://quqruaplt8.execute-api.eu-west-1.amazonaws.com/dev/hello
functions:
  hello: myfirstservice-dev-hello
layers:
  None
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and testing.

This has done an awful lot for us, if we were to do this manually we would need to compile and package the lambda code, define the function and then upload the code to either a bucket or use the built-in bucket mechanism of lambda. If we wanted it repeatable we’d need to write a lot of Terraform or Cloudformation definitions to do that and then set up a mechanism to create that (probably scripts). This did all of that in one command. Lets have a look: We can list the lambda with the aws-cli like this:

aws lambda get-function --function-name myfirstservice-dev-hello --region=eu-west-1

You will get some output which looks a little like this:

{
    "Configuration": {
        "FunctionName": "myfirstservice-dev-hello",
        "FunctionArn": "arn:aws:lambda:eu-west-1:<redacted>:function:myfirstservice-dev-hello",
        "Runtime": "nodejs10.x",
        "Role": "arn:aws:iam::<redacted>:role/myfirstservice-dev-eu-west-1-lambdaRole",
        "Handler": "handler.hello",
        "CodeSize": 319,
        "Description": "",
        "Timeout": 6,
        "MemorySize": 512,
        "LastModified": "2019-09-15T22:22:31.822+0000",
        "CodeSha256": "nAMhMEam8QPdOjMJ9JdnIYAsIZc2JUnZWrXs5/+BwT0=",
        "Version": "$LATEST",
        "TracingConfig": {
            "Mode": "PassThrough"
        },
        "RevisionId": "255e1518-f325-4609-bc52-b731a18b8b57"
    },
    "Code": {
        "RepositoryType": "S3",
        "Location": "https://awslambda-eu-west-1-tasks.s3.eu-west-1.amazonaws.com/snapshots/<redacted>/myfirstservice-dev-hello-033b62e1-fafd-4b6f-b936-9fcf51c545fb?versionId=sNo8GB0HJVbnp.sLGHYFgq.jGl.33aXH&X-Amz-Security-Token=AgoJb3JpZ2luX2VjEIX%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCWV1LXdlc3QtMSJGMEQCIH6x8MzeUx%2FjUvY90hihQOQPd4Jq1Kv0ikaUBXMZut2GAiAu%2BKf%2BSOrrqHEQwWJdL5zDJOqt5FjWcvobQttqO6e0%2FiraAwhOEAEaDDk1NDM2OTA4MjUxMSIMmXidpPgv4z%2FLvAM9KrcDwJ47KqHJ7obSPTLC2qY4VcHzIjfrXCYbt08Jp%2BPA9Vf01%2BbifjCM9SpAolq4wJmGZH5eri5g7rvcCkspuMjc%2FP%2BQSBjM5J59NZyGoHUG1uAR6E%2F2vt95RSPGmxXPNxah5CzYJ4kkVWP0nFErv0ZScJvhf1iYsfdcowW7%2F%2F6EgMfNwtvJ84hz9ie0EK%2FR4Tnnf%2BNJSsthK2n%2BP6s0%2BA8Y0et4k2ea7rsfYAheW9JkuPM6kwev%2B6sFvZuOyaH9XObSkqd12EzkBJI2fyLKzuBXrBrE%2FFWIXBMdcUt4G7F9Lin2a0Ak0r2joU4gCl7gYPpfBZhzii5MglNSpVx5x4JxZHu%2Brw7MRsbblIHA9bLCYLG9yIB1oAUJQ3wprVINqpyuSHVuEgJGCQR9L8E%2Fs0vq%2Btw6HeW5ztoIoysuHxoee9Iy%2F2R7oDNxeart9aF2sxUqrzwzfdmyX%2FpVALW7wB0w2sIQWCiARXzURybXRnfBVEP3q9XmXi0C2Wowf2rzpr33Wr%2FXCl%2FF03tVXrEjBVez%2FGWfGn%2B%2BEtagFCNpfErVK5%2B4BioLubU8VnPkoGk5pZyq94kDcBfBtTDfz%2FrrBTq1AdKN40bQ1SHXprJcF%2Bowapjm2LjslDgWs9UdmvAeAP9LEJjDHx8UN9Tyc1Mws3ePrHBd%2BuFOVXyZaFZej%2FHa8afSBTyGCxW9wUwnm6iT79vHHhMgaiXx0iYl69iMjRfsJ8Lc5c37mRddhzWUK3%2B2aheFy%2BaweV7xVqQ8ryrKZUaFUEU2uJ%2BcgcS36HCcETfHFuxTMe%2B6FGyZsm%2BGSNbC1%2B0Ncn6OTSVXKBTRz8S%2FujPzTpTNEWY%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20190915T222856Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIA54NGUQSHV34AKUNC%2F20190915%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Signature=ee9260d791cbf1accbccc5fa7a684f10ff7f2846a164f843c7891426e9e6989c"
    },
    "Tags": {
        "STAGE": "dev",
        "aws:cloudformation:logical-id": "HelloLambdaFunction",
        "aws:cloudformation:stack-id": "arn:aws:cloudformation:eu-west-1:<redacted>:stack/myfirstservice-dev/32634cd0-d807-11e9-b10e-02abe6210ac8",
        "aws:cloudformation:stack-name": "myfirstservice-dev",
        "environment": "dev",
        "serverless": "true"
    }
}

You can see here the lambda is defined and referred to in a cloud formation stack, as you make more deploys these changes are managed and resources managed for you if you remove an endpoint definition for API Gateway, it will be deleted and so on.

So how do we know this all works, serverless has some tools for testing, we can invoke the function directly from the command line with the following command:

serverless invoke --function hello

This makes a call to the function in the cloud and we get a response, that is a json response as we setup the lambda to return JSON so the lambda proxy for API Gateway could make use of it.

{
“statusCode”: 200,
“body”: “{\n \”message\”: \”Go Serverless v1.0! Your function executed successfully!\”,\n \”input\”: {}\n}”
}

This is the response directly from the function, but what about the API Gateway, well the response from that is a little more verbose you can test with curl and this is what it looks like:

curl https://quqruaplt8.execute-api.eu-west-1.amazonaws.com/dev/hello
{
  "message": "Go Serverless v1.0! Your function executed successfully!",
  "input": {
    "resource": "/hello",
    "path": "/hello",
    "httpMethod": "GET",
    "headers": {
      "Accept": "*/*",
      "CloudFront-Forwarded-Proto": "https",
      "CloudFront-Is-Desktop-Viewer": "true",
      "CloudFront-Is-Mobile-Viewer": "false",
      "CloudFront-Is-SmartTV-Viewer": "false",
      "CloudFront-Is-Tablet-Viewer": "false",
      "CloudFront-Viewer-Country": "GB",
      "Host": "quqruaplt8.execute-api.eu-west-1.amazonaws.com",
      "User-Agent": "curl/7.58.0",
      "Via": "2.0 42322007e22ea6a235ae829b1f254f98.cloudfront.net (CloudFront)",
      "X-Amz-Cf-Id": "1a86mebsjxXKlRgtL1cgAQfEnzCR9jrjzjgWeT2kfIuGciUQZX0Cug==",
      "X-Amzn-Trace-Id": "Root=1-5d7ebeff-babadb407ddaa92c52a236a2",
      "X-Forwarded-For": "<redacted>, <redacted>",
      "X-Forwarded-Port": "443",
      "X-Forwarded-Proto": "https"
    },
    "multiValueHeaders": {
      "Accept": [
        "*/*"
      ],
      "CloudFront-Forwarded-Proto": [
        "https"
      ],
      "CloudFront-Is-Desktop-Viewer": [
        "true"
      ],
      "CloudFront-Is-Mobile-Viewer": [
        "false"
      ],
      "CloudFront-Is-SmartTV-Viewer": [
        "false"
      ],
      "CloudFront-Is-Tablet-Viewer": [
        "false"
      ],
      "CloudFront-Viewer-Country": [
        "GB"
      ],
      "Host": [
        "quqruaplt8.execute-api.eu-west-1.amazonaws.com"
      ],
      "User-Agent": [
        "curl/7.58.0"
      ],
      "Via": [
        "2.0 42322007e22ea6a235ae829b1f254f98.cloudfront.net (CloudFront)"
      ],
      "X-Amz-Cf-Id": [
        "1a86mebsjxXKlRgtL1cgAQfEnzCR9jrjzjgWeT2kfIuGciUQZX0Cug=="
      ],
      "X-Amzn-Trace-Id": [
        "Root=1-5d7ebeff-babadb407ddaa92c52a236a2"
      ],
      "X-Forwarded-For": [
        "<redacted>, <redacted>"
      ],
      "X-Forwarded-Port": [
        "443"
      ],
      "X-Forwarded-Proto": [
        "https"
      ]
    },
    "queryStringParameters": null,
    "multiValueQueryStringParameters": null,
    "pathParameters": null,
    "stageVariables": null,
    "requestContext": {
      "resourceId": "x9w0yw",
      "resourcePath": "/hello",
      "httpMethod": "GET",
      "extendedRequestId": "AFLH-F_mjoEFT8Q=",
      "requestTime": "15/Sep/2019:22:45:19 +0000",
      "path": "/dev/hello",
      "accountId": "142273029879",
      "protocol": "HTTP/1.1",
      "stage": "dev",
      "domainPrefix": "quqruaplt8",
      "requestTimeEpoch": 1568587519762,
      "requestId": "163fd6df-c4db-4898-8f9e-4ff6245bf78b",
      "identity": {
        "cognitoIdentityPoolId": null,
        "accountId": null,
        "cognitoIdentityId": null,
        "caller": null,
        "sourceIp": "86.28.138.172",
        "principalOrgId": null,
        "accessKey": null,
        "cognitoAuthenticationType": null,
        "cognitoAuthenticationProvider": null,
        "userArn": null,
        "userAgent": "curl/7.58.0",
        "user": null
      },
      "domainName": "quqruaplt8.execute-api.eu-west-1.amazonaws.com",
      "apiId": "quqruaplt8"
    },
    "body": null,
    "isBase64Encoded": false
  }
}

Obviously you will need to use the id which serverless told you when you deployed but the domain name will almost definitely be the same so it should take the form <id>.execute-api.<region>.amazonaws.com.

This has covered getting up and running with serverless framework in AWS, now we are finished we should tidy up, we can do so by removing the stack which will delete the resources provisioned, we can do so by typing this:

serverless remove --verbose

I used the verbose flag so we can get some output to see what happens when we do this, this was the output I got:

Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack removal progress...
CloudFormation - DELETE_IN_PROGRESS - AWS::CloudFormation::Stack - myfirstservice-dev
CloudFormation - DELETE_IN_PROGRESS - AWS::Lambda::Permission - HelloLambdaPermissionApiGateway
CloudFormation - DELETE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1568586098127
CloudFormation - DELETE_SKIPPED - AWS::Lambda::Version - HelloLambdaVersionbsuDYFOakPR8fC7BZCJsELhtJfq62Usj2akOdNzcPM
CloudFormation - DELETE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeployment1568586098127
CloudFormation - DELETE_IN_PROGRESS - AWS::ApiGateway::Method - ApiGatewayMethodHelloGet
CloudFormation - DELETE_COMPLETE - AWS::ApiGateway::Method - ApiGatewayMethodHelloGet
CloudFormation - DELETE_IN_PROGRESS - AWS::ApiGateway::Resource - ApiGatewayResourceHello
CloudFormation - DELETE_COMPLETE - AWS::ApiGateway::Resource - ApiGatewayResourceHello
CloudFormation - DELETE_COMPLETE - AWS::Lambda::Permission - HelloLambdaPermissionApiGateway
CloudFormation - DELETE_IN_PROGRESS - AWS::Lambda::Function - HelloLambdaFunction
CloudFormation - DELETE_IN_PROGRESS - AWS::ApiGateway::RestApi - ApiGatewayRestApi
CloudFormation - DELETE_COMPLETE - AWS::Lambda::Function - HelloLambdaFunction
CloudFormation - DELETE_COMPLETE - AWS::ApiGateway::RestApi - ApiGatewayRestApi
CloudFormation - DELETE_IN_PROGRESS - AWS::Logs::LogGroup - HelloLogGroup
CloudFormation - DELETE_IN_PROGRESS - AWS::IAM::Role - IamRoleLambdaExecution
CloudFormation - DELETE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - DELETE_COMPLETE - AWS::Logs::LogGroup - HelloLogGroup
CloudFormation - DELETE_COMPLETE - AWS::S3::Bucket - ServerlessDeploymentBucket
Serverless: Stack removal finished...

As you can see it first emptied the deployment bucket and then triggered the stack removal when this happened the resources were removed.

We have barely scratched the surface of what can be done with the serverless framework, in the next article I will add another function, have it use some resources and then have one function call another, triggered by a single API Gateway endpoint.