Serverless Take 2: AWS CDK

Serverless Take 2: AWS CDK

Symphony Logo
Symphony
April 4th, 2022

Hello again, and welcome to the second part of the Serverless journey. The previous article taught us how we can easily deploy Serverless applications using the Serverless framework, as well as a basic understanding about the AWS environment. Today, we are going to learn about AWS Cloud Development Kit (CDK), and use it to create AWS resources, as well as try AWS Cognito - identity provider.

There are a lot of ‚Äúserverless = vendor lock-in = be extra careful‚ÄĚ discussions, so I would like to set the stage for the rest of the article. Let‚Äôs first try to redefine the ‚Äúvendor lock-in‚ÄĚ.

Vendor Lock-in

Software development is mostly about faster go-to-market, and faster new-feature-releases. What helps us reach the goal is to avoid developing custom solutions when there are existing ones (do not reinvent the wheel). First, there were libraries, packages, and tools we would use in our application to speed up the development. Now, software companies have started hosting CMS, eCommerce, payment, email - sms, queueing, auth, workflow, and other solutions.

It is rarely an easy decision to choose the vendor that fits best. When a decision is made, it speeds up the development because it allows us to focus on our business case (no need to maintain, troubleshoot, improve the service or develop the expertise - the vendor already does it). When you think about it, custom solutions are not easily justified, because software is, simply put, an automation, and automation is justified mainly because of heavy usage. Let's take a look at web hosting. It is an operational construct, and we must choose whether we want/need more in-house operations or less. Do we need IaaC, PaaS, SaaS or FaaS? Today's development is all about choosing vendor(s) that fit our needs. Decisions can be made based on a price, features, support, ease-of-use, urgency, etc., but in the end it is up to us to make the choice and assess the risks that come with it.

So instead of ‚Äúvendor lock-in‚ÄĚ let's think of it as a "vendor pick-in‚Äô." To ‚Äúbe pick-in‚Äô‚ÄĚ is not an easy job. It requires us to get familiar with the available service offerings in order to make a decision whether we ‚Äúbe pick-in‚Äô‚ÄĚ or ‚Äúbe pick-in‚Äôt‚ÄĚ. Since we have only started getting familiar with Serverless, at this point, it is hard to decide whether going Serverless is a good path for us. Let's make it easier and keep exploring the Serverless world.

Living in the COVID-19 Era

Let's start with a real-life example to learn by doing. Given the "COVID-19 digital transformation", working remotely has been widely adopted. It is a common work model in the IT industry, but the pandemic measures complicate the process of returning to normal - therefore, all of us embraced the hybrid way of communication. To ensure the hybrid form of working, we will use Serverless to build a simple app that employees can use to "register" the day they want to come to the office, so the COVID-19 measures are appropriately followed. We will need 3 REST API endpoints for this feature:

  • Add attendance
  • Remove attendance
  • Get attendance(s)

Let’s set up AWS CDK and see how we can use it to create the AWS resources we need. I have chosen to use CDK because I find it easier to use than the Serverless framework. Using it feels more like coding than configuring, which feels just like home to me.

You can find an example code on GitHub to follow through, or write the code along the way.

AWS CDK

In order to use CDK, let’s first setup our workspace.

First, create an empty folder and execute ¬†‚Äúcdk init app --language typescript‚ÄĚ

CDK will generate the following folder structure.

Bin folder contains a single file where the initial AWS CloudFormation Stack is created. If you need to, you can add additional stack(s) here.

import * as cdk from 'aws-cdk-lib';

import { BlogCdkCognitoStack } from '../lib/blog-cdk-cognito-stack';

const app = new cdk.App();

new BlogCdkCognitoStack(app, 'BlogCdkCognitoStack', {});

Lib folder has a file which describes the initial Stack. Resources we want to provision are configured within the stack class constructor.

import { Stack, StackProps } from 'aws-cdk-lib';

import { Construct } from 'constructs';

 

export class BlogCdkCognitoStack extends Stack {

 constructor(scope: Construct, id: string, props?: StackProps) {

   super(scope, id, props);

 }

}

We don‚Äôt need to focus on the ‚Äúcdk.json‚ÄĚ file for this article. You can read more about it¬†here.

AWS Lambda

We will need 3 lambdas for our feature.

import * as lambda from "aws-cdk-lib/aws-lambda";

 

. . . constructor() . . .

const getAttendance = createLambda(this, "getAttendance");

const addAttendance = createLambda(this, "addAttendance");

const removeAttendance = createLambda(this, "removeAttendance");

. . . 

function createLambda(stack: Stack, handler: string) {

 return new lambda.Function(stack, handler, {

   architecture: lambda.Architecture.ARM_64,

   runtime: lambda.Runtime.NODEJS_14_X,

   code: lambda.Code.fromAsset("functions"),

   handler: `${handler}.handler`,

 });

}

Code is self-explanatory, next step is to create a ‚Äúfunctions‚ÄĚ folder and add files to match the lambda name; each TS file should export ‚Äúhandler‚ÄĚ as a lambda function.

As you can see, creating resources using CDK is like creating objects in OOP. And when you develop software you can leverage programming principles. But hold on, It gets more fun in the next steps.

AWS DynamoDB

Setup dynamo db and grant permissions so we can access it from our lambda functions.

