ALBのルーティングを応用してレガシーWEBシステムにモダンなサブシステムを導入する 2. ECR/ECS編

Agaroot IT Partners(AITP)のtomoです。

この記事は前回の記事の続きになります。
ALBやEC2インスタンスなどによって構築されたレガシーWEBシステムに、特定のパスへのリクエストに対してECS on Fargate上の新規アプリによってレスポンスを返却するサブシステムを下記CDKコードで構築します。
https://github.com/aitp-tomo/alb-subsystem

今回はECS on Fargate上のアプリを構築する実装方法について触れます。

実装について触れる前に~説明を省略する部分について~

上記CDKコードではCDK Pipelinesを用いたCI/CDパイプラインの構築やCloudWatchアラームなどを用いたアラート処理も行っています。

これらの構築方法などについては前に投稿した記事と重複する点が多いので、そちらを参考にしていただければと思います。

また前回の記事の「この記事はこのような方におすすめ」にて記載した通り、ECS/Fargate/ECRの概要についても省略させていただきます。必要に応じて下記記事をご参照ください。

ECRでリポジトリやイメージを作成する

この章の内容は下記記事を大いに参考にしております。
https://dev.classmethod.jp/articles/cdk-ecs-ecr/

まずECRにリポジトリやイメージを作成して後述するタスク定義で使用できるようにしましょう。
主なソースはこちらになります。
https://github.com/aitp-tomo/alb-subsystem/blob/main/lib/wrapper/EcrWrapper.ts

  private readonly createRepository = (): void => {
    const id = `${this.appId}-ecr-repo`;
    this.repository = new ecr.Repository(this.scope, id, {
      repositoryName: id,
    });
  };

  private readonly createDockerImageAsset = (): void => {
    const id = `${this.appId}-docker-image-asset`;
    this.dockerImageAsset = new ecrAssets.DockerImageAsset(this.scope, id, {
      directory: path.join(__dirname, "..", "asset"),
      platform: ecrAssets.Platform.LINUX_AMD64,
      assetName: id,
    });
  };

  private readonly deployDockerImage = (): void => {
    new ecrDeploy.ECRDeployment(
      this.scope,
      `${this.appId}-deploy-docker-image`,
      {
        src: new ecrDeploy.DockerImageName(this.dockerImageAsset.imageUri),
        dest: new ecrDeploy.DockerImageName(
          `${this.account}.dkr.ecr.${this.region}.amazonaws.com/${this.repository.repositoryName}:latest`
        ),
      }
    );
  };

リポジトリの作成は見ての通りカスタマイズが不要であれば簡単に終わります。リポジトリ名を設定していますが、これも特に拘らないのであれば特に指定しなくとも自動で設定されます。

イメージをリポジトリにプッシュするには、CodePipelineなどを用いてGitHubなどにプッシュされたソースをECRのリポジトリにプッシュできるようにする方法などもありますが、今回はもっと簡単に/lib/assetなどに配置されたソースをそのままプッシュするようにしましょう(今回のCDKコードではサブシステム全体のCI/CDパイプラインが別途作成されるので、わざわざECRへのプッシュのために別のCI/CDパイプラインを作成する必要もあまりないかと思います)。
ecrAssets.DockerImageAssetを用いてイメージを作成できます。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecr_assets.DockerImageAsset.html

更に作成したイメージに対してタグをつけてプッシュするためにcdk-ecr-deploymentというコンストラクトライブラリを用いましょう(AWS Construct Libraryとは別にインストールが必要です)。
https://constructs.dev/packages/cdk-ecr-deployment/v/3.0.87?lang=typescript

なお、コンテナアプリはサブシステムがリクエストを受け付けるパスで動くように実装してください。それさえできればどのような技術スタックを用いても構いません。

タスク定義とコンテナの作成

次いでタスク定義を作成し、前の章で作成したECR上のイメージからコンテナを作成してタスク定義に追加しましょう。
主なソースはこちらになります。
https://github.com/aitp-tomo/alb-subsystem/blob/main/lib/wrapper/TaskDefinitionWrapper.ts

  private readonly createTaskDefinition = (): void => {
    const taskDefinitionId = `${this.appId}-task-definition`;
    this.taskDefinition = new ecs.FargateTaskDefinition(
      this.scope,
      taskDefinitionId,
      {
        family: `${taskDefinitionId}-family`,
        cpu: this.cpu,
        memoryLimitMiB: this.memoryLimitMiB,
      }
    );
  };

