AWS ECSとconsul で作る docker デプロイ環境

# 前提

1. docker
2. consul及びconsul-template
3. AWS EC2、ELB、ECS

について基礎知識がある前提で記事を作ってます。

# Docker環境をつくる上での目標

1. インフラ設定をせずに新しいテスト環境を立てられるようにする
2. 本番環境と開発環境の差異をできる限りなくす

# AWS ECS(Elastic Container Service) + Consulを選択した理由

swarmやkubernetesが絶賛開発中であることが大きいです。
Dockerの管理部分のほとんどを任せる必要があり、中央集権的なマネージャーが必要となっているため、問題発生時に対応が難しいことが予想されるというのもあります。

Dockerでの環境を作成するにあたってやって欲しいことは下記です。

1. コンテナの維持
1. デプロイ
1. ロードバランサ連携
1. サービスディスカバリ


ちょっとECSの概念に触れますが、ECSでコンテナを動かす場合には下記の２つを設定します。正確にはタスクだけでも動きますが、継続して動かす場合には通常サービスを設定します。

#### タスク

どのようなコンテナを動かすか。

イメージなど `docker run` するときに指定するようなものに加えて、複数のコンテナを指定してlinkを設定できるので `docker-compose` の設定ファイルが近い存在です。

#### サービス

タスクをどのクラスタで何台動かすか。


サービス間連携に使えるのは、dockerのlinkのみです。ECSではタスクでlinkを設定するので、同じサーバには必ず同じコンテナの組み合わせにするしかなくなります。

ELBとの連携はサービスに設定するもので、複数サービスに同じELBを設定するとタスクが動いているインスタンス全てが登録されてしまうため、サービスごとにELBを立てることになってしまいます。特に開発環境でテスト用に気軽に登録したい場合にこれはよろしくありません。

ALBを使えば、サービスごとにELBを立てる問題は解消できそうです。しかしルールの設定をどこから設定するかという問題にぶつかります。

よって、この問題を解消するためにconsulを導入してサービスディスカバリを行い、nginxの設定を生成することで接続できるようにします。

ECSとconsulともにプロダクションでの利用が多くあり、ECSとconsulは密に連携しないためトラブル対応もしやすいだろうという理由となってます。

# 概要

## wercker を使ってECSへデプロイを指示する

http://wercker.com/

werckerを使う理由は下記です。

1. ビルドとデプロイが分離されている
2. 任意のビルドステップを他の人のが使えたり、自作できたりする

これを利用して、既存のECSデプロイステップを改変して、jinja2テンプレートによってECSのタスクを生成するステップを作りました。

https://app.wercker.com/#applications/57073060857da9ef4702a26f/tab/details/

werckerのstepを下記のようにした上で、

```
    - internal/docker-push:
      tag: $TARGET
      username: $QUAY_USERNAME
      password: $QUAY_PASSWORD
      repository: repository
      registry: https://example.com
      working-dir: /pipeline/source

    - wacul/aws-ecs:
      key: $AWS_ACCESS_KEY_ID
      secret: $AWS_SECRET_ACCESS_KEY
      deploy-service-group: web
      services-yaml: infra/services.yml
      environment-yaml: infra/conf/dev.yml
      template-group: app
```

```infra/services.yml
---
services:
  web:
    cluster: app
    serviceGroup: web
    templateGroup: web-repository
    desiredCount: 2
    minimumHealthyPercent: 50
    maximumPercent: 100
    registrator: true
    distinctInstance: true
    taskDefinitionTemplate: default
    vars:
      startupScript: ./infra/script/startup_web.sh
      portMappings:
        - hostPort: 0
          containerPort: 3000
          protocol: tcp

taskDefinitionTemplates:
  default: |
    {
      "family": "{{environment}}-{{item}}",
      "containerDefinitions": [
        {
          "name": "{{environment}}-{{item}}",
          "cpu": {{cpu}},
          "memoryReservation": {{memoryReservation}},
          "image": "quay.io/wacul/ai-analyst-ocha:{{environment}}{% if environment == 'production' %}-{{serviceGroup}}{% endif %}",
          "command": [
            "{{startupScript}}",
            "{{conf}}"
          ],
          "portMappings": {{portMappings|default([])|tojson}},
          "logConfiguration": {
            "logDriver": "syslog",
            "options": {
              "tag": "docker/{{environment}}/{{item}}/{% raw %}{{.ID}}{% endraw %}"
            }
          },
          "volumesFrom": [],
          "mountPoints": [],
          "essential": true
        }
      ]
    }
```

```infra/conf/dev.yml
---
environment: dev
cpu: 64
memoryReservation: 64
conf: dev.yaml

services:
  web:
    desiredCount: 4
    vars:
      cpu: 96
      memoryReservation: 96
```

