📓

AWS CDK & Go でブログサイト構築

2022/03/12に公開

はじめに

GoとCDKの勉強中です。
というわけで、aws-cdk-goで色々遊んでみようと思います。

作成したものは以下の動画教材のハンズオンを参考にさせていただきました。
シングル構成のブログサイトを構築します。

AWS CloudTech
ハンズオン1_基本的なブログサービスを構築する(シングル構成)

GitHub

ソースコードは以下です。
https://github.com/Fiddler25/cdk-go/tree/1.single-configuration

環境

macOS 12.2.1
GoLand 2021.3.3
Go 1.16
aws-cdk-go v2.10.0

ディレクトリ構成

cdk-go
├── bin
│   ├── script
│       ├── user_data.sh
├── lib
│   ├── stacks
│       ├── cdk_db.go
│       ├── cdk_ec2.go
│       ├── cdk_network.go
├── utils
│   ├── env_names.go
├── main.go
├── .env

実装

なにはともあれcdk init。

$ mkdir cdk-go
$ cd cdk-go
$ cdk init app --language go


各Stackの詳細は以下です。

cdk_network.go

package stacks

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

func CdkNetwork(scope constructs.Construct, id string, props *cdk.StackProps) (
	ec2.CfnVPC,
	ec2.CfnSubnet,
	ec2.CfnSubnet,
	ec2.CfnSubnet,
) {
	var sprops cdk.StackProps
	if props != nil {
		sprops = *props
	}
	stack := cdk.NewStack(scope, &id, &sprops)

	// Vpc
	vpc := ec2.NewCfnVPC(stack, jsii.String("Vpc"), &ec2.CfnVPCProps{
		CidrBlock: jsii.String("10.0.0.0/21"),
		Tags:      &[]*cdk.CfnTag{{Key: jsii.String("Name"), Value: jsii.String("MyVPC")}},
	})

	// PublicSubnet
	publicSubnet1 := ec2.NewCfnSubnet(stack, jsii.String("PublicSubnet1"), &ec2.CfnSubnetProps{
		AvailabilityZone:    jsii.String("ap-northeast-1a"),
		CidrBlock:           jsii.String("10.0.0.0/24"),
		VpcId:               vpc.Ref(),
		MapPublicIpOnLaunch: true,
		Tags:                &[]*cdk.CfnTag{{Key: jsii.String("Name"), Value: jsii.String("PublicSubnet1")}},
	})

	ec2.NewCfnSubnet(stack, jsii.String("PublicSubnet2"), &ec2.CfnSubnetProps{
		AvailabilityZone:    jsii.String("ap-northeast-1c"),
		CidrBlock:           jsii.String("10.0.1.0/24"),
		VpcId:               vpc.Ref(),
		MapPublicIpOnLaunch: true,
		Tags:                &[]*cdk.CfnTag{{Key: jsii.String("Name"), Value: jsii.String("PublicSubnet2")}},
	})

	// PrivateSubnet
	privateSubnet1 := ec2.NewCfnSubnet(stack, jsii.String("PrivateSubnet1"), &ec2.CfnSubnetProps{
		AvailabilityZone: jsii.String("ap-northeast-1a"),
		CidrBlock:        jsii.String("10.0.2.0/24"),
		VpcId:            vpc.Ref(),
		Tags:             &[]*cdk.CfnTag{{Key: jsii.String("Name"), Value: jsii.String("PrivateSubnet1")}},
	})

	privateSubnet2 := ec2.NewCfnSubnet(stack, jsii.String("PrivateSubnet2"), &ec2.CfnSubnetProps{
		AvailabilityZone: jsii.String("ap-northeast-1c"),
		CidrBlock:        jsii.String("10.0.3.0/24"),
		VpcId:            vpc.Ref(),
		Tags:             &[]*cdk.CfnTag{{Key: jsii.String("Name"), Value: jsii.String("PrivateSubnet2")}},
	})

	// InternetGateway
	igw := ec2.NewCfnInternetGateway(stack, jsii.String("InternetGateway"), &ec2.CfnInternetGatewayProps{
		Tags: &[]*cdk.CfnTag{{Key: jsii.String("Name"), Value: jsii.String("MyInternetGateway")}},
	})

	ec2.NewCfnVPCGatewayAttachment(stack, jsii.String("VPCGatewayAttachment"), &ec2.CfnVPCGatewayAttachmentProps{
		VpcId:             vpc.Ref(),
		InternetGatewayId: igw.Ref(),
	})

	// RouteTable
	routeTable := ec2.NewCfnRouteTable(stack, jsii.String("RouteTable"), &ec2.CfnRouteTableProps{
		VpcId: vpc.Ref(),
	})

	ec2.NewCfnRoute(stack, jsii.String("Route"), &ec2.CfnRouteProps{
		RouteTableId:         routeTable.Ref(),
		DestinationCidrBlock: jsii.String("0.0.0.0/0"),
		GatewayId:            igw.Ref(),
	})

	ec2.NewCfnSubnetRouteTableAssociation(stack, jsii.String("RouteTableAssociation1"), &ec2.CfnSubnetRouteTableAssociationProps{
		RouteTableId: routeTable.Ref(),
		SubnetId:     publicSubnet1.Ref(),
	})

	return vpc, publicSubnet1, privateSubnet1, privateSubnet2
}

