Fargate バッチ環境構築

概要

Fargate のバッチ処理が動作する環境を構築するファイル群です。
構築するサービスは ECS と Event Bridge Scheduler のみです。
その他のサービス (VPC, SecurityGroup, Role 等) は各自作成が必要です。

サンプルとして example と言うバッチを配置しています。
サンプルのバッチは、slack にメッセージを飛ばす処理です。

用途

Lambda の実行時間(15 分)で処理が終わらない時の代替として。

ファイル群

ファイル群は以下の通りです。
それぞれのファイルの役割は後述します。

.
├── README.md
├── create_cluster.sh
├── create_scheduler.sh
├── create_service.sh
├── create_task.sh
├── execute_task.sh
├── push_image.sh
└── example
    ├── based_scheduler_definition.json
    ├── based_task_definition.json
    ├── config
    ├── docker-compose.yml
    ├── dockerfile
    ├── main.py
    └── requirements.txt

環境構築手順

  1. 対象のバッチのディレクトリに移動する。(cd example)
  2. ファイル config を作成し「Sheduler」以外の値を設定する。(下記参照)
  3. コマンド sh ../push_image.sh でイメージを ECR に配置する。
  4. コマンド sh ../create_cluster.sh で ECS のクラスターを作成する。
  5. コマンド sh ../create_task.sh で ECS のタスクを登録する。
  6. ファイル config に「Scheduler」の値を設定する。(下記参照)
  7. コマンド sh ../create_scheduler で定期実行スケジュールを作成する。

設定ファイル

設定ファイル config を作成し以下のような秘匿情報を設定して下さい。

# Aws
AWS_ACCOUNT_ID=123456789012
# Ecr
ECR_NAME="test_repository"
# Cluster
ECS_CLUSTER_NAME="test_cluster"
# Sercice
ECS_SERVICE_NAME="test_service"
ECS_SERVICE_DESIRED_COUNT="1"
# Task
ECS_TASK_FAMILY_NAME="test_family_name"
ECS_TASK_ROLE_ARN="arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
ECS_TASK_EXECUTION_ROLE_ARN="arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
ECS_TASK_CONTAINER_NAME="test_container_name"
ECS_TASK_IMAGE_URI="123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/test_repository:latest"
ECS_TASK_ENVIRONMENT='[{"name": "EXECUTED_FROM","value": "Fargate"},{"name": "SLACK_URL","value": "https://hooks.slack.com/services/AAAAAAAAAA/BBBBBBBBBB/CCCCCCCCCC"}]'
ECS_TASK_CPU="256"
ECS_TASK_MEMORY="512"
# Execute Net Working
ECS_EXECUTE_SUBNET_ID="subnet-123456789012"
ECS_EXECUTE_SECURITY_GROUP_ID="sg-123456789012"
# Scheduler
SCHEDULER_NAME="test_scheduler"
SCHEDULER_DESCRIPTION="description"
SCHEDULER_EXPRESSION="cron(15 * * * ? *)"
SCHEDULER_TARGET_CLUSTER_ARN="arn:aws:ecs:ap-northeast-1:123456789012:cluster/test_cluster"
SCHEDULER_TARGET_TASK_ARN="arn:aws:ecs:ap-northeast-1:123456789012:task-definition/test_family_name"
SCHEDULER_ROLE_ARN="arn:aws:iam::123456789012:role/service-role/Amazon_EventBridge_Scheduler_ECS_xxxxxxxxxx"
SCHEDULER_TARGET_SUBNETS='["subnet-123456789012"]'
SCHEDULER_TARGET_SECURITY_GROUPS='["sg-123456789012"]'
SCHEDULER_MAXIMUM_EVENT_AGE_IN_SECONDS=3600 # イベントの最大保持時間
SCHEDULER_MAXIMUM_RETRY_ATTEMPTS=5 # 最大再試行回数

実行方法

  1. ローカルからコードだけを実行する。

    • コマンド docker-compose up を実行してください。
    • (ローカル環境に不要なライブラリが入ってもよい場合は python から直接実行しても構いません。)
  2. ローカルからタスクを実行する。

    • スクリプト execute_task.sh を実行してください。

共通ファイルについて

push_image.sh

ECR にイメージファイルをプッシュするスクリプトです。

create_cluster.sh

ECS のクラスターを作成するスクリプトです。

create_service.sh

ECS のサービスを作成するスクリプトです。
(バッチ処理なので今回は不要かと思いますが一応作成しておきました。)

create_task.sh

ECS のタスクを作成するスクリプトです。

execute_task.sh

ECS のタスクを実行するスクリプトです。

create_scheduler.sh

Event Bridge の Scheduler を作成するスクリプトです。

バッチ毎の個別のファイルについて

config

設定ファイルで秘匿情報を記載します。
.gitignore の対象となっています。
(中身は上記参照)

dockerfile

イメージのもととなるファイルです。

docker-compose.yml

ローカルでコードを実行するファイルです。

requirements.txt

必要なライブラリを記載するファイルです。

