This post is the second part of a ‘Let’s CDK’ series. You can catch up on part 1 if you haven’t already here. This post is part of a series that will introduce you to the AWS Cloud Development Kit (CDK) and how it can be used to create an application. This series will introduce you to the core concepts of AWS CDK, how you can deploy to your AWS account, and how services like AWS Lambda Functions, DynamoDB, Step Functions and more can all be used when working with CDK.
Where were we?
In our last episode, we covered the basics of AWS CDK and key concepts like constructs, stacks and the App construct. We also ran through how to get a CDK app setup using typescript as our choice of language. Using the synth and deploy commands, we were also able to get our new CDK app deployed to our AWS account. Feel free to have a read-through of the previous episode if you need any refresher before you continue.
What next?
So, we have the basics of our app setup and deployed to our AWS account. But at the moment, we have some empty stacks with nothing really going on. While it’s great to see stacks we have created and successfully deployed to our AWS account, I’m sure we can do something more exciting.
Let’s have a think about what we’re actually trying to achieve with this app that we’re building. A good way to start the transfer from the brain to a working app is to build what’s called a ‘Logical flow’ diagram. Sheen Brisals has a great series of blog posts on ‘The Significance of Solution Design in Serverless Developments’, which I wholeheartedly recommend reading. Solution designs are definitely something that should be part of your development flow when working outside of a tutorial context, as they are invaluable to not just you but your team as a whole.
We’re not going to create a full solution design as part of this series, as we could likely have an entire series dedicated to it. However, we will employ a couple of key elements from the solution design process. Creating a logical flow diagram will give us something to refer back to during the build process to keep on track with the solution we are implementing.
Logical Flow
Above, you can see a basic diagram outlining the logical flow we want our application to have. A logical flow helps us think about what we want our app to do and what resources we could use to help us achieve it.
Let’s first start with the ‘Receive request’ step. We won’t start with the ‘User sends request step’ as we won’t be building our user. Chat GPT can’t build humans…yet. Various services could be used when thinking about the possible AWS resources we could use to handle incoming user requests. However, we don’t just want a service that the incoming request can invoke; we also want something that will enable us to follow our logical flow.
This is where we turn to the tried and true Amazon API Gateway and its many integrations. Using API Gateway, we can use services like AWS Lambda or AWS Step Functions to run logic against the payload that we receive. We can also use these integrations to return responses back to the user.
API Gateway
If you’re unfamiliar with API Gateway or want a refresher on what it is and how it works, I recommend reading about it here.
RESTful vs. WEBSOCKET API
As we don’t need to worry about real-time two-way communication like in a chat app or similar, we will build a REST API.
Access Control
As we’re not building a public API, we want to ensure we have at least some access control setup. We could go down a few different avenues when considering access control with our API Gateway REST API. For our app, we are going to use a combination of an API Key, usage plan and IAM roles and policies to ensure that we are the only ones that can access our API.
API Gateway and CDK
Before we dive in and get our hands dirty writing some cool code to generate our API via infrastructure as code, I recommend having at least a quick glance over the CDK documentation for the API Gateway construct library. I will, of course, guide you through what we are building, but it’s always good to read the documentation!
Enough talk, let’s build!
At the moment, we have the basic shell of a CDK app. Now we can get down and dirty with some code and add an API Gateway REST API. We’re going to approach this by first building our own API Gateway L3 construct. Feel free to refer back to this post for a refresher on constructs if you want.
Let’s first add a new constructs
directory at the root of the project. We’ll add all our custom constructs here. We will also organise things by having a directory for each construct in reference to the AWS service they are for. As we’re going to be creating an API Gateway construct, go ahead and add an APIGateway
directory inside the constructs
directory.
So now your project structure should look something like this:
Creating our Construct
Now, an empty directory won’t create an API for us. So, let’s go ahead and add an APIGateway.ts file to our APIGateway directory. We will need to import a few things to create our REST API. At the top of your APIGateway.ts file, add the following:
import { Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { ApiKey, RestApi, UsagePlan } from 'aws-cdk-lib/aws-apigateway';
Let’s take a quick look at what we have just imported:
- Stack: This is a root construct that represents a single CloudFormation stack. Remember, we need to scope our construct to a Stack, so this will be needed.
- Construct: We are going to be creating our own APIGateway Construct so we will be extending the core Construct class.
- ApiKey: This is needed so we can generate an API Key
- RestApi: The class that represents a REST API in API Gateway. This is what we’ll use to create our API.
- UsagePlan: This will work with the API key to specify who can use the API and how much they can call it.
Now we have our imports sorted, let’s create the shell of our custom APIGateway
construct:
export class APIGateway extends Construct {
constructor(scope: Stack, id: string, props: APIGatewayProps) {
super(scope, id);
// This is where we're gonna have some fun!
}
}
This is the bare bones of our APIGateway
Construct. You can see we’re extending the Construct
class and initialising it with the constructor. Three arguments are also passed into the constructor:
-
scope: This is the scope the
Construct
is assigned to. In our case, it will be theStack
it sits within. -
id: A string that represents a logical ID for the
Construct
-
props: An object with any useful properties that can help us define our new
Construct
The keen-eyed among you will have also noticed the props being typed as APIGatewayProps. We can go ahead and create the type as:
type ApiGatewayProps = {
serviceName: string;
stage: string;
rateLimit?: number;
burstLimit?: number;
}
Passing the serviceName
and stage
down as part of the props object allows the Construct
to be more easily used across different stacks we may have. We can also use the stage
variable to differentiate across deployment environments. Setting the rateLimit
and burstLimit
as optional allows us to specify defaults inside the Construct
but also override if needed by a particular stack.
Creating the API
Let’s start adding the main logic of our custom Construct
by destructuring variables from our props object and initialising the RestApi class to create our API:
export class APIGateway extends Construct {
public readonly restAPI: RestApi;
constructor(scope: Stack, id: string, props: ApiGatewayProps) {
super(scope, id);
const {
serviceName,
stage,
rateLimit = 100000,
burstLimit = 1000
} = props;
this.restAPI = new RestApi(this, `${serviceName}-restAPI-${stage}`, {
deployOptions: {
stageName: stage
},
defaultMethodOptions: {
apiKeyRequired: true
}
});
}
}
The code above is actually all you really need to create a REST API using AWS CDK. However, we don’t have any methods attached to the API, so we would run into an issue when deploying. Fear not! We will rectify this soon. Above, we destructured our variables from the props object and set the defaults for the rateLimit
and burstLimit
properties. Then, we initialised the RestApi class.
We have passed three arguments to the RestApi class:
- The scope for the RestAPI
- A logical ID
- A props object
We use the serviceName
and stage
variables to create the logical ID for the RestApi. Within the props object, we specify two things:
-
deployOptions: Settings for the API Gateway stage that link to the latest deployment when activated. We set the
stageName
using thestage
variable so we can easily see what environment -
defaultMethodOptions: Method options to use for all methods created within this API. We specify that
apiKeyRequired
is true. This means all methods created in this API will require an API key to be present in the request.
We also specify a public readonly
property on the construct and have this equal to the API. We do this to easily access the API outside of the construct and add methods to the API when needed.
Creating our API Key
So, we’ve got our API setup and have specified that all methods in that API must have an API key present in the request. We should probably create an API key, so we’re able to actually use the API. Luckily, this is nice and easy within CDK world:
const apiKey = new ApiKey(this, `${serviceName}-apiKey-${stage}`, {
apiKeyName: `${serviceName}-apiKey-${stage}`,
generateDistinctId: true,
stages: [this.restAPI.deploymentStage],
});
Using the ApiKey construct we imported earlier. We can pass in the scope, logical ID, and props object to create an API key. Looking at the props object we passed to the ApiKey construct, we can see:
- apiKeyName: A string that is set as the name for the API Key
- generateDistinctId: A boolean value that, when set to true, ensures that the name of the API key is different to the API key name.
-
stages: An array of stages that the API key is associated with. We set this to the
deploymentStage
of the API.
We now have our API key! Woohoo!
Creating our Usage Plan
API? Check. API Key? Check. We can call it a day now, right? Not so fast! We still need to create our usage plan. If you aren’t familiar with usage plans, I recommend having a read over the documentation. As mentioned above, our usage plan will allow us to control and monitor access to our API. We can make use of our rateLimit and burstLimit variables we saw earlier and also link API stages to it.
To create our usage plan, we can do the following:
const usagePlan = new UsagePlan(this, `${serviceName}-usagePlan-${stage}`, {
name: `${serviceName}-${stage}`,
throttle: {
rateLimit,
burstLimit
},
apiStages: [{
stage: this.restAPI.deploymentStage
}]
});
Using the UsagePlan construct we imported, we can pass the scope, logical ID and props object (are you noticing a pattern yet?). Within the props object, we specify the following:
- name: The name given to our usage plan. Using the variables passed via the props object. We can ensure these are dynamic and related to the correct scope.
- throttle: The rate limit and burst limit we want to set to ensure traffic to the API doesn’t hit a limit we don’t want. You can read more here on API request throttling
Don’t forget!
We have our API key and usage plan, but we need to ensure that we add the API key to the usage plan. If we don’t, we’ll have these two resources floating around in our account, with the API key just acting as an identifier without any benefit of the usage plan. Luckily, this is incredibly easy to do:
usagePlan.addApiKey(apiKey);
Here’s One I Made Earlier
Bringing all the pieces we create above together, we should have a custom APIGateway construct that looks like this:
import { Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { ApiKey, RestApi, UsagePlan } from 'aws-cdk-lib/aws-apigateway';
type ApiGatewayProps = {
serviceName: string;
stage: string;
rateLimit?: number;
burstLimit?: number;
}
export class APIGateway extends Construct {
public readonly restAPI: RestApi;
constructor(scope: Stack, id: string, props: ApiGatewayProps) {
super(scope, id);
const {
serviceName,
stage,
rateLimit = 100000,
burstLimit = 1000
} = props;
this.restAPI = new RestApi(this, `${serviceName}-restAPI-${stage}`, {
deployOptions: {
stageName: stage
},
defaultMethodOptions: {
apiKeyRequired: true
}
});
const apiKey = new ApiKey(this, `${serviceName}-apiKey-${stage}`, {
apiKeyName: `${serviceName}-apiKey-${stage}`,
generateDistinctId: true,
stages: [this.restAPI.deploymentStage],
});
const usagePlan = new UsagePlan(this, `${serviceName}-usagePlan-${stage}`, {
name: `${serviceName}-${stage}`,
throttle: {
rateLimit,
burstLimit
},
apiStages: [{
stage: this.restAPI.deploymentStage
}]
});
usagePlan.addApiKey(apiKey);
}
}
Let’s get Stacking
Now that we have our own custom APIGateway
construct. We can use it in our stacks. If you had a browse around the app that was created by running the cdk init
command we ran in the previous episode, you would have seen the EnergyDrinkSelectorStack
within the energy-drink-selector.ts
file in the lib directory. This stack gets imported into the App within the energy-drink-selector.ts
file in the bin
directory. Then, when a deployment is run, it all gets magically transformed into a Cloudformation and then turned into the AWS resources we defined.
The approach we are going to use in this series is to use CDK nested stacks. “What are nested stacks?!” I hear you cry. Well, the clue is partially in the name. A nested stack is basically a child stack of a parent stack. So we can have stacks within stacks. Don’t worry. It sounds more confusing than it is in practice. For us, the EnergyDrinkSelectorStack
will be our parent stack, and within this stack, we will define our child stacks. You can read more about nested stacks here.
Creating an APIGatewayStack
Earlier, we created a new constructs
directory for our custom constructs. Let’s do the same for our stacks. We can go ahead and create a stacks
directory in the root of the project. Within this new directory, we can create an APIGatewayStack.ts
file to house the code for our API Gateway stack.
Let’s start our stack creation by handling our imports first:
import { NestedStack, NestedStackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { APIGateway } from "../constructs/APIGateway/APIGateway";
import { RestApi } from "aws-cdk-lib/aws-apigateway";
Let’s take a quick look at these imports:
- NestedStack: The CDK construct we use to create a nested stack
- NestedStackProps: A type interface for the initialisation props for the NestedStack
- Construct: The scope of the NestedStack will be a Construct, as Stacks are Constructs, and we will be scoping this stack to our main EnergyDrinkSelectorStack
- APIGateway: This is the custom APIGateway construct we created
- RestApi: We will be using this to ensure type safety with the public readonly property we will add to our stack
We’ve got our imports sorted. Let’s create the actual stack:
export class APIGatewayStack extends NestedStack {
public readonly restAPI: RestApi;
constructor(scope: Construct, id: string, props?: NestedStackProps) {
super(scope, id, props);
const apiGateway = new APIGateway(this, 'EnergyDrinkSelectorAPI', {
serviceName: 'energy-drink-selector',
stage: 'dev',
});
this.restAPI = apiGateway.restAPI;
}
};
With the code above, we can see that we have used our custom APIGateway construct and passed in stack-specific values for the props. We can also see that we’re setting the public property equal to the restAPI
we create in the construct, allowing us to add methods where needed.
Last Step…I Promise
We have our custom APIGateway construct and our custom APIGatewayStack, so what’s left? Well, we need to make sure that we are making use of these within our main EnergyDrinkSelectorStack
as that’s the one that is passed to the App that’s then used to synthesise the Cloudformation template used to create the AWS resources.
In our energy-drink-selector-stack.ts
in the lib directory, all we need to do is import our APIGatewayStack and add a method to the API for CDK to allow us to deploy. We can do this as follows:
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { APIGatewayStack } from '../stacks/APIGatewayStack';
export class EnergyDrinkSelectorStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const apiGatewayStack = new APIGatewayStack(this, 'EnergyDrinkSelectorAPIGatewayStack');
apiGatewayStack.restAPI.root.addMethod('GET');
}
}
In the code above, you can see that we import our custom APIGatewayStack
, initialise it and store it in an apiGatewayStack
variable. We then add a GET method to the API to satisfy the deployment requirement of having at least one method on the API.
Go Forth and Deploy!
With all the pieces of the puzzle now in place to create our very own REST API in API Gateway via CDK, we can run cdk deploy
, and as if by magic, we should have a brand new shiny API waiting for us in the AWS console.
Aaaaand relax
We covered quite a lot in this episode. Kudos for sticking with it and making it through! Let’s have a quick recap of what we covered:
- Logical Flow diagram: We took some inspiration from the realm of the solution design and created a logical flow diagram to help us focus on what we wanted to achieve
- API Gateway: We looked at some points around API Gateway in relation to the app we’ll be building. We decided on a RESTful API over a WEBSOCKET API as we don’t need the two-way communication functionality that WEBSOCKET APIs offer.
- Access control: We covered things like API Keys and Usage Plans to ensure that we can both lock down and limit API usage.
- Custom Construct: We created our own custom construct! An L3 construct that allows us to create a REST API in API Gateway
- Nested stacks: We created a stack using the NestedStack construct, allowing us to have child stacks within a parent stack
With all this newly acquired knowledge swirling around in your brain, I’d recommend having another look over the code and familiarising yourself with the syntax and patterns used, as we’ll be seeing similar patterns in the upcoming episodes.
I think you’ve earned a rest after all this fun! Go grab a beverage and a snack, and then start preparing for our next episode, which will focus on a little service called Step Functions.
TL;DR
If you want to peek at the code and don’t want to read my ramblings, navigate to this Github repo and spy on everything we’ll be creating in the series.
Top comments (2)
awesome post
Thank you! I really appreciate it!