続・AWS Fargateのタスク起動時間がzstd圧縮のイメージを使うと早くなるようなので試してみた

# はじめに
以前、AWSブログでzstd圧縮イメージを使用すると起動時間が短縮されると発表されたので実際に試した内容を投稿しました。

https://qiita.com/RH0909/items/65173b164498de4641f8

GoQSystemのバックエンド環境で実際に使用しているDockerfileの内容で実施しましたが、Dockerイメージサイズが小さく（約150MB）イマイチ効果がわかりにくかったので、イメージサイズが大きめの環境を使って再度確認してみることにしました。

本来であれば機械学習（Python）などで使用する数GBイメージサイズのDockerfileを用意できればよかったのですが、PythonやMLには疎いので、今回はNode.jsでNext.jsを実行する環境を用意して検証してみました。

今回は使用したDockerfile、GitHub ActionsでAWS ECSへデプロイ、インフラ構成のTerraformを一部掲載しています。

## ディレクトリ構成
GitHub Actions設定ファイルディレクトリの`/.github`、Next.jsディレクトリの`/example`、ルートディレクトリに`Dockerfile`を置いています。
```shell:ディレクトリ構成
example/
 ┣━ .github/
 ┃    └ workflows/
 ┃        └ deploy.yaml
 ┣━ example/
　　┃　　　　  └ Next.jsのファイル
 ┣━ Dockerfile
```

※ Next.jsは単純に以下のコマンドでアプリケーションを作成しただけです。
```
npx create-next-app@latest
# or
yarn create next-app
# or
pnpm create next-app
```

## ファイル内容
**※ 全て検証用に作成したファイルですので、そのまま使用する場合はご注意ください。**
**※ GitHub ActionsからAWSへの認証はOIDCを使用しています。**
<details><summary>通常のDocker build用GitHub Actions</summary><div>

```yaml:deploy.yaml
name: Aws_Ecs_Deploy
on:
  push:
permissions:
  id-token: write
  contents: read
jobs:
  nextjs:
    runs-on: ubuntu-latest
    env:
      AWS_REGION: AWSリージョン
      AWS_IAM_ROLE_ARN: OIDC用IAMロール
      ECR_REPOSITORY: ECRリポジトリ名
      ECS_CLUSTER: ECSクラスター名
      ECS_SERVICE: ECSサービス名
      ECS_TASK_DEFINITION: ECSタスクARN
    steps:
      -
        name: Checkout
        uses: actions/checkout@v3
      -
        name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ${{ env.AWS_REGION }}
          role-to-assume: ${{ env.AWS_IAM_ROLE_ARN }}
      -
        name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      -
        name: Build, tag, and push docker image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ env.ECR_REPOSITORY }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
      -
        name: Download task definition
        run: |
          aws ecs describe-task-definition --task-definition ${{ env.ECS_TASK_DEFINITION }} --query taskDefinition > task-definition.json
      -
        name: Render Amazon ECS task definition for app container
        id: render-app-container
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: app
          image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
      -
        name: Deploy to Amazon ECS service
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-app-container.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
```
</div></details>

<details><summary>通常のDocker buildx build用GitHub Actions</summary><div>

```yaml:deploy.yaml
name: Aws_Ecs_Deploy
on:
  push:
permissions:
  id-token: write
  contents: read
jobs:
  nextjs:
    runs-on: ubuntu-latest
    env:
      AWS_REGION: AWSリージョン
      AWS_IAM_ROLE_ARN: OIDC用IAMロール
      ECR_REPOSITORY: ECRリポジトリ名
      ECS_CLUSTER: ECSクラスター名
      ECS_SERVICE: ECSサービス名
      ECS_TASK_DEFINITION: ECSタスクARN
    steps:
      -
        name: Checkout
        uses: actions/checkout@v3
      -
        name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: ${{ env.AWS_REGION }}
          role-to-assume: ${{ env.AWS_IAM_ROLE_ARN }}
      -
        name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      -
        name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v2
      -
        name: Build, tag, and push docker image to Amazon ECR
        uses: docker/build-push-action@v3
        with:
          context: .
          push: true
          builder: ${{ steps.buildx.outputs.name }}
          tags: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
          outputs: |
            type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
      -
        name: Download task definition
        run: |
          aws ecs describe-task-definition --task-definition ${{ env.ECS_TASK_DEFINITION }} --query taskDefinition > task-definition.json
      -
        name: Render Amazon ECS task definition for app container
        id: render-app-container
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: app
          image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
      -
        name: Deploy to Amazon ECS service
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-app-container.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
```
</div></details>

<details><summary>Dockerfile</summary><div>

```docker:Dockerfile
FROM node:19

RUN apt-get update

COPY ./example /var/www/app

WORKDIR /var/www/app

RUN yarn --ignore-scripts && yarn build

EXPOSE 3000

CMD ["yarn","start"]
```

