AWS CDK Goを使ってTransit GatewayでVPC間を接続してみた

AWS CDK GoでTransit Gateway周りのリソースを作成してみました。Transit Gateway周りはまだL2コンストラクトがないようですのでL1コンストラクトが多めとなっています。
2023.04.04

こんにちは。AWS事業本部コンサルティング部に所属している今泉(@bun76235104)です。

みなさんGo言語好きですか?

AWS CDKではGo言語で構築可能ですが、検索するとTypeScriptの記事が多いですよね?

最近私はAWS CDKのGo言語を使ってシンプルにEC2を構築したりしています。

今回作成する構成は以下のようなシンプルな構成です。

20230404_cdk_go_transit_architecture

ゴールは以下のようにHub VPCのEC2インスタンスからSpoke VPCのEC2インスタンスにpingで疎通確認をすることです。

20230404_cdk_go_transit_ping

なお、今回作成したコードは細かくファイルを分けており、すべてを紹介する訳ではありません。

全体を確認したい場合は以下のリポジトリをご確認ください。

記述したコード

重要そうなポイントを抜粋しつつ、コードを紹介します。

ディレクトリ構成は以下のようになっています。

.
├── README.md
├── cdk.json
├── cmd
│   ├── hub_spoke #Transit Gateway周りのリソース群を作成
│   │   ├── hub.go
│   │   ├── main.go
│   │   ├── route_establish .go
│   │   └── route_table.go
│   ├── network # VPCやルートテーブルなどのリソース群を作成
│   │   ├── route_table.go
│   │   └── vpc.go
│   └── server # EC2周りのリソース群を作成
│       └── main.go
├── go.mod
├── go.sum
├── transit_gw.go #今回のmainファイルに該当
└── transit_gw_test.go

VPC・EC2関連リソースの作成

VPC・VPCエンドポイントなどのリソースについて、構造体とメソッドを用意して2セット作成しています。

VPC関連リソースを作成するコード

以下のように構造体とメソッドを準備しています。

cmd/network/vpc.go

type Network struct {
	scope          constructs.Construct
	vpcName        string
	cidr           string
	hasSSMEndpoint bool
}

func (nr Network) CreateNetworkResources() awsec2.Vpc {
	// VPC
	vpc := awsec2.NewVpc(nr.scope, &nr.vpcName, &awsec2.VpcProps{
		IpAddresses:        awsec2.IpAddresses_Cidr(jsii.String(nr.cidr)),
		MaxAzs:             jsii.Number(2),
		EnableDnsSupport:   jsii.Bool(true),
		EnableDnsHostnames: jsii.Bool(true),
		VpcName:            jsii.String(nr.vpcName),
		SubnetConfiguration: &[]*awsec2.SubnetConfiguration{
			{
				Name:       jsii.String("TransitGateway"),
				SubnetType: awsec2.SubnetType_PRIVATE_ISOLATED,
                // Transit Gatewayのアタッチメントを配置するサブネットのためCidrMaskは小さくしている
				CidrMask:   jsii.Number(28),
			},
			{
				Name:       jsii.String("Private"),
				SubnetType: awsec2.SubnetType_PRIVATE_ISOLATED,
				CidrMask:   jsii.Number(24),
			},
		},
	})
	// 指定した時のみVPCエンドポイントを追加
	if nr.hasSSMEndpoint {
		vpc.AddInterfaceEndpoint(jsii.String("SSM"), &awsec2.InterfaceVpcEndpointOptions{
			Service: awsec2.InterfaceVpcEndpointAwsService_SSM(),
		})
		vpc.AddInterfaceEndpoint(jsii.String("SSMMessage"), &awsec2.InterfaceVpcEndpointOptions{
			Service: awsec2.InterfaceVpcEndpointAwsService_SSM_MESSAGES(),
		})
		vpc.AddInterfaceEndpoint(jsii.String("EC2Messag"), &awsec2.InterfaceVpcEndpointOptions{
			Service: awsec2.InterfaceVpcEndpointAwsService_EC2_MESSAGES(),
		})
	}
	return vpc
}

これをmain処理から以下のように2セット作成しています。(HubVPCとSpokeVPC分)

transit_gw.go

    // Hub(Shared) VPCに該当
	sharedNetworkResource := network.NewNetwork(stack, "SharedVpc", "10.10.0.0/16", true)
	sharedVpc := sharedNetworkResource.CreateNetworkResources()
	severResource := server.NewServer(stack, "SharedVPCInstance", sharedVpc)
	severResource.CreateServerResources()

	// SpokeVPCに該当
	workloadNetwork := network.NewNetwork(stack, "WorkloadVpc", "10.20.0.0/16", false)
	workloadVpc := workloadNetwork.CreateNetworkResources()
	workloadServer := server.NewServer(stack, "WorkloadVPCInstance", workloadVpc)
	workloadServer.CreateServerResources()

