Features of AWS CDK for Go (differences from TypeScript)
Tags: aws, awscdk, go, typescript
Published: 2023-06-15 12:04:03

AWS CDK is often written in TypeScript, but can be written in a variety of programming languages.

In this article, I would like to write about the features of AWS CDK by Golang compared to TypeScript.

## Assumptions

The Golang codes presented here use v1.18.7.

The AWS CDK version is v2.52.0.

The features of AWS CDK for Go that I will discuss in this article are mainly the **differences from the TypeScript version**.

The basic features can be found in [the document here](https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-go.html).

## Features (differences from TypeScript)

### Directory structure at init

The directory structure created after `cdk init app --language go`, in the case of Go, is as follows.

The directory (repository) named `cdk-go`.

```
❯ tree
.
├── README.md
├── cdk-go.go
├── cdk-go_test.go
├── cdk.json
└── go.mod
```

In the case of TypeScript, the `bin`, `lib` and `test` directories are separated, and modules are installed based on package.json at the same time as init, and `node_modules` are also generated.

Go is much simpler and has no hierarchical structure, which is probably due to the flat directory structure often used when developing in Go.

### File contents at init

In Go, only `cdk-go.go` and `cdk-go_test.go` Go files are generated, as shown above.

In TypeScript, the stack generation code is written in the file generated in `bin` and the stack definition code in the file generated in `lib`, but in Go, both are written together in the same single file (`cdk-go.go`).

This is the initial code of `cdk-go.go`.

```go
package main

import (
	"github.com/aws/aws-cdk-go/awscdk/v2"
	// "github.com/aws/aws-cdk-go/awscdk/v2/awssqs"
	"github.com/aws/constructs-go/constructs/v10"
	"github.com/aws/jsii-runtime-go"
)

type CdkGoStackProps struct {
	awscdk.StackProps
}

func NewCdkGoStack(scope constructs.Construct, id string, props *CdkGoStackProps) awscdk.Stack {
	var sprops awscdk.StackProps
	if props != nil {
		sprops = props.StackProps
	}
	stack := awscdk.NewStack(scope, &id, &sprops)

	// The code that defines your stack goes here

	// example resource
	// queue := awssqs.NewQueue(stack, jsii.String("CdkGoQueue"), &awssqs.QueueProps{
	// 	VisibilityTimeout: awscdk.Duration_Seconds(jsii.Number(300)),
	// })

	return stack
}

func main() {
	defer jsii.Close()

	app := awscdk.NewApp(nil)

	NewCdkGoStack(app, "CdkGoStack", &CdkGoStackProps{
		awscdk.StackProps{
			Env: env(),
		},
	})

	app.Synth(nil)
}

// env determines the AWS environment (account+region) in which our stack is to
// be deployed. For more information see: https://docs.aws.amazon.com/cdk/latest/guide/environments.html
func env() *awscdk.Environment {
	// If unspecified, this stack will be "environment-agnostic".
	// Account/Region-dependent features and context lookups will not work, but a
	// single synthesized template can be deployed anywhere.
	//---------------------------------------------------------------------------
	return nil

	// Uncomment if you know exactly what account and region you want to deploy
	// the stack to. This is the recommendation for production stacks.
	//---------------------------------------------------------------------------
	// return &awscdk.Environment{
	//  Account: jsii.String("123456789012"),
	//  Region:  jsii.String("us-east-1"),
	// }

	// Uncomment to specialize this stack for the AWS Account and Region that are
	// implied by the current CLI configuration. This is recommended for dev
	// stacks.
	//---------------------------------------------------------------------------
	// return &awscdk.Environment{
	//  Account: jsii.String(os.Getenv("CDK_DEFAULT_ACCOUNT")),
	//  Region:  jsii.String(os.Getenv("CDK_DEFAULT_REGION")),
	// }
}
```

The struct `CdkGoStackProps` has functions `NewCdkGoStack`, `main` and `env`.

`NewXxxStack` is the stack definition (in `lib` in TypeScript) and `main` is the stack generation (in `bin` in TypeScript).

The `defer jsii.Close()` in the `main()` function is to make the CDK application clean up automatically (see official reference above).

The function `env` is not found in the TypeScript version, but is unique to the Go version (written from the beginning), and returns an `awscdk.Environment` structure with `Account` and `Region`.

By the way, the test codes at initialization are here.

```go
package main

// import (
// 	"testing"

// 	"github.com/aws/aws-cdk-go/awscdk/v2"
// 	"github.com/aws/aws-cdk-go/awscdk/v2/assertions"
// 	"github.com/aws/jsii-runtime-go"
// )

// example tests. To run these tests, uncomment this file along with the
// example resource in cdk-go_test.go
// func TestCdkGoStack(t *testing.T) {
// 	// GIVEN
// 	app := awscdk.NewApp(nil)

// 	// WHEN
// 	stack := NewCdkGoStack(app, "MyStack", nil)

// 	// THEN
// 	template := assertions.Template_FromStack(stack)

// 	template.HasResourceProperties(jsii.String("AWS::SQS::Queue"), map[string]interface{}{
// 		"VisibilityTimeout": 300,
// 	})
// }
```

### Pointer conversion with jsii

Looking at the above code, in some places, especially when specifying parameters, functions such as `jsii.Number`, `jsii.String` surround the value specified by the parameter.