</div></details>

## インフラ構成
ざっくりですが以下のような一般的なWEBアプリケーション構成を構築しています。

![infra.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/765794/77d8d442-09ef-5ba8-58f4-2adeef850793.png)

#### ECSタスクは以下の内容です。
上述しているアプリケーション用のDockerイメージ1つで構成しています。
<details><summary>ECSタスク(Terraform)</summary><div>

```terraform:ECSタスク
resource "aws_ecs_task_definition" "ecs_task" {
  family                   = var.app_name
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 512
  memory                   = 1024
  task_role_arn            = aws_iam_role.task_role.arn
  execution_role_arn       = aws_iam_role.task_exec_role.arn
  container_definitions = jsonencode([
    {
      "name" : "app",
      "image" : "${aws_ecr_repository.ecr_repo.repository_url}:${var.docker_image_tag}",
      "memoryReservation" : 1024,
      "essential" : true,
      "logConfiguration" : {
        "logDriver" : "awslogs",
        "options" : {
          "awslogs-group" : "${aws_cloudwatch_log_group.cloudwatch_log.name}",
          "awslogs-region" : "${var.region}",
          "awslogs-stream-prefix" : "ecs"
        }
      },
      "portMappings" : [
        {
          "containerPort" : 3000
        }
      ]
    }
  ])
  runtime_platform {
    operating_system_family = "LINUX"
    cpu_architecture        = "X86_64"
  }
}
```

</div></details>

#### ALBターゲットグループは下記設定にしています。（Terraform）

<details><summary>ALBターゲットグループ(Terraform)</summary><div>

```terraform:ターゲットグループ
resource "aws_lb_target_group" "target_group" {
  name                 = "${var.app_name}-TG"
  port                 = 80
  protocol             = "HTTP"
  target_type          = "ip"
  vpc_id               = var.vpc_id
  deregistration_delay = 5

  health_check {
    path                = "/"
    healthy_threshold   = 2
    unhealthy_threshold = 2
    interval            = 10
  }
}
```

</div></details>

## 計測方法
ECS Taskの`Started at`と`Created at`の「差分 = 起動時間」とします。
通常のDocker buildのイメージとbuildxでzstd圧縮したイメージそれぞれを使用したECS Taskを50個立ち上げて確認しています。（10タスク×5回）
ECS Taskなどの構成内容はどちらも同じものです。

**aws cli：ECS Taskの`Started at`と`Created at`を確認**
```
aws ecs describe-tasks --tasks $TASK_ARN --cluster $CLUSTER_NAME
```

※ コンソール画面でも確認できます

## `Docker build`でイメージを使用したECS Task
■ **イメージサイズ** 
* Next.js： 833.05MB

■ **ECS Taskサイズ** 
* CPU： 0.5
* Memory： 1GB

■ **ECS Task起動時間** 
* **平均： 41.96(秒)**
* **最大： 56.00(秒)**
* **最小： 37.00(秒)**
* **中央値： 41.00(秒)**

## `Docker buildx build`でzstd圧縮したイメージを使用したECS Task
■ **イメージサイズ** 
* Next.js： 741.50MB
`Docker build` で生成したDockerイメージサイズより約92MB小さくなっています。

■ **ECS Taskサイズ** 
* CPU： 0.5
* Memory： 1GB

■ **ECS Task起動時間** 
* **平均： 37.05(秒)**
* **最大： 44.00(秒)**
* **最小： 34.00(秒)**
* **中央値： 36.00(秒)**

**`Docker build` で生成したDockerイメージを使用したECSタスクより平均して約4~5秒早く起動するようになりました。**

## まとめ
前回はDockerイメージサイズが比較的小さいECSタスクで検証したので効果がイマイチわかりにくかったですが、上記のように`zstd圧縮`を使用するとECSタスクの起動時間が早くなりました。

AWSブログにあるようにイメージサイズが大きいほど効果的なようです。

https://aws.amazon.com/jp/blogs/containers/reducing-aws-fargate-startup-times-with-zstd-compressed-container-images/

AWS ECS on Fargateのタスク起動時間がzstd圧縮されたDockerイメージを利用すると早くなるということで初めてzstd圧縮にチャレンジし、同時にGitHub ActionsのECSデプロイを勉強できたので有益な検証になりました。

今後も新しい技術や方法を試してより良い環境を構築できるように改善していきたいと思います。

## 最後に
#### **GoQSystemでは一緒に働いてくれる仲間を募集中です！**
求人は出していませんが、SREやQAエンジニアも今後募集していこうと考えています！
ご興味がある方は以下リンクよりご確認ください。

https://goqsystem.recruit.site?qiita=432dcb81f08d7d4dcfca