Using Lambda Versions, Aliases and Safe Deployments with AWS Serverless Application Model

By Adam McQuistan in Serverless  01/18/2021 Comment

AWS SAM Safe Deployments

Introduction

In this article I discuss the merits of using Lambda versioning and aliases along with the AWS Serverless Application Model's (SAM) built-in capability to utilize CodeDeploy with weighted traffic routing for Gradual Deployments. By using these features of AWS Lambda and the SAM framework you can increase the robustness of your serverless applications providing added flexibility and safety for deploying new features and bug fixes.  To make these concepts concrete I demonstrate how versions, aliases and deployment preferences with a simple AWS SAM project written in the Python programming language.

Key Concepts for Lambda Versions, Aliases and Deployment Preferences

Lambda Versions

Lambda functions provide the ability to generate immutable versions of the source code and configuration which are like time specific snapshots or checkpoints. The concept is similar to git based versioning which allow targetting of specific functionality created over the history of publishing changes to the Lambda functions as versions. This provides the ability to invoke a specific version of a Lambda function via any number of method like the AWS CLI, the AWS Console, or one of the many event mappings like S3 or SQS.

If a version has never been explicitly published then the version is referred to as $LATEST.

Lambda Alias

The concept of a Lambda alias is closely tied to Lambda versions and provide a way to associate a human friendly name with one or two specific versions of a Lambda function. The version of a function that an alias is associated with is mutable meaning it can be reassigned to point to another version. Just like a specific version of a function can be invoked by specifying the version while invoking it the same can be done specifying an alias.

Auto Publish Alias

The SAM Function template resource provides a very useful field type named AutoPublishAlias which can be set to any string representing an alias to continually associate with, or point to, the most recently published version of the Lambda Function.

Deployment Preferences

The SAM Function template resource provides another useful field type named DeploymentPreference which provides a way to dictate how invocations of the newly published function version and alias should be distributed. This allows for a way to control how to introduce traffic, or specific version executions, to newly published versions of a Lambda Function ranging from all at once to gradual shifting to the new version.

Demo Project Setup

For this article I will solidify the concepts of Lambda Function versions, aliases and deployment preferences by providing a functional demo SAM project. In order to follow along readers will need to have the AWS CLI and AWS SAM CLI installed so, please use the following AWS SAM Getting Started instructions from the AWS docs if desired.

To start I use the SAM CLI to initialize a new project which I name sam-safe-deployment like so.

$ sam init --name sam-safe-deployment --runtime python3.8
Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1
What package type would you like to use?
	1 - Zip (artifact is a zip uploaded to S3)
	2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
	1 - Hello World Example
	2 - EventBridge Hello World
	3 - EventBridge App from scratch (100+ Event Schemas)
	4 - Step Functions Sample App (Stock Trader)
	5 - Elastic File System Sample App
Template selection: 1

    -----------------------
    Generating application:
    -----------------------
    Name: sam-safe-deployment
    Runtime: python3.8
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .

    Next steps can be found in the README file at ./sam-safe-deployment/README.md

Then I change directory into the newly created sam-safe-deployment directory and rename the Hello World template generated hello_world directory to greeter_api.

cd sam-safe-deployment
mv mv hello_world greeter_api

Next update the template.yaml file to expose a /greet POST endpoint that will be paired to a Lambda function that accepts a name field in the POST request body and returns a greeting message with the name incorporated with it. The SAM YAML template also generates output values for the REST API URL and Function ARN.

Here is the updated template.yaml for the SAM project.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-safe-deployment

  A simple Greeter API for experimenting with Lambda Versions,
  Aliases and Gradual Deployments capable of automated rollbacks.

Globals:
  Function:
    Timeout: 3

Resources:
  GreeterFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: greeter_api/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        GreeterApi:
          Type: Api
          Properties:
            Path: /greet
            Method: post

Outputs:
  GreeterApi:
    Description: "API Gateway endpoint URL for Prod stage for Greeter function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/greet"
  GreeterFunction:
    Description: "Greeter Lambda Function ARN"
    Value: !GetAtt GreeterFunction.Arn

Then here is the updated app.py lambda handler function source which parses the POST request body for the name field and returns a greeting as shown here.

import json

def lambda_handler(event, context):
    request_body = json.loads(event.get('body'))

    name = request_body['name']
 
    return {
        "statusCode": 200,
        "body": json.dumps({
            "greeting": "hello " + name,
        }),
    }

