DEV Community

Cover image for Getting started with AWS CDK
Kevin Odongo
Kevin Odongo

Posted on • Edited on

Getting started with AWS CDK

Hey everyone, how are you doing? We have gone through AWS CloudFormation, and I believe you are anxiously waiting for the final section. Do not worry; I am preparing good examples for you, and once it is ready, I will publish them. But today, I want to talk about AWS CDK?.

What is AWS CDK?

This is a framework for defining cloud infrastructure in code and provisioning it through AWS CloudFormation.

You can now see why I started with AWS CloudFormation to familiarize yourself with the core product that powers most of AWS Infrastructure as code CLI's like AWS CDK, AWS SAM, AWS CDK, etc. Using AWS CDK has a lot of benefits in building scalable, cost-effective applications in the AWS Cloud.

The AWS CDK supports TypeScript, JavaScript, Python, Java, C#/.Net, and (in developer preview) Go. Developers can use one of these supported programming languages to define reusable cloud components known as Constructs. You compose these together into Stacks and Apps.

The team at AWS has built an extraordinary framework to work with while deploying and configuring resources in AWS. This tool simplifies the process. You need to install AWS CDK Toolkit, which is a command-line tool. Use this command to install the kit.

// install toolkit
npm install -g aws-cdk 

// once installed, run the following command:
cdk version
Enter fullscreen mode Exit fullscreen mode

Once you have installed let us take a look at some common commands:
cdk --help
cdk list - list all stacks in the app
cdk synthesize - prints cloudformation template
cdk bootstrap - deploy staging stack
cdk deploy - deploy specified stacks
cdk destroy - destroy a specified stack
cdk metadata - display metadata
cdk init - creates a new project
cdk context - manages cached context values
cdk docs - cdk API reference
cdk doctor - checks for problems

Now that we have familiarized ourselves with AWS CDK before getting our hands dirty let us begin by configuring our credentials. Run the following command to configure your environment.

aws configure
Enter fullscreen mode Exit fullscreen mode

You may optionally use the --role-arn (or -r) option to specify the ARN of an IAM role that should be used for deployment. This role must be assumable by the AWS account being used.

Unsplash image

Let us get started and create our first app with AWS CDK. This application will be simple, whereby a user has two access to two routes; the GET route to retrieve their secret and the PUT route to create their secret. We can add another route, DELETE, to delete the secret they saved.

CDK Tutorial Application

To begin with, let us create a folder called cdk-typescript. I will be using typescript language, but you can use any of the supported languages.

mkdir cdk-typescript
cd cdk-typescript
Enter fullscreen mode Exit fullscreen mode

Once we have created the folder, then we can run the following command to make an application:

cdk init app --language typescript
Enter fullscreen mode Exit fullscreen mode

Now we have an application ready, let us create a folder called lambdaFunction and add a file called app.js.

mkdir lambdaFunction && touch app.js
Enter fullscreen mode Exit fullscreen mode

Then add the following contents in the app.js

const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10' });

// Get resources from enviroment
const secretTableName = process.env.SECRET_TABLE_NAME;


/**
 *
 * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
 * @param {Object} event - API Gateway Lambda Proxy Input Format
 *
 * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html 
 * @param {Object} context
 *
 * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
 * @returns {Object} object - API Gateway Lambda Proxy Output Format
 * 
 */

exports.handler = async (event) => {

  const lambdaResponse = {
    statusCode: 200,
    headers: {
      "Access-Control-Allow-Headers": "Content-Type",
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "OPTIONS,POST,GET,DELETE"
    },
    body: '',
  };

  try {
    switch (event.httpMethod) {
      case 'PUT':
        if (event.queryStringParameters.content) {
          const results = await createSecret(event.queryStringParameters.content)
          lambdaResponse.body = `https://${event.requestContext.apiId}.execute-api.us-east-1.amazonaws.com/prod${event.path}?id=${results}`
        } else {
          lambdaResponse.body = "Please provide a secret"
        }
        break;
      case 'GET':
        if (event.queryStringParameters.id) {
          lambdaResponse.body = await getSecret(event.queryStringParameters.id)
        } else {
          lambdaResponse.body = "Please provide the id of the secret you want to retrive"
        }
        break;
      case 'DELETE':
        if (event.queryStringParameters.id) {
          await deleteSecret(event.queryStringParameters.id)
        } else {
          lambdaResponse.body = "Please provide the id of the secret you want to delete"
        }
        break;
      default:
        break
    }
  } catch (error) {
    lambdaResponse.statusCode = 400
    lambdaResponse.body = error.message
  } finally {
    lambdaResponse.body = JSON.stringify(lambdaResponse.body)
  }

  return lambdaResponse;
};

