PHP Slim 環境構築(16) ECS(EC2)+ローリングデプロイ

# PHP Slim 環境構築(16) ECS(EC2)+ローリングデプロイ


## Introduction

[前回](https://qiita.com/dynamitecoolguy/items/d6aff53757ef475d7942)は、ECS(Fargate)の構築を行いました。
今回は、一つ戻って、ECS(EC2)のDeployを試してみます。

この一連のシリーズは、自分への備忘録が第一目的のため、だいぶ不親切です。
申し訳ございません。

## ローリングデプロイ

### 手動

手動で行う方法は、[前々回](https://qiita.com/dynamitecoolguy/items/1074132b8b082910abcf)に記載していますが、適当なec2のshell上で、docker build → docker push → task 更新 → サービス更新です (詳細は割愛)。

```
# docker build -t hoge-repo:latest --build-arg environment=devaws -f compose/web_hoge/Dockerfile.ecs .
# docker tag hoge-repo:latest $ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-repo:latest
# docker push $ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/hoge-repo:latest

コンソールでタスク更新。そのタスクを使ってサービス更新。
```


### バッチ

次に、Code Deployなどを使わずにバッチでやってみます。なお、デプロイの種類はローリングデプロイです。Blue/Greenはまた後程。。
バッチは、--service-updateオプションを付けるとサービス更新まで行うようにしています。付けない場合は、タスク更新までです。
なお、バッチ内でawsコマンドを実行しているのでaws configureによる設定済みであると想定しています。また、json解析にjqコマンドを用いています。
インストールされていない場合は、yum install jqコマンドでインストールしておいてください。


```shell
#!/bin/sh

# サービス更新するかどうか
SERVICE_UPDATE=0

# 引数解析
for OPT in "$@"
do
    case $OPT in
        --service-update)
            SERVICE_UPDATE=1
        shift 1
        ;;
    esac
done

# Git update (必要ならgit checkoutなども)
git pull

# ECRのログイン手続き
$(aws ecr get-login --no-include-email)

# ECRのリポジトリ名
REPOSITORY="slimtemplate"

# ECSに定義済みのtask名とcluster名
TASK_NAME="hoge-task"
CLUSTER_NAME="hoge-cluster"

# Dockerタグ
DOCKER_TAG="hoge-repo"

# docker buildする際にenvironment環境変数に引き渡す値
ENVIRONMENT="devaws"

# ECRのDockerイメージに付けられるタグ
NOW=$(date -u "+%Y%m%d%H%M%S")

# バッチ実行者のAWSアカウント
ACCOUNT_ID=$(aws sts get-caller-identity | grep Account | grep -oE '[0-9]+')

# docker buildを行う。更新の有無を確認するために、ビルド前後のイメージのidを取得する
OLD_IMAGE_ID=$(docker images ${DOCKER_TAG}:latest -q)
docker build -t ${DOCKER_TAG}:latest --build-arg environment=${ENVIRONMENT} -f compose/web_hoge/Dockerfile.ecs .
NEW_IMAGE_ID=$(docker images ${DOCKER_TAG}:latest -q)

if [ "${OLD_IMAGE_ID}" == "${NEW_IMAGE_ID}" ]; then
  # 更新なし
  echo "Docker image is not updated."
  exit 0
fi

# タグ付けおよびECRへのPUSH
docker tag ${DOCKER_TAG}:latest ${DOCKER_TAG}:${NOW}
docker tag ${DOCKER_TAG}:${NOW} ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY}:${NOW}
docker push ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY}:${NOW}
docker tag ${DOCKER_TAG}:latest ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY}:latest
docker push  ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY}:latest

# タスクを更新
aws ecs describe-task-definition --task-definition ${TASK_NAME} > /tmp/task.json

cat <<__COMMAND__ | tr '\n' ' ' > /tmp/task.cmd
aws ecs register-task-definition --family ${TASK_NAME} --container-definitions
'$(cat /tmp/task.json | jq -Mc '.taskDefinition.containerDefinitions' \
   | sed -re "s|(\"image\":[^:]*):[0-9a-z]*|\1:${NOW}|")'
--task-role-arn $(cat /tmp/task.json | jq -Mc '.taskDefinition.taskRoleArn')
--volumes '$(cat /tmp/task.json | jq -Mc '.taskDefinition.volumes')'
> /dev/null
__COMMAND__

sh /tmp/task.cmd

if [ "$SERVICE_UPDATE" == "1" ]; then
  # 最新のタスク定義family:revisionを取得
  REVISION=$(aws ecs describe-task-definition --task-definition ${TASK_NAME} | jq -M '.taskDefinition.revision')

  # サービス名を取得
  SERVICE=$(aws ecs list-services --cluster ${CLUSTER_NAME} | grep -oP '(?<=[0-9]:service/)([a-z0-9-]+)')

  # サービスのタスクを更新
  aws ecs update-service --cluster ${CLUSTER_NAME} --service ${SERVICE} --task-definition ${TASK_NAME}:${REVISION} > /dev/null
fi
```

少しずつ見ていきます。まずはコマンドラインの処理。

```shell
#!/bin/sh

# サービス更新するかどうか
SERVICE_UPDATE=0

# 引数解析
for OPT in "$@"
do
    case $OPT in
        --service-update)
            SERVICE_UPDATE=1
        shift 1
        ;;
    esac
done
```

すでにgitリポジトリがあるものとしています。もし無い場合は、あらかじめgit checkoutなどをしておく必要があります。

```shell
# Git update (必要ならgit checkoutなども)
git pull
```

スクリプトなどでecrを使うための処理です。ECRに対して適当なdocker loginコマンドを実行してくれます。

```shell
# ECRのログイン手続き
$(aws ecr get-login --no-include-email)
```

リポジトリ名などの設定です。また、作成したdockerイメージには、ビルドの日時をタグとして張り付けるようにしています。そのための日付を取得しています。
また、このコマンドを実行しているユーザーのAWSアカウントも取得しています。

```
# ECRのリポジトリ名
REPOSITORY="slimtemplate"

# ECSに定義済みのtask名とcluster名
TASK_NAME="hoge-task"
CLUSTER_NAME="hoge-cluster"

# Dockerタグ
DOCKER_TAG="hoge-repo"

# docker buildする際にenvironment環境変数に引き渡す値
ENVIRONMENT="devaws"

# ECRのDockerイメージに付けられるタグ
NOW=$(date -u "+%Y%m%d%H%M%S")

# バッチ実行者のAWSアカウント
ACCOUNT_ID=$(aws sts get-caller-identity | grep Account | grep -oE '[0-9]+')
```

docker buildします。更新が無い場合は、ここでおしまい。

```
# docker buildを行う。更新の有無を確認するために、ビルド前後のイメージのidを取得する
OLD_IMAGE_ID=$(docker images ${DOCKER_TAG}:latest -q)
docker build -t ${DOCKER_TAG}:latest --build-arg environment=${ENVIRONMENT} -f compose/web_hoge/Dockerfile.ecs .
NEW_IMAGE_ID=$(docker images ${DOCKER_TAG}:latest -q)

if [ "${OLD_IMAGE_ID}" == "${NEW_IMAGE_ID}" ]; then
  # 更新なし
  echo "Docker image is not updated."
  exit 0
fi
```

buildしたイメージに対して、"latest"以外に日付のタグも付けます。
これにより、最新のイメージは"latest"タグで更新され、それ以外のイメージは日付のタグが残ることになります(残り続けるのでなんらかの掃除が必要になります)。
そして、ECRにdockerイメージをpushします。"latest"イメージは上書きされますが、日付イメージはECR内に残ります。これは、なんらかの理由でバックグレードするためのものです。

```
# タグ付けおよびECRへのPUSH
docker tag ${DOCKER_TAG}:latest ${DOCKER_TAG}:${NOW}
docker tag ${DOCKER_TAG}:${NOW} ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY}:${NOW}
docker push ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY}:${NOW}
docker tag ${DOCKER_TAG}:latest ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY}:latest
docker push  ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY}:latest
```

タスクを更新します。ただし、新規でタスク定義用のjsonを作るのではなく、既存のタスクのjsonのイメージだけを書き換えるようにしています。
したがって、初回は手動でタスクを定義する必要があります。
また、イメージのタグには、日付タグのイメージを指定しています。これにより、サービス内のタスクを前のバージョンにすれば、サービスが巻き戻るようになります(latestにするとイメージが上書きされてしまうため)。

```
# タスクを更新
aws ecs describe-task-definition --task-definition ${TASK_NAME} > /tmp/task.json

cat <<__COMMAND__ | tr '\n' ' ' > /tmp/task.cmd
aws ecs register-task-definition --family ${TASK_NAME} --container-definitions
'$(cat /tmp/task.json | jq -Mc '.taskDefinition.containerDefinitions' \
   | sed -re "s|(\"image\":[^:]*):[0-9a-z]*|\1:${NOW}|")'
--task-role-arn $(cat /tmp/task.json | jq -Mc '.taskDefinition.taskRoleArn')
--volumes '$(cat /tmp/task.json | jq -Mc '.taskDefinition.volumes')'
> /dev/null
__COMMAND__

sh /tmp/task.cmd
```

必要であれば、サービスを更新します。使用するタスクのrevisionは最新のものを自動的に取得しています。

```
if [ "$SERVICE_UPDATE" == "1" ]; then
  # 最新のタスク定義family:revisionを取得
  REVISION=$(aws ecs describe-task-definition --task-definition ${TASK_NAME} | jq -M '.taskDefinition.revision')

  # サービス名を取得
  SERVICE=$(aws ecs list-services --cluster ${CLUSTER_NAME} | grep -oP '(?<=[0-9]:service/)([a-z0-9-]+)')

  # サービスのタスクを更新
  aws ecs update-service --cluster ${CLUSTER_NAME} --service ${SERVICE} --task-definition ${TASK_NAME}:${REVISION} > /dev/null
fi
```

### CodeDeploy

えー、結論から言うと、現在のバージョンではCodeDeployはローリングデプロイをサポートしてません。
どちらかと言うとBlue/Greenデプロイを使うべきということですね。(たぶん)