The last bit of setup required is to create a deployment S3 bucket which I'll do using the AWS CLI.

$ aws s3 mb s3://tci-sam-safe-deployments --region us-east-2

First Deployment and Function Execution

To get the SAM project up to the AWS cloud I must build them deploy using the SAM CLI. To build I do the following.

$ sam build
Building codeuri: greeter_api/ runtime: python3.8 metadata: {} functions: ['GreeterFunction']
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

Then the deployment with the SAM CLI looks as follows.

$ sam deploy --stack-name sam-safe-deployments \
	--confirm-changeset \
	--s3-bucket tci-sam-safe-deployments \
	--region us-east-2 \
	--capabilities CAPABILITY_IAM

The output section looks as follows except that I've replaced my account id in the function ARN with the text "accountid".

CloudFormation outputs from deployed stack
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                         
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 GreeterFunction                                                                                                                             
Description         Greeter Lambda Function ARN                                                                                                                 
Value               arn:aws:lambda:us-east-2:accountid:function:sam-safe-deployments-GreeterFunction-W41O0420HJ3G                                            

Key                 GreeterApi                                                                                                                                  
Description         API Gateway endpoint URL for Prod stage for Greeter function                                                                                
Value               https://c4ev82x8ma.execute-api.us-east-2.amazonaws.com/Prod/greet                                                                           
-----------------------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - sam-safe-deployments in us-east-2

There are several ways to test invoking this function using the AWS CLI, Curl, Postman, HttPie and probably countless others. I really like using the HTTPie http client and will utilize it in this tutorial.

$ http -j POST https://c4ev82x8ma.execute-api.us-east-2.amazonaws.com/Prod/greet name=Adam
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 26
Content-Type: application/json
Date: Sun, 17 Jan 2021 18:48:18 GMT
Via: 1.1 20f3946ecbd7e00876bb64af532d99ab.cloudfront.net (CloudFront)
X-Amz-Cf-Id: VrlUEsj5GjJGx5aieTQyg4yc_2S5x5N_eN24zKXeuoLdn-FbHaXopA==
X-Amz-Cf-Pop: DFW50-C1
X-Amzn-Trace-Id: Root=1-60048671-23819fbe729468ac2732ee70;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: ZTnx0FobCYcF8DQ=
x-amzn-RequestId: c97c8ecd-c0df-4a0d-8841-4e5e9caa6a37

{
    "greeting": "hello Adam"
}

Now if I tail the logs for this stack and it's function I can capture some useful information on the invocation event of the function.

$ sam logs --tail --stack-name sam-safe-deployments --name GreeterFunction
2021/01/17/[$LATEST]e71c043eb9df4db69f6b73bda6551c06 2021-01-17T18:48:18.174000 START RequestId: cf3dea68-be04-4b8a-9c31-2527a03227b6 Version: $LATEST
2021/01/17/[$LATEST]e71c043eb9df4db69f6b73bda6551c06 2021-01-17T18:48:18.175000 END RequestId: cf3dea68-be04-4b8a-9c31-2527a03227b6
2021/01/17/[$LATEST]e71c043eb9df4db69f6b73bda6551c06 2021-01-17T18:48:18.175000 REPORT RequestId: cf3dea68-be04-4b8a-9c31-2527a03227b6  Duration: 1.48 ms       Billed Duration: 2 ms       Memory Size: 128 MB     Max Memory Used: 51 MB  Init Duration: 112.84 ms

From the output you can see the version of the GreeterFunction is specified as $LATEST since there has not yet been an explicit version published.

Auto Publishing Versions and Aliases

At this point I'd like to begin publishing versions of the GreeterFunction and associating each new version with an alias I'll name live. To accomplish this I add the AutoPublishAlias field to the AWS SAM template's Function resource and specify live as the value like so.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-safe-deployment

  A simple Greeter API for experimenting with Lambda Versions,
  Aliases and Gradual Deployments capable of automated rollbacks.

Globals:
  Function:
    Timeout: 3

Resources:
  GreeterFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: greeter_api/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        GreeterApi:
          Type: Api
          Properties:
            Path: /greet
            Method: post
      AutoPublishAlias: live

