背景
最近はLambda + DynamoDBの構成ばかりで大変楽をしていたのですが、RDSと接続する必要が出てきました。
えー、、EC2いじるの?もうsshログインとかしたくない(忘れた
Amazon RDS Proxyが発表されたところなのでこれを使えれば何も問題ないのですが、既存RDSがまだサポート対象外だったので、Lambdaを使わずに楽をするにはどうすればいいか、、、?と考えて構成を作ってみました。
構成案
- API Gateway + VPCLink + VPC(NLB + ECS + Fargate + RDS) + PrivateLink + ECR
- VPCはIsolated Network内に作成
- 全部CDKで書く!
やってみよう
リソースに依存関係があるので、後ろから順番に作成していきます。まずはECRから。
ECR
import {Repository, TagStatus} from '@aws-cdk/aws-ecr';
const repository = new Repository(this, `${this.stackName}Repository}`, {
repositoryName: `repo-name`,
lifecycleRules: [
{
tagStatus: TagStatus.ANY,
maxImageCount: 3
}
]
});
特に何も考えず作成できます。
無駄な課金を避けるのに、ライフサイクルルールで古いイメージは自動削除するように設定。
中身のDocker Imageには8080ポートで動作する、RDSにアクセスするAPIサーバを入れています。
VPC
一番簡単なのはNetworkLoadBalancedFargateServiceを使うことで、これならほとんど何も考えずVPCからFargateまで全部作れるのですが、いくつか難点もありました。
試験環境で最小構成で、、と試してみたら1日4ドルくらい請求されてて、「あれ?Fargateってこんな高い?おかしくない?」と調べたらNAT Gatewayを自動生成してしまっており、もったいない感。
加えてCDKの1.19.0時点では適切なセキュリティグループを自動設定してくれないという問題もあり、諦めて全部手書きすることに。
import {
Peer,
Port,
SecurityGroup,
SubnetType,
Vpc
} from '@aws-cdk/aws-ec2';
const vpc = new Vpc(this, `${this.stackName}VPC`, {
cidr: '10.0.0.0/16',
natGateways: 0,
subnetConfiguration: [
{
cidrMask: 19,
name: 'Isolated',
subnetType: SubnetType.ISOLATED,
},
],
enableDnsHostnames: true,
enableDnsSupport: true
});
const securityGroup = new SecurityGroup(this, `${this.stackName}SecurityGroup`, {
vpc,
securityGroupName: 'API-NLB'
});
securityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(8080)); // ECSとNLBの通信用
はい、これでIsolatedなVPCができました。CIDRは適当。NAT作りたくないという強い意思の下、明示的に「natGateways: 0」って書いた。
なお、このセキュリティグループは既存のRDSにアクセスできるように別途設定しています(既存のRDSはCDKじゃなかったのでこれは手動で)。
ただ、最後まで作成した後にCannotPullContainerErrorが出て「えー、ECRとECSが通信できてないの、、、なんで?」となりました。
調べたところECRはインターネットを介してアクセスすることになってるとか。
...同じAWSリソースやんか、、、まじか、、、
さらに調べると、NATの代わりにPrivateLinkも使えるよ、という情報があり、料金としてはNATよりもマシだったのでこちらを採用することに。
import {GatewayVpcEndpointAwsService, InterfaceVpcEndpointAwsService} from '@aws-cdk/aws-ec2';
securityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(443)); // ECRからコンテナを取得する用
vpc.addInterfaceEndpoint(`${this.stackName}EcrDockerEndpoint`, {
service: InterfaceVpcEndpointAwsService.ECR_DOCKER,
privateDnsEnabled: true,
subnets: {subnetType: SubnetType.ISOLATED},
securityGroups: [securityGroup]
});
vpc.addInterfaceEndpoint(`${this.stackName}CloudWatchLogsEndpoint`, {
service: InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
privateDnsEnabled: true,
subnets: {subnetType: SubnetType.ISOLATED},
securityGroups: [securityGroup]
});
vpc.addGatewayEndpoint(`${this.stackName}S3Endpoint`, {
service: GatewayVpcEndpointAwsService.S3,
subnets: [{subnetType: SubnetType.ISOLATED}]
});
ECRと接続するのに「InterfaceVpcEndpointAwsService.ECR_DOCKER」を作る。これはわかる。
ECSのタスクがログ出力するために「InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS」を作る。仕方ない、これもわかる。
「GatewayVpcEndpointAwsService.S3」、、、S3にアクセスしとらんのに何でやねん?ですが、PrivateネットワークだとS3へのアクセス権もいるんだって。
くぅ、PrivateLink3つ、、それでもまぁ、NATをサブネットごとに作るよりはお安いか。
セキュアな構成ってコストかかるね!と思いつつ次に進みます。
(AWS内で完結した構成のつもりなのになー。腑に落ちないぜ!)
NLB
import {Duration} from '@aws-cdk/core';
import {NetworkLoadBalancer, NetworkTargetGroup, TargetType} from '@aws-cdk/aws-elasticloadbalancingv2';
const nlb = new NetworkLoadBalancer(this, `${this.stackName}NLB`, {
vpc,
loadBalancerName: 'ApiNLB',
vpcSubnets: {subnetType: SubnetType.ISOLATED},
crossZoneEnabled: true,
internetFacing: false
});
const targetGroup = new NetworkTargetGroup(this, `${this.stackName}TargetGroup`, {
targetGroupName: 'ecs-tagtet-group',
vpc,
port: 8080,
targetType: TargetType.IP,
deregistrationDelay: Duration.seconds(0) // 開発環境デプロイ高速化のため。本番環境は数値上げておく
});
nlb.addListener(`${this.stackName}Listener`, {
port: 80,
defaultTargetGroups: [targetGroup]
});
NLBを作成完了しました。この時点ではtargetGroupには何も追加せず、後ほどECSの作成で利用します。
ECS + Fargate
import {Cluster, ContainerImage, FargateService, FargateTaskDefinition, LogDriver} from '@aws-cdk/aws-ecs';
import {CompositePrincipal, Role, ServicePrincipal} from '@aws-cdk/aws-iam';
import {RetentionDays} from '@aws-cdk/aws-logs';
const taskDef = new FargateTaskDefinition(this, `${this.stackName}TaskDefinition`, {
cpu: 256,
memoryLimitMiB: 512,
executionRole: new Role(this, `${this.stackName}ExecutionRole`, {
assumedBy: new CompositePrincipal(
new ServicePrincipal('ecs.amazonaws.com'),
new ServicePrincipal('ecs-tasks.amazonaws.com')
)
}),
taskRole: new Role(this, `${this.stackName}TaskRole`, {
assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),
})
});
const container = taskDef.addContainer(`${this.stackName}Container`, {
image: ContainerImage.fromEcrRepository(repository, 'latest'),
logging: LogDriver.awsLogs({
streamPrefix: 'Fargate',
logRetention: RetentionDays.ONE_WEEK
})
});
container.addPortMappings({
containerPort: 8080,
});
const cluster = new Cluster(this, `${this.stackName}Cluster`, {vpc});
const service = new FargateService(this, `${this.stackName}Service`, {
cluster,
securityGroup,
serviceName: 'ApiService',
taskDefinition: taskDef,
vpcSubnets: {subnetType: SubnetType.ISOLATED},
assignPublicIp: false,
desiredCount: 1
});
service.loadBalancerTarget({
containerName: container.containerName,
containerPort: 8080,
});
service.attachToNetworkTargetGroup(targetGroup);
service.autoScaleTaskCount({minCapacity: 1, maxCapacity: 2})
.scaleOnCpuUtilization(`${this.stackName}AutoScale`, {
targetUtilizationPercent: 80
}
);
はい、できましたー。
とさっくり言うてますが、設定可能なパラメータは大量にあり、ざっと調べて意味が分かったものだけ書いてます。
コンテナ周り奥深すぎて、全然最適な設定にたどり着ける気がしない、、、
この辺はもう少し運用経験を積まないとダメそうです。
おかしいな、EC2より楽になるのを期待してるんだけど、まだあんまり楽な感じがしないw
API Gateway + VPCLink
import {
RestApi,
ConnectionType,
Integration,
IntegrationType,
VpcLink
} from '@aws-cdk/aws-apigateway';
const vpcLink = new VpcLink(this, `${this.stackName}VpcLink`, {
vpcLinkName: 'api-vpc-link',
description: 'APIのNLBに接続します。',
targets: [nlb]
});
const api = new RestApi(this, `${this.stackName}RestApi`, {
restApiName: 'API',
deployOptions: {stageName: 'development'}
});
const v1 = api.root.addResource('v1');
v1.addMethod('GET', new Integration({
type: IntegrationType.HTTP_PROXY,
integrationHttpMethod: 'GET',
uri: `http://${nlb.loadBalancerDnsName}`,
options: {
connectionType: ConnectionType.VPC_LINK,
vpcLink: vpcLink
}}), {});
ここまでやって、API Gatewayのテストからリクエストが通れば完成です!
感想
VPCとか遠い昔の記憶だし、ECSとか「ぜんぜんわからない 俺たちは雰囲気でECSをやっている」という感じなので、動きはしたものの「これ運用に耐えるんか?」という気持ちでいっぱいです。
これからもう少しブラッシュアップして行きたいと思いつつ、どなたかのお役に立てれば幸いです。
追記
CDK最高!