次にEC2の作成部分は以下のようになっています。

EC2を作成

cmd/server/main.go

type Server struct {
	scope constructs.Construct
	name  string
	vpc   awsec2.Vpc
}

func (sr Server) CreateServerResources() {
	sg := awsec2.NewSecurityGroup(sr.scope, jsii.String(sr.name+"SG"), &awsec2.SecurityGroupProps{
		AllowAllOutbound: jsii.Bool(true),
		Vpc:              sr.vpc,
	})
	// allow sg to inbound icmp
	sg.AddIngressRule(awsec2.Peer_Ipv4(jsii.String("10.0.0.0/8")), awsec2.Port_AllIcmp(), jsii.String("allow icmp"), nil)
	awsec2.NewInstance(sr.scope, jsii.String(sr.name), &awsec2.InstanceProps{
		InstanceType: awsec2.InstanceType_Of(awsec2.InstanceClass_T3, awsec2.InstanceSize_MICRO),
		MachineImage: awsec2.MachineImage_LatestAmazonLinux(&awsec2.AmazonLinuxImageProps{
			Generation: awsec2.AmazonLinuxGeneration_AMAZON_LINUX_2,
		}),
		SsmSessionPermissions: jsii.Bool(true),
		Vpc:                   sr.vpc,
		SecurityGroup:         sg,
		VpcSubnets: &awsec2.SubnetSelection{
			SubnetGroupName: jsii.String("Private"),
		},
	})
}

こちらもmain処理から各VPC内のサブネットに作成できるように呼び出します。

    // HubVPCに配置するEC2
	severResource := server.NewServer(stack, "SharedVPCInstance", sharedVpc)
	severResource.CreateServerResources()

	// SpokeVPC配置するEC2
	workloadServer := server.NewServer(stack, "WorkloadVPCInstance", workloadVpc)
	workloadServer.CreateServerResources()

Transit Gateway周りのリソース群を作成

ここから以下ブログを参考に順にリソースを作成します。

  1. Transit Gatewayを作成
  2. Transit Gatewayのアタッチメントを作成(各VPCの専用サブネットを指定)
  3. Transit Gatewayのルートテーブルを作成
  4. 2で作ったアタッチメントと3で作ったルートテーブルを紐付け(アソシエーション)
  5. アタッチメントしたVPCからルートテーブルに経路を伝播(プロパゲーション)
  6. Transit Gatewayのルートテーブルにルートを追加
1. Transit Gatwayを作成

以下のファイルでTransit Gatewayを作成するための構造体とメソッドを用意します。

cmd/hub_spoke/hub.go

type Hub struct {
	scope constructs.Construct
}

func (h Hub) CreateTransitGateway() awsec2.CfnTransitGateway {
	return awsec2.NewCfnTransitGateway(h.scope, jsii.String("TransitGateway"), &awsec2.CfnTransitGatewayProps{
		DefaultRouteTableAssociation: jsii.String("disable"),
		DefaultRouteTablePropagation: jsii.String("disable"),
	})
}

以下のように呼び出します。

	hub := NewHub(hp.scope)
	// Transit Gatewayを作成
	tgw := hub.CreateTransitGateway()

次にTransit Gatewayのアタッチメントを各VPCのTransit Gateway専用のサブネットに作成します。

2. Transit Gatewayのアタッチメントを作成(各VPCの専用サブネットを指定)

これまでと同様に構造体とメソッドを用意します。

cmd/hub_spoke/hub.go

type VpcAttachment struct {
	name            string
	vpc             awsec2.Vpc
	tgw             awsec2.CfnTransitGateway
	subnetGroupName string
}

func (va VpcAttachment) Attach() awsec2.CfnTransitGatewayAttachment {
	return awsec2.NewCfnTransitGatewayAttachment(va.vpc, jsii.String("VpcAttachment"), &awsec2.CfnTransitGatewayAttachmentProps{
        // ↓各VPCのTransit Gateway専用のサブネットを指定
		SubnetIds:        va.vpc.SelectSubnets(&awsec2.SubnetSelection{SubnetGroupName: &va.subnetGroupName}).SubnetIds,
		TransitGatewayId: va.tgw.Ref(),
		VpcId:            va.vpc.VpcId(),
		Tags: &[]*awscdk.CfnTag{
			{
				Key:   jsii.String("Name"),
				Value: jsii.String(va.name),
			},
		},
	})
}

