やったこと
前回の続きになります。
- ハンズオンのAWSマネジメントコンソール操作をすべてcdkに置き換える
- aws-ecs-patterns は使わない
今回はFargateへのデプロイから、前回のcdk deployが終わらない問題に対処するためスタックを分割するまでです。
早速成果
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { VpcStack } from "../lib/vpc-stack";
import { EcrStack } from "../lib/ecr-stack";
import { ApplicationStack } from "../lib/appliation-stack";
const app = new cdk.App();
const account = process.env.CDK_DEFAULT_ACCOUNT;
const region = process.env.CDK_DEFAULT_REGION;
const vpcStack = new VpcStack(app, "EcsHandsonVpcStack", {
description: "VPCStack",
env: {
account: account,
region: region,
},
});
const ecrStack = new EcrStack(app, "EcsHandsonEcrStack", {
description: "ECRStack",
env: {
account: account,
region: region,
},
});
const applicationStack = new ApplicationStack(
app,
"EcsHandsonApplicationStack",
{
description: "ApplicationStack",
env: {
account: account,
region: region,
},
vpc: vpcStack.vpc,
ecrRepository: ecrStack.ecrRepository,
}
);
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
export class VpcStack extends Stack {
public readonly vpc: ec2.Vpc;
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
this.vpc = new ec2.Vpc(this, "EcsHandsOnVpc", {
vpcName: "EcsHandsOnVpc",
ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
subnetConfiguration: [
{
cidrMask: 24,
name: "Public1",
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: "Public2",
subnetType: ec2.SubnetType.PUBLIC,
},
],
// To avoid the subnets being created in the same AZ, specify the maxAzs property
maxAzs: 2,
});
}
}
import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ecr from "aws-cdk-lib/aws-ecr";
export class EcrStack extends Stack {
public readonly ecrRepository: ecr.Repository;
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
this.ecrRepository = new ecr.Repository(this, "EcsHandsOnRepository", {
repositoryName: "ecs_handson_repository",
// To remove the repository when the stack is deleted, you must explicitly set the removal policy to DESTROY
removalPolicy: RemovalPolicy.DESTROY,
// To remove the repository when the stack is deleted even if it has images,
// you must explicitly set the removal policy to DESTROY and set the emptyOnDelete property to true
emptyOnDelete: true,
});
}
}
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as elb from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as autoscaling from "aws-cdk-lib/aws-applicationautoscaling";
interface ApplicationStackProps extends StackProps {
vpc: ec2.Vpc;
ecrRepository: ecr.Repository;
}
export class ApplicationStack extends Stack {
constructor(scope: Construct, id: string, props: ApplicationStackProps) {
super(scope, id, props);
const { vpc, ecrRepository } = props;
const cluster = new ecs.Cluster(this, "EcsHandsOnCluster", {
vpc: vpc,
clusterName: "EcsHandsOnCluster",
enableFargateCapacityProviders: true,
});
const taskDefinition = new ecs.TaskDefinition(this, "EcsHandsOnTask", {
compatibility: ecs.Compatibility.EC2_AND_FARGATE,
cpu: "256",
memoryMiB: "512",
runtimePlatform: {
cpuArchitecture: ecs.CpuArchitecture.ARM64,
},
});
const container = taskDefinition.addContainer("EcsHandsOnContainer", {
image: ecs.ContainerImage.fromEcrRepository(ecrRepository),
memoryLimitMiB: 512,
cpu: 256,
portMappings: [
{
containerPort: 5000,
},
],
});
const albSecurityGroup = new ec2.SecurityGroup(this, "AlbSecurityGroup", {
vpc,
allowAllOutbound: true,
});
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
"Allow HTTP traffic"
);
const ecsService = new ecs.FargateService(this, "EcsHandsOnService", {
cluster,
taskDefinition,
desiredCount: 1,
assignPublicIp: true,
capacityProviderStrategies: [
{
capacityProvider: "FARGATE",
weight: 1,
},
],
});
const spotService = new ecs.FargateService(this, "EcsHandsOnSpotService", {
cluster,
taskDefinition,
desiredCount: 1,
assignPublicIp: true,
capacityProviderStrategies: [
{
capacityProvider: "FARGATE_SPOT",
weight: 1,
},
],
});
const alb = new elb.ApplicationLoadBalancer(this, "MyALB", {
vpc: vpc,
internetFacing: true,
securityGroup: albSecurityGroup,
vpcSubnets: {
subnetType: ec2.SubnetType.PUBLIC,
onePerAz: true,
},
});
const targetGroup = new elb.ApplicationTargetGroup(this, "MyTargetGroup", {
vpc,
port: 5000,
protocol: elb.ApplicationProtocol.HTTP,
targets: [ecsService, spotService],
deregistrationDelay: cdk.Duration.seconds(10),
});
alb.addListener("Listener", {
port: 80,
defaultAction: elb.ListenerAction.forward([targetGroup]),
});
const autoScaling = ecsService.autoScaleTaskCount({
minCapacity: 1,
maxCapacity: 10,
});
autoScaling.scaleOnRequestCount("RequestScaling", {
requestsPerTarget: 1000,
targetGroup,
scaleInCooldown: cdk.Duration.seconds(300),
scaleOutCooldown: cdk.Duration.seconds(10),
});
const spotAutoScaling = spotService.autoScaleTaskCount({
minCapacity: 0,
maxCapacity: 10,
});
spotAutoScaling.scaleOnMetric("SpotMetricScaling", {
metric: targetGroup.metrics.requestCountPerTarget({
period: cdk.Duration.minutes(1), // 短い期間でデータを収集
statistic: "sum",
}),
scalingSteps: [
{ upper: 500, change: -1 }, // リクエストが500以下ならスケールイン
{ lower: 700, change: +1 }, // リクエストが700以上ならスケールアウト
],
evaluationPeriods: 1, // データポイントの評価回数を1回に
adjustmentType: autoscaling.AdjustmentType.CHANGE_IN_CAPACITY,
cooldown: cdk.Duration.seconds(10), // スケールアウトのクールダウン時間を短く
});
}
}
これには前回からの差分として以下が含まれます。
- スタックの分割
これによって、ECRリポジトリの作成だけデプロイした後にイメージをプッシュでき、さらにそのあとでECSサービスを始めるデプロイがかけられるということでcdk deployが終わらない問題は無くなりました - Fargate Spotを利用したECSサービスと、オートスケーリングの設定
スケールアウト時にはFatgate Spotが優先的に立ち上がるような設定になります
学び
スタック分割をしないのがベストプラクティスといわれている
cdkのエントリーポイントのファイルをみると、スタックAで作られたリソースに対応するオブジェクトをスタックBに渡すとそのリソースを参照できています。
しかしマネジメントコンソールを見ればそれぞれのスタックは独立しており、スタックAだけの変更でスタックBで参照するリソースを削除することもできてしまいます。
こういった不整合を防ぐ「仕組み」はないので分けない方がいいということのようですね。
今回の場合もスタックを分割するのではなく、白紙の状態から初回デプロイの時はコマンドライン引数でそれを伝え、ECRリポジトリ作成だけ実施してreturnさせるなどスタックをわけないほうがいいなと思います。
CodeCommitは新規利用できない
やっていたハンズオンには「CI/CD ハンズオン」という説があり、CodeCommit、CodePipeline、CodeBuildを使ったCI/CDをやってみるという内容があります。
このためにECR周りだけスタックを切り出してそこにCI/CD周りは追記していくぞ〜と思っていたのですが、、
今年の7月から、codecommitを今まで利用したことがないアカウントでの新規利用ができなくなったようです。