Categories
AWS CDK

Our first Construct in AWS CDK

Okay, so after understanding the bootstrap, testing, and why have chosen TypeScript – it’s time to define our first Construct and understand how to use this foundational element in AWS CDK.

Constructs are one of the foundational things available in the AWS CDK. Outside of being a key element, they require a significant shift in thinking, especially in stacks composition and reusability.

In this article, we will address that difference in practice, returning to our example related to the preparation of workshop environments for our participants.

What is a Construct in AWS CDK?

Let’s start with the official definition:

Constructs are the basic building blocks of AWS CDK apps. A construct represents a cloud component and encapsulates everything AWS CloudFormation needs to create the component.

A construct can represent a single resource, such as an Amazon Simple Storage Service (Amazon S3) bucket, or it can represent a higher-level component consisting of multiple AWS CDK resources. Examples of such components include a worker queue with its associated compute capacity, a cron job with monitoring resources and a dashboard, or even an entire app spanning multiple AWS accounts and regions.

Definition of the CDK Construct from the official documentation.

Well, that is a start, although I had some questions initially: what the hell is a cloud component? Here lies the thing which, in my opinion, leads to the biggest culprit related to the AWS CDK.

This tool is advertised as a compiler high-level programming language (imperative) code to the cloud assembly represented by the CloudFormation templates. That association points into thinking that our experience with CF can be helpful, and that’s why we are projecting those experiences into a new tool. And it may sound counterintuitive, but that is a mistake.

We are grouping resources into CloudFormation stacks and templates using totally different reasons than we should apply here, e.g., with CF, we were directed by convenience (maintaining the lesser number of stacks in the end) and a common reason to change.

First thing first: we should not mix Construct with a Stack. Secondly: this tool tries to resolve both concerns by introducing automation around stacks and their management, plus introducing Construct as a way to abstract certain reusable parts without using the Stack concept.

Let’s talk about hierarchies and layers!

There are different levels of constructs available in the AWS CDK.

  1. Low-level constructs, which we call CFN Resources. These constructs represent all of the AWS resources that are available in AWS CloudFormation. They are crude and directly transferable to CloudFormation templates, so you need to configure and provide everything as it is available in the CloudFormation.
  2. The next level of constructs also represent AWS resources, but with a higher-level, intent-based API. They provide the same functionality but handle much of the details, boilerplate, and glue logic required by CFN constructs. Remember the intent-based term because it will reappear soon, and it is essential.
  3. Last, but not least – higher-level constructs, which we call patterns. These constructs are designed to help you complete everyday tasks in AWS, often involving multiple kinds of resources, e.g., one of aws-ecs-patterns: ALBFargateService construct, which represents (as the name suggests) an architecture that includes an AWS Fargate container cluster employing an Application Load Balancer (ALB).

If you are a programmer, you see the pattern here: we are employing the most powerful technique available to us (and no, I’m not talking about bugs ­čśé) – composition.

As you can see above, we are defining higher-level abstractions through constructs. Such a high-level construct can be composed of any number of lower-level constructs, and in turn, those could be formed from even lower-level constructs. Here comes the game-changer: you are not optimizing your resource graph for a tool and convenience, but you should and can think about its purpose/intent.

To enable this, constructs are always defined within the scope of another construct. This scoping pattern results in a hierarchy of constructs known as a construct tree. In the AWS CDK, the root of the tree is always represented by AWS CDK App. Inside it, you typically define one or more Stacks, which are the unit of deployment, analogous to AWS CloudFormation stacks. Inside stacks, you create resources or other constructs that will eventually contain resources.

Now here comes the real power! Thanks to the composition of constructs, we can define reusable components and share them like any other code. And that refers to all pros and cons of that approach – sharing commons between people and teams is excellent but requires tools and attention when it comes to updates, bug fixes, and improvements. Luckily, as this is a regular code, you can apply all battle-tested the workflows you already have.

The talk is cheap. Show me the code

So, how can it look like a real-world example Thankfully AWS documentation has a great example in the form of NotifyingBucket:

import { Construct } as cdk from '@aws-cdk/core'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; export interface NotifyingBucketProps { prefix?: string; } export class NotifyingBucket extends Construct { public readonly topic: sns.Topic; constructor(scope: Construct, id: string, props: NotifyingBucketProps) { super(scope, id); const bucket = new s3.Bucket(this, 'bucket'); this.topic = new sns.Topic(this, 'topic'); bucket.addObjectCreatedNotification(this.topic, { prefix: props.prefix }); } }

And then in another file, we can import construct presented above and use it in the following way:

const queue = new sqs.Queue(this, 'NewImagesQueue'); const images = new NotifyingBucket(this, 'Images'); images.topic.addSubscription(new sns_sub.SqsSubscription(queue));

Let’s return to our example: we need to provide a working workshop environment with shared infrastructure and a dedicated set of resources for each participant, including his IAM user.

How we solved that in our code?

We basically created 4 constructs:

  1. First is a stack for the shared resources used by the everybody,
  2. Second is a stack for the participant’s resources.
    • There we have used constructs for representing:
      1. Participant’s VPC,
      2. IAM account for the participant, including other resources (e.g., a private bucket).

Below you can find an excerpt from the whole project – a definition of the VPC construct (you can find complete sources┬áon my Github account):

import * as cdk from '@aws-cdk/core'; import * as ec2 from '@aws-cdk/aws-ec2'; const DEFAULT_CIDR_IPV4: string = "10.0.0.0/16"; const DEFAULT_NUMBER_OF_AZS: number = 3; export interface ParticipantVpcProps { cidrIPv4?: string numberOfAZs?: number } export class ParticipantVpc extends ec2.Vpc { constructor(scope: cdk.Construct, id: string, props?: ParticipantVpcProps) { super(scope, id, { cidr: props?.cidrIPv4 || DEFAULT_CIDR_IPV4, enableDnsHostnames: true, enableDnsSupport: true, maxAzs: props?.numberOfAZs || DEFAULT_NUMBER_OF_AZS, natGateways: props?.numberOfAZs || DEFAULT_NUMBER_OF_AZS }); // We need a security group that allows // for SSH to this VPC. const ssh = new ec2.SecurityGroup(this, 'SG-SSH', { securityGroupName: 'ssh-opened', vpc: this, allowAllOutbound: true }); ssh.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(22)); ssh.addIngressRule( ec2.Peer.anyIpv6(), ec2.Port.tcp(22)); // We need an S3 VPC gateway endpoint for the // private subnets in this VPC. const selection = [ { subnetType: ec2.SubnetType.PRIVATE } ]; this.addS3Endpoint('S3-VPCE', selection); } }

New Possibilities

So knowing what it is and how it looks, we need to practice and prepare more and more of those. We also know that this approach enables internal constructs and patterns available in the framework. However, that is not everything – as there are external repositories, like this one cdk-patterns/serverless, which contains more and more patterns built by the community. It’s not hard to imagine that with a proper momentum and time spent on it, we will have a significant library of existing patterns waiting for reuse in our projects.

It was a longer article, but a necessary one – constructs are so prevalent in the CDK, so we need to dedicate some space for them. Understanding this concept is critical in the effective use of the AWS CDK.

So what’s next? We will tackle another topic that requires a shift in thinking – parametrization of CDK stacks and application.

By Wojciech Gawroński

Principal Cloud Architect at Pattern Match. Infrastructure as Code, DevOps culture and AWS aficionado. Functional Programming wrangler (Erlang/Elixir).

Comments

This site uses Akismet to reduce spam. Learn how your comment data is processed.