🤖

【AWS CDK】コンテナイメージをビルドしてECS Fargateで起動する

2024/02/22に公開

概要

今回はCDKをデプロイする中でコンテナイメージのビルドを行い、そのままイメージをECRにプッシュ。
さらにプッシュしたイメージを使ってECSのサービスを立ち上がるところまでをワンセットで行うコードを紹介します。
サンプルなので簡単のためシンプルにNginxを立ち上げるだけに留めますが、アプリケーションまるまるコンテナ化している場合でも理屈は同じなので流用可能です。

環境

Node

node -v
v20.11.1

package.jsonから抜粋

{
  "dependencies": {
    "aws-cdk": "2.127.0",
    "aws-cdk-lib": "2.127.0",
    "typescript": "~5.3.3"
  }
}

ディレクトリ構成

以下のような構成をイメージしています。
cdksrc(今回は使わないが、アプリケーションのコードが格納されてるイメージです)とinfraがあり、infraの中にDockerfile等々が収まっています。

root
  - cdk
    - lib
      - stack.ts
  - src(アプリケーション本体)
  - infra
    - docker
      - nginx
        - Dockerfile
        - default.conf

Dockerfile

Dockerfileですが、コンテキストはroot直下を指定した場合を想定しています。
なのでCOPYのパス指定が./infraから始まっています。
後述のStack.synthesizer.addDockerImageAssetを使う場合にコンテキストを指定することができますが、注意が必要です。

root/infra/docker/nginx/Dockerfile
FROM nginx:1.25
# default.confコピー
COPY ./infra/docker/nginx/default.conf /etc/nginx/conf.d/

cdk

いよいよ本体のcdkです。
Stack.synthesizer.addDockerImageAssetで先のDockerfileのパスとコンテキストを指定していますが、基準となるのはスタックファイルのパス(ここでいうところのroot/cdk/lib/stack.ts)なので、そこからの相対パスを指定する点に注意です。

https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_core.IStackSynthesizer.html

Nginxへのアクセスを確認するためにALBなどを作成していますが、用途によっては不要です。

root/cdk/lib/stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as path from 'path';

export class MainStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // nginxのDockerfileからイメージをビルドしてECRにプッシュ
    const webLocation = this.synthesizer.addDockerImageAsset({
      // buildcontextの指定
      directoryName: path.join(__dirname, '../..'),
      // Dockerfileの指定
      dockerFile: path.join(__dirname, '../../infra/docker/nginx/Dockerfile'),
      sourceHash: "web",
    });

    // VPC作成
    const vpc = new ec2.Vpc(this, 'MyVPC', {
      ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
    });

    // セキュリティグループの作成
    const publicSecurityGroup = new ec2.SecurityGroup(this, 'MyPublicSecurityGroup', {
      vpc,
      description: 'Allow all inbound and outbound traffic',
      allowAllOutbound: true, // すべてのアウトバウンド通信を許可
    });
    // すべてのIPからのすべてのプロトコルを許可するインバウンドルールを追加
    publicSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.allTraffic(),
      'Allow all inbound traffic'
    );

    // ECSクラスタを作成
    const cluster = new ecs.Cluster(this, 'MyCluster', {
      vpc,
      enableFargateCapacityProviders: true
    });

    // ECSタスク定義を作成
    const taskDefinition = new ecs.FargateTaskDefinition(this, 'MyTaskDefinition', {
      memoryLimitMiB: 3072,
      cpu: 1024
    });
    // コンテナを作成
    const container = taskDefinition.addContainer('web', {
      containerName: "web",
      image: ecs.ContainerImage.fromRegistry(webLocation.imageUri),
      portMappings: [
        { name: "web-80-tcp", containerPort: 80, hostPort: 80, protocol: ecs.Protocol.TCP, },
      ],
      logging: new ecs.AwsLogDriver({ streamPrefix: 'web' }),
    });

    // ALBを作成
    const alb = new elbv2.ApplicationLoadBalancer(this, 'MyALB', {
      internetFacing: true,
      vpc,
      vpcSubnets: {
        subnets: vpc.publicSubnets
      },
      securityGroup: publicSecurityGroup
    });

    // ターゲットグループの作成
    const targetGroup = new elbv2.ApplicationTargetGroup(this, 'MyTargetGroup', {
      // ヘルスチェックの設定
      healthCheck: {
        healthyHttpCodes: '200',
        healthyThresholdCount: 5,
        interval: Duration.seconds(30),
        path: '/',
        timeout: Duration.seconds(5),
        unhealthyThresholdCount: 2
      },
      port: 3000,
      protocol: elbv2.ApplicationProtocol.HTTP,
      targetGroupName: 'my-tg',
      vpc,
      targetType: elbv2.TargetType.IP
    });
    // HTTP(80)のリスナー設定
    alb.addListener('MyHttpListener', {
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      defaultTargetGroups: [targetGroup]
    });

    // ALBが参照するFargateサービスを定義
    const service = new ecs.FargateService(this, 'FargateService', {
      cluster,
      taskDefinition,
      // Fargateに直接アクセスさせたくないならPrivateSecurityGroupのみアタッチする
      securityGroups: [publicSecurityGroup],
      capacityProviderStrategies: [
        {
          capacityProvider: 'FARGATE',
          weight: 1
        }
      ],
      assignPublicIp: false
    });
    service.attachToApplicationTargetGroup(targetGroup);
  }
}

まとめ

今回はコンテナイメージのビルドからECSのサービス起動までの一連の流れをCDKで行うコードを紹介しました。
Stack.synthesizer.addDockerImageAssetを使用しましたが、他にも@aws-cdk/aws-ecr-assetsを使うなどがあります。
軽く調べた感触としては使い勝手はそこまで変わらないようですが、そちらは非推奨になった項目が多く、今後どうなるかが不安だったのでStack.synthesizer.addDockerImageAssetを選択しました。

今回の内容が役立ちましたら幸いです。

参考

Discussion