AWS CDK Goを使ってシンプルにEC2を構築してみた

AWS CDKのGo版を使ってシンプルにEC2を構築してみました。この調子で次回以降も記事を執筆予定です。
2023.04.03

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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

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

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

私はTypeScriptも好きですが、Go言語も好きなのでもうちょっと日本語記事が増えてもよいかなと思っています。

ということで前回AWS CDKのGo言語版でハローワールドしてみました。

今回はもう少し構築するリソースを増やしてみてEC2を構築してみようと思います。

この記事で作るもの

  • 以下のように単一のAWSアカウント・単一VPCなシンプルな構成でEC2を構築します。

20230402_cdk_go_vpc_and_ec2_architecture

  • VPCにはVPCエンドポイントを構築して、EC2にはSSM Session Managerで接続できるようにします。

なお、今回作成したソースファイル群は以下のリポジトリに格納しています。

ディレクトリ構成

前回の記事では、以下のようにシンプルな単一ファイル構成でした。

.
├── README.md
├── cdk.json
├── cdk.out
├── go.mod
├── go.sum
├── hello_go_cdk.go //このファイルのみ
└── hello_go_cdk_test.go

今回はEC2とVPCを作成するにあたりパッケージ分割とソースファイルの分割をしています。

.
├── README.md
├── cdk.json
├── cmd //パッケージを分割
│   ├── network //VPCなどのリソース
│   │   └── main.go
│   └── server //EC2
│       └── main.go
├── go.mod
├── go.sum
├── vpc_and_ec2.go
└── vpc_and_ec2_test.go

この構成については、以下の記事を参考にちょっとずつ大きくなるプロジェクトを想定しています。

各ファイルの中身(リソースの作成)

mainパッケージ

vpc_and_ec2.goが今回のプロジェクトのmain処理を行うファイルとなります。

以下のように、networkとserverパッケージを呼び出すようにしています。

func main() {
	defer jsii.Close()

	app := awscdk.NewApp(nil)
	// 共通タグを設定
	awscdk.Tags_Of(app).Add(jsii.String("Project"), jsii.String("Sample"), nil)
	awscdk.Tags_Of(app).Add(jsii.String("Env"), jsii.String("Dev"), nil)

	stack := NewVpcAndEc2Stack(app, "Sample", &VpcAndEc2StackProps{
		awscdk.StackProps{
			Env: env(),
		},
	})

	sharedNetwork := network.NewNetwork(stack, "SharedVpc", "10.10.0.0/16", true)
	sharedVpc := sharedNetwork.CreateNetworkResources()

	severResource := server.NewServer(stack, sharedVpc)
	severResource.CreateServerResources()

	app.Synth(nil)
}
vpc_and_ec2.goの全体像
package main

import (
	"vpc_and_ec2/cmd/network"
	"vpc_and_ec2/cmd/server"

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

type VpcAndEc2StackProps struct {
	awscdk.StackProps
}

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

func main() {
	defer jsii.Close()

	app := awscdk.NewApp(nil)
	// 共通タグを設定
	awscdk.Tags_Of(app).Add(jsii.String("Project"), jsii.String("Sample"), nil)
	awscdk.Tags_Of(app).Add(jsii.String("Env"), jsii.String("Dev"), nil)

	stack := NewVpcAndEc2Stack(app, "Sample", &VpcAndEc2StackProps{
		awscdk.StackProps{
			Env: env(),
		},
	})

	sharedNetwork := network.NewNetwork(stack, "SharedVpc", "10.10.0.0/16", true)
	sharedVpc := sharedNetwork.CreateNetworkResources()

	severResource := server.NewServer(stack, sharedVpc)
	severResource.CreateServerResources()

	app.Synth(nil)
}

func env() *awscdk.Environment {
	return &awscdk.Environment{
		Region: jsii.String("ap-northeast-1"),
	}
}

networkパッケージ

cmd/network/main.goではVPCとVPCエンドポイントを作成します。

構築に際しては公式のドキュメントを参照しています。

Session Managerを利用してEC2に接続するために必要なVPCエンドポイントを作成しています。

なお、次回以降でVPCエンドポイントを作るVPC・作らないVPCの両方を作成予定であるためifで分岐するようにしています。

// 受け取る必要のあるパラメーターを定義した構造体
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("Shared"),
		SubnetConfiguration: &[]*awsec2.SubnetConfiguration{
			{
				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
}
cmd/network/main.goの全体像
package network

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

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

func NewNetwork(scope constructs.Construct, vpcName string, cidr string, hasSSMEndpoint bool) Network {
	return Network{
		scope:          scope,
		vpcName:        vpcName,
		cidr:           cidr,
		hasSSMEndpoint: hasSSMEndpoint,
	}
}

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("Shared"),
		SubnetConfiguration: &[]*awsec2.SubnetConfiguration{
			{
				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
}

serverパッケージ

こちらでは作成したVPCを引数としてEC2を作成します。

なお、作成に際してはVPCと同様に公式のドキュメントを参照します。

cmd/server/main.go

package server

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

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

func NewServer(scope constructs.Construct, vpc awsec2.Vpc) Server {
	return Server{
		scope: scope,
		vpc:   vpc,
	}
}

func (sr Server) CreateServerResources() {
	awsec2.NewInstance(sr.scope, jsii.String("Server"), &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,
	})
}

なおNewIsntanceの関数呼び出しでSsmSessionPermissionsというパラメータを渡しています。

こちらはAWS CDKのver:2.70.0から利用できるようになったパラメータで、trueとすることでEC2インスタンスに割り当てられたIAMロールにAmazonSSMManagedInstanceCoreを簡単にアタッチできます。

詳細は弊社ののんピがブログ化しておりますので、以下の記事をご参照ください。

リソース作成

ここまでファイルを作成したのち、以下コマンドによりリソースを作成します。

cdk diff #作られるリソースを確認
cdk deploy

実際に構成図のようにVPCとEC2などのリソースが作成され、EC2に対してSession Managerで接続できることを確認しました。

  • EC2インスタンスの接続画面

20230402_go_cdk_ec2_connect

  • 接続後の様子

20230331_cdk_go_connected

最後に

今回はAWS CDKのGo版を利用してシンプルにVPC・EC2を構築しました。

次回はTransit Gatewayなどを利用して他VPC上のEC2との疎通確認などを行う予定です。

これからもCDKのGo版の記事が増えれば良いなと思います。

以上今泉でした。このブログがどなたかの参考になればうれしいです。