作成しているリソースは以下です。

  • Vpc: 1
  • PublicSubnet: 2
  • PrivateSubnet: 2
  • InternetGateway: 1
  • RouteTable: 1

シングル構成ですが、後ほどRDSのサブネットグループを作成する都合上、パブリックサブネットとプライベートサブネットを2つずつ作成します。

cdk_ec2.go

package stacks

import (
	"encoding/base64"
	"github.com/Fiddler25/cdk-go/utils"
	"os"

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

type CdkEc2Props struct {
	cdk.StackProps
	Vpc           ec2.CfnVPC
	PublicSubnet1 ec2.CfnSubnet
}

func CdkEc2(scope constructs.Construct, id string, props *CdkEc2Props) ec2.CfnSecurityGroup {
	var sprops cdk.StackProps
	if props != nil {
		sprops = props.StackProps
	}
	stack := cdk.NewStack(scope, &id, &sprops)

	// SecurityGroup for EC2
	webSg := ec2.NewCfnSecurityGroup(stack, jsii.String("SecurityGroupEc2"), &ec2.CfnSecurityGroupProps{
		GroupName:        jsii.String("Web-SG"),
		GroupDescription: jsii.String("for Web"),
		VpcId:            props.Vpc.Ref(),
		SecurityGroupIngress: &[]*ec2.CfnSecurityGroup_IngressProperty{
			{
				IpProtocol: jsii.String("tcp"),
				CidrIp:     jsii.String("0.0.0.0/0"),
				FromPort:   jsii.Number(22),
				ToPort:     jsii.Number(22),
			},
			{
				IpProtocol: jsii.String("tcp"),
				CidrIp:     jsii.String("0.0.0.0/0"),
				FromPort:   jsii.Number(80),
				ToPort:     jsii.Number(80),
			},
		},
	})

	// SecurityGroup for RDS
	rdsSg := ec2.NewCfnSecurityGroup(stack, jsii.String("SecurityGroupRds"), &ec2.CfnSecurityGroupProps{
		GroupName:        jsii.String("Rds-SG"),
		GroupDescription: jsii.String("for Rds"),
		VpcId:            props.Vpc.Ref(),
		SecurityGroupIngress: &[]*ec2.CfnSecurityGroup_IngressProperty{
			{
				IpProtocol:            jsii.String("tcp"),
				FromPort:              jsii.Number(3306),
				ToPort:                jsii.Number(3306),
				SourceSecurityGroupId: webSg.AttrGroupId(),
			},
		},
	})

	// Instance
	ec2.NewCfnInstance(stack, jsii.String("Ec2Instance1"), &ec2.CfnInstanceProps{
		ImageId:          jsii.String("ami-03d79d440297083e3"),
		InstanceType:     jsii.String("t2.micro"),
		SubnetId:         props.PublicSubnet1.Ref(),
		SecurityGroupIds: jsii.Strings(*webSg.AttrGroupId()),
		KeyName:          jsii.String(utils.EnvNames().KeyName),
		UserData:         jsii.String(getUserData()),
		Tags:             &[]*cdk.CfnTag{{Key: jsii.String("Name"), Value: jsii.String("WebServer1")}},
	})

	return rdsSg
}

func getUserData() string {
	dir, err := os.Getwd()
	if err != nil {
		panic(err)
	}

	f, err := os.ReadFile(dir + "/bin/script/user_data.sh")
	if err != nil {
		panic(err)
	}

	return base64.StdEncoding.EncodeToString(f)
}

作成しているリソースは以下です。

  • SecurityGroup: 2
  • Instance: 1

インスタンス起動時にMySQLやWordPressなど諸々インストールしてくれるように、getUserData関数でユーザーデータを取得します。
関数内の処理内容は以下です。

  • os.Getwdでワーキングディレクトリを取得
  • os.ReadFileでファイルの読み込み
  • base64にエンコードしてreturn

実行するスクリプトはこちら

また、インスタンスに指定するキーペア名は環境変数から取得します。(後述)

cdk_db.go

package stacks

import (
	"github.com/Fiddler25/cdk-go/utils"
	cdk "github.com/aws/aws-cdk-go/awscdk/v2"
	ec2 "github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
	rds "github.com/aws/aws-cdk-go/awscdk/v2/awsrds"
	"github.com/aws/constructs-go/constructs/v10"
	"github.com/aws/jsii-runtime-go"
)

type CdkDbProps struct {
	cdk.StackProps
	PrivateSubnet1   ec2.CfnSubnet
	PrivateSubnet2   ec2.CfnSubnet
	SecurityGroupRds ec2.CfnSecurityGroup
}

func CdkDb(scope constructs.Construct, id string, props *CdkDbProps) {
	var sprops cdk.StackProps
	if props != nil {
		sprops = props.StackProps
	}
	stack := cdk.NewStack(scope, &id, &sprops)

	subnetGroup := rds.NewCfnDBSubnetGroup(stack, jsii.String("SubnetGroup"), &rds.CfnDBSubnetGroupProps{
		DbSubnetGroupName:        jsii.String("SubnetGroup"),
		DbSubnetGroupDescription: jsii.String("SubnetGroup"),
		SubnetIds:                jsii.Strings(*props.PrivateSubnet1.Ref(), *props.PrivateSubnet2.Ref()),
	})

	rds.NewCfnDBInstance(stack, jsii.String("DbInstance"), &rds.CfnDBInstanceProps{
		DbInstanceClass:      jsii.String("db.t2.micro"),
		AllocatedStorage:     jsii.String("100"),
		DbInstanceIdentifier: jsii.String("database"),
		DbName:               jsii.String("wordpress"),
		DbSubnetGroupName:    subnetGroup.Ref(),
		Engine:               jsii.String("mysql"),
		MasterUsername:       jsii.String("root"),
		MasterUserPassword:   jsii.String(utils.EnvNames().MasterUserPassword),
		MultiAz:              true,
		Port:                 jsii.String("3306"),
		VpcSecurityGroups:    jsii.Strings(*props.SecurityGroupRds.AttrGroupId()),
	})
}

作成しているリソースは以下です。

  • SubnetGroup: 1
  • Instance: 1

RDSのマスターユーザーパスワードも環境変数から取得します。(後述)

env_names.go

環境変数を取得するため、godotenvをインストールします。

$ go get github.com/joho/godotenv

rootディレクトリに.envファイルを作成し、環境変数を指定します。

KEY_NAME=<sample_key_name>
MASTER_USER_PASSWORD=<sample_password>

環境変数を取得する関数を定義します。

package utils

import (
	"log"
	"os"

	"github.com/joho/godotenv"
)

type Env struct {
	KeyName            string
	MasterUserPassword string
}

func EnvNames() Env {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file", err)
	}

	return Env{
		KeyName:            os.Getenv("KEY_NAME"),
		MasterUserPassword: os.Getenv("MASTER_USER_PASSWORD"),
	}
}

