DevOps with ECS Blue/Green

殴り書き。

1. docker-compose.ymlで楽したい
2. ECS Blue/Greenを使いたい

## ディプロイライフサイクルは二つ

1. タスク定義の更新に伴ったライフサイクル(インフラの変更に伴うOpsサイクルやDevOpsサイクルにおける環境変数の追加など)
2. イメージの更新に伴ったライフサイクル（純粋なアプリケーションイメージの更新に伴うDevOpsサイクル）

2のサンプルはいっぱいあるけど、1のサンプルはみつけられなかった。

1の更新では、過去のタスク定義は不要になる。と考える。
2の更新では、タスク定義は基本変わらず、イメージのみ更新していく。(イメージ変更に伴うrevの積立)

1のライフサイクルが大きく周り、その中で2のライフサイクルが小さく回る感じ。
awsのblue/greenのサンプルは、2を対象としている。

## 1. タスク定義の更新に伴ったライフサイクル

このライフサイクルのゴールは、タスク定義を更新し、それに伴う、 `taskdef.json`と （固定の）`appspec.yml`を生成すること。
この二つの成果を用いて、2のライフサイクルを回す。

楽したいので`ecs-cli`を用いる。

1. `ecs-cli`を用いて、`docker-compose.yml` + `ecs-params.yml`から、taskdefを登録する。
2. この際、既存のtaskdefのrevを全て消す。（変数が変わるため、過去のrevを使うことはほぼないと推測。　汚くしたくない＋inactiveのワーニング表示で気持ちいい。）
3. `aws describe-tasks` + `jq`で`taskdef.json`を生成

っということで、上のルールを、 `Makefile`で

### 準備

#### docker-compose.yml

2つに分けて書いておくと、良い。

```yaml
# docker-compose.yml
version: 3.0
services:
  app:
    build: ./app
    image: ...:${VERSION:-latest}

# docker-compose.ecs.yml
version: 3.0
services:
  app:
    env_file:
      - app/.env
    logging:
      ...
    ports:
      - 0:80
```

#### ecs-params.yml

```yaml
version: 1.0
task_definition:
  ...
```

#### Makefile
上記のルールをMakefile化しておくと楽。

```makefile
AWS_PROFILE?=default
TASKDEF_NAME:=my-sample-task
AWS_REGION?=ap-northeast-1
CLUSTER_NAME:=my-cluster

.PHONY: init
init: deinit register-task
  # init 完了後、taskdef.jsonを生成する。
  $(MAKE) -e taskdef.json

# 過去のrev削除
.PHONY: deinit
deinit:
  aws ecs --profile=${AWS_PROFILE} list-task-definitions --family-prefix ${TASKDEF_NAME} --query taskDefinitionArns --output text | xargs -J % -t -n1 aws ecs --profile=${AWS_PROFILE} deregister-task-definition --task-definition %
  rm taskdef.json

# タスク登録
.PHONY: register-task
register-task:
  ecs-cli compose --aws-profile ${AWS_PROFILE} -p ${TASKDEF_NAME} --region ${AWS_REGION} --ecs-params ecs-params.yml --cluster ${CLUSTER_NAME} -f docker-compose.yml -f docker-compose.ecs.yml create --create-log-groups

# taskdef.jsonの生成
taskdef.json:
  # コンテナの数に合わせて、適当に、 `select(.name == "app") | .image) = "<APP_IMAGE>" `を増やす。
  aws ecs --profile=${AWS_PROFILE} describe-task-definition --task-definition ${ECS_PROFILE} |  jq '.taskDefinition | (.containerDefinitions[] | select(.name == "app") | .image) = "<APP_IMAGE>" | del(.taskDefinitionArn, .revision, .status, .requiresAttributes) ' > $@
```

### 実行

```shell
make init
```

これで、過去のtaskdefを抹殺＋docker-composeにリンクした`taskdef.json`に更新される・・・はず。

-------------------------------

## 2. イメージの更新に伴ったライフサイクル

次は、イメージを更新していく。

いろいろな例があるけど、個人的には、 `release`とか `prod`をpushしたら、あとはpipelineに任せたい。
ってことで・・・。

1. CodeBuildでポチッとすると、`VERSION=prod docker-compose build app`
2. ECRの`app:prod`をソースに、CodePipeline　（CloudTrailないとイベント飛ばないかも・・・・）
3. CodePipelineの一発目に、 CodeBuild `retag`
4. あとは、定番。 taskdef.json + appspec.yamlからBlue/Green


特記することはないので、
https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/tutorials-ecs-ecr-codedeploy.html
こことかを参考に。

ここでは、 `retag`だけ。

### retag

ECRソースは、`taskDetail.json`を渡してくれる。

https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/file-reference.html#file-reference-ecs-bluegreen
(`generated by ECR`)

ってこで、`taskDetail.json`を受け取って、 `taskDetail.json`をartifactするCodeBuildを作ればいける。

```yaml
# ソースがECRなので、consoleのエディタなどでコピペ
# とりあえず適当に
version: 0.2
env:
  variables:
    AWS_DEFAULT_REGION: "ap-northeast-1"
    TARGET_TAG: "prod"
phases:
  install:
    runtime-versions:
      docker: 18
    commands:
      - apt -y update
      - apt -y install jq
  pre_build:
    commands:
      - cat ${CODEBUILD_SRC_DIR}/imageDetail.json
      - REPO=`jq -rM ".ImageURI" ${CODEBUILD_SRC_DIR}/imageDetail.json | awk -F'@|:' '{print $1;}'`
      - DATETIME=`date +%Y%m%d_%H%M`
      - printf 'REPO:%s,TAG=%s,DATETIME=%s' $REPO $TARGET_TAG $DATETIME
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
  build:
    commands:
      - docker pull ${REPO}:${TARGET_TAG}
      - docker tag ${REPO}:${TARGET_TAG} ${REPO}:${TARGET_TAG}_${DATETIME}
      - docker push ${REPO}:${TARGET_TAG}_${DATETIME}
  post_build:
    commands:
      - printf '{"ImageURI":"%s"}' ${REPO}:${TARGET_TAG}_${DATETIME} > imageDetail.json
artifacts:
  files:
    - imageDetail.json
```

## まとめ

とりあえずこんな感じ。