CircleCIからAWS ECSに自動デプロイ

### 概要

CircleCIでDockerをビルドし、そのままAWS ECR/ECSにデプロイする為の構成例（主に.circleci/config.yml）を紹介するもの
ググっても日本語文献が少なかったり、CircleCIのversionが古かったりだったので、作りました
Nuxt.jsアプリ用に作ったものですが、使い回しは効くかと思います
CircleCI公式の下記リンクを参考に、terraformなしでも動くように改変する形で作りました
あとオマケで環境面を切り替える機能をつけています

https://circleci.com/docs/2.0/ecs-ecr/
https://github.com/CircleCI-Public/circleci-demo-aws-ecs-ecr

### 以下で省略しているけれど必要なモノ

- ECS/ECRの設定
これだけで別の記事が書けてしまうので割愛
ecsTaskExecutionRoleの割り当てとか忘れがちですかね

- githubリポジトリ
言わずもがな。CircleCIとの接続設定はされているものとします

- Dockerfile
リポジトリのルートに。Dockerfileの書き方は解説しません

- requirements.txt
リポジトリのルートに。awscliを使えるようにします
https://github.com/CircleCI-Public/circleci-demo-aws-ecs-ecr/blob/master/requirements.txt

- CircleCIの環境変数設定
github上にaccess_keyを晒さなくていいように、入れておきます
https://circleci.com/docs/2.0/env-vars/#setting-an-environment-variable-in-a-project
この手順で設定した環境変数はCircleCIのジョブから参照できます


### .circleci/config.yml:build
```
version: 2
jobs:
～省略～
  build:
    docker:
      - image: circleci/node:10-stretch
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Setup common environment variables
          command: |
            echo 'export ECR_REPOSITORY_NAME="YOUR-ECR-REPO-NAME"' >> $BASH_ENV
            echo 'export FULL_IMAGE_NAME="${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR=REPOSITORY_NAME}:${CIRCLE_SHA1}"' >> $BASH_ENV
      - run:
          name: Build image
          command: |
            docker build -t $FULL_IMAGE_NAME .
      - run:
          name: Save image to an archive
          command: |
            mkdir docker-image
            docker save -o docker-image/image.tar $FULL_IMAGE_NAME
      - persist_to_workspace:
          root: .
          paths:
            - docker-image
～省略～
```


**YOUR-HOGEHOGE**箇所はご自身のECS/ECRの設定に合わせて置き換えてください

```- image: circleci/node:10-stretch```
node:10を使っていますが、これはビルドする対象が nuxtであるためです。Dockerビルド時に必要なモノに合わせて修正してください

#### .circleci/config.yml:deploy
```
～省略～
  deploy:  
    docker:
      - image: circleci/python:3.6.1
    environment:
      AWS_DEFAULT_OUTPUT: json
    steps:
      - checkout
      - setup_remote_docker
      - attach_workspace:
          at: workspace
      - restore_cache:
          key: v1-{{ checksum "requirements.txt" }}
      - run:
          name: Install awscli
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install -r requirements.txt
      - save_cache:
          key: v1-{{ checksum "requirements.txt" }}
          paths:
            - "venv"
      - run:
          name: Load image
          command: |
            docker load --input workspace/docker-image/image.tar
      - run:
          name: Setup target environment variables
          command: |
            echo 'export TARGET=`echo $CIRCLE_BRANCH | sed -e s/develop.*/dev/ -e s/release.*/stg/ -e s/master/prd/`' >> $BASH_ENV
            source $BASH_ENV
      - run:
          name: Setup common environment variables
          command: |
            echo 'export ECR_REPOSITORY_NAME="YOUR-ECR-REPO-NAME"' >> $BASH_ENV
            echo 'export ECS_CLUSTER_NAME="YOUR-ECS-CULSTER-NAME-PREFIX${TARGET}"' >> $BASH_ENV
            echo 'export ECS_SERVICE_NAME="YOUR-ECS-SERVICE-NAME-PREFIX${TARGET}"' >> $BASH_ENV
      - run:
          name: Push image
          command: |
            . venv/bin/activate
            eval $(aws ecr get-login --region ap-northeast-1 --no-include-email)
            docker push $AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/$ECR_REPOSITORY_NAME:$CIRCLE_SHA1
      - run:
          name: Deploy
          command: |
            . venv/bin/activate
            export AWS_DEFAULT_REGION="ap-northeast-1"
            export ECS_TASK_FAMILY_NAME="YOUR-ECS-TASK-NAME-PREFIX${TARGET}"
            export ECS_CONTAINER_DEFINITION_NAME="YOUR-ECS-CONTAINER-DEF-NAME-PREFIX${TARGET}"
            export EXECUTION_ROLE_ARN="arn:aws:iam::$AWS_ACCOUNT_ID:role/ecsTaskExecutionRole"
            bash ./deploy.sh
～省略～
```