main.py

メインのバッチ処理を記載したコードです。
slack に通知するのみの処理となっています。

based_task_definition.json

スクリプト create_task.sh が実行されると、このファイルに秘匿情報を追加して「task_definition.json」を出力します。
この出力されたファイルを元に task が生成されます。

(設定したい値が、秘匿でなかったり固定でよい場合は直接書き換えてコミットして使ってよいと思います。)

based_scheduler_definition.json

スクリプト create_scheduler.sh が実行されると、このファイルに秘匿情報を追加して「scheduler_definition.json」を出力します。
この出力されたファイルを元に scheduler が生成されます。

(設定したい値が、秘匿でなかったり固定でよい場合は直接書き換えてコミットして使ってよいと思います。)

コード

push_image.sh

#!/bin/bash

# 設定ファイルを読み込む。
. "$PWD/config"

# イメージをECRに配置する。
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t $ECR_NAME:latest -f dockerfile .
docker tag $ECR_NAME:latest $AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/$ECR_NAME:latest
docker push $AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/$ECR_NAME:latest

create_cluster.sh

#!/bin/bash

# 設定ファイルを読み込む。
. "$PWD/config"

# クラスターを作成する。
aws ecs create-cluster --cluster-name $ECS_CLUSTER_NAME
status=$?

# 結果を表示する。
if [ $status -eq 0 ]; then
  echo "クラスターの作成が完了しました。"
else
  echo "クラスターの作成に失敗しました。終了ステータス: $status"
fi

create_service.sh

#!/bin/bash

# 設定ファイルを読み込む。
. "$PWD/config"

# サービスを作成する。
aws ecs create-service \
    --cluster $ECS_CLUSTER_NAME \
    --service-name $ECS_SERVICE_NAME \
    --task-definition $ECS_TASK_FAMILY_NAME \
    --desired-count $ECS_SERVICE_DESIRED_COUNT \
    --launch-type FARGATE \
    --network-configuration "awsvpcConfiguration={subnets=[$ECS_EXECUTE_SUBNET_ID],securityGroups=[$ECS_EXECUTE_SECURITY_GROUP_ID],assignPublicIp=ENABLED}"
status=$?

# 結果を表示する。
if [ $status -eq 0 ]; then
  echo "サービスの作成が完了しました。"
else
  echo "サービスの作成に失敗しました。終了ステータス: $status"
fi

create_task.sh

#!/bin/bash

# 設定ファイルを読み込む。
. "$PWD/config"

# タスクの設定の雛形となる based_task_definition.json から task_definition.json を作成する。
INPUT_JSON="based_task_definition.json"
OUTPUT_JSON="task_definition.json"

# 設定ファイル config の値を設定して task_definition.json を出力する。
jq \
    --arg ecs_task_family_name "$ECS_TASK_FAMILY_NAME" \
    --arg ecs_task_role_arn "$ECS_TASK_ROLE_ARN" \
    --arg ecs_task_execution_role_arn "$ECS_TASK_EXECUTION_ROLE_ARN" \
    --arg ecs_task_container_name "$ECS_TASK_CONTAINER_NAME" \
    --arg ecs_task_image_uri "$ECS_TASK_IMAGE_URI" \
    --argjson ecs_task_environment "$ECS_TASK_ENVIRONMENT" \
    --arg ecs_task_cpu "$ECS_TASK_CPU" \
    --arg ecs_task_memory "$ECS_TASK_MEMORY" \
    '.family = $ecs_task_family_name |
    .taskRoleArn = $ecs_task_role_arn |
    .executionRoleArn = $ecs_task_execution_role_arn |
    .containerDefinitions[0].name = $ecs_task_container_name |
    .containerDefinitions[0].image = $ecs_task_image_uri |
    .containerDefinitions[0].environment = $ecs_task_environment |
    .cpu = $ecs_task_cpu |
    .memory = $ecs_task_memory' \
    "$INPUT_JSON" > $OUTPUT_JSON

# タスクを作成する。
aws ecs register-task-definition --cli-input-json file://$OUTPUT_JSON
status=$?

# 結果を表示する。
if [ $status -eq 0 ]; then
  echo "タスクの作成が完了しました。"
else
  echo "タスクの作成に失敗しました。終了ステータス: $status"
fi

# 出力されたJSONを削除する。
rm -Rf $OUTPUT_JSON

execute_task.sh

#!/bin/bash

# 設定ファイルを読み込む。
. "$PWD/config"

# タスクを実行する。
aws ecs run-task \
    --cluster $ECS_CLUSTER_NAME \
    --launch-type FARGATE \
    --task-definition $ECS_TASK_FAMILY_NAME \
    --count $ECS_SERVICE_DESIRED_COUNT \
    --network-configuration "awsvpcConfiguration={subnets=[$ECS_EXECUTE_SUBNET_ID],securityGroups=[$ECS_EXECUTE_SECURITY_GROUP_ID],assignPublicIp=ENABLED}"
status=$?

