eliasbrange.dev
How To Separate Your Serverless Infrastructure

How To Separate Your Serverless Infrastructure

2022-07-11
| #AWS #Serverless

Some time ago, you had a brilliant business idea. You started building it on AWS and read that Serverless was the way to go. Good on you! After quickly prototyping an MVP and pitching it to investors, you found yourself surrounded by a team of developers. You worked hard to get the beta version ready. By iterating quickly and listening to customers, the beta got tremendously popular, and you had a successful launch. Money was pouring in, and you felt unstoppable, so you set out to add new features and services to your product to continue to please customers and investors alike.

To move fast, you kept all code and infrastructure in the same repository. Suddenly, something happened. You were not releasing features as quickly as before. As time passed, making changes got more challenging and time-consuming. After continuously adding services and resources, your infrastructure was now a mess. You found yourself with a tangled web of Lambda Functions, API Gateways, Dynamo Tables, SQS Queues, and other resources.

To avoid the tangled web of dependent services, you can separate your infrastructure into more manageable chunks. These chunks can then be deployed individually and have different lifecycles. Even within a single service, say a Lambda-backed API with a DynamoDB table, you might want to separate the stateless API Gateway and Lambda Function(s) from the stateful DynamoDB table.

All of your services are most likely not living in total isolation and will have dependencies on other services and resources. In this article, you will learn different ways to separate and share resources between services in your Serverless infrastructure, with examples for AWS CDK, Serverless Framework, AWS SAM, and Terraform. You can even mix and match frameworks. Perhaps you want to use Terraform for your DynamoDB Table and Serverless framework for your API Gateway and Lambda functions.

1. CloudFormation Outputs

CloudFormation allows you to declare output values that you can import into other stacks. There are some restrictions to keep in mind when using outputs:

1.1. Exporting outputs

AWS CDK

CDK tip

If you have multiple stacks in the same CDK app, you can directly pass resources between stacks.

Use the CfnOutput construct to define an output.

1
import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
2
import { Construct } from 'constructs';
3
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
4
5
export class ExportStack extends Stack {
6
constructor(scope: Construct, id: string, props?: StackProps) {
7
super(scope, id, props);
8
9
const table = new dynamodb.Table(this, 'Table', { ... });
10
11
new CfnOutput(this, 'TableNameOutput', {
12
value: table.tableName,
13
exportName: 'ExportedTableName',
14
});
15
}
16
}

AWS SAM

You define your outputs in the Outputs section of your template.

1
AWSTemplateFormatVersion: '2010-09-09'
2
Transform: AWS::Serverless-2016-10-31
3
Description: SAM Export
4
5
Resources:
6
Table:
7
Type: AWS::DynamoDB::Table
8
Properties: ...
9
10
Outputs:
11
TableName:
12
Description: 'DynamoDB Table name'
13
Value: !Ref Table
14
Export:
15
Name: ExportedTableName

Serverless Framework

Serverless Framework tip

If you have multiple services in the same Serverless project, you can use Serverless compose to share resources between services.

Non-function resources in Serverless framework is defined using CloudFormation syntax in the resources section. Thus, it looks very similar to the SAM example above.

1
service: sls-export
2
provider:
3
name: aws
4
5
resources:
6
Resources:
7
Table:
8
Type: AWS::DynamoDB::Table
9
Properties: ...
10
11
Outputs:
12
TableName:
13
Description: 'DynamoDB Table name'
14
Value: !Ref Table
15
Export:
16
Name: ExportedTableName

Terraform

Terraform differs from the other frameworks and does not use CloudFormation as an engine to manage your infrastructure. It is thus not possible to create CloudFormation outputs when using Terraform.

1.2. Importing outputs

AWS CDK

You can use Fn.importValue to import the value of an output exported by another stack.

1
import { Stack, StackProps, Fn } from 'aws-cdk-lib';
2
import { Construct } from 'constructs';
3
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
4
5
export class ImportStack extends Stack {
6
constructor(scope: Construct, id: string, props?: StackProps) {
7
super(scope, id, props);
8
9
const tableName = Fn.importValue('ExportedTableName');
10
11
new lambda.NodejsFunction(this, 'MyFunction', {
12
...
13
environment: {
14
TABLE_NAME: tableName,
15
},
16
});
17
}
18
}

AWS SAM

You can use the intrinsic function ImportValue to import the value of an output exported by another stack.

1
AWSTemplateFormatVersion: '2010-09-09'
2
Transform: AWS::Serverless-2016-10-31
3
Description: SAM Import
4
5
Resources:
6
HelloWorldFunction:
7
Type: AWS::Serverless::Function
8
Properties:
9
...
10
Environment:
11
Variables:
12
TABLE_NAME: !ImportValue 'ExportedTableName'

Serverless Framework

You can import exported outputs with the ImportValue intrinsic function. For non-exported outputs, you can use the syntax ${cf:stackName.outputName}.

1
service: sls-import
2
provider:
3
name: aws
4
5
functions:
6
hello:
7
handler: src/functions/app.lambdaHandler
8
environment:
9
TABLE_NAME: !ImportValue ExportedTableName

Terraform

Fetching an exported CloudFormation outputs is done with the aws_cloudformation_export data source.

1
data "aws_cloudformation_export" "table_name" {
2
name = "ExportedTableName"
3
}

2. SSM Parameters

You can also use the Systems Manager Parameter Store to pass values between stacks by creating a parameter and dynamically reference it in a CloudFormation stack or Terraform configuration.

Compared to CloudFormation outputs, SSM does not give you the same guardrails. Nothing stops you from deleting a parameter, even if another stack dynamically references it. By doing so, the next update of the other stack will result in errors. You will also most likely break the service if you also remove the resources referenced by the SSM parameter.

