ECS Web ApplicationハンズオンをAWSを使って実施 2 Fargateへのデプロイ・スタック分割

## やったこと

https://qiita.com/k5trismegistus/items/e08c605b5db39396b952

前回の続きになります。

https://catalog.workshops.aws/ecs-web-application-handson/ja-JP
- ハンズオンのAWSマネジメントコンソール操作をすべてcdkに置き換える
- aws-ecs-patterns は使わない

今回はFargateへのデプロイから、前回のcdk deployが終わらない問題に対処するためスタックを分割するまでです。

## 早速成果

```bin/cdk.ts
#!/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,
  }
);
```

```lib/vpc-stack.ts
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,
    });
  }
}
```

```lib/ecr-stack.ts
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,
    });
  }
}
```

```lib/application-stack.ts
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で参照するリソースを削除することもできてしまいます。

![スクリーンショット 2024-10-17 23.44.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133157/7fa66424-2fad-eb60-a1db-92fe8681336c.png)
↑なんか一つ失敗しているのは無視。

こういった不整合を防ぐ「仕組み」はないので分けない方がいいということのようですね。

今回の場合もスタックを分割するのではなく、白紙の状態から初回デプロイの時はコマンドライン引数でそれを伝え、ECRリポジトリ作成だけ実施してreturnさせるなどスタックをわけないほうがいいなと思います。
 
### CodeCommitは新規利用できない

やっていたハンズオンには「CI/CD ハンズオン」という説があり、CodeCommit、CodePipeline、CodeBuildを使ったCI/CDをやってみるという内容があります。

このためにECR周りだけスタックを切り出してそこにCI/CD周りは追記していくぞ〜と思っていたのですが、、

https://dev.classmethod.jp/articles/aws-start-to-restrict-codecommit-and-cloudsearch/

今年の7月から、codecommitを今まで利用したことがないアカウントでの新規利用ができなくなったようです。