Outputs:
  GreeterApi:
    Description: "API Gateway endpoint URL for Prod stage for Greeter function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/greet"
  GreeterFunction:
    Description: "Greeter Lambda Function ARN"
    Value: !GetAtt GreeterFunction.Arn

I'll also add a modification to the source code to utilize the safer get(...) method of the request_data dictionary.

import json

def lambda_handler(event, context):
    request_body = json.loads(event.get('body'))

    name = request_body.get('name')

    return {
        "statusCode": 200,
        "body": json.dumps({
            "greeting": "hello " + name,
        }),
    }

Now I can build and redeploy to publish the version and alias.

$ sam build
$ sam deploy --stack-name sam-safe-deployments \
	--confirm-changeset \
	--s3-bucket tci-sam-safe-deployments \
	--region us-east-2 \
	--capabilities CAPABILITY_IAM

Then I'll send another request off to the POST api endpoint generating some new data.

$ http -j POST https://c4ev82x8ma.execute-api.us-east-2.amazonaws.com/Prod/greet name=Batman  
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 28
Content-Type: application/json
Date: Sun, 17 Jan 2021 18:54:58 GMT
Via: 1.1 c650b968d15de7f70750f5bb6bd4f469.cloudfront.net (CloudFront)
X-Amz-Cf-Id: bFw-3KkuwFqsWBFUTnA3kF7WHEsr3WHJWGz0xGP_YDlT4rAwCuWDEg==
X-Amz-Cf-Pop: DFW50-C1
X-Amzn-Trace-Id: Root=1-60048802-1a8709474660a2cc506cfbc9;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: ZTowcFNQiYcF-AQ=
x-amzn-RequestId: 3f06b2e4-e95b-4a4b-8c6c-fb12f4b92b46

{
    "greeting": "hello Batman"
}

Again I tail the logs to see the effect of publishing the new version and alias.

$ sam logs --tail --stack-name sam-safe-deployments --name GreeterFunction                  
2021/01/17/[$LATEST]e71c043eb9df4db69f6b73bda6551c06 2021-01-17T18:48:18.174000 START RequestId: cf3dea68-be04-4b8a-9c31-2527a03227b6 Version: $LATEST
2021/01/17/[$LATEST]e71c043eb9df4db69f6b73bda6551c06 2021-01-17T18:48:18.175000 END RequestId: cf3dea68-be04-4b8a-9c31-2527a03227b6
2021/01/17/[$LATEST]e71c043eb9df4db69f6b73bda6551c06 2021-01-17T18:48:18.175000 REPORT RequestId: cf3dea68-be04-4b8a-9c31-2527a03227b6  Duration: 1.48 ms       Billed Duration: 2 ms       Memory Size: 128 MB     Max Memory Used: 51 MB  Init Duration: 112.84 ms
2021/01/17/[1]900a477cb7d2430aaf9abc9c79c8da13 2021-01-17T18:54:58.993000 START RequestId: a4997a9e-62ed-479f-895d-a23c9aa06864 Version: 1
2021/01/17/[1]900a477cb7d2430aaf9abc9c79c8da13 2021-01-17T18:54:58.994000 END RequestId: a4997a9e-62ed-479f-895d-a23c9aa06864
2021/01/17/[1]900a477cb7d2430aaf9abc9c79c8da13 2021-01-17T18:54:58.994000 REPORT RequestId: a4997a9e-62ed-479f-895d-a23c9aa06864        Duration: 1.40 ms       Billed Duration: 2 ms       Memory Size: 128 MB     Max Memory Used: 52 MB  Init Duration: 127.70 ms

As you can see the last request invoked the newly published version number 1.

Using Deployment Preference Control How New Versions are Introduced

There is another more complex type available to the SAM Template for a Function resource named DeploymentPreference which can be used specify the distribution and rate at which new requests get routed to each new version of the function. The DeploymentPreference object contains a type field for specifying one of several options such as

  • Canary10Percent30Minutes
  • Canary10Percent5Minutes
  • Canary10Percent10Minutes
  • Canary10Percent15Minutes
  • Linear10PercentEvery10Minutes
  • Linear10PercentEvery1Minute
  • Linear10PercentEvery2Minutes
  • Linear10PercentEvery3Minutes
  • AllAtOnce

The Canary deployments will allocate 10 percent of the requests to the latest version until the specified time of either 5, 10, 15, or 30 minutes elapse at which time all traffic will shift to the latest version.