/**
 * Creates a new secret
 * @param id
*/
const createSecret = async (content) => {
  const secretId = uuid(16)

  const params = {
    TableName: secretTableName,
    Item: {
      "id": secretId,
      "content": content,
    },
  }

  await docClient.put(params).promise()
  return secretId
}


/**
 * Get user secret
 * @param id 
 * @returns {object}
*/
const getSecret = async (id) => {
  const result = await docClient.get({
    TableName: secretTableName,
    Key: {
      "id": id
    }
  }).promise()
  if (!result) {
    return null
  }

  return result.Item
}

/**
 * Delete user secret 
 * @param id 
*/
const deleteSecret = async (id) => {
  var params = {
    TableName: secretTableName,
    Key: {
      "id": id,
    },
  };
  await docClient.delete(params).promise()
}

/**
 * Generate random uuid
 * @returns uuid
*/
const uuid = () => {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
    var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}
Enter fullscreen mode Exit fullscreen mode

With our function ready, we need to update cdk-typescript-stack.ts.. We will do this step by step to understand how straightforward it is to work with AWS CDK. This tutorial will be exciting because you will note how AWS CDK has simplified building backend infrastructure in the cloud. To begin with, let us create our database. AWS CDK has an API reference whereby you can get all AWS resources documentation on how to deploy and configure using AWS CDK.

Add the following contents in the following file ./lib/cdk-typescript-stack.ts.

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

export class CdkTypescriptStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // create a dynamodb secret table
    const table = new dynamodb.Table(this, 'SecretTable', {
      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
    });

  }
}

Enter fullscreen mode Exit fullscreen mode

Since we use typescript, we need to build our application with the following command and deploy the application.

// build the application
npm run build

// Optional
cdk synth

// Deploy
cdk deploy
Enter fullscreen mode Exit fullscreen mode

On completion, you should be able to see something like this in your terminal.

Unspalsh image

Next, we want to add the lambda that we created. This lambda function will interact with the table we created above, and as you know, AWS IAM permissions for the function need to be updated so the function can interact with the table. Update the cdk-typscript-stack.ts as follows, then build and deploy. Oh, one more thing you can always run the following command cdk diff to check on the changes that are going to happen in the backend.

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';

export class CdkTypescriptStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // create a dynamodb table
    const table = new dynamodb.Table(this, 'SecretTable', {
      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
    });

    // create lambda function
    const secretFunction = new lambda.Function(this, 'SecretFunction', {
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: 'app.handler',
      code: lambda.Code.fromAsset('./lambdaFunction')
    });

    // update function IAM polify grant full access to dynamodb
    table.grantFullAccess(secretFunction)

    // update the function enviroment
    secretFunction.addEnvironment("SECRET_TABLE_NAME", table.tableName)

  }
}

Enter fullscreen mode Exit fullscreen mode

We have the last resource to create, which is API Gateway. Generally, you can see how straightforward working with AWS CDK is. And it reduces the lines of code we need compared to working with vanilla CloudFormation. Update the cdk-typscript-stack.ts as follows, then build and deploy.

Now log into AWS Console and check the list of lambdas in the region you selected when you configured your credentials with the following command aws configure.We have now deployed our first application using AWS CDK.

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';

export class CdkTypescriptStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // create a dynamodb table
    const table = new dynamodb.Table(this, 'SecretTable', {
      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
    });

    // create lambda function
    const secretFunction = new lambda.Function(this, 'SecretFunction', {
      runtime: lambda.Runtime.NODEJS_14_X,
      handler: 'app.handler',
      code: lambda.Code.fromAsset('./lambdaFunction'),
    });

    // update function IAM polify grant full access to dynamodb
    table.grantFullAccess(secretFunction)

    // update the function enviroment
    secretFunction.addEnvironment("SECRET_TABLE_NAME", table.tableName)

    // create rest api
    const api = new apigateway.LambdaRestApi(this, 'secret-api', {
      handler: secretFunction,
      proxy: false
    });

    // add resource and methods
    const secret = api.root.addResource('secret');

    secret.addMethod('GET');
    secret.addMethod('PUT');
    secret.addMethod('DELETE');

  }
}
Enter fullscreen mode Exit fullscreen mode

On completion, you should be able to see something like this in your terminal.

✨  Deployment time: 91.55s

Outputs:
CdkTypescriptStack.secretapiEndpointC5C4044F = https://[apiId].execute-api.us-east-1.amazonaws.com/prod/       
Stack ARN:
arn:aws:cloudformation:us-east-1:440343172651:stack/CdkTypescriptStack/d191a320-7e0d-11ec-a2aa-1249d52492bf       

✨  Total time: 103.7s
Enter fullscreen mode Exit fullscreen mode

Photo from Unsplash
We have created the backend of our application we can test the url using postman to ensure everything works correctly.

PUT METHOD

Replace the apiId and run the following command:

curl --location --request PUT 'https://[apiId].execute-api.us-east-1.amazonaws.com/prod/secret?content=kevinodongo'
Enter fullscreen mode Exit fullscreen mode

This command will create a new record in dynamodb and return a URL that you can use to retrieve the secret.

"https://[apiId].execute-api.us-east-1.amazonaws.com/prod/secret?id=54a7a7b9-972e-4b2e-9422-715c0ea8733d"
Enter fullscreen mode Exit fullscreen mode

GET METHOD

Replace the apiId and run the following command:

curl -i --location --request PUT 'https://[apiId].execute-api.us-east-1.amazonaws.com/prod/secret?id=54a7a7b9-972e-4b2e-9422-715c0ea8733d'
Enter fullscreen mode Exit fullscreen mode

Your response should look something like this.

{"content":"kevinodongo","id":"54a7a7b9-972e-4b2e-9422-715c0ea8733d"}
Enter fullscreen mode Exit fullscreen mode

DELETE METHOD

Replace the apiId and run the following command:

curl --location --request DELETE 'https://[apiId].execute-api.us-east-1.amazonaws.com/prod/secret?id=54a7a7b9-972e-4b2e-9422-715c0ea8733d'
Enter fullscreen mode Exit fullscreen mode

Now that we have completed our application let us go through some of the terms you need to learn with AWS CDK. I will be going through them briefly, but you can read more about them here

Constructs

These are building blocks that contain everything AWS CloudFormation requires to create an environment.

Composition

Composition is the critical pattern for defining higher-level abstractions through constructs. A high-level construct can be composed of any number of lower-level constructs. In turn, it could form those from even lower-level constructs, which eventually composed from AWS resources.

Initialization

Constructs are implemented in classes that extend the Construct base class. You define a construct by instantiating the class. When they are initialized, all constructs take three parameters: Scope, id, and Props.

Apps and stacks

Stacks in AWS CDK apps extend the Stack base class, as shown in the previous example. This approach is a typical pattern when creating a stack within your AWS CDK app:

  • Extend the Stack class.
  • Define a constructor that accepts scope, id, and props.
  • Invoke the base class constructor via super with the received scope, id, and props, as shown in the following example.

The unit of deployment in the AWS CDK is called a stack. All AWS resources defined within the scope of a stack, either directly or indirectly, are provisioned as a single unit.

Each Stack instance in your AWS CDK app is explicitly or implicitly associated with an environment (env). An environment is the target AWS account and region the stack is deployed.

const regionEU = { account: '2383838383', region: 'eu-west-1' }; 
const regionUSA = { account: '8373873873', region: 'us-east-1' }; 

new USAStack(app, 'stack-us', { env: regionUSA }); 
new EUStack(app, 'stack-eu', { env: regionEU });
Enter fullscreen mode Exit fullscreen mode

Hey! take your time and understand the following concepts well:

  • Bootstrapping
  • Escape hatches
  • Aspects
  • Feature flags
  • Context
  • Permissions
  • Assets
  • Tagging
  • Parameters
  • Tokens
  • Identifiers
  • Resources
  • Enviroments

Best Practices

One thing you need to first take into consideration is to have a continuous delivery pipeline for deployment. You should organize the application into building separate individual building blocks, for example, API, database, monitoring, etc.

  1. The organization should define the standards and policies to guide its cloud infrastructures. A landing zone is a pre-configured, secure, scalable, multi-account AWS environment based on best practice blueprints. You can tie together the services that make up your landing zone with AWS Control Tower. This high-level service configures and manages your entire multi-account system from a single user interface.
  2. Start simple and add complexity only when needed.
  3. Align with the AWS Well-Architected framework
  4. Infrastructure and runtime code live in the same package.
  5. Every application starts with a single package in a single repository.
  6. Move code into repositories based on code lifecycle or team ownership.
  7. Model with constructs, deploy with stacks.
  8. Configure with properties and methods, not environment variables
  9. Unit test your infrastructure
  10. Don't change the logical ID of stateful resources.
  11. Make decisions at synthesis time.
  12. Use generated resource names, not physical names.
  13. Define removal policies and log retention
  14. Separate your application into multiple stacks as dictated by deployment requirements
  15. Let the AWS CDK manage roles and security groups.
  16. Model all production stages in code
  17. Measure everything

That is all I have to share in getting started with AWS CDK. This article should get you started and ensure you can work with AWS CDK. Do not stop with this article research more. I will keep sharing more about AWS CDK. Thank you and see you next time.

Top comments (2)

Collapse
 
jamesgmorgan profile image
James Morgan

One correction:
"mkdir lambdaFunction && touch app.js" should be: "mkdir lambdaFunction && touch lambdaFunction/app.js"

Nice sample app!

Collapse
 
jamesgmorgan profile image
James Morgan

Also, the GET method curl test starts with:
curl -i --location --request PUT
... it should be:
curl -i --location --request GET