Exercise CDK-VPC

1 Init

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.

2 Modularize setup

The init app creates a monolithic setup, so we create modules. If you are unsure about GO modules, read Chapter GO modules.

2.1 Create main.go in own directory

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

2.2 Update 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:

  • Deleting unused imports (line 3-6)
  • Delete unused lines in func env() (line 20)
  • Import the package vpc (line 5)
  • Change NewVPCStack to the new namespace vpc (line 11)
  • Change the name of the vpc construct to a more easy name. If you have to type the name often, you should use shorter names. If you are using CDK, its obvious that you are creating stacks, so you do not need “stack” in the name (line 11)

Now the main file is done.

2.3 update vpc.go

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:

  • Delete unused imports (line 3…)
  • Import ec2 module (line 5), because vpc is in ec2 module
  • Import systems manager (ssm) (line 6) to store the vpc id as a parameter in ssm. This is helpful to use the vpc id in other stacks
  • Update name of vpc construct (optional) (line 22)
  • Create a parameter (line 29-35)

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.

2.4 Synthetize template

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. `

2.4 Update vpc_test.go

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:

  • Change the package name to vpc_test (line 1)
  • Import the package vpc, so we can use the vpc construct (line 7)
  • Add new vpc namespace to NewVpcStack (line 19)
  • Change the Resource path (line 28). Use the logical name* of the vpc for that
  • Change the attribute (line 29)

3 Test and deploy

3.1 Test

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:

  1. Change attribute in test
  2. test => Fail
  3. Update code
  4. Test => pass

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.

3.2 Deploy

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

3.3 Check created resources

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

4 Destroy

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.

4.1 Destroy

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

4.2 Check if its really 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 also

Source

See the full source on github.

Sources