Shining Wizzard with ECS

概要

tech.toreta.in

こちらのサイトにあるシャイニングウィザードをECS環境で行おうとしたメモ 本家のシャイニングウィザードは、github の webhookを受け取って、シャイニングウィザードが実施されるけど、そこはまだ作ってなくて、一旦Localから叩くようにしてる。

シャイニングウィザードって?

開発時に、現在開発中のブランチの内容を、ディレクターさんとかにチェックしてほしいってことが良くある。
このとき、stagingとかdevelop ブランチにあげて、確認してもらうと、その確認が終わるまで、他の人がリリースできない。
自分のlocalを直接みてもらうこともできるけど、それはそれで自分が他ブランチで作業できず、いろいろ面倒。
そのため、github pushで、確認環境を自動生成しようというのがシャイニングウィザード

EC2の時は、先輩が作ったシャイニングウィザードを使ってたけど、ECS環境に変わったので自分で作ってみた。

やったこと

ECRとECSを利用しているのを前提とする。

1. Dockerファイルを作成

// 既にECRにあるイメージを利用することで、yarn install を早くする
FROM {Dev環境で使ってるImageのECR上のURL}
RUN apk update && apk add git

// .git フォルダをgithubからcloneする。
RUN git clone --no-checkout https://{User名}:{GithubToken}@github.com/{レポジトリ名} gittmp
RUN cp -r gittmp/.git .
RUN git reset --hard HEAD

// Branch Nameは外から渡す
ARG BRANCH_NAME

# この時点以降で、キャッシュを効かせないためにdummyfileをADD(必要か微妙)
ADD dummyfile /data/

// fetchしてbranch情報を最新に
RUN git fetch

// 対象ブランチをチェックアウト
RUN git checkout -b ${BRANCH_NAME} origin/${BRANCH_NAME}

// アプリケーションを起動
RUN yarn install
RUN yarn run build
CMD yarn run start

2. Shell ファイルを作成

#!/bin/bash

BRANCH_RAW_NAME="$1"
if [ "$1" == "" ]; then
  BRANCH_RAW_NAME=$(git symbolic-ref --short HEAD)
fi

AWS_ACCOUNT_ID={ECRとかECSの頭についてるID}
APP_NAME={アプリケーションの名前}
BRANCH_NAME=`echo ${BRANCH_RAW_NAME} | sed -e 's/#//g' -e 's/\//_/g'`
CLUSTER_NAME=`echo ${APP_NAME}_${BRANCH_NAME} | sed 's/_/-/g'`

echo "${BRANCH_RAW_NAME} shining wizzard start"

echo 'Create Docker Image'

# Dockerログイン
$(aws ecr get-login --no-include-email --region ap-northeast-1)

# docker Build時に、キャッシュ利用をやめさせるために追加
touch dummyfile