The Linear type deployments gradually increase the proportion of requests to the latest version in a linear fashion every 1, 2, 3, or 10 minutes until all requests eventually are shifted to the latest version.

Finally, there is also the straight forward option of sending all traffic immediately to the new version.

For this demo I will specify the Canary10Percent5Minutes option and also update the source code to include a default name value of World for cases where the field is missing.

Here is the updated template.yaml file.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-safe-deployment

  A simple Greeter API for experimenting with Lambda Versions,
  Aliases and Gradual Deployments capable of automated rollbacks.

Globals:
  Function:
    Timeout: 3

Resources:
  GreeterFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: greeter_api/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        GreeterApi:
          Type: Api
          Properties:
            Path: /greet
            Method: post
      AutoPublishAlias: live
      DeploymentPreference:
        Type: Canary10Percent5Minutes

Outputs:
  GreeterApi:
    Description: "API Gateway endpoint URL for Prod stage for Greeter function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/greet"
  GreeterFunction:
    Description: "Greeter Lambda Function ARN"
    Value: !GetAtt GreeterFunction.Arn

And here is the updated function source code.

import json

def lambda_handler(event, context):
    request_body = json.loads(event.get('body'))

    name = request_body.get('name', 'World')

    return {
        "statusCode": 200,
        "body": json.dumps({
            "greeting": "hello " + name,
        }),
    }

After making those changes I build and deploy the project once again then invoke the function with several calls to the same API endpoint, enough to cause the requests to be distributed across the newest version and the one previously published.

The following script will make 20 requests to the /greeter api.

#!/bin/bash

for i in {1..20}
do
  http -j POST \
  	  https://c4ev82x8ma.execute-api.us-east-2.amazonaws.com/Prod/greet \
  	  name=Joker >> /dev/null
done

After I run the above bash script to make 20 subsequent requests I tail the logs. Below I've pasted the output of tailing the logs which shows about 10% of the requests have gone to the new version (version 2).

$ sam logs --tail --stack-name sam-safe-deployments --name GreeterFunction
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:22.846000 START RequestId: 85431ea5-59d3-4e63-8ea4-4ecd7be22f46 Version: 1
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:22.848000 END RequestId: 85431ea5-59d3-4e63-8ea4-4ecd7be22f46
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:22.848000 REPORT RequestId: 85431ea5-59d3-4e63-8ea4-4ecd7be22f46   Duration: 1.42 ms       Billed Duration: 2 ms   Memory Size: 128 MB        Max Memory Used: 51 MB  Init Duration: 124.05 ms
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:23.433000 START RequestId: a5dd8d1e-9af9-471d-86b8-761d60ef16ec Version: 1
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:23.437000 END RequestId: a5dd8d1e-9af9-471d-86b8-761d60ef16ec
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:23.437000 REPORT RequestId: a5dd8d1e-9af9-471d-86b8-761d60ef16ec   Duration: 1.32 ms       Billed Duration: 2 ms   Memory Size: 128 MB        Max Memory Used: 51 MB
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:24.112000 START RequestId: 8cedece2-3308-4217-837f-a0b98df29d3a Version: 1
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:24.117000 END RequestId: 8cedece2-3308-4217-837f-a0b98df29d3a
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:24.117000 REPORT RequestId: 8cedece2-3308-4217-837f-a0b98df29d3a   Duration: 1.36 ms       Billed Duration: 2 ms   Memory Size: 128 MB        Max Memory Used: 52 MB
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:24.679000 START RequestId: 2ac29ad5-442f-46aa-b785-a5a33aa945c2 Version: 1
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:24.684000 END RequestId: 2ac29ad5-442f-46aa-b785-a5a33aa945c2
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:24.684000 REPORT RequestId: 2ac29ad5-442f-46aa-b785-a5a33aa945c2   Duration: 1.52 ms       Billed Duration: 2 ms   Memory Size: 128 MB        Max Memory Used: 52 MB
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:27.622000 START RequestId: fa2b4f0e-beeb-4b11-aad0-3faa0c47a9bb Version: 1
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:27.627000 END RequestId: fa2b4f0e-beeb-4b11-aad0-3faa0c47a9bb
2021/01/17/[1]f050d1f3d084476daf163f151d33b2ca 2021-01-17T19:57:27.627000 REPORT RequestId: fa2b4f0e-beeb-4b11-aad0-3faa0c47a9bb   Duration: 1.61 ms       Billed Duration: 2 ms   Memory Size: 128 MB        Max Memory Used: 52 MB
2021/01/17/[2]c4f0975e3d3a4737919b2aa8cb43ab63 2021-01-17T19:57:28.395000 START RequestId: eedf85c0-fc76-457c-a638-c55083f6da90 Version: 2
2021/01/17/[2]c4f0975e3d3a4737919b2aa8cb43ab63 2021-01-17T19:57:28.397000 END RequestId: eedf85c0-fc76-457c-a638-c55083f6da90
2021/01/17/[2]c4f0975e3d3a4737919b2aa8cb43ab63 2021-01-17T19:57:28.397000 REPORT RequestId: eedf85c0-fc76-457c-a638-c55083f6da90   Duration: 1.54 ms       Billed Duration: 2 ms   Memory Size: 128 MB        Max Memory Used: 52 MB  Init Duration: 130.86 ms