```echo 'export TARGET=`echo $CIRCLE_BRANCH | sed -e s/develop.*/dev/ -e s/release.*/stg/ -e s/master/prd/`' >> $BASH_ENV```
ブランチ名に応じた値（developブランチなら"dev"）を環境変数に入れています。これをsuffixとして使い、デプロイ先のクラスタ名等に反映しています

**YOUR-HOGEHOGE**箇所はご自身のECS/ECRの設定に合わせて置き換えてください

余談ですが、ECSはクラスタ・サービス・タスク・タスク定義といろいろ設定しないといけないのですが、なかなか直感的にわかりにくくてイマイチな印象です

#### .circleci/config.yml:workflows

```
～省略～
workflows:
  version: 2
  build-deploy:
    jobs:
      - build:
          filters:
            branches:
              only:
                - develop
                - /release\/.*/
                - master
      - deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - develop
                - /release\/.*/
                - master
```

この辺はよしなに

#### deploy.sh
```
#!/usr/bin/env bash
set -eo pipefail
# more bash-friendly output for jq
JQ="jq --raw-output --exit-status"

deploy_cluster() {
  make_task_def   
  register_definition

  if [[ $(aws ecs update-service --cluster $ECS_CLUSTER_NAME --service $ECS_SERVICE_NAME --task-definition $revision | \
      $JQ '.service.taskDefinition') != $revision ]]; then
    echo "Error updating service."
    return 1
  fi

  # wait for older revisions to disappear
  # not really necessary, but nice for demos
  for attempt in {1..30}; do
    if stale=$(aws ecs describe-services --cluster $ECS_CLUSTER_NAME --services $ECS_SERVICE_NAME | \
        $JQ ".services[0].deployments | .[] | select(.taskDefinition != \"$revision\") | .taskDefinition"); then
      echo "Waiting for stale deployment(s):"
      echo "$stale"
      sleep 30
    else
      echo "Deployed!"
      return 0
    fi
  done
  echo "Service update took too long - please check the status of the deployment on the AWS ECS console"
  return 1
}

make_task_def() {
  task_template='[
    {
      "name": "%s",
      "image": "%s.dkr.ecr.%s.amazonaws.com/%s:%s",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 80
        }
      ],
      "environment": [
        {
          "name": "TARGET",
          "value": "%s"
        }
      ]
    }
  ]'
	
  task_def=$(printf "$task_template" $ECS_CONTAINER_DEFINITION_NAME $AWS_ACCOUNT_ID $AWS_DEFAULT_REGION $ECR_REPOSITORY_NAME $CIRCLE_SHA1 $TARGET)
}

register_definition() {
  if revision=$(aws ecs register-task-definition --requires-compatibilities FARGATE --cpu 256 --memory 1024 --network-mode awsvpc --execution-role-arn $EXECUTION_ROLE_ARN --container-definitions "$task_def" --family $ECS_TASK_FAMILY_NAME | $JQ '.taskDefinition.taskDefinitionArn'); then
    echo "New deployment: $revision"
  else
    echo "Failed to register task definition"
    return 1
  fi
}

deploy_cluster
```

デプロイジョブから呼び出すスクリプトです
task_templateの内容を色々弄るとカスタマイズできます

設定可能な項目はこちらを参照
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/create-task-definition.html

この例では、environmentで環境変数TARGETを設定しています
ECSがdocker runする際に引数として付与され、docker内のアプリから環境変数として参照可能になります

当方の環境では、nuxt.config.jsからTARGETを参照し、APIサーバの向け先を切り替えたり等しています