My Study Notes on AWS Cloud Development Kit (AWS CDK)

(Illustration: In those days, integrated circuits (IC) organized some commonly used and reused circuits, and then reduced the size, standing on the shoulders of predecessors, and driving the progress of the times. The AWS CDK has done a similar sorting. Image source: Image by Gerd Altmann from Pixabay。)


Briefing

The AWS Cloud Development Kit (AWS CDK) allows us to define our own cloud infrastructure as code (IaC) in a programming language we are familiar with (currently 5 supported).

AWS CDK is composed of various large and small constructs to create stacks and apps.

The neighbor of AWS CDK downstairs is AWS CloudFormation. Use cdk synth command tool to convert to AWS CloudFormation Template, and then use cdk deploy command to easily deploy various AWS products/resources, or use cdk destroy command to enjoy one-click restoration.

For the first getting started, I will recommend to watch Pahud Dev’s AWS CDK series of videos, or CDK Workshop to experience the whole process which is simple (and enjoyable).

In the very beginning of playing with AWS CDK, most of time are using the default settings and default environment. We will gradually use more detailed settings when further use on the mass production environment. Sometimes the delicate link is not yet ready, we can consider to have contribution on L2/L3 constructs. Find more contribution resources at the Reference section below.


History

From ancient to modern times. Get through all the context.


Terms

Here is a list of nouns that appear on the scene, and the full name of the original text, noun definition and source are noted.

  • AWS Cloud Development Kit (AWS CDK) is a software development framework for defining cloud infrastructure in code and provisioning it through AWS CloudFormation.
  • Constructs
    • 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.
    • the basic building blocks.
    • Can be big or small.
      • Small enough to correspond to a cloud product (for example, an Amazon S3 bucket).
      • Big to be able to span multiple AWS accounts and multiple regions.
    • In the world of AWS CDK, everything is a Construct.
      • Similar to the Flutter world, everything is Widget.
    • AWS Construct library
    • The Matrix: The Construct Explained
  • Apps
    • App is also one kind of Construct.
    • The App construct doesn’t require any initialization arguments, because it’s the only construct that can be used as a root for the construct tree.
  • Stacks
    • 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.
  • Environments
    • 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 AWS Region into which the stack is intended to be deployed.

Use Cases


Limits


Pricing

  • Free!
    • AWS CDK itself is a development tool.
    • Please refer to the latest official document. This is just shorthand.
    • The invocation/use of AWS CDK corresponds to various AWS Products and charges are based on the charging rules of each product.

Frequently Information

Prerequisites

Reference docs

  • Node.js
    • 10.3.0 or later.
    • even those working in languages other than TypeScript or JavaScript.
    • The AWS CDK Toolkit (cdk command-line tool) and the AWS Construct Library are developed in TypeScript and run on Node.js.
    • AWS suggests the latest LTS version.
    • 13.0.0 through 13.6.0 are not compatible with the AWS CDK.
  • AWS credentials
  • AWS CLI

Frequently Commands: Install/Update AWS CDK Toolkit

  • Install AWS CDK Toolkit

    npm install -g aws-cdk
    
  • Install a specific version (lock version) AWS CDK Toolkit

    npm install -g aws-cdk@1.50.0
    
  • Update AWS CDK Toolkit

    npm update -g aws-cdk
    
  • Check version

    cdk --version
    

Frequently Commands: Operation

  • mkdir hello-cdk
    • cd hello-cdk
  • cdk init --language typescript
    • npm run build (compile typescript to js)
    • npm install @aws-cdk/aws-s3
    • Edit your code
    • npm run build
  • cdk bootstrap (Bootstrap once, per account per region)
  • cdk synth (Synthesizing an AWS CloudFormation Template. Generate file cdk.context.json.)
  • cdk deploy
  • Do some extra editing on your code.
    • npm run build
    • cdk diff
    • cdk deploy
  • cdk destroy

Concepts

