The Open Construct Foundation

OCF

CDK Helpers and Useful Functions

2020-06-09cdkhelpers Matthew Bonig @mattbonig

Thanks to Intellisense and type safety, working with constructs in the CDK is pretty easy. Just start typing and you’ve a got wealth of information on how to use the particular construct (yay strongly typed languages!).

intellisense

But what happens when that’s not enough? It can be hard to find the right function calls to wire together.

The following are a number of useful helper functions you can use to get the job done.

Stack

static of(construct) docs

Stack.of can be handy when you’re deep inside a construct tree and you want to get information about the Stack itself:

new Pipeline(this, `${Stack.of(this).stackName}-cicd`, {
    stages: [
        {
            stageName: 'get',
            actions: [this.githubSourceAction]
        },
        {
            stageName: 'build',
            actions: [this.buildAction]
        },
        {
            stageName: 'deploy',
            actions: [this.deployAction]
        }
    ]
})

formatArn docs

Sometimes you need to generate an Arn and can’t just use ones given to you by constructs. For example, when working with ECS the Arn provided by the task definition has a revision number on it:

ecs arns Image courtesy of @phadut.

Don’t use string concatenation, use helpers! Stack has a helper that will automatically fill in some values for you:

resources: [
    `${this.props.ecsStack.apiRepository.repositoryArn}`,
    Stack.of(this.props.ecsStack.apiRepository).formatArn({
      resource: '*',
      service: 's3'
    })
]

If you don’t have a stack, you can use the helper on the Arn class directly:

const arn = Arn.format(
    {
        resource: "instance",
        service: "ec2",
        resourceName: instance.ref,
    },
    instance.stack);

source

In this case, instance.stack can be undefined, but only if all the other components are supplied.

IAM Policies

Don’t write your own (*unless you have to)

Sometimes you have to construct your own IAM policy:

const taskExecutionRolePolicyStatement = new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  actions: [
    'iam:PassRole',
  ],
  resources: [
    taskDefinition.obtainExecutionRole().roleArn,
    taskDefinition.taskRole.roleArn,
  ]
});
extractLambda.addToRolePolicy(taskExecutionRolePolicyStatement);

source

But often it’s better to use the various .grantXXXsdfasdfadf helpers:

new dynamo.Table(...).grantReadWriteData(someLambda);

In this case, the someLambda function will get access to read and write data from the new Table. No concern about what policies you need or anything.

Reference managed policies

You can also easily reference existing managed policies:

iam.ManagedPolicy.fromAwsManagedPolicyName(
  "service-role/AWSLambdaBasicExecutionRole"
)

S3 Object Arns

You often need a resource Arn for objects in an S3 Bucket. The Bucket construct has a handy function for this.

bucket.arnForObjects("*")

source

Fn

Fn is directly related to the intrinsic CloudFormation helper functions. You can use these in a similar manner.

policies: [
    {
        statement: new PolicyStatement({
            actions: ['dynamodb:Batch*', 'dynamodb:DeleteItem', 'dynamodb:Get*', 'dynamodb:PutItem', 'dynamodb:UpdateItem', 'dynamodb:Query', 'dynamodb:Scan'],
            // tslint:disable-next-line:no-invalid-template-strings
            resources: [Fn.sub('arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${AttributesTable}')],
        }),
    },
],

source

Schedule

When using CloudWatch events it’s often easiest to use the .cron helper for strongly typed schedules:

schedule: Schedule.cron({
    minute: '05',
    hour: '15',
    month: '*',
    weekDay: '*',
    year: '*',
})

source

Elastic Container Services

If you’re working with Elastic Container Services you’re likely to need a few extra helpers along the way:

obtainExecutionRole()

taskDefinition.obtainExecutionRole().roleArn

source

fromRegistry

taskDef.addContainer("AppContainer", {
  image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
})

source

Placement Strategies

service.addPlacementStrategies(
  ecs.PlacementStrategy.packedBy(ecs.BinPackResource.MEMORY),
  ecs.PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE));

source

EC2

Sometimes image management is a pain to get good references, check out some of the helper classes:

const asg = new autoscaling.AutoScalingGroup(this, 'ASG', {
  vpc,
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
  machineImage: new ec2.AmazonLinuxImage(),
});

source

const asg = new autoscaling.AutoScalingGroup(this, 'MyFleet', {
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.XLARGE),
  machineImage: new ecs.EcsOptimizedAmi(),
  updateType: autoscaling.UpdateType.REPLACING_UPDATE,
  desiredCapacity: 3,
  vpc,
});

Connections and Security Groups

Many of the AWS Resources will have a .connections property that is an IConnectable interface. There are some helpers here for setting up access. See more here.

listener.connections.allowDefaultPortFromAnyIpv4('Open to the world');

source

ecsService.service.connections.allowFromAnyIpv4(EPHEMERAL_PORT_RANGE);

source

sg.addIngressRule(Peer.ipv4(ip), Port.tcp(22), "Ssh Client incoming")

source

Overrides

Overrides are a nice release-valve and aren’t terribly common, but here are some good examples:

const bucketResource = bucket.node.defaultChild as s3.CfnBucket;
const anotherWay = bucket.node.children.find(c => (c as cdk.CfnResource).cfnResourceType === 'AWS::S3::Bucket') as s3.CfnBucket;

source

bucketResource.node.addDependency(otherBucket.node.defaultChild as cdk.CfnResource);
bucketResource.cfnOptions.metadata = { MetadataKey: 'MetadataValue' };
bucketResource.cfnOptions.updatePolicy = {
    autoScalingRollingUpdate: {
        pauseTime: '390'
    }
};

Route 53

When setting up Route53, you’ll use the RecordTarget factories

const aTarget = RecordTarget.fromIpAddresses("192.168.0.11","192.168.0.13")

const aRecord = new ARecord(this,"ARecord", {
  target: aTarget,
  comment: "Demo  Record",
  ttl: cdk.Duration.seconds(300),
  recordName: "www",
  zone: demoZone,
});

source

Conclusion

A lot of the CDK API is self-explanitory. When it’s not, helper functions usually fill in the gaps.

If you see any that I’ve missed, submit an Issue/PR on the Github page