Check for CDK Version 2
cdk --version
If version is 1, goto Chapter CDK with GO start and install V2
Create a directory and create the skeleton in it.
mkdir vpc
cd vpc
cdk init app --language=go
Now you have a base setup - which would create a SNS topic.
The init app creates a monolithic setup, so we create modules. If you are unsure about GO modules, read Chapter GO modules.
The skeleton does not use modules. For very small apps thats ok, but if you have more complex setups modules are better.
mkdir main
cp vpc.go main/main.go
package main
import (
	"github.com/aws/aws-cdk-go/awscdk/v2"
	"vpc"
)
func main() {
	app := awscdk.NewApp(nil)
	vpc.NewVpcStack(app, "basevpc", &vpc.VpcStackProps{
		awscdk.StackProps{
			Env: env(),
		},
	})
	app.Synth(nil)
}
func env() *awscdk.Environment {
	return nil
}
  1 package main
  2
  3 import (
  4         "github.com/aws/aws-cdk-go/awscdk/v2"
  5         "vpc"
  6 )
  7
  8 func main() {
  9         app := awscdk.NewApp(nil)
 10
 11         vpc.NewVpcStack(app, "basevpc", &vpc.VpcStackProps{
 12                 awscdk.StackProps{
 13                         Env: env(),
 14                 },
 15         })
 16
 17         app.Synth(nil)
 18 }
 19
 20 func env() *awscdk.Environment {
 21         return nil
 22
 23 }
Update main/main.go in the directory vpc/main as shown above.
The changes are:
Now the main file is done.
package vpc
import (
        "github.com/aws/aws-cdk-go/awscdk/v2"
        "github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
        "github.com/aws/aws-cdk-go/awscdk/v2/awsssm"
        "github.com/aws/aws-sdk-go-v2/aws"
        "github.com/aws/constructs-go/constructs/v10"
)
type VpcStackProps struct {
        awscdk.StackProps
}
func NewVpcStack(scope constructs.Construct, id string, props *VpcStackProps) awscdk.Stack {
        var sprops awscdk.StackProps
        if props != nil {
                sprops = props.StackProps
        }
        stack := awscdk.NewStack(scope, &id, &sprops)
        myVpc := awsec2.NewVpc(stack, aws.String("basevpc"),
                &awsec2.VpcProps{
                        Cidr: aws.String("10.0.0.0/16"),
                        MaxAzs: aws.Float64(1),
                },
        )
        awsssm.NewStringParameter(stack, aws.String("basevpc-parm"),
                &awsssm.StringParameterProps{
                        Description:    aws.String("Created VPC"),
                        ParameterName:  aws.String("/network/basevpc"),
                        StringValue:    myVpc.VpcId(),
                },
        )
        return stack
}
  1 package vpc
  2
  3 import (
  4         "github.com/aws/aws-cdk-go/awscdk/v2"
  5         "github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
  6         "github.com/aws/aws-cdk-go/awscdk/v2/awsssm"
  7         "github.com/aws/aws-sdk-go-v2/aws"
  8         "github.com/aws/constructs-go/constructs/v10"
  9 )
 10
 11 type VpcStackProps struct {
 12         awscdk.StackProps
 13 }
 14
 15 func NewVpcStack(scope constructs.Construct, id string, props *VpcStackProps) awscdk.Stack {
 16         var sprops awscdk.StackProps
 17         if props != nil {
 18                 sprops = props.StackProps
 19         }
 20         stack := awscdk.NewStack(scope, &id, &sprops)
 21
 22         myVpc := awsec2.NewVpc(stack, aws.String("basevpc"),
 23                 &awsec2.VpcProps{
 24                         Cidr: aws.String("10.0.0.0/16"),
 25                         MaxAzs: aws.Float64(1),
 26                 },
 27         )
 28
 29         awsssm.NewStringParameter(stack, aws.String("basevpc-parm"),
 30                 &awsssm.StringParameterProps{
 31                         Description:    aws.String("Created VPC"),
 32                         ParameterName:  aws.String("/network/basevpc"),
 33                         StringValue:    myVpc.VpcId(),
 34                 },
 35         )
 36
 37         return stack
 38 }
