In the realm of Infrastructure as Code (IaC) within AWS, CloudFormation stands out as the foundational service. Many other IaC solutions, whether it’s the Serverless Framework, AWS SAM, or even Terraform, ultimately get translated into CloudFormation. Additionally, the AWS Cloud Development Kit (CDK) has emerged as a distinct player in this space.
What sets AWS CDK apart is its embrace of familiar programming languages. Instead of relying on YAML or proprietary syntax, with AWS CDK, developers can define infrastructure using languages like Python, Javascript, Typescript, Java, C#, and Go. This familiarity accelerates the adoption curve, enabling developers to direct their energy toward crafting business solutions rather than wrestling with infrastructure nuances.
Today, crafting RESTful APIs has become a staple task for backend developers. What once involved intricate configurations and deployments can now be streamlined with serverless architectures.
Here’s the architecture of building serverless RESTful APIs leveraging the AWS CDK:
This architecture embodies a classic three-tiered design. At its forefront, AWS API Gateway serves as the conduit for front-end interactions. AWS Lambda provides the computational muscle in a serverless fashion, ensuring scalable and efficient processing. Meanwhile, Amazon DynamoDB anchors the setup, offering reliable storage and persistence for data.
Prerequisites
-
Set up a development environment or development account in AWS.
-
Install Node, AWS CDK, and the AWS CLI.
-
Configure AWS credentials locally.
Initializing a new AWS CDK App
Next, to initialize a fresh project with AWS CDK, input the following command:
npx cdk init app --language=typescript
Using npx
ensures the utilization of the latest version of AWS CDK.
TypeScript is selected as the programming language for this architecture with AWS CDK. If TypeScript is not installed on the system, it can be added with this command:
npm install -g typescript
Bootstrapping
Bootstrapping is an essential initial step when using AWS CDK. It grants AWS CDK the necessary permissions to operate within your account. This foundational step must be completed before deploying any AWS CDK apps. During the bootstrapping process, requisite resources are provisioned, including storage for crucial files and IAM roles to facilitate deployments. This action results in the creation of a CloudFormation Stack, typically named CDKToolKit
.
To initiate the bootstrapping process for your application, run the command:
cdk bootstrap
In AWS CDK, constructs serve as the foundational elements of infrastructure, similar to Lego bricks in a construction set. Each construct corresponds to a logical resource in CloudFormation, which then translates into tangible or physical resources within the cloud environment. These constructs are housed within Stacks in the AWS CDK, mirroring the concept of CloudFormation Stacks. Essentially, an AWS CDK app outlines one or several such stacks.
The process of establishing an app and stack can be observed in bin/tutorials-dojo-cdk-app.ts
. Upon deploying this app to the cloud, it will instantiate a CloudFormation stack named TutorialsDojoCdkAppStack
.
As previously mentioned, constructs reside within AWS CDK Stacks. AWS offers a suite of pre-configured constructs, empowering developers and DevOps professionals to seamlessly craft Infrastructure as Code. An exemplar Stack can be found in lib/tutorials-dojo-cdk-app-stack.ts
, which AWS generates during the initialization of an AWS CDK application. To further assist customers, AWS recommends an SQS construct as a guide for declaring other constructs. The serverless RESTful API’s implementation would be integrated within this file.
The DynamoDB Table AWS CDK Construct
First, extract the DynamoDB construct from the aws-cdk-lib
NodeJS package and incorporate it into the lib/tutorials-dojo-cdk-app-stack.ts
Stack. This aws-cdk-lib
package was already installed when the AWS CDK application was initialized.
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
Within the Stack class, instantiate the DynamoDB table. The following snippet showcases the AWS CDK method for creating a DynamoDB table, specifying core properties like the partitionKey
and tableName
. It’s essential to understand that these properties represent just a fraction of the offerings from the DynamoDB Table Construct. When delving into new constructs, consulting the official AWS documentation is a prudent step to ensure comprehensive utilization.
The Lambda Function AWS CDK Construct
Begin by importing the Lambda construct from the aws-cdk-lib
NodeJS package and adding it to the Stack:
import * as lambda from 'aws-cdk-lib/aws-lambda';
Subsequently, within the Stack class, establish the Lambda function. The provided sample highlights several properties essential for crafting this Lambda function, with the runtime
, handler
, and code
properties being mandatory.
// Create a Lambda function const lambdaFunction = new lambda.Function(this, 'TutorialsDojoLambdaFunction', { runtime: lambda.Runtime.NODEJS_18_X, handler: 'index.handler', code: lambda.Code.fromAsset('lambda'), environment: { DYNAMO_TABLE_NAME: dynamoTable.tableName, } });
The runtime
is specified to determine what programming language and the version the Lambda function would be running (yes, it is possible for a Lambda function to run in python even if the AWS CDK construct used in declaring this function is written in Typescript — same goes for other programming languages).
For the handler
, the convention <file-name>.<function-name>
is used. Here, file-name
denotes the name of the file housing the Lambda function code. Conversely, function-name
indicates the specific method or function Lambda should trigger upon invocation. In this instance, the Lambda function is contained within the index.ts
file, and the primary method to be invoked is termed handler
. This explains the handler’s designation as index.handler
.
The code
property pinpoints the Lambda function’s location within the codebase. This capability stands out in IaC tools, including AWS CDK, since CloudFormation lacks this direct feature. Conventionally, CloudFormation necessitates the Lambda code to be uploaded to Amazon S3 first, followed by referencing its bucket and key in the CloudFormation script. Conversely, in this context, the Lambda function code is situated in the lambda
directory at the project’s root.
Finally, the environment
property in the Lambda function construct allows for the specification of the Lambda function’s environment variables, although it’s not mandatory. In this context, the DynamoDB table name is relayed as an environment variable. This facilitates its use during the Lambda function’s runtime, especially when engaging with the AWS SDK to carry out operations on the designated DynamoDB table.
The official AWS documentation for the Lambda function construct offers a comprehensive list of properties not showcased in this example. It’s essential for customers, especially those looking to optimize their Lambda functions via AWS CDK as their preferred IaC tool, to be aware of these additional configuration options. These details provide avenues for further fine-tuning and customization of the Lambda functions.
To align with the Lambda function construct defined, modifications to the current AWS CDK project’s file structure are imperative. Here’s what needs to be done:
-
Establish a new directory at the root level of the project, titled
lambda
. -
Within the
lambda
directory, generate a file namedindex.ts
.
This is what the updated file structure looks like:
Lastly, paste the following code inside the newly created index.ts
file:
const AWS = require('aws-sdk'); const dynamodb = new AWS.DynamoDB.DocumentClient(); const tableName = process.env.DYNAMO_TABLE_NAME; // This matches the tableName you provided in the CDK code exports.handler = async (event: any) { const httpMethod = event.httpMethod; switch (httpMethod) { case 'POST': const item = JSON.parse(event.body); return await writeToDynamoDB(item); case 'GET': return await readAllFromDynamoDB(); default: return { statusCode: 400, body: JSON.stringify('Invalid HTTP method') }; } }; const writeToDynamoDB = async (item: string) { const params = { TableName: tableName, Item: item }; try { await dynamodb.put(params).promise(); const body = { success: true, message: 'Successfully wrote item', } return { statusCode: 200, body: JSON.stringify(body) }; } catch (error: any) { return { statusCode: 500, body: JSON.stringify(error.message) }; } }; const readAllFromDynamoDB = async () { const params = { TableName: tableName }; try { const result = await dynamodb.scan(params).promise(); const body = { success: true, message: 'Successfully read all items', data: result.Items } return { statusCode: 200, body: JSON.stringify(body) }; } catch (error: any) { return { statusCode: 500, body: JSON.stringify(error.message) }; } };
Grant the Lambda Function Read and Write Access to the DynamoDB Table
To provide the Lambda function with read and write privileges to the DynamoDB table, consider the constructs dynamoTable
and lambdaFunction
. Here, dynamoTable
pertains to the earlier defined DynamoDB table, while lambdaFunction
references the newly set up Lambda function.
// Grant the Lambda function read/write permissions to the DynamoDB table dynamoTable.grantReadWriteData(lambdaFunction);
This scenario underscores the elegance of AWS CDK’s abstraction. Traditionally, granting a Lambda function access to a DynamoDB table involves a detailed process: creating an execution role and then defining permissions in an extensive JSON format, a task that can span multiple lines of code, especially when using CloudFormation. However, AWS CDK transforms this intricate setup. Thanks to its abstraction capabilities, what was once a lengthy configuration can be distilled into a succinct, single line of code. This not only simplifies the task but also enhances the developer’s experience.
The Lambda Layer AWS CDK Construct
Lambda layers offer a distinctive feature within AWS Lambda, facilitating the sharing of code and various assets among multiple Lambda functions. This not only champions code reusability but also enhances the efficiency of Lambda cold starts. Through offloading recurrent code to layers it diminishes the size of customers’ Lambda deployment packages.
In the context of this project, the aws-sdk
package is anticipated to be a staple across numerous Lambda functions as they scale. Persistently creating distinct package.json
files and installing identical dependencies for every Lambda function is inefficient and is flagged as an anti-pattern by AWS. Thus, for managing shared code and packages recurrent across Lambda functions, the best approach is to harness the power of Lambda layers.
Given our current Stack, this is how to instantiate a Lambda Layer:
// Create Lambda layer const lambdaLayer = new lambda.LayerVersion(this, 'TutorialsDojoLambdaLayer', { compatibleRuntimes: [lambda.Runtime.NODEJS_18_X], code: lambda.Code.fromAsset('layers'), description: 'Tutorials Dojo Lambda Layer', }); // Add the Lambda layer to the Lambda function lambdaFunction.addLayers(lambdaLayer);
It’s crucial to ensure that the compatibleRuntimes
property of the Lambda layer matches that of the Lambda function it’s intended for. Mismatches in runtime can lead to deployment failures in CloudFormation. The code
property, akin to its role in the Lambda function construct, designates the layer’s location in the codebase. Additionally, while the description
property is optional in the Lambda layer construct, it offers a way to annotate the layer, making it identifiable in the AWS Console.
Once the Lambda layer is established, the subsequent step is to associate it with the desired Lambda function. Without this linkage, the function remains oblivious to the layer.
For a comprehensive understanding and potential customization, it’s always recommended to consult the official AWS documentation related to the Lambda layer construct.
To seamlessly integrate with the Lambda layer construct, adjustments to the codebase’s file structure are required. Here are the steps to ensure the Lambda layer’s proper utilization:
-
At the project’s root level, introduce a new directory named
layers
. -
Within the
layers
directory, instantiate a sub-directory callednodejs
. This aligns with the prerequisites for crafting a Lambda layer targeting a Node runtime. For a deeper dive, the official AWS Lambda layers documentation provided in this guide’s references offers further insights. -
Navigate to the
nodejs
directory and initiate a new Node project with thenpm init -y
command. Crucially, it’s vital to ensure that the local Node version mirrors the specifiedcompatibleRuntimes
in the Lambda layer construct to guarantee smooth deployments. -
Install the
aws-sdk
package inside thenodejs
directory with thenpm install aws-sdk
command.
This is what the updated file structure looks like:
The API Gateway REST API AWS CDK Construct
The API Gateway serves as the public-facing portal to the backend infrastructure. Upon its deployment, it generates a URL, enabling applications and API clients to interface with it. In the context of this project, AWS’s REST API construct is leveraged to forge a publicly accessible REST API in the AWS cloud.
One standout feature of AWS API Gateway is its robust authorization capabilities, among its myriad of functionalities. This allows for only authenticated and authorized users to be able to invoke APIs in the AWS API Gateway. A deeper exploration of these features is available in the official AWS API Gateway documentation. While crafting an API Gateway from the ground up offers granular control and flexibility, it’s a labor-intensive endeavor. While manual creation provides users with adaptability, it also introduces the possibility of overlooking certain AWS API Gateway features and integrations inherent to other AWS services.
The following is how to instantiate a API Gateway REST API AWS CDK Construct:
// Create an API Gateway const apiGateway = new api.RestApi(this, "TutorialsDojoApiGateway", { restApiName: "TutorialsDojoApiGateway", description: "This is the Tutorials Dojo API Gateway", });
The API Gateway REST API construct boasts a plethora of properties. A comprehensive list of these properties can be found in the official AWS API Gateway REST API construct documentation. These configurations empower users to meticulously tailor their API Gateway, adjusting aspects like CORS settings, headers, permitted methods, and more. In this example, only the restApiName
and description
are used.
Integration of the Lambda Function to the API Gateway
Integrating Lambda functions with API Gateway has emerged as a dominant serverless pattern in AWS. Recognizing this trend, AWS introduced a dedicated construct for this purpose. In this example, the LambdaIntegration
construct facilitates the seamless integration of a Lambda function with an API Gateway.
// Integrate the Lambda function to the API Gateway const lambdaIntegration = new api.LambdaIntegration(lambdaFunction);
Additionally, AWS CDK can define and manage both resources and their respective methods. This example shows how to integrate a specific resource and its associated methods with a Lambda function:
// Add GET method to the root resource apiGateway.root.addMethod("GET", lambdaIntegration); // Add POST method to the root resource apiGateway.root.addMethod("POST", lambdaIntegration);
In this example, the apiGateway
variable represents the previously instantiated API Gateway REST API construct. These commands link the GET
and POST
methods to the API Gateway’s root resource. This root resource aligns with the default path in API Gateway, denoted by the / path.
Moreover, the earlier established LambdaIntegration
construct is referenced here, indicating which Lambda function will handle these requests once deployed to the cloud.
Deployment of the CDK Application
After creating all these constructs inside the lib/tutorials-dojo-cdk-app-stack.ts
Stack. The file should look like this:
Revisiting the architecture, it’s apparent that the right-most resource, the DynamoDB table in this instance, was the first to be defined. This approach aligns with a common best practice in AWS CDK and other IaC tools: begin with the service that has minimal integrations. Typically, this service corresponds to the resource that’s furthest in the architectural diagram.
To deploy the TutorialsDojoCdkAppStack
AWS CDK Stack, run the following command in the root directory:
tsc cdk deploy
The tsc
command compiles all TypeScript code in the project, depending on the configurations set in tsconfig.json
, and translates it into JavaScript. This conversion is vital as Lambda natively interprets JavaScript and lacks built-in support for TypeScript.
Furthermore, executing cdk deploy
initiates the deployment of the TutorialsDojoCdkAppStack
. The deployment’s progress can be monitored either through the terminal or directly in CloudFormation. In cases of deployment failures, it’s recommended to inspect the process in CloudFormation for a clearer understanding of the issues encountered.
Testing the Serverless REST API
Evaluating the functionality of the REST API is feasible through any API client. In this demonstration, Postman serves as the chosen tool for probing the freshly deployed REST API endpoints.
Prerequisites
-
Ensure Postman is installed.
-
Set up the REST API’s base URL as an environment variable within Postman. This URL gets displayed in the terminal upon the Stack’s deployment.
-
Create a Postman Collection.
Testing
Within the curated Postman Collection, fashion a new request. The initial test targets the POST
request, with the configuration depicted in the following image:
Upon hitting “Send”, expect a response mirroring the format illustrated above.
The subsequent test focuses on the GET
request. Replicate the initial POST
request, modify the method to GET
, and for the body, select “none” since GET
requests typically lack a body.
The corresponding configuration can be observed below:
After hitting “Send”, anticipate a response reflecting the format showcased above.
It’s worth noting: as more entries are appended via the PUT Item API, they get enumerated in the output of the GET Items API.
The Most Common Pitfall: Should I Still Learn CloudFormation?
Amidst the rise of popular IaC tools, a question arises: Is mastering CloudFormation still pertinent for deploying AWS infrastructure using IaC? The answer is a resounding yes. A common misconception likens CloudFormation to assembly language and tools like AWS CDK, Terraform, and Serverless Framework to high-level languages. However, such an analogy is oversimplified. It’s crucial to recognize that there isn’t a distinct AWS CDK or Terraform service within AWS. At its core, CloudFormation is the foundational service, and other IaC tools simply offer layers of abstraction. Ultimately, all paths lead back to CloudFormation. Grasping the fundamental principles and mechanics of CloudFormation is essential for anyone leveraging IaC tools for AWS infrastructure deployment. Ignoring this foundation can lead to pitfalls and misunderstandings in the deployment process.
Final Remarks
AWS CDK, alongside other IaC tools, offers a myriad of benefits that redefine modern infrastructure management. The term “Infrastructure as Code” now feels somewhat dated. A more apt phrase in today’s context might be “Infrastructure IS Code.” Today’s infrastructure isn’t merely described or handled using code; it fundamentally materializes as code. Every element, from networking components to databases, is articulated, set up, and overseen through coding practices. Relying on manual deployment, particularly in production environments, has become an outdated approach for cloud-native applications. Contemporary standards gravitate towards IaC tools. From automation prowess and guaranteed consistency to fluid CI/CD integration, the advantages of IaC are manifold, significantly benefiting both developers and DevOps experts.
Constructing infrastructure in AWS with the AWS CDK offers users an enriched development experience, thanks to CDK’s adoption of widely-used programming languages. It’s akin to harnessing CloudFormation but with the familiarity and flexibility of a chosen programming language. This approach provides high-level, pertinent abstractions that elevate the entire development journey.
What to Expect in Part 2?
Part 2 delves deeper into the art of abstraction within the AWS CDK. This section introduces the diverse types of constructs in the AWS CDK, emphasizing the significance of developing custom and reusable constructs. By adopting this approach, the goal is to uphold the DRY (Don’t Repeat Yourself) principle and amplify code clarity. See you next time!
References:
https://docs.aws.amazon.com/cdk/v2/guide/home.html
https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html
https://docs.aws.amazon.com/cdk/v2/guide/hello_world.html
https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-typescript.html
https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html
https://aws.amazon.com/api-gateway/