Constructs

  • 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.

  • The basic building blocks

  • Can be big or small.

    • Small enough to correspond to a cloud product (for example, an Amazon S3 bucket).
    • Big to be able to span multiple AWS accounts and multiple regions.
  • In the world of AWS CDK, everything is a Construct.

    • Similar to the Flutter world, everything is a Widget.

    AWS Construct library

    • Docs
    • This library includes constructs that represent all the resources available on AWS.
      • For example, the s3.Bucket class represents an Amazon S3 bucket, and the dynamodb.Table class represents an Amazon DynamoDB table.
    • There are different levels of constructs in AWS Construct Library.
      • Bottom level: CFN Resources. (AWS CloudFormation-only) (L1) (Level 1)
        • These constructs represent all of the AWS resources that are available in AWS CloudFormation. CFN Resources are generated from the AWS CloudFormation Resource Specification on a regular basis. They are named CfnXyz, where Xyz represents the name of the resource.
        • For example, s3.CfnBucket represents the AWS::S3::Bucket CFN Resource.
      • Higher level: AWS constructs. (Curated) (L2)
        • They provide the same functionality, but handle much of the details, boilerplate, and glue logic required by CFN constructs. AWS constructs offer convenient defaults and reduce the need to know all the details about the AWS resources they represent, while providing convenience methods that make it simpler to work with the resource.
        • Example: s3.Bucket.
      • Even higher-level: Patterns. (Patterns) (L3)
        • These constructs are designed to help you complete common tasks in AWS, often involving multiple kinds of resources.
        • For example, the aws-ecs-patterns.ApplicationLoadBalancedFargateService construct represents an architecture that includes an AWS Fargate container cluster employing an Application Load Balancer (ALB).

    Composition

    The key pattern for defining higher-level abstractions through constructs is called composition.

    Initialization

    Constructs are implemented in classes that extend the Construct base class. You define a construct by instantiating the class. All constructs take three parameters when they are initialized:

    • scope
      • Usually using this
    • id
      • identifier
      • namespace
      • unique
      • resource names
      • AWS CloudFormation logical IDs
    • props
      • A set of properties or keyword arguments

Apps

  • Docs

  • App is also one kind of Construct.

  • The App construct doesn’t require any initialization arguments, because it’s the only construct that can be used as a root for the construct tree.

  • You can use the App instance as a scope for defining a single instance of your stack.

  • App lifecycle:

    • The following diagram shows the phases that the AWS CDK goes through when you call the cdk deploy.

    (Image source: Official docs)

  • To work with the CDK CLI, you need to let it know how to execute an AWS CDK app.

    • cdk --app executable cdk-command

    • cdk.json:

      1
      2
      3
      
      {
        "app": "node bin/my-app.js"
      }
    • Example: The following example lists the stacks defined in the cloud assembly stored under ./my-cloud-assembly.

      cdk --app ./my-cloud-assembly ls
      

Stacks

  • Docs, Stack API, Class Stack

  • 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.

  • When you run the cdk synth command for an app with multiple stacks, the cloud assembly includes a separate template for each stack instance.

  • Because AWS CDK stacks are implemented through AWS CloudFormation stacks, they have the same limitations as in AWS CloudFormation.

    1
    2
    3
    4
    5
    6
    
    const app = new App();
    
    new MyFirstStack(app, 'stack1');
    new MySecondStack(app, 'stack2');
    
    app.synth();
  • To list all the stacks in an AWS CDK app, run the cdk ls command, which for the previous AWS CDK app would have the following output.

    stack1
    stack2
    

    Nested stacks

    • class NestedStack (construct)
    • A CloudFormation nested stack.
    • The NestedStack construct offers a way around the AWS CloudFormation 500-resource limit for stacks.
    • The scope of a nested stack must be a Stack or NestedStack construct.
    • parent stacks v.s nested stacks
      • Nested stacks are bound to their parent stack and are not treated as independent deployment artifacts; they are not listed by cdk list nor can they be deployed by cdk deploy.

