前提
- docker
- consul及びconsul-template
- AWS EC2、ELB、ECS
について基礎知識がある前提で記事を作ってます。
Docker環境をつくる上での目標
- インフラ設定をせずに新しいテスト環境を立てられるようにする
- 本番環境と開発環境の差異をできる限りなくす
AWS ECS(Elastic Container Service) + Consulを選択した理由
swarmやkubernetesが絶賛開発中であることが大きいです。
Dockerの管理部分のほとんどを任せる必要があり、中央集権的なマネージャーが必要となっているため、問題発生時に対応が難しいことが予想されるというのもあります。
Dockerでの環境を作成するにあたってやって欲しいことは下記です。
- コンテナの維持
- デプロイ
- ロードバランサ連携
- サービスディスカバリ
ちょっとECSの概念に触れますが、ECSでコンテナを動かす場合には下記の2つを設定します。正確にはタスクだけでも動きますが、継続して動かす場合には通常サービスを設定します。
タスク
どのようなコンテナを動かすか。
イメージなど docker run
するときに指定するようなものに加えて、複数のコンテナを指定してlinkを設定できるので docker-compose
の設定ファイルが近い存在です。
サービス
タスクをどのクラスタで何台動かすか。
サービス間連携に使えるのは、dockerのlinkのみです。ECSではタスクでlinkを設定するので、同じサーバには必ず同じコンテナの組み合わせにするしかなくなります。
ELBとの連携はサービスに設定するもので、複数サービスに同じELBを設定するとタスクが動いているインスタンス全てが登録されてしまうため、サービスごとにELBを立てることになってしまいます。特に開発環境でテスト用に気軽に登録したい場合にこれはよろしくありません。
ALBを使えば、サービスごとにELBを立てる問題は解消できそうです。しかしルールの設定をどこから設定するかという問題にぶつかります。
よって、この問題を解消するためにconsulを導入してサービスディスカバリを行い、nginxの設定を生成することで接続できるようにします。
ECSとconsulともにプロダクションでの利用が多くあり、ECSとconsulは密に連携しないためトラブル対応もしやすいだろうという理由となってます。
概要
wercker を使ってECSへデプロイを指示する
werckerを使う理由は下記です。
- ビルドとデプロイが分離されている
- 任意のビルドステップを他の人のが使えたり、自作できたりする
これを利用して、既存のECSデプロイステップを改変して、jinja2テンプレートによってECSのタスクを生成するステップを作りました。
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
---
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
}
]
}
---
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を参照してください。
ECSが起動したdockerを、gliderlabs/registratorによってconsulにサービス登録
コンテナが起動すると、consulのサービスに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に登録します。その辺りはドキュメントに記述されています。
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に送りつける方法は別記事にしてます。
監視周りはsensuを使っているので
https://github.com/sensu-plugins/sensu-plugins-docker
https://github.com/sensu-plugins/sensu-plugins-consul
でdockerとconsulを
のecsのプラグインを使ってecsサービスのチェックをしています。
また、ECSサービスが増えてくるとAWSのウェブUIからの操作がきつくなってくるので、サービスに紐付いてしまっているところがあるので公開してませんが、slack経由でサービスをアップダウンできるようにしています。