以下のように呼び出します。

cmd/hub_spoke/main.go

	// Transit GatewayにHub(Shared)VPCをアタッチ
	attachmentShared := NewVpcAttachment("HubVpcAttachment", hp.sharedVpc, tgw, "TransitGateway")
	attachmentSharedVpc := attachmentShared.Attach()
    // Transit GatewayにSpoke VPCをアタッチ
	attchmentWorkload := NewVpcAttachment("SpokeVpcAttachment", hp.hubVpc, tgw, "TransitGateway")
	attachmentWorkloadVpc := attchmentWorkload.Attach()

次にTransit Gateway用のルートテーブルを作成します。

3. Transit Gatewayのルートテーブルを作成

Transit Gateway用のルートテーブルを作成する構造体・メソッドを用意します。

cmd/hub_spoke/route_table.go

type RouteTable struct {
	name string
	tgw  awsec2.CfnTransitGateway
}

func (ra RouteTable) Create() awsec2.CfnTransitGatewayRouteTable {
	return awsec2.NewCfnTransitGatewayRouteTable(ra.tgw, jsii.String("RouteTable"), &awsec2.CfnTransitGatewayRouteTableProps{
		TransitGatewayId: ra.tgw.Ref(),
		Tags: &[]*awscdk.CfnTag{
			{
				Key:   jsii.String("Name"),
				Value: jsii.String(ra.name),
			},
		},
	})
}

以下のように呼び出します。

cmd/hub_spoke/main.go

	// Transit Gatewayのルートテーブル作成
	rt := NewRouteTable("RouteTable", tgw)
	routeTable := rt.Create()

以下4と5を行います。

4.2で作ったアタッチメントと3で作ったルートテーブルを紐付け(アソシエーション)

5.アタッチメントしたVPCからルートテーブルに経路を伝播(プロパゲーション)

4,5 ルートテーブルへのアタッチメントのアソシエーションとプロパゲーション

構造体とメソッドを以下のように定義します。

cmd/hub_spoke/route_establish.go

type VpcRouteAssociation struct {
	name          string
	vpcAttachment awsec2.CfnTransitGatewayAttachment
	routeTable    awsec2.CfnTransitGatewayRouteTable
}

func (vra VpcRouteAssociation) Create() {
    // Association
	awsec2.NewCfnTransitGatewayRouteTableAssociation(vra.vpcAttachment, jsii.String(vra.name+"Association"), &awsec2.CfnTransitGatewayRouteTableAssociationProps{
		TransitGatewayAttachmentId: vra.vpcAttachment.Ref(),
		TransitGatewayRouteTableId: vra.routeTable.Ref(),
	})
	// Propagation
	awsec2.NewCfnTransitGatewayRouteTablePropagation(vra.vpcAttachment, jsii.String(vra.name+"Propagation"), &awsec2.CfnTransitGatewayRouteTablePropagationProps{
		TransitGatewayAttachmentId: vra.vpcAttachment.Ref(),
		TransitGatewayRouteTableId: vra.routeTable.Ref(),
	})
}

これまでと同様に呼び出します。

cmd/hub_spoke/main.go

	// Hub(Shared)VPCのアタッチメントとルートをアソシエーション・プロパゲーション
	hubVpcRouteAssociation := NewVpcRouteAssociation("HubVpcAssocation", attachmentSharedVpc, routeTable)
	hubVpcRouteAssociation.Create()

	// Spoke VPCのアタッチメントとルートをアソシエーション・プロパゲーション
	spokeVpcRouteAssociation := NewVpcRouteAssociation("SpokeVpcAssociation", attachmentWorkloadVpc, routeTable)
	spokeVpcRouteAssociation.Create()

次にHub(Shared)VPCとSpoke VPCが相互に通信できるようにTransit Gatewayのルートテーブルにルートを追加します。

6. Transit Gatewayのルートテーブルにルートを追加

cmd/hub_spoke/route_establish.go

type VpcsConnection struct {
	hubVpc             awsec2.Vpc
	hubVpcAttachment   awsec2.CfnTransitGatewayAttachment
	spokeVpc           awsec2.Vpc
	spokeVpcAttachment awsec2.CfnTransitGatewayAttachment
	routetable         awsec2.CfnTransitGatewayRouteTable
}