Environments

  • 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 AWS Region into which the stack is intended to be deployed.

  • For production stacks, AWS recommends that you explicitly specify the environment for each stack in your app using the env property.

  • Example: specifies different environments for its two different stacks.

    1
    2
    3
    4
    5
    
    const envEU  = { account: '2383838383', region: 'eu-west-1' };
    const envUSA = { account: '8373873873', region: 'us-west-2' };
    
    new MyFirstStack(app, 'first-stack-us', { env: envUSA });
    new MyFirstStack(app, 'first-stack-eu', { env: envEU });
  • To make the stack deployable to a different target, but to determine the target at synthesis time, your stack can use two environment variables provided by the AWS CDK CLI: CDK_DEFAULT_ACCOUNT and CDK_DEFAULT_REGION.

    • These variables are set based on the AWS profile specified using the --profile option, or the default AWS profile if you don’t specify one.
    1
    2
    3
    4
    5
    
    new MyDevStack(app, 'dev', { 
      env: { 
    account: process.env.CDK_DEFAULT_ACCOUNT, 
    region: process.env.CDK_DEFAULT_REGION 
    }});
  • You can set env however you like, using any valid expression.

    • Example: you might write your stack to support two additional environment variables to let you override the account and region at synthesis time.
    1
    2
    3
    4
    5
    
    new MyDevStack(app, 'dev', { 
      env: { 
    account: process.env.CDK_DEPLOY_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT, 
    region: process.env.CDK_DEPLOY_REGION || process.env.CDK_DEFAULT_REGION 
    }});
    • With your stack’s environment declared this way, you can now write a short script or batch file like the following to set the variables from command line arguments, then call cdk deploy:

      • cdk-deploy-to.sh:
      1
      2
      3
      4
      5
      6
      
      #!/bin/bash
      export CDK_DEPLOY_ACCOUNT=$1
      shift
      export CDK_DEPLOY_REGION=$1
      shift
      cdk deploy "$@"
      • cdk-deploy-to-test.sh:
      1
      2
      
      #!/bin/bash
      bash cdk-deploy-to.sh 123457689 us-east-1 "$@"
      • cdk-deploy-to-prod.sh:
      1
      2
      3
      
      #!/bin/bash
      bash cdk-deploy-to.sh 135792468 us-west-1 "$@" || exit
      bash cdk-deploy-to.sh 246813579 eu-west-1 "$@"

