Building a Serverless API with AWS CDK, Lambda, and RDS
Prerequisites
- Basic knowledge of serverless functions like AWS Lambda and API Gateway
- Basic knowledge of CloudFormation stack
- Basic knowledge of AWS CDK
- Basic understanding of REST API
- Basic understanding of RDS database and VPC
- AWS CLI installed and configured with the correct credentials
- Node and npm installed
The Architecture
- API Gateway will expose REST Endpoints
- API Gateway communicates with Lambda function
- Lambda functions will connect to RDS database instance inside a VPC
- Internet connection and private VPC connection
Instantiate The CDK
Let's make a directory for our project:
mkdir cdk-demo cd cdk-demo cdk init --language typescript
This command will generate some files and directories for us, the most important ones are bin
and lib
. We will be executing most of codes inside these two directories.
The bin
directory contain a file <project-name>.ts
while the lib
directory contains a file <project-name>-stack.ts
The generated file in the bin
directory will be used to synthesize your CDK application into a CloudFormation template, while the file in lib
directory defines your CDK stack. All the AWS resources you want to provision will be defined here.
Our Plan
- Create a VPC or find default VPC
- Instantiate secrets manager
- Find Security group
- Find subnet group
- Find secret
- Create Database Instance
- Create APIGateway class
- Create Lambda class
- Create service class
Creating a Database Instance
Create a new file, I will call mine rds.ts
in our lib
directory. Create a function that accepts vpc
and scope
as argument, I will call mine rds
.
export const rds = (vpc: IVpc, scope: Construct) => {
}
Define Security Group
Define our security group by lookup name. The default security group is okay for this demo; however, you can also create a new security group if you want. Attach the VPC to this security group.
export const rds = (vpc: IVpc, scope: Construct) => const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);
Define Subnet Group
Define our subnet group, use findLookupByName
and pass the name of the default subnet group in our AWS account with other parameters.
export const rds = (vpc: IVpc, scope: Construct) => {
const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);
const dbSubNet = SubnetGroup.fromSubnetGroupName(scope, "my-sub-net","default-vpc-078eccc7aal3f");
}
Database Credentials
Get the database credentials from secrets manager or create a new one. I already have my credentials stored and auto rotated by secrets manager.
export const rds = (vpc: IVpc, scope: Construct) => {
const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);
const dbSubNet = SubnetGroup.fromSubnetGroupName(scope, "my-sub-net","default-vpc-078eccc7aal3f");
const newSecret = new DatabaseSecret(scope,"my-secret", {
username: "postgres",
secretName: "dev/cdkdemo"
});
}
Create Credentials
Cast the secret value to our database credential.
export const rds = (vpc: IVpc, scope: Construct) => {
const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);
const dbSubNet = SubnetGroup.fromSubnetGroupName(scope, "my-sub-net","default-vpc-078eccc7aal3f");
const newSecret = new DatabaseSecret(scope,"my-secret", {
username: "postgres",
secretName: "dev/cdkdemo"
});
const cred = Credentials.fromSecret(newSecret, "postgres");
}
Database Instance
Let's create our database instance and return all the necessary variables.
export const rds = (vpc: IVpc, scope: Construct) => {
const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);
const dbSubNet = SubnetGroup.fromSubnetGroupName(scope, "my-sub-net","default-vpc-078eccc7aal3f");
const newSecret = new DatabaseSecret(scope,"my-secret", {
username: "postgres",
secretName: "dev/cdkdemo"
});
const cred = Credentials.fromSecret(newSecret, "postgres");
const newInstance = new DatabaseInstance(scope, "NewRdsInstance", {
instanceIdentifier: "mydemo-withpg",
databaseName: "postgres",
engine: DatabaseInstanceEngine.POSTGRES,
instanceType: InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.MICRO),
storageEncrypted: true,
vpc,
allocatedStorage: 20,
maxAllocatedStorage: 100,
deletionProtection: false,
removalPolicy: RemovalPolicy.DESTROY,
storageType: StorageType.GP2,
securityGroups: [dbSec],
subnetGroup: dbSubNet,
credentials: cred,
multiAz: false,
});
const dbProxy = undefined;
return { newInstance, dbSec, dbSubNet, dbProxy, vpc, newSecret };
}
Create API Gateway Class
This is a generic class that can be reused to create all our API Gateway instances. The constructor takes one parameter,scope
of that Construct
. Construct
is a logical node in a CDK stack.
We will provide a name for our REST API, indicate cloudwatch role and other properties of the cloudwatch log.
We will also define a method called addIntegration()
that takes 3 parameters: method
, path
, lambda
.
export class ApiGateway extends RestApi {
constructor(scope: Construct) {
super(scope, "ApiGateway", {
restApiName: "user",
cloudWatchRole: true,
deployOptions: {
accessLogDestination: new LogGroupLogDestination(
new LogGroup(scope, "ApiLogGroup", {
logGroupName: "essl-customers",
retention: RetentionDays.ONE_DAY,
removalPolicy: RemovalPolicy.DESTROY,
})
),
},
});
}
addIntegration(method: string, path: string, lambda: IFunction) {
const resources = this.root.resourceForPath(path);
resources.addMethod(method, new LambdaIntegration(lambda));
}
}
Create Generic Lambda Class
This generic class will be used to create and deploy NodeJS lambda functions. It takes 3 parameters:scope
(A construct object), filename
(our NodeJS Lambda function code) and functionName
.
The class constructor also specifies the architecture of our lambda functions, the runtime, entry point for our codes and log retention duration.
export class Lambda extends NodejsFunction {
constructor(scope: Construct, fileName: string, fnName: string) {
super(scope, fnName, {
architecture: Architecture.ARM_64,
runtime: Runtime.NODEJS_18_X,
entry: path.join(__dirname, ../resources/{fileName}),
logRetention: RetentionDays.ONE_DAY,
});
}
}
Next Steps
In the next part of this tutorial, we'll cover:
- Creating the Service Files for CRUD operations
- Implementing Lambda Functions
- Connecting Lambda Functions to RDS
- Testing our API
- Deploying our CDK application
Note: Make sure to have the necessary imports at the top of each file. We will cover those in detail in the next part of this tutorial.
Femi Adigun
AWS Certified Solutions Architect