SST is The Most Underrated Serverless Framework You Need to Discover (part 2)

Sat, 13 Nov 2021

#serverless #aws #sst #cdk

SST Series

This article is part of a series around SST - Serverless Stack. I will try to let you discover some amazing aspects of this particular solution in the serverless world.

So you start building using serverless principles, and you discover the Serverless framework. Great ! You will discover here another option, that I consider superior in multiple area, the Serverless Stack (SST). In this second article, I will focus on some available constructs after introducing the concept. It can help you to build faster!

Introducing some concepts

What is CDK?

The Serverless Stack (SST) is based on the AWS Cloud Development Kit (CDK). This solution has been introduced by AWS a few years ago, it allow to build infrastructure as code (IaC) by using a real programming language. If you already know Terraform, it is an equivalent product.

Terraform allow you to declare cloud resources via the HashiCorp Configuration Language (HCL):

resource "aws_instance" "iac_in_action" {
  ami               = var.ami_id
  instance_type     = var.instance_type
  availability_zone = var.availability_zone

  // dynamically retrieve SSH Key Name
  key_name = aws_key_pair.iac_in_action.key_name

  // dynamically set Security Group ID (firewall)
  vpc_security_group_ids = [aws_security_group.iac_in_action.id]

  tags = {
    Name = "Terraform-managed EC2 Instance for IaC in Action"
  }
}

On the opposite CDK allow you to declare cloud resources via TypeScriptPythonJava or .Net (maybe more now?)!

import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ... rest

    // 👇 create the EC2 Instance
    const ec2Instance = new ec2.Instance(this, 'ec2-instance', {
      vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      role: webserverRole,
      securityGroup: webserverSG,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO,
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      }),
      keyName: 'ec2-key-pair',
    });
  }
}

(this is a TypeScript example from here).

A few more differences exists between the solutions:

This will be enough to introduce you the next important concept: constructs!

What are constructs?

From AWS Documentation:

Constructs are the basic building blocks of AWS CDK apps. A construct represents a "cloud component" and encapsulates everything AWS CloudFormation needs to create the component.

You can think about a set of basic cloud resources are managed behind a construct, and it help you to handle the complexity more easily with a few low number of lines.

For example here we can see how it's possible to use the construct ApplicationLoadBalancedFargateService in a few lines of codes:

const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', {
  cluster,
  memoryLimitMiB: 1024,
  cpu: 512,
  taskImageOptions: {
    image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
  },
});

This single object is responsible to create the following:

And it can also generate for you a ECS Cluster if you provide a VPC. So in just 8 lines you create not less than 4 or 5 AWS resources!

Constructs Provided By SST

As said previously SST is based on CDK so it use intrinsically all CDK concepts, including Constructs.

One example provided in the first article was using the Api object, it's probably the most basic SST Construct available! Let's get a view on the most important constructs available in the framework and understand how it can help you.

sst3.png
(Go here to get the full list!)

Api

const api = new Api(this, "Api", {
  routes: {
    "GET  /notes": "src/list.main",
    "POST /notes": "src/create.main",
  },
})

The most simple one, to declare API HTTP endpoints very quickly. It mimics what can be done in the classic Serverless Framework. See my first article for a more in-depth comparison.

Auth

import { Auth } from "@serverless-stack/resources";

const auth = new Auth(this, "Auth", {
  cognito: true,
});

Auth will allow you to manage user authentication on your application, API... It's based on AWS services: Cognito User Pool and Cognito Identity Pool.

You can then manage the permissions:

auth.attachPermissionsForAuthUsers([
  api,
]);

Authenticated user of the auth object will be allowed to access the API endpoints declared by the api object (you need to secure your API for that, by default API is not secured and everyone can use it).

Table

The Table construct will handle for you a DynamoDB table:

import { Table, TableFieldType } from "@serverless-stack/resources";

const table = new Table(this, "Notes", {
  fields: {
    userId: TableFieldType.STRING,
    noteId: TableFieldType.STRING,
  },
  primaryIndex: { partitionKey: "noteId", sortKey: "userId" },
});

Then if you want to allow the Lambda functions inside your api object to be able to read / write in the table you have to specify that with the permissions prop:

new Api(this, "Api", {
  defaultFunctionProps: {
    permissions: [table],
  },
  routes: {
    "GET  /notes": "src/list.main",
    "POST /notes": "src/create.main",
  },
});

ReactStaticSite

ReactStaticSite is a very cleaver constructs that will allow you to include your frontend application inside the SST application!

Let's imagine you have a frontend/ folder inside the SST application with a classic ReactJs application. Add the following to your SST stack:

const site = new ReactStaticSite(this, "ReactSite", {
  path: "frontend/",
});

This 3-lines of code will do the following:

And you can also imagine the power behind the StaticSite or the NextjsSite constructs 😅

Handle Environment Variables with ReactStaticSite

Generally you will use some REACT_APP_* variables in your ReactJs application in order to share the URL endpoint of your API, the Cognito User Pool Id, etc... A classic way to do that is to create a .env file at the root level of your ReactJs application (here for Create React App documentation)

SST is providing a very efficient way to handle that without messing with a .env file anymore! First define the variable in the ReactStaticSite construct like that:

new ReactStaticSite(this, "ReactSite", {
  path: "path/to/src",
  environment: {
    REACT_APP_API_URL: api.url,
    REACT_APP_USER_POOL_CLIENT: auth.cognitoUserPoolClient.userPoolClientId,
  },
});

Then in combination with the Live Lambda Development environment (it will be my next article!) you will be able to start your local React application by using the SST live information!

Add the following package to your ReactJs application:

npm install --save-dev @serverless-stack/static-site-env

And change the start script in package.json:

"scripts": {
  "start": "sst-env -- react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},

More on this feature in the dedicated documentation.

Conclusion

This is the end of this second article about SST framework. I hope you understand better the power behind each construct provided by the framework. The team behind is very active and ship new features very often. I remember a few months ago, saying to myself: "if only some kind of static site could be embed into SST it will be just magic". Then I see v0.33.0 😱.

Just give a look at the follow resources:

In the next episode of this series, I will speak about the local development feature of this framework, you can follow me to be informed when it will be out!