```go
	queue := awssqs.NewQueue(stack, jsii.String("CdkGoQueue"), &awssqs.QueueProps{
		VisibilityTimeout: awscdk.Duration_Seconds(jsii.Number(300)),
	})
```

The `jsii.Number` and `jsii.String` are functions that **only return a pointer to the variable you give them**.

In fact, this is **a rule** when specifying parameters in the Go version of CDK.

Because the Go language does not have a null-allowed type, and the only type that can contain `nil` is the pointer type. (The word "only" may be misleading, but I am not going to go into Go here, but translate the above  document.)

So, to realize "no property" for an optional parameter, I can pass a pointer type that can be `nil` (see document above).

This is the main area of use.

- jsii.String
- jsii.Number
- jsii.Bool
- jsii.Time

This is one of the major and troublesome features of the Go version of the CDK, but once you get used to it, it is nothing(?). I think it's not a problem once you get used to it. (It will make the outlook worse, but...)

### Field and method names in CDK module are Pascal case

In the TypeScript version, I think it is CamelCase, but in the Go version, basically the field and method names provided in **CDK are PascalCase (beginning with a capital letter)** (see the above  document).

This is Go's language specification that Public (out-of-package) calls can only be invoked with Pascal case.

When coding in Go, if the scope is Private (properly, a package), it will be CamelCase.

Here's an example: be careful when you try to bring a sample written in TypeScript to Go by copying it.

```go
cfnAppRunner.SetHealthCheckConfiguration(&awsapprunner.CfnService_HealthCheckConfigurationProperty{
	Path:     jsii.String("/"),
	Protocol: jsii.String("HTTP"),
})
```

### Cannot do escape hatch.

Yes, I am having trouble with the escape hatch. This is a **very big problem** unique to the Go version.

First, CDK has a convenient feature called L2 Construct that allows you to build resources according to best practices by simply specifying a few required values.

There is also L1 Construct, which allows you to build resources by specifying them in a manner consistent with CloudFormation.

While L2 Construct is convenient and easy to create resources, it also abstracts parameters, so there are some parameters that **are not specified in L2**.

Therefore, there is a way to cast a resource created with L2 Construct to an L1 Construct type and later override the parameters that can only be specified with L1, which is the "escape hatch".

However, the CDK for Go does not currently support escape hatch according to [the workshop](https://catalog.us-east-1.prod.workshops.aws/workshops/d93fec4c-fb0f-4813-ac90-758cb5527f2f/en-US/walkthrough/go/sample/source-construct/s3-bucket).

> CDK has a concept called the escape hatch , which allows you to modify an L2 construct's underlying CFN Resource to access features that are supported by CloudFormation but not yet supported by CDK. Unfortunately, CDK for Go does not yet support this functionality, so I have to create the resource through the L1 construct. See this GitHub issue for more information and to follow progress on support for CDK escape hatches in Go.

So what do you do, if you want to specify a parameter that can't be specified in L2 Construct, it has to be L1 Construct?

In the meantime, there is a way to do an escape hatch. You can do it by using the method **`jsii.Get`**.

```go
var cfnAppRunner awsapprunner.CfnService
jsii.Get(apprunnerServiceL2.Node(), "defaultChild", &cfnAppRunner)

cfnAppRunner.SetAutoScalingConfigurationArn(autoScalingConfigurationArn)
```

However, this **`jsii.Get` is deprecated** (see [this page](https://pkg.go.dev/github.com/aws/jsii-runtime-go#Get)), so I feel like I have no choice but to use it.

### Snapshot test

The "snapshot test" is a test that you will almost certainly see as a CDK test.

You can do it with `jest` in TypeScript (javascript), but in Go, you can do it as follows.

Generate a template from the stack and pass the JSONized version to `cupaloy.SnapshotT` to execute the snapshot.

```go
	stack := NewAppRunnerStack(app, "AppRunnerStack", appRunnerStackProps)

	template := assertions.Template_FromStack(stack, nil)
	templateJson := template.ToJSON()

	t.Run("Snapshot Test", func(t *testing.T) {
		cupaloy.SnapshotT(t, templateJson)
	})
```

## Extra

The following is not so much a difference from TypeScript, but more like a development method unique to Go.

### Workspaces mode

When developing with CDK, you probably have multiple code groups.

First, there is the CDK code group, and then there is the code group used for Lambda and ECS.

In such cases, it is possible to prepare those directories in one repository and build them by sharing go.mod in the root directory, but you will want to prepare a go.mod that **combines what is needed in each one (multi-module)**.

(By the way, in TypeScript, you can use `aws-lambda-nodejs` to get a nice bundle of Lambda and CDK definitions in a single package.json file, respectively.)

Go introduced a feature called ["Workspaces mode"](https://go.dev/ref/mod#workspaces) in version 1.18 that allows multi-module (easier than before).

This allows for multi-module development and build with a go.mod for each subdirectory.

Hitting the following command will put you in Workspaces mode.

```sh
go work init moduleA moduleB
```

The following go.work file will then be created to enable multi-module development.

```
go 1.18

use (
	./moduleA
	./moduleB
)
```

## Example Implementation

There is [an actual example](https://github.com/go-to-k/go-cdk-go-managed-apprunner) that I made with AWS CDK for Go, if you would like to see it. (In the example, I am making App Runner.)

## Finally

I have summarized my experience with Go's CDK because I found it to be quite different from the TypeScript version.

To be honest, it is still in its infancy compared to the TypeScript version of CDK, but I am glad to be able to write in Go!