# 結果を表示する。
if [ $status -eq 0 ]; then
  echo "タスクの実行が完了しました。"
else
  echo "タスクの実行に失敗しました。終了ステータス: $status"
fi

create_scheduler.sh


#!/bin/bash

# 設定ファイルを読み込む。
. "$PWD/config"

# スケジューラーの設定の雛形となる based_scheduler_definition.json から scheduler_definition.json を作成する。
INPUT_JSON="based_scheduler_definition.json"
OUTPUT_JSON="scheduler_definition.json"

# 設定ファイル config の値を設定して scheduler_definition.json を出力する。
jq \
    --arg scheduler_name "$SCHEDULER_NAME" \
    --arg scheduler_description "$SCHEDULER_DESCRIPTION" \
    --arg scheduler_expression "$SCHEDULER_EXPRESSION" \
    --arg scheduler_target_cluster_arn "$SCHEDULER_TARGET_CLUSTER_ARN" \
    --arg scheduler_target_task_arn "$SCHEDULER_TARGET_TASK_ARN" \
    --arg scheduler_role_arn "$SCHEDULER_ROLE_ARN" \
    --argjson scheduler_target_subnets "$SCHEDULER_TARGET_SUBNETS" \
    --argjson scheduler_target_security_groups "$SCHEDULER_TARGET_SECURITY_GROUPS" \
    --arg scheduler_maximum_event_agein_seconds "$SCHEDULER_MAXIMUM_EVENT_AGE_IN_SECONDS" \
    --arg scheduler_maximum_retry_attempts "$SCHEDULER_MAXIMUM_RETRY_ATTEMPTS" \
    '.Name = $scheduler_name |
    .Description = $scheduler_description |
    .ScheduleExpression = $scheduler_expression |
    .Target.Arn = $scheduler_target_cluster_arn |
    .Target.EcsParameters.NetworkConfiguration.awsvpcConfiguration.Subnets = $scheduler_target_subnets |
    .Target.EcsParameters.NetworkConfiguration.awsvpcConfiguration.SecurityGroups = $scheduler_target_security_groups |
    .Target.EcsParameters.TaskDefinitionArn = $scheduler_target_task_arn |
    .Target.RoleArn = $scheduler_role_arn |
    .Target.RetryPolicy.MaximumEventAgeInSeconds = ($scheduler_maximum_event_agein_seconds | tonumber) |
    .Target.RetryPolicy.MaximumRetryAttempts = ($scheduler_maximum_retry_attempts | tonumber)' \
    "$INPUT_JSON" > $OUTPUT_JSON

# スケジューラーを作成する。
aws scheduler create-schedule --cli-input-json file://scheduler_definition.json
status=$?

# 結果を表示する。
if [ $status -eq 0 ]; then
  echo "スケジューラの作成が完了しました。"
else
  echo "スケジューラの作成に失敗しました。終了ステータス: $status"
fi

# 出力されたJSONを削除する。
rm -Rf $OUTPUT_JSON

dockerfile

FROM python:3.8
ENV PYTHONUNBUFFERED 1
RUN apt-get update && apt-get install
RUN mkdir /work
WORKDIR /work
ADD requirements.txt /work/
RUN pip install -r requirements.txt
ADD main.py /work/
CMD ["python3", "main.py"]

docker-compose.yml

version: "3"
services:
  batch:
    build:
      context: .
      dockerfile: dockerfile

requirements.txt

slackweb==1.0.5

main.py

import os
import slackweb
import time

def execute():

    executed_from = os.environ.get("EXECUTED_FROM", None)
    slack_url = os.environ.get("SLACK_URL", None)

    start_message = f"""
    hogehoge 処理を開始しました。
    実行環境: {executed_from}
    """
    end_message = f"""
    hogehoge 処理を終了しました。
    実行環境: {executed_from}
    """

    slack = slackweb.Slack(url=slack_url)
    slack.notify(text=start_message)

    time.sleep(2)

    slack.notify(text=end_message)

if __name__ == '__main__':
    execute()

based_task_definition.json

{
  "family": "",
  "networkMode": "awsvpc",
  "taskRoleArn": "",
  "containerDefinitions": [
    {
      "name": "",
      "image": "",
      "environment": [],
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ]
    }
  ],
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "",
  "memory": ""
}

based_scheduler_definition.json

{
  "FlexibleTimeWindow": {
    "Mode": "OFF"
  },
  "Name": "",
  "Description": "",
  "ScheduleExpression": "",
  "ScheduleExpressionTimezone": "Asia/Tokyo",
  "Target": {
    "Arn": "",
    "EcsParameters": {
      "LaunchType": "FARGATE",
      "NetworkConfiguration": {
        "awsvpcConfiguration": {
          "AssignPublicIp": "ENABLED",
          "SecurityGroups": [],
          "Subnets": []
        }
      },
      "TaskDefinitionArn": ""
    },
    "RoleArn": "",
    "RetryPolicy": {
      "MaximumEventAgeInSeconds": null,
      "MaximumRetryAttempts": null
    }
  }
}