Update vpc.go in the directory vpc (the base directory) as shown above.
The changes are:
A word about SSM parameters. When working with different stacks, there are several method to feed outputs of one stack into another stack. You may just use attributes and return values inside the language. Thats leads to a coupling of the stacks. That means, if you deploy one stack, all dependent stacks will be deployed, if something is changed there. Thats fine for environments which may be updated all-stacks-at-once. If you want to decouple stacks, working with parameters is the way to go.
Now the app itself is done. Last step is the test file.
Before that we have to synth the template once:
cdk synth
If everything is ok, the file cdk.out/basevpc.template.json is generated. The firs lines are:
{
  "Resources": {
    "basevpc24F855EE": {
      "Type": "AWS::EC2::VPC",
      "Properties": {
        "CidrBlock": "10.0.0.0/16",
In this example basevpc24F855EE is the CloudFormation logical name of the vpc resource. We need this name for the unit tests in the next step. `
package vpc_test
import (
        "encoding/json"
        "testing"
        "vpc"
        "github.com/aws/aws-cdk-go/awscdk/v2"
        "github.com/stretchr/testify/assert"
        "github.com/tidwall/gjson"
)
func TestVpcStack(t *testing.T) {
        // GIVEN
        app := awscdk.NewApp(nil)
        // WHEN
        stack := vpc.NewVpcStack(app, "MyStack", nil)
        // THEN
        bytes, err := json.Marshal(app.Synth(nil).GetStackArtifact(stack.ArtifactId()).Template())
        if err != nil {
                t.Error(err)
        }
        template := gjson.ParseBytes(bytes)
        cidr := template.Get("Resources.basevpc24F855EE.Properties.CidrBlock").String()
        assert.Equal(t, "10.0.0.0/16", cidr)
}
  1 package vpc_test
  2
  3 import (
  4         "encoding/json"
  5         "testing"
  6
  7         "vpc"
  8
  9         "github.com/aws/aws-cdk-go/awscdk/v2"
 10         "github.com/stretchr/testify/assert"
 11         "github.com/tidwall/gjson"
 12 )
 13
 14 func TestVpcStack(t *testing.T) {
 15         // GIVEN
 16         app := awscdk.NewApp(nil)
 17
 18         // WHEN
 19         stack := vpc.NewVpcStack(app, "MyStack", nil)
 20
 21         // THEN
 22         bytes, err := json.Marshal(app.Synth(nil).GetStackArtifact(stack.ArtifactId()).Template())
 23         if err != nil {
 24                 t.Error(err)
 25         }
 26
 27         template := gjson.ParseBytes(bytes)
 28         cidr := template.Get("Resources.basevpc24F855EE.Properties.CidrBlock").String()
 29         assert.Equal(t, "10.0.0.0/16", cidr)
 30 }
Update vpc_test.go as shown above.
The changes are:
Short version
go test
Output:
PASS
ok  	vpc	4.737s
Long version
go test -v
Output
=== RUN   TestVpcStack
--- PASS: TestVpcStack (4.39s)
PASS
ok  	vpc	4.905s
Why do these tests, if we have to put in the logical name manually?
With the test you can now check changes and of the CloudFormation template is generated ok. So changing something, like the cidr range would mean:
With this simple vpc this seems trivial and not worth the effort. But when programs become more complex, the test give you a safety net. Especially when you start using computed cidr ranges e.g. out of a database (or an excel file), some tests really make your life better.
cdk deploy
Output
basevpc: deploying...
[0%] start: Publishing 989bae47474159e1edd440abd0dc1a557dba752fc4c2d7faf96a00fdc817043c:current_account-current_region
[100%] success: Published 989bae47474159e1edd440abd0dc1a557dba752fc4c2d7faf96a00fdc817043c:current_account-current_region
basevpc: creating CloudFormation changeset...
 ✅  basevpc
Stack ARN:
arn:aws:cloudformation:eu-central-1:795048271754:stack/basevpc/df55f150-3240-11ec-94f8-065e3dcecf82
You may check the resources in the AWS console, or with cdkstat
Create stacks.csv
cdk ls >stacks.csv
Check deployment status of the stack vpc:
 cdkstat
Output before deployment:
Name                             Status                           Description
----                             ------                           -----------
basevpc                          LOCAL_ONLY
Output after deployment:
Name                             Status                           Description
----                             ------                           -----------
basevpc                          CREATE_COMPLETE                  -
Challenge: Add an description to the stack!
To check all created resources:
cdkstat basevpc
Which shows you:
Logical ID                       Pysical ID                       Type                             Status
----------                       ----------                       -----------                      -----------
CDKMetadata                      564824a0-324a-11ec-91fe-0a953ea  AWS::CDK::Metadata               CREATE_COMPLETE
basevpc24F855EE                  vpc-0d50cd3c702d69630            AWS::EC2::VPC                    CREATE_COMPLETE
basevpcIGWF722C55C               igw-0cb1ef7dbfce83191            AWS::EC2::InternetGateway        CREATE_COMPLETE
basevpcPrivateSubnet1DefaultRou  basev-basev-19L6M1CE07F51        AWS::EC2::Route                  CREATE_COMPLETE
basevpcPrivateSubnet1RouteTable  rtb-063a9d28de07fdfaa            AWS::EC2::RouteTable             CREATE_COMPLETE
basevpcPrivateSubnet1RouteTable  rtbassoc-0b43ef72b20773cbb       AWS::EC2::SubnetRouteTableAssoc  CREATE_COMPLETE
basevpcPrivateSubnet1SubnetC819  subnet-071d24ec8fda79fb0         AWS::EC2::Subnet                 CREATE_COMPLETE
basevpcPublicSubnet1DefaultRout  basev-basev-ND0H47E84NRF         AWS::EC2::Route                  CREATE_COMPLETE
basevpcPublicSubnet1EIPED7F596D  52.58.21.103                     AWS::EC2::EIP                    CREATE_COMPLETE
basevpcPublicSubnet1NATGateway8  nat-0a167548993037b0b            AWS::EC2::NatGateway             CREATE_COMPLETE
basevpcPublicSubnet1RouteTableA  rtbassoc-0d59a3d802cffb161       AWS::EC2::SubnetRouteTableAssoc  CREATE_COMPLETE
basevpcPublicSubnet1RouteTableB  rtb-0cd438b9d8a2e0045            AWS::EC2::RouteTable             CREATE_COMPLETE
basevpcPublicSubnet1Subnet86B77  subnet-030211e874aeab999         AWS::EC2::Subnet                 CREATE_COMPLETE
basevpcVPCGW69B42E41             basev-basev-OHGDOS7NEFL3         AWS::EC2::VPCGatewayAttachment   CREATE_COMPLETE
basevpcparm4B69C157              /network/basevpc                 AWS::SSM::Parameter              CREATE_COMPLETE
When evaluating or learning the creation of AWS resources, it saves money of you clean up often. So we destroy the vpc if we are not using it.
cdk destroy
If you dont want to be asked or if you use cdk in a CI/CD pipeline, you may skip the question:
cdk destroy --force
Output
basevpc: destroying...
 ✅  basevpc: destroyed
cdkstat
Make sure you check the right region, cdkstat or the aws cli commands only show you the current region.
Regions can be configured in your AWS profile or in the environment.
Checking the env for AWS region:
env|grep REGION
Example output:
AWS_REGION=eu-central-1
AWS_DEFAULT_REGION=eu-central-1
See the full source on github.