DEV Community

Kenta Goto for AWS Heroes

Posted on

AWS CDK Implementation Constraints and How jsii Enables Multi-language Support

Target Audience

  • AWS CDK contributors
  • Custom Construct library developers
    • Those who use tools like projen to publish to Construct Hub for compatibility with languages other than TypeScript
  • Other core AWS CDK fans
    • Those interested in the above two points
    • Those interested in CDK's own mechanisms and the CDK ecosystem

AWS CDK Implementation Constraints

CDK and TypeScript

AWS CDK is an IaC tool that can be implemented using several programming languages, but I believe many users implement CDK using TypeScript.

Some possible reasons for this include that AWS CDK itself, which is OSS, is implemented in TypeScript, and the official documentation contains many TypeScript sample codes. (Within my personal observation range, I have the impression that many materials appearing in the community, such as presentations and blogs, use TypeScript.)

TypeScript-specific implementation methods are deprecated

TypeScript has several convenient notations, and I think many people use them extensively when actually writing CDK code.

However, some of these TypeScript-specific notations are deprecated "in the context of CDK contributions and publishing custom Constructs to Construct Hub".

*This is only applicable to the above areas. When you usually develop products using CDK, this limitation does not apply. Feel free to use them without any problems.

For example, Union types allow a variable to have any of multiple types (or values), and I think many people often use them when writing CDK in TypeScript.

However, this Union type is deprecated in CDK contributions.

export interface MyConstructProps { readonly myValue: 'VALUE_A' | 'VALUE_B'; readonly myType: MyTypeA | MyTypeB; } export class MyConstruct extends Construct { constructor(scope: Construct, id: string, props: MyConstructProps) { // ... } } 
Enter fullscreen mode Exit fullscreen mode

Alternative to Union Types

The reason why Union types are deprecated in CDK contributions will be explained later, but as an alternative, we solve this with an implementation technique called "Union-Like Class" as follows.

*CDK's design guidelines also describe that "Union types are deprecated" and their alternatives.

Design Guidelines - Unions

Example of Union-Like Class:

  • Want to create a Union type consisting of three types: InlineCode, AssetCode, and S3Code
  • Create an abstract class called Code and have those types inherit from this class
  • Create static methods called fromFoo in that abstract class that return instances of each class
    • Naming doesn't have to be fromFoo
  • Specify the type of that abstract class for properties you want to specify as Union types, and generate and pass instances of each type with fromFoo methods
export abstract class Code { public static fromInline(code: string): InlineCode { return new InlineCode(code); } public static fromAsset(assetPath: string, options?: s3_assets.AssetOptions): AssetCode { return new AssetCode(assetPath, options); } public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3Code { return new S3Code(bucket, key, objectVersion); } } export class InlineCode extends Code { // ... } export class AssetCode extends Code { // ... } export class S3Code extends Code { // ... } export interface MyConstructProps { readonly code: Code; // InlineCode | AssetCode | S3Code } new MyConstruct(scope, 'MyConstruct', { code: Code.fromAsset('...'), // When passing AssetCode type }); 
Enter fullscreen mode Exit fullscreen mode

Also, Union types consisting of concrete values like 'VALUE_A' | 'VALUE_B' can be replaced by simply defining them as enum types.

export enum MyEnum { VALUE_A = 'VALUE_A', VALUE_B = 'VALUE_B', } export interface MyConstructProps { readonly myValue: MyEnum; // 'VALUE_A' | 'VALUE_B' } new MyConstruct(scope, 'MyConstruct', { myValue: MyEnum.VALUE_A, }); 
Enter fullscreen mode Exit fullscreen mode

*When developing applications in TypeScript normally, enum types are often not used or recommended, but this is not the case for CDK code. I won't touch on the reason here, but I think the next chapter will give you a hint.

*There is also something called "Enum-Like Class" as a CDK implementation tip. Roughly speaking, it's a pattern frequently seen in CDK that allows users to specify arbitrary values in addition to regular enum elements (originally also in the context of DDD). Please see below.

Design Guidelines - Enums

export class Cpu { /** * 1 vCPU */ public static readonly ONE_VCPU = new Cpu('1 vCPU'); /** * 2 vCPU */ public static readonly TWO_VCPU = new Cpu('2 vCPU'); /** * Custom CPU unit * * @param unit custom CPU unit */ public static of(unit: string): Cpu { return new Cpu(unit); } /** * * @param unit The unit of CPU. */ private constructor(public readonly unit: string) {} } new MyConstruct(scope, 'MyConstruct1', { cpu: Cpu.ONE_VCPU, }); new MyConstruct(scope, 'MyConstruct2', { cpu: Cpu.of('4 vCPU'), }); 
Enter fullscreen mode Exit fullscreen mode

Multi-language Support Tool jsii

What is jsii

In the above chapter, I explained that TypeScript notations like Union types are deprecated in CDK contributions and similar contexts.

Before explaining the reason, let me explain how AWS CDK achieves multi-language support, even though it can be written in several programming languages.

Specifically, how is it possible to use AWS CDK itself (or Construct libraries) written in TypeScript with languages other than TypeScript?

The answer lies in a tool called jsii.

jsii is a tool used in AWS CDK to convert code (modules) defined in TypeScript so that they can be used by code in other languages (it can also be used for code other than AWS CDK).

Taking an example from jsii's official documentation, it converts something defined in TypeScript as shown below so that it can be used in each language.

