AWS Cloud Development Framework (CDK)
If you are starting a new project it is recommended you checkout the AWS Project Development Kit (AWS PDK).
What is CDK (Cloud Development Kit)?
A detailed description of what CDK is can be found here.
A short version is that CDK is how we define infrastructure as code. It allows us to define our infrastructure as code. This is commonly referred to as IaC (Infrastructure as Code).
In CDK we define an app
as a collection of stacks
. In CDK we define a stack
as a group of resources that are deployed together. Each stack
is comprised of constructs
that then declare the specific resources that are needed. That can be Lambda functions, DynamoDB tables, etc.
This diagram shows how stacks are grouped together in an application.
Structure
CDK project examples
For examples of how to structure a backend CDK project in TypeScript, see the following projects.
CRUK projects
bank-verification-service (microservice repo) - https://github.com/CRUKorg/bank-verification-service online-payments-services (monorepo) - https://github.com/CRUKorg/online-payments-services
External projects
CDK Patterns - https://github.com/cdk-patterns/serverless Serverless Land - https://github.com/aws-samples/serverless-patterns
Cross stack communication
A common requirement is to have resources in one stack reference resources in another stack. For example a Lambda function requires access to an s3 bucket that has been created in another stack.
Resources referencing
This article explains how to reference resources in another stack: https://docs.aws.amazon.com/cdk/v2/guide/resources.html#resources_referencing
Storing references in SSM
You can find out more about AWS Systems Manager Parameter Store (SSM) here: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html
Example
First up we create the bucket and store the name in SSM.
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { Bucket } from "aws-cdk-lib/aws-s3";
import { StringParameter } from "aws-cdk-lib/aws-ssm";
export interface BucketStackProps extends StackProps {
bucketName: string;
ssmParameterName: string;
}
export class BucketStack extends Stack {
public readonly bucket: Bucket;
constructor(scope: Construct, id: string, props: BucketStackProps) {
super(scope, id, props);
// Create the S3 bucket
const bucket = new Bucket(this, "MyBucket", {
bucketName: props.bucketName,
});
// Save the bucket ARN in SSM Parameter Store
new StringParameter(this, "BucketArnParameter", {
parameterName: props.ssmParameterName,
stringValue: bucket.bucketArn,
});
this.bucket = bucket;
}
}
Next we load the bucket via the SSM parameter in a stack that creates a Lambda function that requires permissions to write to the bucket.
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { StringParameter } from "aws-cdk-lib/aws-ssm";
import { Bucket } from "aws-cdk-lib/aws-s3";
import { Function, Runtime, Code } from "aws-cdk-lib/aws-lambda";
export interface LambdaStackProps extends StackProps {
ssmParameterName: string;
}
export class LambdaStack extends Stack {
constructor(scope: Construct, id: string, props: LambdaStackProps) {
super(scope, id, props);
// Retrieve the bucket ARN from SSM Parameter Store
const bucketArn = StringParameter.valueForStringParameter(
this,
props.ssmParameterName,
);
// Import the bucket using the ARN
const bucket = Bucket.fromBucketArn(this, "ImportedBucket", bucketArn);
// Create the Lambda function
const lambdaFunction = new Function(this, "MyFunction", {
runtime: Runtime.NODEJS_18_X,
handler: "index.handler",
code: Code.fromInline(`
exports.handler = async () => {
console.log("Lambda function invoked");
};
`),
});
// Grant the Lambda function permissions to write to the bucket
bucket.grantWrite(lambdaFunction);
}
}
Finally, in the app.ts
file we create the stacks and pass the SSM parameter name to the Lambda stack.
import { App } from "aws-cdk-lib";
import { BucketStack } from "./BucketStack";
import { LambdaStack } from "./LambdaStack";
const app = new App();
const bucketStack = new BucketStack(app, "BucketStack", {
bucketName: "my-doc-example-bucket",
ssmParameterName: "/my-app/bucket-arn",
});
new LambdaStack(app, "LambdaStack", {
ssmParameterName: "/my-app/bucket-arn",
});
app.synth();
AWS Web Application Firewall (WAF)
AWS WAF is AWS's Web Application Firewall. This should be present on all APIs (i.e. API Gateway, Cloudfront, AWS AppSync) to protect against common attacks.
An example of it in use in a CDK can be found in the below projects bank-verification-service activity management
The following rule sets should be applied in order of priority:
- AWSManagedRulesAmazonIpReputationList
- AWSManagedRulesCommonRuleSet (size restrictions may need to be overwritten for AppSync APIs)
- AWSManagedRulesKnownBadInputsRuleSet
- RateLimiting - The limit should be set to an acceptable level for your application at the IP level.
AWSManagedRulesBotControlRuleSet Bot management can also be enabled.
If enabling a new rule, it is possible to initially set the Action
to Count
instead of Block
, in order to collect data on how the rule behaves and affects traffic.
Enable Cloudwatch logs for WAF in order to inspect API requests in more detail and perform queries on ranges of requests.
CDK-nag
For CDK stacks, to ensure your stack follows best practices and to reduce any security insights concerns in AWS Security Hub, install the npm module for cdk-nag.
In some cases it is necessary to suppress the warnings when best practices cannot be followed for valid reasons. See the documentation on how to create suppressions.
Example cdk stack using cdk-nag with suppressions: https://github.com/CRUKorg/bank-verification-service/blob/main/cdk/regional/stack.ts#L409
Deployments
Deployments are done using GitHub Actions. Guidelines on how to use GitHub Actions can be found here.