// 中略

  private readonly addContainer = (): void => {
    const image = ecs.EcrImage.fromDockerImageAsset(
      this.ecrWrapper.dockerImageAsset
    );
    this.taskDefinition.addContainer(this.containerName, {
      image,
      containerName: this.containerName,
      cpu: this.cpu,
      memoryLimitMiB: this.memoryLimitMiB,
      portMappings: [
        {
          containerPort: 80,
        },
      ],
      logging: this.logDriver,
    });
  };

Fargate向けのタスク定義を作成するにはecs.FargateTaskDefinitionを用いるのが最適でしょう。今回はファミリー名の他CPU及びメモリの割り当て量を指定していますが、全くプロパティ無しでも作成可能です。

作成したタスク定義に、前述のECRイメージをもとにaddContainerメソッドを用いてコンテナを追加しています。
その際にコンテナへのCPU及びメモリの割り当て量も指定していますが、今回はタスク定義内にコンテナを1つのみ追加するためタスク定義とコンテナとで割り当て量を同一にしています。もしもコンテナを複数用いるような場合には、それぞれのコンテナへの割り当て量の総和がタスク定義への割り当て量を超えないように設定してください。

尚、ソース内にはログに関する記述もありますが、その点については次回の記事にてKinesisなどと共に説明していきます。

クラスタ及びサービスの作成

次にECSのクラスタ並びにサービスを作成していきます。
主なソースはこちらになります。
https://github.com/aitp-tomo/alb-subsystem/blob/main/lib/wrapper/EcsWrapper.ts

  private readonly createCluster = (): void => {
    const clusterId = `${this.appId}-cluster`;
    this.cluster = new ecs.Cluster(this.scope, clusterId, {
      clusterName: clusterId,
      containerInsights: true,
      vpc: this.vpcWrapper.vpc,
    });
  };

  private readonly createService = (): void => {
    const serviceId = `${this.appId}-service`;
    this.service = new ecs.FargateService(this.scope, serviceId, {
      serviceName: serviceId,
      cluster: this.cluster,
      taskDefinition: this.taskDefinitionWrapper.taskDefinition,
      assignPublicIp: true,
      deploymentController: {
        type: ecs.DeploymentControllerType.ECS,
      },
      securityGroups: [this.vpcWrapper.securityGroup],
      vpcSubnets: { subnets: this.vpcWrapper.subnets },
      desiredCount: this.desiredCount,
      minHealthyPercent: this.minHealthyPercent,
      maxHealthyPercent: this.maxHealthyPercent,
    });
  };

  private readonly autoScale = (): void => {
    if (this.isAutoScalable) {
      const scalableTarget = this.service.autoScaleTaskCount({
        minCapacity: this.minCapacity,
        maxCapacity: this.maxCapacity!,
      });
      if (this.cpuScaling) {
        scalableTarget.scaleOnCpuUtilization(`${this.appId}-cpu-scaling`, {
          targetUtilizationPercent: this.cpuScaling,
        });
      }
      if (this.memoryScaling) {
        scalableTarget.scaleOnMemoryUtilization(
          `${this.appId}-memory-scaling`,
          {
            targetUtilizationPercent: this.memoryScaling,
          }
        );
      }
    }
  };

クラスターの作成時、VPCを指定しない場合にはVPCが新規作成されます。今回は既存VPCにクラスターを作成するので指定を行っておきましょう。
他のプロパティについても任意で指定してください。

ECS on Fargateのサービスを作成する方法の1つがecs.FargateServiceの利用です。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.FargateService.html
タスク定義やサブネットなどの指定の他、deploymentControllerというプロパティの指定によってブルー/グリーンデプロイに変更することもできます(その代わりターゲットグループの作成などがややこしくなるので今回は断念しました💦)。

そしてサービスからScalableTaskCountというコンストラクトを取得し、オートスケーリングの設定もできます。

次回はALB/Kinesis編

次回は既存ALBに新規ターゲットグループを追加し、その転送先にECSサービスを指定する方法のほか、Kinesisなどを用いてログを長期間保存できるようにする方法を紹介します。どうぞご期待ください👋

追記

続きが公開されたので、是非ご一読ください。

3. ALB/Kinesis編

関連するタグ