このようにファイルを配置しておくことで、ECSのタスクを作ってサービスを配置することがwercker上から可能になります。`services-yaml:`で読み込んでるテンプレートの変数は環境変数が優先されるので、werckerの環境変数で設定を上書きが可能です。


ecsのデプロイの前にdockerにpushをしています。pushするときのタグを `TARGET` としています。
この`TARGET`をwerckerのworkflowの環境変数として設定しておけば、環境ごとのタグをつけたdockerとecsのタスクおよびサービスができることになります。

`registrator:` を有効にすることで環境変数に、`SERVICE_NAME` および `SERVICE_TAGS` が入ります。後述のgliderlabs/registratorが利用します。

詳しくはレポジトリのREADMEを参照してください。

https://github.com/wacul/wercker-aws-ecs

## ECSが起動したdockerを、gliderlabs/registratorによってconsulにサービス登録

コンテナが起動すると、consulのサービスにgliderlabs/registratorが登録してくれます。

https://github.com/gliderlabs/registrator

登録するものは現在起動しているものではなく、起動/終了するものを対象としているので、まっさきに起動していてもらう必要があります。

よってECSで起動させるのではなく、dockerに直接登録して`--restart=always`をつけて必ず起動してもらうようにしています。また、ECSでは現在指定できない`--net=host`を使ってローカルのconsulをそのまま指定できるようになってます。

基本的にインスタンスはansibleからAMIを作って起動しているので下記のようなansibleのtaskを使っています。
AMIを作っている最中には起動せずに、インスタンスとして起動する時に起動して欲しいので`docker create`で作成します。

```
- name: check container
  command: docker inspect registrator
  register: registrator
  failed_when: registrator.rc > 1
  changed_when: registrator.rc == 1
  tags: registrator

- name: create container
  command: docker create --name=registrator --net=host --restart=always --volume=/var/run/docker.sock:/tmp/docker.sock gliderlabs/registrator:latest consul://localhost:8500
  tags: registrator
  when: registrator.changed
```

`SERVICE_NAME`及び`SERVICE_TAGS` を元にconsulに登録します。その辺りはドキュメントに記述されています。

http://gliderlabs.com/registrator/latest/user/services/


## consul-templateがnginxのコンフィグを生成してnginxを再起動する

consul-templateはconsulに変更が起こることでテンプレートを元に設定ファイルを生成して、任意のスクリプトを実行することができます。

`SERVICE_NAME` をバーチャルホスト名の先頭部分として使い、`SERVICE_TAGS` にコンテナの役割としてテンプレートを作ります。

```
{{range $service := services}}
    {{range $tag, $servicebytag := service .Name | byTag}}{{if $servicebytag}}
    upstream {{$service.Name}}-{{$tag}} {
        {{range $servicebytag}}{{if ne .Port 0}}server {{.Address}}:{{.Port}} max_fails=3;{{end}}
        {{end}}
    }

{{if eq $tag "web"}}
    server {
        listen 80;
        server_name {{.Name}}.example.com;
        location / {
            proxy_pass http://{{.Name}}-web$request_uri;
        }
    }
{{end}}

{{end}}{{end}}
{{end}}
```
consulに登録したサービス名とタグ名の組み合わせてupstreamを作り、サービス名でバーチャルホストを作成しています。

このような形式だと、同一バーチャルホスト内で別のupstreamを指定したり、タグにもたせた役割名ごとに別の設定を持たせることができます。

consul-templateで負荷をかけすぎるとconsulが一時応答できなくなって、更新できなくなったりリーダーが失われたりといったことが発生してしまうのでなるべく複雑な記述とならないようにし、serviceを減らしたり、de-duplicationを有効にしたりしたほうがいいようです。

当然ながらconsul自身が不安定になっても崩壊するので、consulは最新にしておいたほうがいいでしょう。特にv0.7になってから高負荷時でも安定しやすいです。

あとは、nginxを動かしているサーバに、ELBをつなげば冗長化されることになります。

# その他

ログはsyslog経由にして、ファイルに出力およびlogglyに送りつけて管理しています。
dockerのログドライバーはいろいろあって、fluentdも使えるので困ることはないでしょう。
logglyに送りつける方法は別記事にしてます。

http://qiita.com/kanagi/items/d7345fde4fcb83151feb


監視周りはsensuを使っているので

https://github.com/sensu-plugins/sensu-plugins-docker
https://github.com/sensu-plugins/sensu-plugins-consul

でdockerとconsulを

https://github.com/sensu-plugins/sensu-plugins-aws

のecsのプラグインを使ってecsサービスのチェックをしています。

また、ECSサービスが増えてくるとAWSのウェブUIからの操作がきつくなってくるので、サービスに紐付いてしまっているところがあるので公開してませんが、slack経由でサービスをアップダウンできるようにしています。