はじめに
シェルでECSのタスクスケジューラーを登録するスクリプトを書いていた。
複数の環境で同じタスクをECSのスケジュールに登録する際、ハマったことを記事に残しておく
シェルの内容
流れとしては以下のようなスクリプト
1)タスク定義を作成
2)CloudWatchのルールを作成
3)1と2を紐付けたものをECSのタスクスケジューラーに登録
シェル全文
# !/bin/bash
set -eu
# $1 : stg or prod
ENV_ARG=$1
args_count=$#
# 引数チェック
check_args() {
if [ $args_count -ne 1 ]; then
echo "指定された引数は $args_count 個です。" 1>&2
echo "実行するには 1 個の引数が必要です。" 1>&2
exit 0
fi
if [ "$ENV_ARG" = prod ]; then
ENV_NAME_JP="本番環境"
elif [ "$ENV_ARG" = stg ]; then
ENV_NAME_JP="ステージング環境"
else
echo "第1引数には、stgまたはprodを指定してください" 1>&2
exit 0
fi
}
select_branch() {
DEFAULT_BRANCH_NAME=master
echo "$ENV_NAME_JP へのデプロイを実行します。"
if [ "$ENV_ARG" = stg ]; then
echo 'デフォルト値は Enter キーを入力してください。'
while true; do
read -p "> ブランチ名を指定してください。[$DEFAULT_BRANCH_NAME] : " BRANCH_NAME
case $BRANCH_NAME in
"" ) BRANCH_NAME=$DEFAULT_BRANCH_NAME
echo ""
break;;
* ) echo ""
break;;
esac
done
else
BRANCH_NAME=$DEFAULT_BRANCH_NAME
fi
}
fetch_SHA1() {
echo "$BRANCH_NAME ブランチの最新のコミットSHA1を検索しています...\n"
BRANCH_SHA1=$(git ls-remote $(git config --get remote.origin.url) $BRANCH_NAME | awk '{ print $1 }')
# リモートリポジトリ存在チェック
if [ -z $BRANCH_SHA1 ]; then
echo "リモートブランチが存在しないため最新のコミットSHA1が取得できません。" 1>&2
exit 0
fi
}
check_ecr_build_artifacts () {
echo "$BRANCH_SHA1 のタグのイメージを検索しています...\n"
APP_IMAGE=$(aws ecr list-images --repository-name sample-api --query "imageIds[*].imageTag")
if echo $APP_IMAGE | jq .[] | xargs echo | grep -q $BRANCH_SHA1; then
echo "$BRANCH_SHA1 のタグのsample-apiのイメージが見つかりました\n"
else
echo "$BRANCH_SHA1 のタグのsample-apiのイメージが見つかりませんでした"
exit 0
fi
}
get_commmit_message () {
if [ ${#BRANCH_SHA1} -gt 0 ]; then
echo "git fetch しています...\n"
$(git fetch origin $BRANCH_NAME &> /dev/null)
COMMIT_MESSAGE=$(git log --format=%B -n 1 $BRANCH_SHA1 2>/dev/null | awk 'NR==1')
fi
COMMIT_MESSAGE=${COMMIT_MESSAGE:-"[取得出来ませんでした。]"}
}
select_release_file() {
while true; do
read -p "> リリースする対象ファイル名を入力してください。: " file_name
case $file_name in
"" ) continue;;
*) break;;
esac
done
file_name_ext="$file_name.json"
ls -l docker/batch/commands/$file_name_ext 2>&1 > /dev/null
release_file_name=$file_name_ext
}
pick_params_from_json() {
task_name=$(cat docker/batch/commands/$release_file_name | jq -r '.name')
cron=$(cat docker/batch/commands/$release_file_name | jq -r '.cron')
description=$(cat docker/batch/commands/$release_file_name | jq -r '.description')
command=$(cat docker/batch/commands/$release_file_name | jq -r '.command')
}
confirm_release() {
echo "========================================"
echo "> 環境 : $ENV_NAME_JP" 1>&2
echo "> ブランチ名 : $BRANCH_NAME" 1>&2
echo "> コミットSHA1 : $BRANCH_SHA1" 1>&2
echo "> コミットメッセージ : $COMMIT_MESSAGE" 1>&2
echo "> タスク名 : $task_name" 1>&2
echo "> cron情報 : $cron" 1>&2
echo "> 説明 : $description" 1>&2
echo "> 実行コマンド : $command" 1>&2
echo "========================================"
while true; do
read -p "> 以上でスケジュールタスクを作成してもよろしいですか? (y/n) [n] : " yn
case $yn in
[Yy] ) break;;
[Nn] ) echo "終了します。\n"
exit 0;;
"" ) echo "終了します。\n"
exit 0;;
* ) echo "(y/n) で入力してください。\n";;
esac
done
}
# タスク定義の作成
create_task_definision() {
export COMMAND=$command
export SHA1=$BRANCH_SHA1
export LOG_GROUP="/ecs/sample-batch-${ENV_ARG}"
task_definition_name="sample-batch-$ENV_ARG-${task_name}"
ecs-cli configure \
--cluster sample-${ENV_ARG} \
--region ap-northeast-1 \
--default-launch-type FARGATE
ecs-cli compose \
--project-name ${task_definition_name} \
--file docker/docker-compose.${ENV_ARG}.yml \
--ecs-params docker/ecs-params.${ENV_ARG}.yml \
create
task_arn=$(aws ecs describe-task-definition --task-definition $task_definition_name --query 'taskDefinition.taskDefinitionArn' --output text)
echo "\nタスク定義を作成しました。タスク定義ARN: $task_arn\n"
}
# CloudWatchのルール作成
create_cloudwatch_rule() {
rule_arn=$(aws events put-rule --schedule-expression "$cron" --name $task_name --description "$description" --query 'RuleArn' --output text)
echo "CloudWatchのルールを作成しました。ルールARN: $rule_arn\n"
}
# タスク定義とルールの紐付け
associate() {
cluster_arn="arn:aws:ecs:ap-northeast-1:784286759117:cluster/sample-${ENV_ARG}"
json=$(cat "./docker/batch/env/${ENV_ARG}.json" | jq '.[0].Id = "'$task_name'" | .[0].Arn = "'$cluster_arn'" | .[0].EcsParameters.TaskDefinitionArn = "'$task_arn'"')
aws events put-targets --rule $task_name --targets "$json"
echo "\n$task_definition_name をsample-${ENV_ARG} クラスターのスケジュールタスクに登録しました。"
# 読み込んでいるjsonはこんな感じのやつ
# [
# {
# "Id": "タスク名",
# "Arn": "クラスターのARN",
# "RoleArn": "RoleのARN",
# "Input": "",
# "EcsParameters": {
# "TaskDefinitionArn": "タスク定義のARN",
# "TaskCount": 1,
# "LaunchType": "FARGATE",
# "NetworkConfiguration": {
# "awsvpcConfiguration": {
# "Subnets": [
# "サブネットのid"
# ],
# "SecurityGroups": [
# "セキュリティグループのid"
# ],
# "AssignPublicIp": "ENABLED"
# }
# }
# }
# }
# ]
}
check_args
select_branch
fetch_SHA1
check_ecr_build_artifacts
get_commmit_message
select_release_file
pick_params_from_json
confirm_release
create_task_definision
create_cloudwatch_rule
associate
リリースするバッチの内容はこんな感じのJSONを事前に用意しておき、それをスクリプトで読み込んで使う。
{
"name": "sample",
"cron": "cron(0/5 * * * ? *)",
"description": "コマンドのhelpを表示する",
"command": "node dist/src/command help"
}
例として上記のJSONを読み込んだ場合、CloudWatchのルール名はsampleで作成される。(jsonのnameは $task_name
に入っている)
rule_arn=$(aws events put-rule --schedule-expression "$cron" --name $task_name --description "$description" --query 'RuleArn' --output text)
作成したsampleのルールを、作成したタスク定義に紐付けてECSに登録する。
aws events put-targets --rule $task_name --targets "$json"
起こったこと
上述のシェルを使ってstg用のECSにタスクを登録。
sampleのタスクが登録された。