import * as dynamodb from "aws-cdk-lib/aws-dynamodb";

 

. . . constructor() . . .

const dynamoDb = dynamoDbSetup(this);

 

dynamoDb.grantReadData(getAttendance);

dynamoDb.grantReadWriteData(addAttendance);

dynamoDb.grantReadWriteData(removeAttendance);

. . .

 

function dynamoDbSetup(stack: Stack) {

 return new dynamodb.Table(stack, "AttendanceTable", {

   partitionKey: {

     name: "Date",

     type: dynamodb.AttributeType.STRING,

   },

   sortKey: {

     name: "Email",

     type: dynamodb.AttributeType.STRING,

   },

   billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,

 });

}

Adding permissions using CDK is the fun part (at least for some of us). Not just fun, but it does not require me to explain what the code does.

AWS ApiGateway

Setup api gateway and connect endpoints with lambda functions.

import * as apigateway from "aws-cdk-lib/aws-apigateway";

 

. . . constructor() . . .

const api = new apigateway.RestApi(this, "api");

const resource = api.root.addResource("attendance");

resource.addMethod("GET", new apigateway.LambdaIntegration(getAttendance));

resource.addMethod("POST", new apigateway.LambdaIntegration(addAttendance));

resource.addMethod("DELETE", new apigateway.LambdaIntegration(removeAttendance));

Creating REST resources is simple and intuitive. True benefit of working with CDK is the ease of exploring the options using code auto-completion - intellisense.

CDK Deploy

Let's deploy what we have built so far. If you are using typescript for lambda handlers, start the ‚Äúnpm run watch‚ÄĚ script in a separate terminal so the code is continuously compiled to JavaScript. Lambda functions support JavaScript, not TypeScript. To deploy the stack use the ‚Äúcdk deploy‚ÄĚ command.

You can test APIs you just created to confirm everything is good so far.

So far we have covered the following Serverless offerings:

  • Lambda functions
  • API Gateway
  • DynamoDB

Let’s see what else the Serverless world has to offer.

Information about who will be in the office on a specific day should not be publicly available or manageable, so we need to add some kind of security around our data. Given the data will be accessed through the AWS ApiGateway, AWS can offer AWS Cognito which works great with ApiGateway. We will use it as an OpenID Connect provider, but keep in mind that it supports a lot more than that.

AWS Cognito

In order to add security around our APIs, we first need to set up Cognito.

import * as cognito from "aws-cdk-lib/aws-cognito";

. . . 

const userPool = new cognito.UserPool(this, "employeeUserPool", {

   userPoolName: "emoloyee-userpool",

   selfSignUpEnabled: true

});

const cognitoDomain = new cognito.UserPoolDomain(this, "attendanceLogin", {

   cognitoDomain: {

     domainPrefix: "attendance-demo-123",

   },

   userPool: userPool,

});

userPool.addClient("attendanceApp", {

   oAuth: {

     callbackUrls: [CLIENT_URL],

     scopes: [cognito.OAuthScope.EMAIL],

   },

 });

We have created a user pool for our employees. Self sign-up is enabled for testing purposes; you do not want anyone to just be able to sign up.

Then, we create a user pool domain. This will allow us to use Cognito Hosted UI for login - sign-up screens.

Then we register an OAuth client which will consume the API. Scope is set to an email because we will need the user's email address within idToken.

We can now provision Cognito service using ‚Äúcdk deploy‚ÄĚ.

In order to sign up - obtain idToken use the checkout Cognito  Hosted UI page.

Finally, we are going to wire up Cognito as an identity provider for our API.

const api = new apigateway.RestApi(this, "api", {

   defaultCorsPreflightOptions: {

allowHeaders: [

   "Content-Type",

   "X-Amz-Date",

   "Authorization",

   "X-Api-Key",

],

allowMethods: apigateway.Cors.ALL_METHODS,

allowCredentials: true,

allowOrigins: [CLIENT_URL],

   },

});

// add Cognito authorizer for our attendance resource

const apiAuthorizer = new apigateway.CognitoUserPoolsAuthorizer(

   this,

   "employeeAuthorizer",

   {

     cognitoUserPools: [userPool],

   }

);

 

const resource = api.root.addResource("attendance", {

  defaultMethodOptions: {

    authorizationType: apigateway.AuthorizationType.COGNITO,

    authorizer: apiAuthorizer,

  },

});

We have updated how we create a RestApi - updated cors in order to support authentication-related headers, and allow requests from our app origin. We have also updated how we create a Rest resource - bind Cognito authorizer. Result = We now have Rest API secured using Cognito. Simple as that!

It is amazing how interoperable these services are!

So far, Serverless took points for *infinite scalability, availability, and interoperability. Not bad!

About the author

Robert Sebescen is a Solutions Architect working at our engineering hub in Novi Sad.

Robert’s strongest points are problem-solving skills and figuring out how stuff works. Most experience and most comfortable working with .NET Core framework on the backend and React on the front-end. Practices Scrum and considers all of its components equally important. As a solution architect, he did estimations for a few integration projects covering: scoping the functional and non-functional requirements, investigating the feasibility of the integration by analyzing platforms (and their APIs) that participate in the integration, defining a solution, and estimating the required effort and workforce.

Contact us if you have any questions about our company or products.

We will try to provide an answer within a few days.