Ok so that is intersting but its pretty impractical to sit there and tail the logs immediately following a deployment watching for failed requests and, in a panic, manually rollback a deployment.

Luckily, the DeploymentPreference options object also provides an Alarms section that can be used in conjunction with CloudWatch alarms to fire in the event of an error in the newly deployed function.

Below I've added a new CloudWatch alarm which will fire if the published alias version errors at least twice in a 60 second period. Therefore, if that alarm fires during the deployment timeline specified by my Canary10Percent5Minutes option (ie, 5 minutes) the deployment will automatically rollback to the previous version.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-safe-deployment

  A simple Greeter API for experimenting with Lambda Versions,
  Aliases and Gradual Deployments capable of automated rollbacks.

Globals:
  Function:
    Timeout: 3

Resources:
  GreeterFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: greeter_api/
      Handler: app.lambda_handler
      Runtime: python3.8
      Events:
        GreeterApi:
          Type: Api
          Properties:
            Path: /greet
            Method: post
      AutoPublishAlias: live
      DeploymentPreference:
        Type: Canary10Percent5Minutes
        Alarms:
          - !Ref GreeterFunctionAliasErrorAlarm

  GreeterFunctionAliasErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      MetricName: Errors
      Namespace: "AWS/Lambda"
      ComparisonOperator: GreaterThanThreshold
      EvaluationPeriods: 1
      Period: 60
      Statistic: Sum
      Threshold: 2
      AlarmDescription: Fire alarm if more than 3 errors occur in any 60 second period
      Dimensions:
        - Name: Resource
          Value: !Sub "${GreeterFunction}:live"
        - Name: FunctionName
          Value: !Ref GreeterFunction
      TreatMissingData: notBreaching

Outputs:
  GreeterApi:
    Description: "API Gateway endpoint URL for Prod stage for Greeter function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/greet"
  GreeterFunction:
    Description: "Greeter Lambda Function ARN"
    Value: !GetAtt GreeterFunction.Arn

I then update the source code to raise an error if the name submitted for greeting is Joker so, if I execute the same set of 20 requests an error will be raised aborting the new deployment and rolled back to the previous version.

import json

def lambda_handler(event, context):
    request_body = json.loads(event.get('body'))

    name = request_body.get('name', 'World')

    if name == 'Joker':
        raise ValueError('Bad guys should not be greeted')

    return {
        "statusCode": 200,
        "body": json.dumps({
            "greeting": "hello " + name,
        }),
    }

Finally I build and deploy the SAM project then execute the 20 requests for a Joker greeting expecting two errors to occur thus triggering the alarm and causing the deployment to abort and rollback.

Now if I go and checkout the CloudWatch alarm in the AWS console I see that it has been triggered and in the Alarm State.

CloudWatch Alarm

Knowing that the CloudWatch alarm has been triggered I can expect that the deployment has been rolled back so, if I head over to the specific deployment in CodeDeploy I see that yes in fact the deployment has been rolledback.

Aborted Deployment

 

Conclusion

In this article I have discussed and demonstrated why AWS Lambda Function Versions, Aliases and Gradual Deployment preferences are useful and how to implement them using a AWS SAM Python based application.

As always, thanks for reading and please do not hesitate to critique or comment below.

Share with friends and colleagues

[[ likes ]] likes

Community favorites for Serverless

theCodingInterface