# Docker Buildして、push! Imageはいらないので削除。
AWS_IMAGE_NAME=${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/{ECRのフォルダ名}:${BRANCH_NAME}
docker build . -t ${AWS_IMAGE_NAME} -f Dockerfile_nuxt --build-arg BRANCH_NAME=${BRANCH_RAW_NAME}
docker push ${AWS_IMAGE_NAME}
docker rmi ${AWS_IMAGE_NAME}

echo 'ECS Task Create'
# ECSタスク追加
NET_CONFIG="awsvpcConfiguration={subnets=[{subnet}],securityGroups=[{securitygroup}],assignPublicIp=ENABLED}"
VPC=[{VPC}]
ROLE={ROLE}

echo 'Check Task Definition'
TASK_DEFINITION_ARN=$(aws ecs describe-task-definition --task-definition ${CLUSTER_NAME} --query='taskDefinition.taskDefinitionArn')
if [ "$TASK_DEFINITION_ARN" == "" ]; then
  echo 'Create Task Definition'

  # Task Definition 作成
  TASK_DEFINITION= {ECSのタスク定義をJSON形式で挿入}
fi

echo 'Check Cluster'
CLUSTER_STATUS=$(aws ecs describe-clusters --clusters ${CLUSTER_NAME} --query='clusters[].status')
CLUSTER_STATUS=`echo $CLUSTER_STATUS | tr '\n' ' ' | sed -e "s/ //g" -e "s/\"//g" -e "s/\[//g" -e "s/\]//g"`

// Clusterが存在するかチェック
if [ "$CLUSTER_STATUS" == "ACTIVE" ]; then
  // Clusterが既にあるなら、タスクもあるよねってことで古いタスクはストップ
  echo 'Old Task STOP'
  OLD_TASK_ID=$(aws ecs list-tasks --cluster ${CLUSTER_NAME} --query='taskArns[0]')
  OLD_TASK_ID=`echo $OLD_TASK_ID | sed "s/\"//g"`
  STOP_TASK_INFO=$(aws ecs stop-task --cluster ${CLUSTER_NAME} --task $OLD_TASK_ID)
else
  # cloudwatch logs groupを作成しておく
  aws logs create-log-group --log-group-name /ecs/${CLUSTER_NAME}
  aws logs put-retention-policy --log-group-name /ecs/${CLUSTER_NAME} --retention-in-days 30

  # ECSのクラスターとか作成
  echo 'ECS Cluster create'
  aws ecs create-cluster --cluster-name ${CLUSTER_NAME}

  echo 'Task Definition create'
  aws ecs register-task-definition --family ${CLUSTER_NAME} --task-role-arn ${ROLE} --execution-role-arn=${ROLE} --network-mode awsvpc --container-definitions ${TASK_DEFINITION} --requires-compatibilities FARGATE --cpu 512 --memory 1024
fi

echo 'New Task Run'
RUN_TASK_INFO=$(aws ecs run-task --cluster ${CLUSTER_NAME} --task-definition arn:aws:ecs:ap-northeast-1:${AWS_ACCOUNT_ID}:task-definition/${CLUSTER_NAME} --network-configuration ${NET_CONFIG} --launch-type FARGATE)

TASK_ID=$(aws ecs list-tasks --cluster ${CLUSTER_NAME} --query='taskArns[0]')
TASK_ID=`echo $TASK_ID | sed "s/\"//g"`

echo 'タスクが生成されるまで待機'
sleep 60

NETWORK_INTERFACE_ID=$(aws ecs describe-tasks --cluster ${CLUSTER_NAME} --tasks ${TASK_ID} --query='tasks[].attachments[].details[?name==`networkInterfaceId`].value[]')
NETWORK_INTERFACE_ID=`echo $NETWORK_INTERFACE_ID | sed -e "s/\[ \"//g" -e "s/\" \]//g"`

IP_INFO=$(aws ec2 describe-network-interfaces --network-interface-ids ${NETWORK_INTERFACE_ID} --query='NetworkInterfaces[].Association[]')

printf "\e[33m IP情報 \e[m"
printf "\e[1;31m ${IP_INFO} \e[m"
// ちなみに削除コマンドもここに
// task definitionは一個しかないはずなので、一個だけDeregister
aws ecs deregister-task-definition --task-definition arn:aws:ecs:ap-northeast-1:${AWS_ACCOUNT_ID}:task-definition/${CLUSTER_NAME}:1

# Taskは常に一個しかないはずなので、一個TaskをSTOP
OLD_TASK_ID=$(aws ecs list-tasks --cluster ${CLUSTER_NAME} --query='taskArns[0]')
OLD_TASK_ID=`echo $OLD_TASK_ID | sed "s/\"//g"`
aws ecs stop-task --cluster ${CLUSTER_NAME} --task $OLD_TASK_ID

# クラスターを削除
aws ecs delete-cluster --cluster ${CLUSTER_NAME}

# ロググループも削除
aws logs delete-log-group --log-group-name /ecs/${CLUSTER_NAME}

printf "\e[33m 削除完了しました \e[m"

3.実行

上のshellファイルを自分の環境に合わせて修正して、実行すればうまく行くはず。

ざっくり流れ

DockerFile上で対象ブランチの変更を取得して、それを利用してDocker buildして、ECR上にDocker push。
ECSのClusterやTask定義、cloudwatchのlog groupを作成して、ECSのTaskを生成。
上記設定だったら、IPアドレスが振られるので、それを表示!

もっとシャイニングウィザードに近づけるために

  1. Github push で更新。delete で task削除を行えるようにする。
  2. Route53とIPアドレスを紐づけて、更新してもURLが変わらないようにする。

注意

shellファイル書くの初めてだったので、変なところとかあったらすいません。
terraform とか https://github.com/aws/amazon-ecs-agent で作った方が楽なのかも。
Dockerの部分は、実際はLocalのものをDocker buildして、pushしちゃった方が楽です!
(今回は、一つのコマンドで、Backend APIRailsと、Frontend の Nuxt を行いたかったから、こういう特殊な方法を取りました!)

感想

aws ecs cli をがっつり触るのは初めてだったけど、めっちゃ便利だった。
あと、先輩に流れのアドバイスをもらったぐらいで、作業自体は、自分の力で簡単にできた。