main.go

最後に、定義した各Stackを呼び出します。

package main

import (
	"github.com/Fiddler25/cdk-go/lib/stacks"
	cdk "github.com/aws/aws-cdk-go/awscdk/v2"
)

func main() {
	app := cdk.NewApp(nil)

	vpc, publicSubnet1, privateSubnet1, privateSubnet2 := stacks.CdkNetwork(app, "CdkNetwork", &cdk.StackProps{})
	securityGroupRds := stacks.CdkEc2(app, "CdkEc2", &stacks.CdkEc2Props{Vpc: vpc, PublicSubnet1: publicSubnet1})
	stacks.CdkDb(app, "CdkDb", &stacks.CdkDbProps{PrivateSubnet1: privateSubnet1, PrivateSubnet2: privateSubnet2, SecurityGroupRds: securityGroupRds})

	app.Synth(nil)
}

デプロイ

$ cdk deploy --all --profile <プロファイル名>

CDKで定義した各リソースがAWSコンソール上で確認できたら、生成されたパブリックIPアドレスからブログサイトに接続できます。

WordPressの画面に遷移できたらデータベースの接続情報を求められますので、以下の情報を入力します。

  • データベース名: wordpress
  • ユーザー名: root
  • パスワード: 環境変数に指定したMASTER_USER_PASSWORD
  • データベースのホスト名: RDS > データベース > database > 接続とセキュリティ > エンドポイント

あとは画面の指示に従ってログインを完了すると、WordPressのブログサイトが表示できます 🙌


後片付け

$ cdk destroy --all --profile <プロファイル名>

おわりに

簡単ですが、CDKv2とGoでシングル構成のブログサービスにアクセスできるところまででした。
基本的にはリファレンスに従ってパラメーターを指定するだけですが、ユーザーデータの取得は結構ハマりつつもよい勉強になりました。

今後は冗長構成や独自ドメイン設定なども進めていきたいと思います。

最後までお読みいただきありがとうございました(_ _)

参考文献

  • AWS CloudTech
    • 今回構築したブログサイトの参考にさせていただきました
    • 自分はこちらの教材でAWS SAAも取得しており、大変お世話になってます
  • 実践!AWS CDK
    • CDKの勉強でもの凄くお世話になりました
    • 本記事ではCDKの詳細に触れていないため、CDKとは?という方は是非ご一読されることをお勧めします
  • AWS CDK API Reference
  • aws-cdk-go

Discussion