Resources

  • The AWS CDK provides a rich class library of constructs, called AWS constructs, that represent all AWS resources.

    Resource attributes

    Most resources in the AWS Construct Library expose attributes, which are resolved at deployment time by AWS CloudFormation.

    1
    2
    3
    4
    
    import * as sqs from '@aws-cdk/aws-sqs';
        
    const queue = new sqs.Queue(this, 'MyQueue');
    const url = queue.queueUrl; // => A string representing a deploy-time value
    

    Referencing resources

    • Many AWS CDK classes require properties that are AWS CDK resource objects (resources). To satisfy these requirements, you can refer to a resource in one of two ways:

      • By passing the resource directly
      • By passing the resource’s unique identifier, which is typically an ARN, but it could also be an ID or a name
    • If a construct property represents another AWS construct, its type is that of the interface type of that construct. Examples:

      • the Amazon ECS service takes a property cluster of type ecs.ICluster;
      • the CloudFront distribution takes a property sourceBucket (Python: source_bucket) of type s3.IBucket.
    • Example: define an Amazon ECS cluster and then uses it to define an Amazon ECS service.

    1
    2
    3
    
    const cluster = new ecs.Cluster(this, 'Cluster', { /*...*/ });
    
    const service = new ecs.Ec2Service(this, 'Service', { cluster: cluster });

    Physical names

    • The logical names of resources in AWS CloudFormation are different from the physical names of resources that are shown in the AWS Management Console after AWS CloudFormation has deployed the resources. The AWS CDK calls these final names physical names.

    • For example, AWS CloudFormation might create the Amazon S3 bucket with the logical ID Stack2MyBucket4DD88B4F from the previous example with the physical name stack2mybucket4dd88b4f-iuv1rbv9z3to.

    • You can specify a physical name when creating constructs that represent resources by using the property <resourceType>Name. The following example creates an Amazon S3 bucket with the physical name my-bucket-name.

      1
      2
      3
      
      const bucket = new s3.Bucket(this, 'MyBucket', {
        bucketName: 'my-bucket-name',
      });
    • (Disadvantage) Assigning physical names to resources has some disadvantages in AWS CloudFormation.

      • Most importantly, any changes to deployed resources that require a resource replacement, such as changes to a resource’s properties that are immutable after creation, will fail if a resource has a physical name assigned.

      • (Can only be deleted and re-deploy once.) If you end up in a state like that, the only solution is to delete the AWS CloudFormation stack, then deploy the AWS CDK app again.

      • (Some cross-environment needs to specify fixed physical names, which can be named by AWS CDK.) In some cases, such as when creating an AWS CDK app with cross-environment references, physical names are required for the AWS CDK to function correctly. In those cases, if you don’t want to bother with coming up with a physical name yourself, you can let the AWS CDK name it for you by using the special value PhysicalName.GENERATE_IF_NEEDED

        1
        2
        3
        
        const bucket = new s3.Bucket(this, 'MyBucket', {
          bucketName: core.PhysicalName.GENERATE_IF_NEEDED,
        });

    Importing existing external resources

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    // Construct a resource (bucket) just by its name (must be same account)
    s3.Bucket.fromBucketName(this, 'MyBucket', 'my-bucket-name');
    
    // Construct a resource (bucket) by its full ARN (can be cross account)
    s3.Bucket.fromArn(this, 'MyBucket', 'arn:aws:s3:::my-bucket-name');
    
    // Construct a resource by giving attribute(s) (complex resources)
    ec2.Vpc.fromVpcAttributes(this, 'MyVpc', {
    vpcId: 'vpc-1234567890abcde',
    });
    
    
    // quert by default
    ec2.Vpc.fromLookup(this, 'DefaultVpc', { 
    isDefault: true 
    });
    
    // query by tag
    ec2.Vpc.fromLookup(this, 'PublicVpc', 
    {tags: {'aws-cdk:subnet-type': "Public"}
    });

    Permission grants

    • The grant methods return an iam.Grant object.
    1
    2
    3
    4
    5
    
    if (bucket.grantReadWrite(func).success) {
    // ...
    }
    
    table.grant(func, 'dynamodb:CreateBackup');

    Metrics and alarms

    • Docs
    • AWS constructs have metric methods that allow easy access to the metrics without having to look up the correct name to use.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    import * as cw from '@aws-cdk/aws-cloudwatch';
    import * as sqs from '@aws-cdk/aws-sqs';
    import { Duration } from '@aws-cdk/core';
    
    const queue = new sqs.Queue(this, 'MyQueue');
    
    const metric = queue.metricApproximateNumberOfMessagesNotVisible({
    label: 'Messages Visible (Approx)',
    period: Duration.minutes(5),
    // ...
    });
    metric.createAlarm(this, 'TooManyMessagesAlarm', {
    comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD,
    threshold: 100,
    // ...
    });

    Network traffic

    • Docs
    • Resources that establish or listen for connections expose methods that enable traffic flows, including setting security group rules or network ACLs.
    • IConnectable resources have a connections property that is the gateway to network traffic rules configuration.

    Event handling

    • Some resources can act as event sources. Use the addEventNotification method (Python: add_event_notification) to register an event target to a particular event type emitted by the resource.

    • In addition to this, addXxxNotification methods offer a simple way to register a handler for common event types.

    • The following example shows how to trigger a Lambda function when an object is added to an Amazon S3 bucket:

      1
      2
      3
      4
      5
      
      import * as s3nots from '@aws-cdk/aws-s3-notifications';
      
      const handler = new lambda.Function(this, 'Handler', { /*…*/ });
      const bucket = new s3.Bucket(this, 'Bucket');
      bucket.addObjectCreatedNotification(new s3nots.LambdaDestination(handler));

    Removal policies

    • Resources that maintain persistent data, such as databases and Amazon S3 buckets, have a removal policy that indicates whether to delete persistent objects when the AWS CDK stack that contains them is destroyed.

    • The values specifying the removal policy are available through the RemovalPolicy enumeration in the AWS CDK core module.

    • Example:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      
      import * as cdk from '@aws-cdk/core';
      import * as s3 from '@aws-cdk/aws-s3';
        
      export class CdkTestStack extends cdk.Stack {
        constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
      super(scope, id, props);
        
      const bucket = new s3.Bucket(this, 'Bucket', {
        removalPolicy: cdk.RemovalPolicy.DESTROY,
      });
        }
      }
    • Example: You can also apply a removal policy directly to the underlying AWS CloudFormation resource via the applyRemovalPolicy() method.

      1
      2
      
      const resource = bucket.node.findChild('Resource') as cdk.CfnResource;
      resource.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);

