Serverless Framework ️<3 AWS CDK
Tags: serverless, cdk, typescript, aws
Published: 2022-01-26 10:25:21

There are plenty of articles out there comparing major Infrastructure as Code frameworks when it comes to building and deploying an AWS serverless stack. A serverless newcomer can easily be overwhelmed by the wide variety of solutions available: Serverless Framework, AWS CDK, Pulumi, Terraform, SST, Architect...
Rather than pitting those solutions against each other, this article focus on leveraging two of them, [Serverless Framework](https://github.com/serverless/serverless) and [AWS CDK](https://github.com/aws/aws-cdk/), for what they are best at and making them work together seamlessly.

This solution does not require any additional dependency, plugin or CLI tool. It works right out of the box, using native CDK API and Serverless Framework service definition properties.

## TL;DR

The following service file definition provides a simple way to deploy an application relying on Lamba-centric resources as well as other type of resources, leveraging respectively Serverless Framework and CDK, all using a single CloudFormation deployment. This solution improves greatly the developer experience by:
- leveraging the simple configuration format of the Serverless Framework
- leveraging AWS CDK constructs rather than vanilla CloudFormation for resources outside of the scope of the Serverless Framework
- using a single language to define the entire stack

```ts
// serverless.ts

import type { AWS } from '@serverless/typescript';
import { App, DefaultStackSynthesizer, Stack } from 'aws-cdk-lib';
import { AttributeType, Table } from 'aws-cdk-lib/aws-dynamodb';

const app = new App();
const stack = new Stack(app, undefined, {
  // Override default synthetizer to prevent check related to CDK bootstrap
  synthesizer: new DefaultStackSynthesizer({
    generateBootstrapVersionRule: false,
  })
});

const table = new Table(stack, 'MyDynamoDBTable', {
  partitionKey: { name: 'PK', type: AttributeType.STRING }
});

const serverlessConfiguration: AWS = {
  service: 'serverless-framework-loves-aws-cdk',
  provider: {
    name: 'aws',
    runtime: 'nodejs14.x',
    iam: {
      role: {
        statements: [
          {
            Effect: 'Allow',
            Action: [ 'dynamodb:PutItem' ],
            Resource: [ stack.resolve(table.tableArn) ]
          }
        ]
      }
    }
  },
  functions: {
    saveUser: {
      handler: 'saveUser.main',
      environment: {
        TABLE_NAME: stack.resolve(table.tableName)
      },
      events: [
        { http: 'POST /user' }
      ]
    }
  },
  package: { individually: true },
  resources: app.synth().getStackByName(stack.stackName).template
};

module.exports = serverlessConfiguration;
```

## How does it actually work?

Let's deep dive, step by step, in this service file, and understand how Serverless Framework and AWS CDK work together seamlessly at provisioning a state of the art serverless application.

### Serverless Framework TypeScript service file

Since v1.72.0, the [Serverless framework accepts `serverless.ts` as a valid service file](https://www.serverless.com/framework/docs/providers/aws/guide/intro#services) in addition to the more commonly-known `serverless.yml`, `serverless.json` and `serverless.js` file formats. Using `serverless.js` or `serverless.ts` service definition is a requirement to implement this solution. Both those formats allow programmatic execution using Node.js in order to build the output service definition. In addition, you benefit from TypeScript definitions exported by [`@serverless/typescript` package](https://github.com/serverless/typescript) to help you build your Serverless Framework service definition properly. 

```ts
// serverless.ts

import type { AWS } from '@serverless/typescript';

const serverlessConfiguration: AWS = {
  service: 'serverless-framework-loves-aws-cdk',
  provider: {
    name: 'aws',
    runtime: 'nodejs14.x'
  }
};

module.exports = serverlessConfiguration;
```

### AWS CDK pure resources

Within the same `serverless.ts` file, or in any other file, you can bootstrap your CDK application, creating a new `App` and `Stack`. You can then start to add resources not natively handled by the Serverless Framework, based on your app's requirements, referencing the newly created stack as the resource scope.

```ts
import { App, Stack } from 'aws-cdk-lib';
import { AttributeType, Table } from 'aws-cdk-lib/aws-dynamodb';

const app = new App();
const stack = new Stack(this.app, undefined, {
  // Override default synthetizer to prevent check related to CDK bootstrap
  synthesizer: new DefaultStackSynthesizer({
    generateBootstrapVersionRule: false,
  });

const table = new Table(stack, 'MyDynamoDBTable', {
  partitionKey: { name: 'PK', type: AttributeType.STRING }
});
```

### Shipping AWS CDK resources within Serverless Framework generated CloudFormation

In order to bridge AWS CDK and Serverless Framework, you can use native features from both frameworks.

On one side, Serverless Framework provides an option to [include custom CloudFormation](https://www.serverless.com/framework/docs/providers/aws/guide/resources), using the `resources` property from the service definition. This property is usually used to inject vanilla CloudFormation, but we'll leverage AWS CDK to avoid doing so.

On the other, AWS CDK provides an API to programmatically generate a Cloud Assembly using the `synth` method of any `App` instance. A Cloud Assembly instance is a CDK internal class used to represent a deployable cloud application. Each CDK App can contain multiple CDK Stack, but once again, the CDK provides an API to select a specific stack representation from a Cloud Assembly instance. Finally, each stack of a Cloud Assembly instance exposes the underlying CloudFormation template, as generated by AWS CDK CLI operations.

Using AWS CDK API to generate desired template, and injecting it within Serverless Framework service definition `resources` property does the trick bridging both frameworks.

```ts
const serverlessConfiguration: AWS = {
  // ...
  resources: app.synth().getStackByName(stack.stackName).template
};
```

### Adding references to AWS CDK resources within Serverless Framework service definition

There's no good bridging both frameworks if they can't interact with each other. Serverless Framework Lambda handlers usually need resource specific identifiers in order to interact with those resources.

#### Exporting CDK-generated DynamoDB table name to Serverless function

In order to insert new items within AWS CDK provisioned DynamoDB table, let's create a new function using Serverless Framework service definition. This function will implement AWS SDK `@aws-sdk/lib-dynamodb` to execute a `PutItem` command. This method requires the DynamoDB table name in order to insert the new item in the correct table. One way to pass the variable representing the DynamoDB table name is to use the `environment` property of the function definition.

```ts
const serverlessConfiguration: AWS = {
  // ...
  functions: {
    saveUser: {
      handler: 'saveUser.main',
      environment: {
        TABLE_NAME: 'Injecting DynamoDB table name here'
      },
      events: [
        { http: 'POST /user' }
      ]
    }
  },
};
```

The actual value for the DynamoDB table, provisioned using AWS CDK, is not known before deployment. One could solve this using [`tableName` props from CDK Table construct](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-dynamodb.Table.html#tablename), however it is good practice **NOT** constraining its name value as conflict may occur if two tables share the same name within the same AWS account and region (which will happen if you deploy the same Serverless service twice with different stage value). This goes for many resources where unicity constraints are imposed within the same AWS account, or even globally (like S3 bucket name)!

In order to resolve at deployment the actual table name, one can use CloudFormation intrinsic functions. In the case of a DynamoDB table name, the `Ref` intrinsic functions will do the trick following [DynamoDB CloudFormation documentation](https://docs.aws.amazon.com/fr_fr/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#aws-resource-dynamodb-table-return-values). With AWS CDK, there is no need to bother with intrinsic function syntax. Each construct exposes a set of property representing the resource return values, that will ultimately resolves to intrinsic functions under the hood. For DynamoDB `Table` construct, `table.tableName` resolves to the actual table name.

```ts
TABLE_NAME: table.tableName
```

The above statement will however translate to a completely unexpected value in the generated CloudFormation template:

```yml
TABLE_NAME: ${Token[TOKEN.183]}
```

AWS CDK actually resolves some value at a later stage of the app's lifecycle and uses [tokens](https://docs.aws.amazon.com/cdk/v2/guide/tokens.html) as placeholder in the meantime. In order to get the underlying value behind this CDK generated token, we can use `resolve` method from the CDK Stack:

```ts
TABLE_NAME: stack.resolve(table.tableName)
```

This completes proper setup of the Serverless Framework function in order to perform operations with the CDK generated DynamoDB table.

```ts
const serverlessConfiguration: AWS = {
  // ...
  functions: {
    saveUser: {
      handler: 'saveUser.main',
      environment: {
        TABLE_NAME: stack.resolve(table.tableName)
      },
      events: [
        { http: 'POST /user' }
      ]
    }
  },
};
```

#### Granting Serverless function permissions to interact with CDK-generated DynamoDB table

Similarly, Serverless Framework service definition will require additional configuration to ensure the `saveUser` function is actually allowed to insert new items in the DynamoDB table.

This is usually done using the `provider.iam.role.statements` configuration, adding a new IAM Policy statement which allows DynamoDB PutItem operation.

```ts
const serverlessConfiguration: AWS = {
  // ...
  provider: {
    iam: {
      role: {
        statements: [
          {
            Effect: 'Allow',
            Action: [ 'dynamodb:PutItem' ],
            Resource: *,
          }
        ]
      }
    }
  }
};
```

While opening up IAM Policy statement effect to all DynamoDB table is tempting and very easy to implement, it is not recommended, is considered a way too broad permission and may raise security concerns.

You can however overcome this issue following the same principal than before: CDK `Table` construct exposes a `tableArn` property than can be used to narrow down resources with whom `PutItem` is actually permitted.

```ts
Resource: stack.resolve(table.tableArn)
```

## Limitations

Using `synth` API from AWS CDK to access the underlying Cloud Assembly instance and its CloudFormation template has its limits: no asset can be uploaded on AWS as part of the normal deployment cycle of AWS CDK. AWS CDK relies on a [bootstrapping separate stack](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html), that needs to be provisioned independently, whenever one of the resource leverages CDK Assets. For example, deploying `Function` constructs as part of your CDK stack requires file and/or Docker images to be uploaded to your AWS account, and therefore requires the bootstrapping stack to perform such upload.

Using AWS CDK to complement the Serverless Framework often mean that you won't actually ever rely on `Function` construct, Lambda provisioning being Serverless Framework scope, but it's worth mentioning as a limitation in case another resource type, not handled by Serverless Framework, also relies on assets being uploaded.

## Conclusion

This code snippet and detailed explanations is of course only demonstration of what can be achieved using both Serverless Framework and AWS CDK to deploy a serverless application, using best of both worlds to improve consequently the overall developer experience.

I strongly advise to structure and split your CDK resources in a dedicated project directory, in order to keep `serverless.ts` as minimal as possible and to implement CDK recommendations in terms of construct architecture.