今度は同じsampleのタスクを本番用のECSに登録する。
ちゃんとsampleが登録された。

原因
「CloudWatchのルール sample
の実行内容は stg用のタスク定義
。それを stg用のecsクラスター
に登録」
としていたのを、
「CloudWatchのルール sample
の実行内容は prod用のタスク定義
。それを prod用のecsクラスター
に登録」
に上書きしたことで、stg用のECSからsampleのスケジュールタスクが消えた。
対応
CloudWatchのルール名は一意になるように変更した。
# タスク定義名に使っているやつをCloudWatchのルール名にも利用
task_definition_name="sample-batch-$ENV_ARG-${task_name}"
# CloudWatchのルール作成
rule_arn=$(aws events put-rule --schedule-expression "$cron" --name $task_definition_name --description "$description" --query 'RuleArn' --output text)
# CloudWatchのルールをタスク定義、ECSに紐付け
aws events put-targets --rule ${task_definition_name} --targets "$json"
最後に
CloudWatchのルール名はグローバル変数みたいな物だから、同じ処理だからといって同じルール名で作成すると今回みたいなことになる。複数の環境で登録するのであれば環境名などをルール名に加えておき、一意にする必要がある。
考えてみれば当たり前のことだが、今回のこの挙動をはじめみたとき、「んん??」と思ったので残しておく。