Identifiers

Docs

  • The AWS CDK deals with many types of identifiers and names.

  • Identifiers must be unique within the scope in which they are created; they do not need to be globally unique in your AWS CDK application.

    Construct IDs

    • The most common identifier, id, is the identifier passed as the second argument when instantiating a construct object.

    Paths

    • We refer to the collection of IDs from a given construct, its parent construct, its grandparent, and so on to the root of the construct tree, as a path.

    • The AWS CDK typically displays paths in your templates as a string, with the IDs from the levels separated by slashes, starting at the node just below the root App instance, which is usually a stack. For example, the paths of the two Amazon S3 bucket resources in the previous code example are Stack1/MyBucket and Stack2/MyBucket.

      1
      
        const path: string = myConstruct.node.path;

    Unique IDs

    • Since the AWS CDK already has paths that are globally unique, the AWS CDK generates these unique identifiers by concatenating the elements of the path, and adds an 8-digit hash. The hash is necessary, as otherwise two distinct paths, such as A/B/C and A/BC would result in the same identifier. The AWS CDK calls this concatenated path elements and hash the unique ID of the construct.

      1
      
        const uid: string = myConstruct.node.uniqueId;

    Logical IDs

    • Unique IDs serve as the logical identifiers, which are sometimes called logical names, of resources in the generated AWS CloudFormation templates for those constructs that represent AWS resources.
    • Avoid changing the logical ID of a resource between deployments. Since AWS CloudFormation identifies resources by their logical ID, if you change the logical ID of a resource, AWS CloudFormation deletes the existing resource, and then creates a new resource with the new logical ID.

Tokens

  • Tokens represent values that can only be resolved at a later time in the lifecycle of an app (see App lifecycle). For example, the name of an Amazon S3 bucket that you define in your AWS CDK app is only allocated by AWS CloudFormation when you deploy your app. If you print the bucket.bucketName attribute, which is a string, you see it contains something like ${TOKEN[Bucket.Name.1234]}.

Parameters

  • AWS CloudFormation templates can contain parameters—custom values that are supplied at deployment time and incorporated into the template. Since the AWS CDK synthesizes AWS CloudFormation templates, it too offers support for deployment-time parameters.
  • In general, we recommend against using AWS CloudFormation parameters with the AWS CDK. Parameter values are not available at synthesis time and thus cannot be easily used in other parts of your AWS CDK app, particularly for control flow.

Tagging

  • The following example applies the tag key with the value value to a construct:

    1
    
    Tag.add(myConstruct, 'key', 'value');
  • The following example deletes the tag key from a construct:

    1
    
    Tag.remove(myConstruct, 'key');
  • By default, applying a tag has a priority of 100 and removing a tag has a priority of 200. To change the priority of applying a tag, pass a priority property to Tag.add() or Tag.remove().

  • The following applies a tag with a priority of 300 to a construct:

    1
    2
    3
    
    Tag.add(myConstruct, 'key', 'value', {
      priority: 300
    });

    Tag.add()

    1
    2
    3
    4
    5
    6
    
    Tag.add(myConstruct, 'tagname', 'value', {
      applyToLaunchedInstances: false,
      includeResourceTypes: ['AWS::Xxx::Yyy'],
      excludeResourceTypes: ['AWS::Xxx::Zzz'],
      priority: 100,
    });
    • These properties have the following meanings.
      • applyToLaunchedInstances
        • By default, tags are applied to instances launched in an Auto Scaling group. Set this property to false to not apply tags to instances launched in an Auto Scaling group
      • includeResourceTypes/excludeResourceTypes
        • Use these to apply tags only to a subset of resources, based on AWS CloudFormation resource types. By default, the tag is applied to all resources in the construct subtree, but this can be changed by including or excluding certain resource types. Exclude takes precedence over include, if both are specified.
      • priority
        • Use this to set the priority of this operation with respect to other Tag.add() and Tag.remove() operations. Higher values take precedence over lower values. The default is 100.