func (cv VpcsConnection) Create() {
	awsec2.NewCfnTransitGatewayRoute(cv.spokeVpc, jsii.String("ToSpokeVpc"), &awsec2.CfnTransitGatewayRouteProps{
		DestinationCidrBlock:       cv.spokeVpc.VpcCidrBlock(),
		TransitGatewayAttachmentId: cv.spokeVpcAttachment.Ref(),
		TransitGatewayRouteTableId: cv.routetable.Ref(),
	})
	awsec2.NewCfnTransitGatewayRoute(cv.hubVpc, jsii.String("ToHubVpc"), &awsec2.CfnTransitGatewayRouteProps{
		DestinationCidrBlock:       cv.hubVpc.VpcCidrBlock(),
		TransitGatewayAttachmentId: cv.hubVpcAttachment.Ref(),
		TransitGatewayRouteTableId: cv.routetable.Ref(),
	})
}

長くなりましたがこれでTransit Gateway周りのリソースを作成できました。

各サブネットのルートテーブルにTransit Gatewayへのルートを追加

最後に各サブネットのルートテーブルにTransit Gatewayへのルートを追加します。

サブネットと紐づいているルートテーブルへルートを追加

各VPCのEC2を配置しているサブネットからTransit Gatewayにたどり着けるようにルートを追加します。

cmd/network/route_table.go

type routeToTransitGateway struct {
	scope         constructs.Construct
	name          string
	vpc           awsec2.Vpc
	tgw           awsec2.CfnTransitGateway
	tgwAttachment awsec2.CfnTransitGatewayAttachment
}

func (rttg routeToTransitGateway) CreateRouteToTransitGateway() {
	subnets := rttg.vpc.SelectSubnets(&awsec2.SubnetSelection{
		SubnetGroupName: jsii.String("Private"),
	}).Subnets
	for i, subnet := range *subnets {
		routeName := fmt.Sprintf("%s%d", rttg.name, i)
		awsec2.NewCfnRoute(rttg.scope, jsii.String(routeName), &awsec2.CfnRouteProps{
			RouteTableId:         subnet.RouteTable().RouteTableId(),
			DestinationCidrBlock: jsii.String("0.0.0.0/0"),
			TransitGatewayId:     rttg.tgw.Ref(),
		}).AddDependency(rttg.tgwAttachment)
	}
}

上記の構造体とメソッドを呼び出します。

transit_gw.go

	// EC2が属するサブネットのルートテーブルからTransit Gatewayへのルートを追加
	routeHubSubnetToTransit := network.NewRouteToTransitGateway(stack, "HubSubnetToTransitGW", sharedVpc, hubResult.Tgw, hubResult.HubAttachment)
	routeHubSubnetToTransit.CreateRouteToTransitGateway()
	routeSpokeSubnetToTransit := network.NewRouteToTransitGateway(stack, "SpokeSubnetToTransitGW", workloadVpc, hubResult.Tgw, hubResult.SpokeAttachment)
	routeSpokeSubnetToTransit.CreateRouteToTransitGateway()

これにより以下の図のような構成ができました。

20230404_cdk_go_transit_architecture

EC2からEC2にpingしてみる

最後に以下のようにpingマンドによりVPCを跨いだEC2間で疎通確認をしてみます。

20230404_cdk_go_transit_ping

今回はそれぞれのVPCにインターネットへの経路はありませんが、Session ManagerによるEC2への接続をできるようにしている(Hub VPCのみ)ため、接続してpingコマンドを実行します。

ping 10.20.1.209

以下のように無事疎通を確認できました!

64 bytes from 10.20.1.209: icmp_seq=1 ttl=254 time=0.879 ms
64 bytes from 10.20.1.209: icmp_seq=2 ttl=254 time=0.718 ms
64 bytes from 10.20.1.209: icmp_seq=3 ttl=254 time=0.603 ms
64 bytes from 10.20.1.209: icmp_seq=4 ttl=254 time=0.551 ms
64 bytes from 10.20.1.209: icmp_seq=5 ttl=254 time=0.564 ms
64 bytes from 10.20.1.209: icmp_seq=6 ttl=254 time=0.603 ms
64 bytes from 10.20.1.209: icmp_seq=7 ttl=254 time=0.552 ms

最後に: 参考ブログまとめ

今回構築するにあたって以下の記事を参考にしました。

どれも良い記事なのでTransit Gatewayとは?なぜEC2を配置するサブネットとTransit Gatewayのアタッチメントを配置するサブネットを分けているの?と思った方はご参照ください。

次回は異なるVPC間での名前解決の共有について引き続きAWS CDK With Go言語で構成を作って見ようと思います。

このブログが誰かの時間を1秒でも削ればうれしいです。

以上今泉でした。