AWS CDK で Amazon ECR に Docker イメージを push して Amazon ECS のタスク定義に使う

AWS CDK で Amazon ECR の特定のリポジトリに Docker イメージを push して Amazon ECS のタスク定義に使う。

環境

  • aws-cdk v2.56.1
  • aws-cdk-lib v2.56.1
  • cdk-ecr-deployment v2.5.6

本文

ディレクトリ構成

ルート
|
|-app
| |-(アプリケーションのソースコード)
|
|-cdk
| |-(cdk init でできたファイル群)
|
|-docker
| |-go
|   |- Dockerfile.prod
|
|-.dockerignore
|-(その他のファイル)

Dockerfile

前述のディレクトリ構成の「ルート」を Docker の build context とし、Dockerfile.prod の親階層にあるアプリケーションのソースコードをコピーする設計となっている。

FROM golang:1.19 AS build-env
WORKDIR /app
COPY app ./
RUN go mod tidy && \
    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server server.go

FROM gcr.io/distroless/static-debian11
WORKDIR /app
COPY --from=build-env /app/server ./

ENTRYPOINT ["./server"]

.dockerignore

Docker イメージビルド時に無視するファイルを指定する。

ポイントとして、今回は build context に cdk ディレクトリが含まれているので cdk ディレクトリは無視する必要がある。
無視しないと cdk/cdk.out のファイルの中身で ENAMETOOLONG: name too long というエラーが出る。

# このファイルがある階層のファイルは全部無視
.gitignore
.env.*
env.*
*.yaml
*.md
# cdk は無視
cdk/*
# ローカル開発用のファイルを無視
docker/db/*
app/tmp/*
# ローカル開発時に生成される機密ファイルを無視
app/*.json

cdk/lib/app-stack.ts

ECR のリポジトリを CDK で作成することもできるのだが、cdk deploycdk destroy を繰り返したら「同名のリポジトリは作成できない」というエラーになったので、既存のリポジトリを取得するようにした。

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { DockerImageAsset } from "aws-cdk-lib/aws-ecr-assets";
import { ECRDeployment, DockerImageName } from "cdk-ecr-deployment";
import { Repository } from "aws-cdk-lib/aws-ecr";
import {
  ContainerImage,
  FargateTaskDefinition,
  LogDriver,
} from "aws-cdk-lib/aws-ecs";
import { Role, ServicePrincipal, ManagedPolicy } from "aws-cdk-lib/aws-iam";
import { LogGroup } from "aws-cdk-lib/aws-logs";
import path = require("path");

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

    const repositoryName = "sololog-back-repository";
    // 既存のリポジトリを取得
    const repository = Repository.fromRepositoryName(
      this,
      "SolologBackRepository",
      repositoryName
    );

    // directory には build context を指定
    const image = new DockerImageAsset(this, "SolologBackImageAsset", {
      directory: path.join(__dirname, "..", ".."),
      file: path.join("docker", "go", "Dockerfile.prod"),
    });

    new ECRDeployment(this, "SolologBackImage", {
      src: new DockerImageName(image.imageUri),
      dest: new DockerImageName(
        `${cdk.Aws.ACCOUNT_ID}.dkr.ecr.${cdk.Aws.REGION}.amazonaws.com/${repositoryName}:latest`
      ),
    });

    const taskExecutionRole = new Role(this, "SolologBackTaskExecutionRole", {
      roleName: "sololog-back-task-execution-role",
      assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com"),
      managedPolicies: [
        ManagedPolicy.fromAwsManagedPolicyName(
          "service-role/AmazonECSTaskExecutionRolePolicy"
        ),
      ],
    });
    const taskRole = new Role(this, "SolologBackTaskRole", {
      roleName: "sololog-back-task-role",
      assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com"),
      managedPolicies: [
        // 私のタスクは SSM にアクセスするので指定しているが、アクセスしないなら不要
        ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMReadOnlyAccess"),
      ],
    });
    const taskDefinition = new FargateTaskDefinition(
      this,
      "SolologBackTaskDef",
      {
        taskRole,
        executionRole: taskExecutionRole,
        cpu: 256,
        memoryLimitMiB: 512,
      }
    );
    const logGroup = new LogGroup(this, "ServiceLogGroup", {
      // 固定名にすると deploy と destroy を繰り返したときエラーになるので適当に乱数をつけておく
      logGroupName: `sololog-back-logs-${Math.random()}`,
    });
    taskDefinition.addContainer("SolologBackAppContainer", {
      image: ContainerImage.fromEcrRepository(repository, "latest"),
      portMappings: [
        {
          containerPort: 8080,
        },
      ],
      logging: LogDriver.awsLogs({
        streamPrefix: "app",
        logGroup,
      }),
    });

    // 以下略
  }
}

以上