ECSとECRのコンテナ構成をCDKで実装してみた

2023.09.03

こんにちは、つくぼし(tsukuboshi0755)です!

最近案件でCDK(TypeScript)を使うため、色んなAWSリソースをCDKで書けるように絶賛勉強中の身です。

今回は何番煎じになるか分からないですが、CDKv2を使って、ECSとECRのコンテナ構成を実装してみます!

前提条件

今回は以下の通り、CDKv2を使ってコードを書いていきます。

$ cdk version
2.83.1 (build 006b542)

またDockerクライアントとしては、Rancher Desktopを使用します。

$ rdctl version
rdctl client version: 1.1.0, targeting server version: v1

全体構成

今回はVPC+ALB+ECS(Fargate)+ECRの構成を作成し、nginxコンテナをパブリック公開します。

なおDockerfileを用いてビルドしたイメージをECRにプッシュし、そのイメージを用いてECSタスクを起動する処理についても、全てCDKコードで自動化します。

リポジトリ

コード全体については、以下のリポジトリに格納していますのでご参照ください。

tsukuboshi/cdk-microservices-template

コード解説

CDKコードの中核となるlib/cdk-microservices-template-stack.ts について説明します。

AWSアカウントID/リージョン/リソース名の設定

lib/cdk-microservices-template-stack.ts

    // Get AWS Account ID and Region
    const { accountId, region } = new ScopedAws(this);

    // Set Resource Name
    const resourceName = "test";

ScopedAwsを用いて、ECRリポジトリURLの設定に必要なAWSアカウントIDとリージョンを取得します。

またAWSリソース全体で使用するリソース名についても、ここで任意の名前を設定します。

ECR作成/イメージプッシュ実施

lib/cdk-microservices-template-stack.ts

    // Create ECR Repository
    const ecrRepository = new ecr.Repository(this, "EcrRepo", {
      repositoryName: `${resourceName}-ecr-repo`,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteImages: true,
    });

    // Create Docker Image Asset
    const dockerImageAsset = new DockerImageAsset(this, "DockerImageAsset", {
      directory: path.join(__dirname, "..", "app"),
      platform: Platform.LINUX_AMD64,
    });

    // Deploy Docker Image to ECR Repository
    new ecrdeploy.ECRDeployment(this, "DeployDockerImage", {
      src: new ecrdeploy.DockerImageName(dockerImageAsset.imageUri),
      dest: new ecrdeploy.DockerImageName(
        `${accountId}.dkr.ecr.${region}.amazonaws.com/${ecrRepository.repositoryName}:latest`
      ),
    });

今回のECRリポジトリは検証用のため、autoDeleteImagesをtrueで設定し、リポジトリにイメージが存在する場合でも削除できるようにしています。

またDockerImageAssetを用いて、appディレクトリ内に存在するDockerfileを元に、イメージをビルドします。

さらにcdk-ecr-deploymentを用いて、ビルドしたイメージにlatastタグを付与してECRリポジトリにプッシュします。

なおcdk-ecr-deploymentcdklabsモジュールでの提供になるため、別途npmでインストールする必要がありますのでご注意ください。

VPC作成

lib/cdk-microservices-template-stack.ts

    // Create VPC and Subnet
    const vpc = new ec2.Vpc(this, "Vpc", {
      vpcName: `${resourceName}-vpc`,
      maxAzs: 2,
      ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/20"),
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: `${resourceName}-public`,
          subnetType: ec2.SubnetType.PUBLIC,
        },
      ],
    });

ECSクラスターにVPCの指定が必要になるため、事前にVPCを作成します。

今回はVPC内にパブリックサブネットを2つ作成し、コンテナを作成したサブネット内で稼働させます。

ALB/ECS作成

lib/cdk-microservices-template-stack.ts

    // Create ECS Cluster
    const cluster = new ecs.Cluster(this, "EcsCluster", {
      clusterName: `${resourceName}-cluster`,
      vpc: vpc,
    });

    // Create CloudWatch Log Group
    const logGroup = new logs.LogGroup(this, "LogGroup", {
      logGroupName: `/aws/ecs/${resourceName}`,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // Create ALB and ECS Fargate Service
    const service = new ecs_patterns.ApplicationLoadBalancedFargateService(
      this,
      "FargateService",
      {
        loadBalancerName: `${resourceName}-lb`,
        publicLoadBalancer: true,
        cluster: cluster,
        serviceName: `${resourceName}-service`,
        cpu: 256,
        desiredCount: 2,
        memoryLimitMiB: 512,
        assignPublicIp: true,
        taskSubnets: { subnetType: ec2.SubnetType.PUBLIC },
        taskImageOptions: {
          family: `${resourceName}-taskdef`,
          containerName: `${resourceName}-container`,
          image: ecs.ContainerImage.fromEcrRepository(ecrRepository, "latest"),
          logDriver: new ecs.AwsLogDriver({
            streamPrefix: `container`,
            logGroup: logGroup,
          }),
        },
      }
    );

事前にECSクラスターとロググループを作成した後、ecs_patterns.ApplicationLoadBalancedFargateServiceモジュールを用いて、ALB/ECSサービス及び関連リソースをまとめて作成します。

今回はパブリックサブネット内にALB及びECSサービスを作成するため、publicLoadBalancerassignPublicIpはtrueで設定します。

またtaskImageOptionimageにて、ECRリポジトリにプッシュしたlatestタグを持つイメージを指定し、タスクを起動させます。

さらにtaskImageOptionslogDriverにて、タスクのログを事前に作成したロググループへ出力するように設定します。

動作確認

CDKコードのデプロイを行う事で、ECSタスクが正常に起動するか確認します。

初めに、必要なnpmパッケージのインストールとCDK実行環境の整備を事前に実施しておきます。

$ git clone https://github.com/tsukuboshi/cdk-microservices-template
$ cd cdk-microservices-template
$ npm install
$ cdk bootstrap

またイメージビルドに必要なDockerクライアントを事前に起動させてください。

Rancher Desktopを使用している場合は、以下のコマンドで起動できます。

$ rdctl start

準備が整いましたら、以下の通りCDKコードをデプロイします。

変更セットを確認し、問題がなければyを入力します。

正常にデプロイが完了すると、アウトプットにALBのDNS名とECSサービスのURLが表示されます。

$ cdk deploy
(中略)
Do you wish to deploy these changes (y/n)? y
(中略)
Outputs:
CdkMicroservicesTemplateStack.FargateServiceLoadBalancerDNSXXXXXXXX = <ALBのDNS名>
CdkMicroservicesTemplateStack.FargateServiceServiceURLXXXXXXXX = <ECSサービスURL>

curlコマンドでALBのDNS名にアクセスすると、以下の通りnginxのデフォルトページが表示されました!

$ curl <ALBのDNS名>
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

最後に

今回はCDKv2を使って、ECSとECRのコンテナ構成を実装してみました。

DockerImageAssetモジュールとcdk-ecr-deploymentモジュールを組み合わせる事で、CDKコード内でイメージのビルドとECRへのプッシュが自動化できるのは非常に便利ですね。

またL2のaws_ecs_patternsモジュールを使用する事で、ECSに必要なリソースを作成するためのコード記述量を減らせるので、ECS構成をCDKで書く場合は積極的に利用すると良さそうです。

ぜひCDKでECS構成を実装する際に、参考にしてみてください。

以上、つくぼし(tsukuboshi0755)でした!