AWS Batch × サイドカーパターン:マルチコンテナジョブで既存資産を有効活用し責務を分離する

こんにちは。makeshop事業本部開発部の井上です。 今回はmakeshopの管理画面のバッチ処理AWS Batch マルチコンテナジョブを利用して実装したのでご紹介します。

2024年のアップデート以前、AWS Batchは「1ジョブ1コンテナ」が基本でしたが、現在は最大10個までのマルチコンテナ構成に対応しており、設計の自由度が大幅に向上しています。

マルチコンテナ構成を採用した背景

今回、以下の3つの役割を個別のコンテナに分割する「サイドカーパターン」を採用しました。

  • メインコンテナ: Go言語で書かれたバッチの主ロジック
  • ログルーティング (log_router): Fluent Bit(AWS FireLens)を利用したログ転送
  • 画像処理 (image_conversion): 画像変換を行う専用APIコンテナ

なぜコンテナを分けたのか

単一のコンテナにまとめず、あえて分離したのには以下の2つの大きな理由があります。

既存資産の有効活用と共通化

画像変換処理は既にバッチ以外の管理画面でサービスとして稼働していました。これをバッチ用に再実装するのではなく、「すでに実績のあるコンテナをそのままサイドカーとして再利用する」ことで、コードの重複を避け、スケールしやすい構成にしました。

cgo依存の分離

画像変換処理には cgo を利用しています。cgoを利用すると、ビルド環境に特定のCライブラリが必要になったり、コンパイル時間が延びたりと、メインロジックの管理が複雑になりがちです。これらをサイドカーに切り出すことで、メインコンテナを「純粋なGoの実行環境」として軽量かつクリーンに保つことができました。

ジョブ定義

taskProperties.containersに複数のコンテナを指定できます。現在は1~10個指定することができます。

{
  "jobDefinitionName": "job-definition-name",
  "type": "container",
  "platformCapabilities": [
    "FARGATE"
  ],
  "ecsProperties": {
    "taskProperties": [
      {
        "executionRoleArn": "arn:aws:iam::000000000000:role/RoleName",
        "taskRoleArn": "arn:aws:iam::000000000000:role/RoleName",
        "containers": [
          {
            "name": "main",
            "essential": true,
            "image": "__image__",
            "mountPoints": [],
            "readonlyRootFilesystem": true,
            "command": [
              "/go/bin/app"
            ],
            "environment": [
              {
                "name": "IMAGE_CONVERSION_API_URL",
                "value": "http://localhost:8080"
              }
            ],
            "secrets": [
              {
                "name": "PASSWORD",
                "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:0000000000:secret:******************"
              }
            ],
            "logConfiguration": {
              "logDriver": "awsfirelens",
              "secretOptions": [],
              "options": {
                "Name": "stdout"
              }
            },
            "dependsOn": [
              {
                "containerName": "log_router",
                "condition": "START"
              },
              {
                "containerName": "image_conversion",
                "condition": "START"
              }
            ],
            "resourceRequirements": [
              {
                "type": "VCPU",
                "value": "0.5"
              },
              {
                "type": "MEMORY",
                "value": "2048"
              }
            ]
          },
          {
            "name": "log_router",
            "image": "000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/*************:latest",
            "essential": true,
            "firelensConfiguration": {
              "type": "fluentbit",
              "options": {
                "config-file-type": "file",
                "config-file-value": "/fluent-bit/etc/fluent-bit.conf"
              }
            },
            "readonlyRootFilesystem": true,
            "resourceRequirements": [
              {
                "type": "VCPU",
                "value": "0.25"
              },
              {
                "type": "MEMORY",
                "value": "512"
              }
            ]
          },
          {
            "name": "image_conversion",
            "image": "000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/*************:latest",
            "command": [
              "/go/bin/image"
            ],
            "essential": true,
            "readonlyRootFilesystem": true,
            "environment": [
              {
                "name": "ENV1",
                "value": "value"
              }
            ],
            "mountPoints": [
              {
                "sourceVolume": "image-conversion-ephemeral",
                "containerPath": "/tmp",
                "readOnly": false
              }
            ],
            "resourceRequirements": [
              {
                "type": "VCPU",
                "value": "0.25"
              },
              {
                "type": "MEMORY",
                "value": "1536"
              }
            ],
            "logConfiguration": {
              "logDriver": "awsfirelens",
              "secretOptions": [],
              "options": {
                "Name": "stdout"
              }
            }
          }
        ],
        "volumes": [
          {
            "name": "image-conversion-ephemeral"
          }
        ]
      }
    ]
  }
}

resourceRequirements

Fargateを利用する場合、タスク全体のVCPU/メモリの組み合わせ制限がありますが、これは全コンテナの合計値で計算されます。個々のコンテナ単位では制限を気にせず、柔軟な割り振りが可能です。

SDK (Go) からのジョブ実行

実行時に動的な引数を渡したい場合はEcsPropertiesOverrideで指定することができます。 バッチ処理側では spf13/cobra を利用しているため、例のようにコマンド名や引数をスライス形式で渡すことで、柔軟な実行制御を実現しています。

この他ResourceRequirementsで実行時にVCPUやMEMORYを指定することも可能です。

import (
    "context"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/batch"
    "github.com/aws/aws-sdk-go-v2/service/batch/types"
)

func submitJob(
    ctx context.Context,
    argString string,
    jobDefinition string,
    jobName string,
) error {
    cfg, err := config.LoadDefaultConfig(ctx)
    if err != nil {
        return err
    }

    client := batch.NewFromConfig(cfg)
    if _, err := client.SubmitJob(ctx, &batch.SubmitJobInput{
        JobDefinition: aws.String(jobDefinition),
        JobName:       aws.String(jobName),
        JobQueue:      aws.String(JobQueue),
        EcsPropertiesOverride: &types.EcsPropertiesOverride{
            TaskProperties: []types.TaskPropertiesOverride{
                {
                    Containers: []types.TaskContainerOverrides{
                        {
                            Name: aws.String("main"),
                            Command: []string{
                                "/go/bin/app",
                                "command-name",
                                "--args",
                                argString,
                            },
                                                            // ResourceRequirements: []types.ResourceRequirement{},
                        },
                    },
                },
            },
        },
    }); err != nil {
        return err
    }

    return nil
}

おわりに

AWS Batchのマルチコンテナジョブを活用することで、サイドカーパターンを用いた柔軟なバッチ基盤を構築できました。 この記事が、AWS Batchでの高度なコンテナ設計を検討している方の参考になれば幸いです。