こんにちは。
サーバーサイドエンジニアのkoheiです。
今回は新プロダクトのAPIサーバー構築にCDKを採用してみたので、基本的な使い方と感想をお伝えします。
CDKは正式名称をAWS Cloud Development Kitといいます。
TypeScriptやPythonなどのプログラミング言語を用いてAWSリソースを定義・操作することができるIaCツールとなります。
Terraformで採用されているHCLなどの独自言語を使用していないため、アプリケーションエンジニアでも気軽に入門することができます。
今回CDKを選定した理由としては以下の2点です。
また採用するプログラミング言語としては普段の業務で使用しているGoを選択しました。
一般的にはTypeScriptを採用することが多いそうですが、弊社では自社プロダクトのバックエンド開発にはGoをメインに採用しており、統一感をだすためにGoにしてみました。
それでは実際にコードを書いていきましょう!
実装する前に今回構築するインフラ構成を簡単に説明します。
構成図としては以下のようになります。
CDKのインストール方法は公式のチュートリアルを参考にしました。
日本語版はないですが、読みやすいので環境は簡単に整います。
https://aws.amazon.com/jp/getting-started/guides/setup-cdk/
それでは実際の実装が以下となります。
(※ブログ用に一部実装を変更しています。)
main.go
package main
import (
cdk "github.com/aws/aws-cdk-go/awscdk/v2"
ec2 "github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
ecr "github.com/aws/aws-cdk-go/awscdk/v2/awsecr"
ecs "github.com/aws/aws-cdk-go/awscdk/v2/awsecs"
ecs_patterns "github.com/aws/aws-cdk-go/awscdk/v2/awsecspatterns"
"github.com/aws/aws-cdk-go/awscdk/v2/awsrds"
secretmanager "github.com/aws/aws-cdk-go/awscdk/v2/awssecretsmanager"
"github.com/aws/constructs-go/constructs/v10"
"github.com/aws/jsii-runtime-go"
)
type EMoshUStackProps struct {
cdk.StackProps
}
func NewEMoshUFargateWithALBStack(scope constructs.Construct, id string, props *EMoshUStackProps) cdk.Stack {
var sprops cdk.StackProps
if props != nil {
sprops = props.StackProps
}
stack := cdk.NewStack(scope, &id, &sprops)
// Create VPC
vpc := ec2.NewVpc(stack, jsii.String("EMoshUVpc"), &ec2.VpcProps{
MaxAzs: jsii.Number(2),
// FargateをPublicSubnetに配置するため、NatGatewayは作成しない
NatGateways: jsii.Number(0),
SubnetConfiguration: &[]*ec2.SubnetConfiguration{
{
Name: jsii.String("application"),
SubnetType: ec2.SubnetType_PUBLIC,
},
{
Name: jsii.String("rds"),
SubnetType: ec2.SubnetType_PRIVATE_ISOLATED,
},
},
})
// セキュリティグループの作成
ecsSecurityGroup := ec2.NewSecurityGroup(stack, jsii.String("EMoshU-ecs-security-group"), &ec2.SecurityGroupProps{
Vpc: vpc,
})
rdsSecurityGroup := ec2.NewSecurityGroup(stack, jsii.String("EMoshU-rds-security-group"), &ec2.SecurityGroupProps{
Vpc: vpc,
AllowAllOutbound: jsii.Bool(true),
})
rdsSecurityGroup.Connections().AllowFrom(ecsSecurityGroup.Connections(), ec2.Port_Tcp(jsii.Number(3306)), jsii.String("allow from ecs"))
// RDSのマスタユーザーとパスワード設定
databaseCredential := secretmanager.NewSecret(stack, jsii.String("EMoshU-secret"), &secretmanager.SecretProps{
RemovalPolicy: cdk.RemovalPolicy_DESTROY,
SecretName: jsii.String("mysql"),
GenerateSecretString: &secretmanager.SecretStringGenerator{
IncludeSpace: jsii.Bool(false),
ExcludePunctuation: jsii.Bool(true),
GenerateStringKey: jsii.String("password"),
SecretStringTemplate: jsii.String(`{"username": "EMoshU"}`),
},
})
// RDSクラスターの作成
rdsCluster := awsrds.NewDatabaseCluster(stack, jsii.String("EMoshU-rds-cluster"), &awsrds.DatabaseClusterProps{
Engine: awsrds.DatabaseClusterEngine_AuroraMysql(&awsrds.AuroraMysqlClusterEngineProps{Version: awsrds.AuroraMysqlEngineVersion_VER_3_01_1()}),
Vpc: vpc,
VpcSubnets: &ec2.SubnetSelection{SubnetType: ec2.SubnetType_PRIVATE_ISOLATED},
SecurityGroups: &[]ec2.ISecurityGroup{rdsSecurityGroup},
RemovalPolicy: cdk.RemovalPolicy_DESTROY,
Port: jsii.Number(3306),
Credentials: awsrds.Credentials_FromPassword(
databaseCredential.SecretValueFromJson(jsii.String("username")).UnsafeUnwrap(),
cdk.SecretValue_UnsafePlainText(databaseCredential.SecretValueFromJson(jsii.String("password")).UnsafeUnwrap()),
),
DefaultDatabaseName: jsii.String("EMoshU"),
Writer: awsrds.ClusterInstance_Provisioned(jsii.String("writer"), &awsrds.ProvisionedClusterInstanceProps{
InstanceType: ec2.InstanceType_Of(ec2.InstanceClass_T3, ec2.InstanceSize_MEDIUM),
}),
Readers: &[]awsrds.IClusterInstance{
awsrds.ClusterInstance_Provisioned(jsii.String("reader"), &awsrds.ProvisionedClusterInstanceProps{
InstanceType: ec2.InstanceType_Of(ec2.InstanceClass_T3, ec2.InstanceSize_MEDIUM),
}),
},
})
// ECSクラスター作成
cluster := ecs.NewCluster(stack, jsii.String("EMoshUECSCluster"), &ecs.ClusterProps{
Vpc: vpc,
ClusterName: jsii.String("EMoshU-ecs-cluster"),
})
// タスク定義の作成
taskDef := ecs.NewFargateTaskDefinition(stack, jsii.String("EMoshUTaskDefinition"), &ecs.FargateTaskDefinitionProps{
// 最小構成に設定する
MemoryLimitMiB: jsii.Number(512),
Cpu: jsii.Number(256),
})
// ECRのリポジトリを取得
repository := ecr.Repository_FromRepositoryName(stack, jsii.String("EMoshU-repository"), jsii.String("EMoshU-mvp"))
// タスク定義にコンテナを追加
container := taskDef.AddContainer(jsii.String("EMoshUFargateContainer"), &ecs.ContainerDefinitionOptions{
Image: ecs.ContainerImage_FromEcrRepository(repository, jsii.String("latest")),
Environment: &map[string]*string{
"DB_ADDRESS": rdsCluster.ClusterEndpoint().Hostname(),
"DATABASE": jsii.String("EMoshU"),
"DB_USER": databaseCredential.SecretValueFromJson(jsii.String("username")).UnsafeUnwrap(),
"DB_PASSWORD": databaseCredential.SecretValueFromJson(jsii.String("password")).UnsafeUnwrap(),
},
Logging: ecs.AwsLogDriver_AwsLogs(&ecs.AwsLogDriverProps{
StreamPrefix: jsii.String("EMoshU-mvp"),
}),
})
// コンテナとのポートマッピングを設定
container.AddPortMappings(&ecs.PortMapping{
ContainerPort: jsii.Number(8080),
Protocol: ecs.Protocol_TCP,
})
// サービスを作成
ecs_patterns.NewApplicationLoadBalancedFargateService(stack, jsii.String("ALBFargoService"), &ecs_patterns.ApplicationLoadBalancedFargateServiceProps{
Cluster: cluster,
TaskDefinition: taskDef,
TaskSubnets: &ec2.SubnetSelection{
Subnets: vpc.PublicSubnets(),
},
DesiredCount: jsii.Number(2),
ListenerPort: jsii.Number(80),
PublicLoadBalancer: jsii.Bool(true),
AssignPublicIp: jsii.Bool(true),
SecurityGroups: &[]ec2.ISecurityGroup{ecsSecurityGroup},
HealthCheckGracePeriod: cdk.Duration_Seconds(jsii.Number(3600)),
})
return stack
}
func main() {
// CDKアプリケーションの初期化
app := cdk.NewApp(nil)
// ECSのサービスとタスク定義
NewEMoshUFargateWithALBStack(app, "EMoshUFargateWithALBStack", &EMoshUStackProps{
cdk.StackProps{},
})
app.Synth(nil)
}
VPCやDBなどの細かい説明は省略しますが、公式のサンプルコードが書き方などの参考になります。
https://github.com/aws-samples/aws-cdk-examples/tree/master/go/ecs
以上のファイルが配置されたディレクトリで
$ cdk deploy
を行うと、上記の構成のインフラが整います。
一旦APIを動かす環境を作りたかったので、上記のコードでは以下のことが対応できていません。
ここら辺はサービスリリースまでに対応したいところです。
ただインフラ未経験の私でも構築することができたので、CDKは便利なツールだと思います(Goだと情報量が少なかったので、情報量が多いTypeScriptにすれば良かったと途中で後悔していました。)。
ここまで読んでいただきありがとうございました。
EMoshUでは一緒に開発できる仲間を募集しております(インフラに強い人求む)。
ご興味を持たれましたら、ぜひ募集要項をご覧ください。カジュアル面談なども実施しております!