Assets

  • Assets are local files, directories, or Docker images that can be bundled into AWS CDK libraries and apps; for example, a directory that contains the handler code for an AWS Lambda function. Assets can represent any artifact that the app needs to operate.

  • You typically reference assets through APIs that are exposed by specific AWS constructs. For example, when you define a lambda.Function construct, the code property lets you pass an asset (directory). Function uses assets to bundle the contents of the directory and use it for the function’s code.

  • Similarly, ecs.ContainerImage.fromAsset uses a Docker image built from a local directory when defining an Amazon ECS task definition. Practical!

    Docker image assets

    • The AWS CDK supports bundling local Docker images as assets through the aws-ecr-assets module.

    • The following example defines a docker image that is built locally and pushed to Amazon ECR. Images are built from a local Docker context directory (with a Dockerfile) and uploaded to Amazon ECR by the AWS CDK CLI or your app’s CI/CD pipeline, and can be naturally referenced in your AWS CDK app.

    • The my-image directory must include a Dockerfile. The AWS CDK CLI builds a Docker image from my-image, pushes it to an Amazon ECR repository, and specifies the name of the repository as an AWS CloudFormation parameter to your stack. Docker image asset types expose deploy-time attributes that can be referenced in AWS CDK libraries and apps. The AWS CDK CLI command cdk synth displays asset properties as AWS CloudFormation parameters.

      1
      2
      3
      4
      5
      
      import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets';
      
      const asset = new DockerImageAsset(this, 'MyBuildImage', {
        directory: path.join(__dirname, 'my-image')
      });

    Amazon ECS task definition example

    • A common use case is to create an Amazon ECS TaskDefinition to run docker containers. The following example specifies the location of a Docker image asset that the AWS CDK builds locally and pushes to Amazon ECR.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      import * as ecs from '@aws-cdk/aws-ecs';
      import * as path from 'path';
      
      const taskDefinition = new ecs.FargateTaskDefinition(this, "TaskDef", {
        memoryLimitMiB: 1024,
        cpu: 512
      });
      
      taskDefinition.addContainer("my-other-container", {
        image: ecs.ContainerImage.fromAsset(path.join(__dirname, "..", "demo-image"))
      });

Deep Dive

The general direction is a path, but beware of pits on the ground.

How to deploy multiple CfnResource in a specific/given order?

The deployment order of CfnResource we wrote in TypeScript may not necessarily be the order of AWS CloudFormation. If you want to establish the dependency (sequential correlation) between multiple CfnResources, you can use cfnResource.addDependsOn method.

#asynchronous #synchronous #await #dependsOn #resourceDependencies

1
2
3
4
const resourceA = new CfnResource(this, 'ResourceA', resourceProps);
const resourceB = new CfnResource(this, 'ResourceB', resourceProps);

resourceB.addDependsOn(resourceA);

The order in the case will be:

resourceA = deployment 1st
resourceB = deployment 2nd

Reference:


Reference

Docs

Awesome Lists

Community

Workshops

Getting Started

Examples

Source Code

Articles & Talks

Comparison

Contribution


Trivia

  • Q: How many files are included in the first init commit of the git repo of the CDK project?
  • Q: What is the first version number released in the GitHub repo of the CDK project?

comments powered by Disqus