Managing complex infrastructure using AWS CDK and Go

Managing complex infrastructure using AWS CDK and Go

In a previous article we dove into the topic of "Managing K8S Infrastructure and Applications on AWS". There, using the AWS Cloud Development Kit (CDK) and the Python language, we saw how we can manage a complete solution around Kubernetes (K8s) on AWS; that is deploying the whole infrastructure as well as the workloads from a single codebase using one high level programming language, namely Python. As promised by the end of that article, I was going to template the same project in other programming languages supported by the CDK family of frameworks. The time has come to reproduce the same solution using Golang.

In this article we are not going to discuss K8s infrastructure and the applications. You can find all this information in the original article referenced above. In the attempt to rewrite code in Go that would produce very similar output to my original project in Python, I stumbled upon several challenges. I would like to share this journey with you and hopefully help anybody who is interested in getting started with the AWS CDK in combination with the Go language.

Project Structure

There are several examples one can find online on writing Infrastructure as Code (IaC) using the AWS CDK and Go. All of these examples are focused on a single stack solutions that deploys very limited amount of resources. These examples can be very helpful when trying to identify how you can get started with the AWS CDK and the Go language. For me, starting to work on an IaC project that would potentially would end up having a high number of resources, it is very important to structure the solution accordingly. AWS for several years now supports the concept of Nested Stacks. I believe, it is of paramount importance for any production grade project of non trivial complexity on AWS to take advantage of Nested Stacks.

While looking around for best practices of how one would structure an AWS CDK project in Go, to my surprise I could not find any information other than single file - single stack examples. I would like to share with you here my two cents on how I would structure any complex IaC project.

Folder Structure

With the idea in mind that I would use many Nested Stacks and different configurations per deployed environment (I have written a separate article on this which can be found here), this would be a boilerplate of how the folder structure should look like:

├── charts/
 │   ├── cdk8s-chart-1.go
 │   ├── cdk8s-chart-2.go
├── config/
 │   ├── development.yaml
 │   ├── test.yaml
 │   ├── acceptance.yaml
 │   ├── production.yaml
├── helper/
 │   ├── helper-functions-1.go
 │   ├── helper-functions-2.go
├── stacks/
 │   ├── nested-stack-1.go
 │   ├── nested-stack-2.go
 │   ├── nested-stack-3.go
├── cdk.context.json
├── cdk.json
├── parent-stack-main.go
└──

Go Packages

Go is renowned for its simplicity and ease of use, and one of the fundamental ways it achieves this is through its package system. Packages play a pivotal role in structuring and organizing Go code, making it modular, reusable, and maintainable. They provide a mechanism for code encapsulation, collaboration, and code reusability, which are crucial aspects of any programming language.

The primary purpose of Go packages is to create logical and self-contained units of code that can be imported and utilized in other parts of the application. These packages act as containers for related functions, types, variables, and other components that collectively solve a specific problem or provide a certain functionality.

With the aforementioned folder structure in mind, the application could be split into 4 packages:

  • main

  • charts

  • helper

  • stacks

More packages could also be used, for example if someone would want to separate the nested stacks further. The above 4 packages would be the minimum recommended.

Passing values between stacks

Most of the languages supported by the CDK family of frameworks are employing Object Oriented Programming (OOP) concepts. Although not all of them are pure OOP languages, the concepts behind object-oriented software deployment apply: You can split (nested) stacks in classes and you can expose resources and values by making them publicly available in your application.

Go is not an OOP language. How could you share then resources between stacks?

You can make them available as return values of the stack functions.

Let's assume you want to create a NetworkingStack that creates and shares the VPC with all the other Nested Stacks. How can you achieve this in Go?

Let's create a networking-stack.go file within the stacks/ folder. The contents of the file should looks like this:

package stacks

import (
    "github.com/aws/aws-cdk-go/awscdk/v2"
    "github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
    "github.com/aws/constructs-go/constructs/v10"
    // https://docs.aws.amazon.com/sdk-for-go/api/aws/
    "github.com/aws/aws-sdk-go-v2/aws"
)

type NetworkingNestedStackProps struct {
    awscdk.NestedStackProps
}

func NetworkingStack(scope constructs.Construct, id string, props *NetworkingNestedStackProps) (awscdk.NestedStack, awsec2.IVpc) {
    var nsprops awscdk.NestedStackProps
    if props != nil {
        nsprops = props.NestedStackProps
    }
    stack := awscdk.NewNestedStack(scope, &id, &nsprops)

    vpc := awsec2.NewVpc(stack, aws.String("sample-vpc"), 
        &awsec2.VpcProps{
            IpAddresses: awsec2.IpAddresses_Cidr(aws.String("10.0.0.0/16"),
            SubnetConfiguration: &[]*awsec2.SubnetConfiguration{
                {
                    CidrMask: aws.Float64(28),
                    SubnetType: awsec2.SubnetType_PUBLIC,
                    Name: aws.String("public"),
                },
                {
                    CidrMask: aws.Float64(28),
                    SubnetType: awsec2.SubnetType_PRIVATE_WITH_EGRESS,
                    Name: aws.String("private"),
                },
            },
        },
    )

    return stack, vpc
}

As you can see, the NetworkingStack defines to return values. One is of type awscdk.NestedStack and the other of type awsec2.IVpc. Finally, in the bottom, both values are returned with the return stack, vpc statement.

In the main() function then, we can instantiate a new stack that we want to pass the value of the Vpc. We can do that as follows:

package main

import (
    "parent-stack-main/stacks"
         ...
)

func main() {
    defer jsii.Close()

    app := awscdk.NewApp(nil)

         stack := NewParentStack(app, "NewParentStack", &NewParentStackProps{...})

    _, nsvpc := stacks.NetworkingStack(stack, "NetworkingStack", nil)
    stacks.MyNewNestedStack(stack, "MyNewNestedStack", &stacks.MyNewNestedStackProps{
        Vpc: nsvpc,
    })

    app.Synth(nil)
}

Conclusion

You can find the whole codebase that replicates the solution described in "Managing K8S Infrastructure and Applications on AWS" rewritten in Go in GitHub. By comparing the Python and the Go implementation, you can further identify differences and solutions on how you can implement the same functionality but in these different high level programming languages. I hope you find this useful to get you going in the world of AWS CDK using Go.

Main image by svstudioart on Freepik

*This article was originally posted here.