So why would you use SSM parameters over CloudFormation outputs? If you want to mix Infrastructure-as-Code tools and, for example, export values from Terraform and use them in a CloudFormation-based tool.

You can also use SSM parameters during runtime instead of deploy time. You can use the AWS SDK and dynamically fetch parameters in your Lambda functions. It gives you the possibility of updating parameters without having to redeploy consuming functions. However, it makes it harder to follow the principle of least privilege. You will have to use less granular permissions to account for different parameter values (if you, for example, have created a new DynamoDB from a snapshot and intend to switch the parameter to point to the restored table).

Be aware that reading during runtime will result in some overhead and longer execution times in cold starts. It will also increase the execution time of warm lambdas if you do the fetch inside the handler code.

2.1. Exporting parameters

AWS CDK

Create an SSM parameter with the StringParameter construct.

1
import { Stack, StackProps } from 'aws-cdk-lib';
2
import { Construct } from 'constructs';
3
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
4
import * as ssm from 'aws-cdk-lib/aws-ssm';
5
6
export class ExportStack extends Stack {
7
constructor(scope: Construct, id: string, props?: StackProps) {
8
super(scope, id, props);
9
10
const table = new dynamodb.Table(this, 'Table', { ... });
11
12
new ssm.StringParameter(this, 'SSMParam', {
13
parameterName: '/some/path/tableName',
14
type: ssm.ParameterType.STRING,
15
stringValue: table.tableName,
16
});
17
}
18
}

AWS SAM

Create an SSM parameter with the AWS::SSM::Parameter resource.

1
AWSTemplateFormatVersion: '2010-09-09'
2
Transform: AWS::Serverless-2016-10-31
3
Description: SAM Export
4
5
Resources:
6
Table:
7
Type: AWS::DynamoDB::Table
8
Properties: ...
9
10
SSMParam:
11
Type: AWS::SSM::Parameter
12
Properties:
13
Type: String
14
Name: '/some/path/tableName'
15
Value: !Ref Table

Serverless Framework

Creating a parameter in Serverless framework is done the same way as in AWS SAM.

1
service: sls-export
2
provider:
3
name: aws
4
5
resources:
6
Resources:
7
Table:
8
Type: AWS::DynamoDB::Table
9
Properties: ...
10
11
SSMParam:
12
Type: AWS::SSM::Parameter
13
Properties:
14
Type: String
15
Name: '/some/path/tableName'
16
Value: !Ref Table

Terraform

Use the aws_ssm_parameter in the AWS provider to create a parameter.

1
resource "aws_dynamodb_table" "dynamo_table" {
2
name = "dynamodb-table"
3
...
4
}
5
6
resource "aws_ssm_parameter" "ssm_dynamo_table_name" {
7
name = "/some/path/tableName"
8
type = "String"
9
value = aws_dynamodb_table.dynamo_table.name
10
}

2.2. Importing parameters

AWS CDK

Use StringParameter.valueForStringParameter to fetch the value of a parameter.

1
import { Stack, StackProps } from 'aws-cdk-lib';
2
import { Construct } from 'constructs';
3
import * as ssm from 'aws-cdk-lib/aws-ssm';
4
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
5
6
export class ImportStack extends Stack {
7
constructor(scope: Construct, id: string, props?: StackProps) {
8
super(scope, id, props);
9
10
const tableName = ssm.StringParameter.valueForStringParameter(
11
this,
12
'/some/path/tableName',
13
);
14
15
new lambda.NodejsFunction(this, 'MyFunction', {
16
...
17
environment: {
18
TABLE_NAME: tableName,
19
},
20
});
21
}
22
}

AWS SAM

You can use SSM parameters in your template with the '{{resolve:ssm:parameter-name:version}}' pattern, where version is optional. If you do not specify a version, CloudFormation will use the latest version of the parameter whenever you create or update the stack.

1
AWSTemplateFormatVersion: '2010-09-09'
2
Transform: AWS::Serverless-2016-10-31
3
Description: SAM Import
4
5
Resources:
6
HelloWorldFunction:
7
Type: AWS::Serverless::Function
8
Properties:
9
...
10
Environment:
11
Variables:
12
TABLE_NAME: '{{resolve:ssm:/some/path/tableName}}'

Serverless Framework

Reading parameters in Serverless framework is done with the ${ssm:/path/to/param} syntax:

1
service: sls-import
2
provider:
3
name: aws
4
5
functions:
6
hello:
7
handler: src/functions/app.lambdaHandler
8
environment:
9
TABLE_NAME: ${ssm:/some/path/tableName}

Terraform

You can use the data source aws_ssm_parameter to fetch a parameter value.

1
data "aws_ssm_parameter" "table_name" {
2
name = "/some/path/tableName"
3
}

2.3. Reading at Runtime in a Lambda function

The following code uses the AWS SDK for TypeScript to fetch parameter values. Be aware that the function IAM role will need permission to fetch the parameter.

1
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
2
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
3
4
const ssmClient = new SSMClient({});
5
const input = { Name: '/some/path/tableName' };
6
const command = new GetParameterCommand(input);
7
const parameterPromise = ssmClient.send(command);
8
9
export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
10
const parameter = await parameterPromise;
11
...

3. Summary

You have now learned how to separate your infrastructure stacks into more manageable chunks with the use of CloudFormation Outputs and/or SSM Parameters. You’ve seen examples for AWS CDK, AWS SAM, Serverless Framework and Terraform.

Now go build something awesome!


About the author

I'm Elias Brange, a Cloud Consultant and AWS Community Builder in the Serverless category. I'm on a mission to drive Serverless adoption and help others on their Serverless AWS journey.

Did you find this article helpful? Share it with your friends and colleagues using the buttons below. It could help them too!

Are you looking for more content like this? Follow me on LinkedIn & Twitter !