  • TypeScript
export class Greeter { public greet(name: string) { return `Hello, ${name}!`; } } 
Enter fullscreen mode Exit fullscreen mode
  • C#
var greeter = new Greeter(); greeter.Greet("World"); // => Hello, World! 
Enter fullscreen mode Exit fullscreen mode
  • Go
greeter := NewGreeter() greeter.Greet("World") // => Hello, World! 
Enter fullscreen mode Exit fullscreen mode
  • Java
final Greeter greeter = new Greeter(); greeter.greet("World"); // => Hello, World! 
Enter fullscreen mode Exit fullscreen mode
  • JavaScript
const greeter = new Greeter(); greeter.greet('World'); // => Hello, World! 
Enter fullscreen mode Exit fullscreen mode
  • Python
greeter = Greeter() greeter.greet("World") # => Hello, World! 
Enter fullscreen mode Exit fullscreen mode

Why TypeScript-specific Implementation Methods are Deprecated

As you may have guessed from jsii's role described above, it's because TypeScript-specific implementation methods may not be supported in other languages.

CDK can be written in several programming languages, but regarding the Union type example mentioned earlier, languages like Go do not have Union types.

In this case, jsii doesn't throw an error when converting to each language, but converts to an irreversibly loose type.

Specifically, what is a Union type consisting of multiple classes in TypeScript gets converted to just an object type or a type like any in TypeScript in other languages. (Union types consisting of concrete values like 'VALUE_A' | 'VALUE_B' are converted to string types.)

*Strictly speaking, it's converted to "the most generic reference type available".

*For details, refer to jsii documentation.

jsii Type System - Type Unions

  • TypeScript
public myMethod(myType: MyTypeA | MyTypeB) { ... } 
Enter fullscreen mode Exit fullscreen mode
  • Java
public void myMethod(Object myType) { ... } 
Enter fullscreen mode Exit fullscreen mode
  • C#
public void MyMethod(object myType) { ... } 
Enter fullscreen mode Exit fullscreen mode
  • Go
func (this *Sample) myMethod(myType interface{}) { ... } 
Enter fullscreen mode Exit fullscreen mode

How jsii Works

I mentioned that AWS CDK code is converted from TypeScript to other languages by jsii, which allows users to write CDK in various languages.

So, what kind of mechanism makes this possible?

First, TypeScript Construct code defined in AWS CDK itself or Construct libraries is converted to libraries for each language by jsii (this is done on the library provider side, not the user side).

When actually writing CDK code in each language, users use (install/import) libraries generated by jsii and call those Constructs within CDK code according to each language's writing method.

At this time, each library bundles Construct code (actually Javascript code) written in AWS CDK itself or Construct libraries, and the actual processing of those Constructs is executed not by the process of each CDK project's implementation language, but by processing through javascript code.

In other words, those libraries run on the node (nodejs) runtime as a separate process from the host of each language where users are writing CDK code in their CDK projects. Therefore, to execute code that depends on jsii libraries, the node runtime must be available.

*Although the node runtime is already required for executing CDK CLI, this means that the node runtime is also required for CDK unit tests that can normally be executed from pytest etc. without going through CDK CLI.

jsii architecture

This ultimately means that through types defined by library creators assuming use in each language, such as "not using Union types" mentioned earlier, values are just being passed to Javascript-side internal processing.

From this point, we can understand that as long as the input and output types to that Construct are in a form that can be used in each language, there is no need to consider use in each language for the actual processing, i.e., internal implementation.

Therefore, there is (basically) no problem with using TypeScript-specific writing styles in internal processing.

(In fact, AWS CDK's own Construct code also uses TypeScript-specific writing styles in internal processing in some cases: user-defined type guard functions, generics, etc.)

*For details about jsii's mechanism (runtime architecture), refer to jsii documentation.

jsii Runtime Architecture


Summary

While CDK code is often written in TypeScript, I discussed how TypeScript-specific implementation methods are deprecated when contributing to CDK (AWS CDK main code) or publishing custom Construct libraries.

I also explained the reason for this through the existence and mechanism of jsii, a multi-language support tool.

However, this limitation does not apply when you usually develop products using CDK, so feel free to use them without any concerns. But I think this is good knowledge for those who will contribute to CDK in the future or those who publish custom Constructs as libraries assuming use in multiple languages.

Top comments (0)