AWS CDK & Go でブログサイト構築
はじめに
GoとCDKの勉強中です。
というわけで、aws-cdk-goで色々遊んでみようと思います。
作成したものは以下の動画教材のハンズオンを参考にさせていただきました。
シングル構成のブログサイトを構築します。
AWS CloudTech
ハンズオン1_基本的なブログサービスを構築する(シングル構成)
GitHub
ソースコードは以下です。
環境
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
- UserDataはbase64で指定する必要がある
- cfn-ec2-instance-userdata
実行するスクリプトはこちら
また、インスタンスに指定するキーペア名は環境変数から取得します。(後述)
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