yukofebの日記

個人的な技術メモ。

Amazon ECSを使ってHubotが動いているdockerコンテナをEC2へ自動デプロイする

最近dockerをよく使っているので、Amazon ECSも触ってみることにした。
せっかくなのでCircleCIも使って、githubにpushしたら自動でデプロイするように設定する。

hubot構築

まずはローカルで動かしてみるところから。
作業環境はMacです。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21

Dockerfileを作って、そこからビルドする。
あと、Slackと連携させるための設定も入れる。

$ cat Dockerfile
FROM ubuntu
MAINTAINER yukofeb <yuko.february@gmail.com>

# Install packages 
RUN apt-get update
RUN apt-get -y install expect redis-server nodejs npm
RUN ln -s /usr/bin/nodejs /usr/bin/node

RUN npm install -g coffee-script
RUN npm install -g yo generator-hubot

# Create hubot user
RUN useradd -d /hubot -m -s /bin/bash -U hubot

# Log in as hubot user
USER hubot
WORKDIR /hubot

# Install hubot
Run yo hubot --owner="yukofeb" --name="Hubot" --description="Hubot in docker" --defaults

# Configure
ADD files/external-scripts.json ./ 
ADD scripts/*.coffee scripts/

# Support slack
RUN npm install hubot-slack --save && npm install
CMD bin/hubot -a slack

これを元にイメージを作る。
追ってdockerhubにアップするので、(user_name)/(repo_name)の名称で保存しておく。

$ docker build -t yukofeb/hubot-in-docker .

# 確認
$ docker images
REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
yukofeb/hubot_in_docker   latest              d4de1ddf744c        33 seconds ago      452.8 MB

このイメージをもとにrunする。
Slack用の変数については、Slack Integration設定画面から確認できる。

$ docker run -e HUBOT_SLACK_TOKEN=*** -d yukofeb/hubot_in_docker

# 確認
$ docker ps -a
CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS              PORTS               NAMES
a12644feaf43        yukofeb/hubot_in_docker   "/bin/sh -c 'bin/hubo"   5 seconds ago       Up 4 seconds                            hubot_test

これで自分のSlackにhubotが出現しているはず。
dockerだとhubot構築がほんとに簡単すぎてびっくりする。。

dockerhubへアップロード

dockerhubに登録後、createボタンから該当レポジトリを登録しておく。
f:id:yukofeb:20160326120035p:plain

アップロードのために事前に認証コマンドを実行しておく必要がある。

$ docker login --username=*** --email=***@***

手動でアップロードするコマンド。

$ docker push (user_name)/(repo_name)

その他詳細は公式ページを参照。
Tag, push, & pull your image

CircleCI設定

今までやってきた"dockerイメージ作成、起動、dockerhubへのpush"を自動でできるように設定する。

$ cat circle.yml
machine:
  services:
    - docker

dependencies:
  override:
    - docker build -t yukofeb/hubot_in_docker:v_$CIRCLE_BUILD_NUM .

test:
  override:
    - docker run -d yukofeb/hubot_in_docker:v_$CIRCLE_BUILD_NUM

deployment:
  hub:
    branch: master
    commands:
      - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
      - docker push yukofeb/hubot_in_docker:v_$CIRCLE_BUILD_NUM

dockerhubログインのための情報はレポジトリにあげたくないので、CircleCIの環境変数で対応する。
Builds > Settings > Tweaks > Environment variablesから設定可能。
f:id:yukofeb:20160326120219p:plain

ここから$DOCKER_EMAIL, $DOCKER_USER, $DOCKER_PASSを入れる。
(あとで追加で環境変数を追加するが、それはまたあとで。。)

ここまでやれば、githubへpushすれば自動でdockerhubプッシュまでやってくれるはず。

EC2へのデプロイ

次はいよいよECSを使ってEC2にdockerをデプロイしていく。
基本的にはLaunching an Amazon ECS Container Instance - Amazon EC2 Container Serviceに従ってやっていけばOK。
また作業する前にAmazon EC2 Container Service(ECS)の概念整理 - Qiitaをさらっと見ておくと理解が深まるので進めやすい。

IAM作成

まずはIAMを作成する。
ポリシーはAmazonEC2ContainerServiceforEC2Roleで設定。
このIAMは、ECSがインスタンスとして扱うEC2に適用する。
f:id:yukofeb:20160326120247p:plain

EC2作成

次はECSインスタンス用のEC2を作成する。
ECS用のイメージがあるので、それを選択する。
2016/03現在はami-b3afa2dd を使うといいらしい。
最新の情報は公式ドキュメントを確認してください。

IAMロールの項目については、先ほど作成したものを設定する。

また、高度な詳細項目で設定できるユーザーデータ欄に以下を入力する必要がある。
(これを設定しないとEC2のインスタンスとして認識されない)
(your_cluster_name)には次項目で設定するcluster名を入力すること。

#!/bin/bash
echo ECS_CLUSTER=(your_cluster_name) >> /etc/ecs/ecs.config

ECS設定

ここまで出来たらようやくECSコンソールに移動。

cluster作成

何はともあれCreate Clusterボタンを押す。
今回はCluster名はhubotにする。
f:id:yukofeb:20160326120313p:plain

先ほどのEC2作成がうまくいっていれば、それがECS Instancesにリストされる。
f:id:yukofeb:20160326120324p:plain

Task Definition作成

次にTask Definitionを作成していく。
Task Difinitionとは、Task(Dockerコンテナをグルーピングするもの)のテンプレみたいなもの。
GUIでも作成できるが、今後のためにymlにまとめた。

$ cat ecs_task_definition.json
[
  {
    "image": "yukofeb/hubot_in_docker:v_***",
    "name": "myhubot",
    "cpu": 10,
    "memory": 400,
    "essential": true,
    "entryPoint": [
      "bin/hubot",
      "-a",
      "slack",
      "-d"
    ],
    "environment": [
      { "name" : "HUBOT_SLACK_TOKEN", "value" : "***" }
    ],
    "portMappings": [
      {
        "containerPort": 80,
        "hostPort": 80
      }
    ]
  }
]

以下のコマンドで、アップロードできる。

$ aws ecs register-task-definition --family myhubot --container-definitions file://./ecs_task_definition.json

# 確認
$ aws ecs list-task-definitions

アップロードしたTask DefinitionはGUIでも確認できる。
f:id:yukofeb:20160326120341p:plain

ちなみにawscliのインストール方法はこちら。
Macだとhomebrewでインストールできるから簡単。
Installing the AWS Command Line Interface - AWS Command Line Interface

Task作成

Taskは、先ほどのTask Definitionを実体化したものと考えればOK。
今回はコマンドラインから作成。

$ aws ecs run-task --cluster hubot --task-definition myhubot:1 -count 1 

# 確認
$ aws ecs list-tasks --cluster hubot
{
    "taskArns": [
        "arn:aws:ecs:ap-northeast-1:***"
    ]
}

GUIからも確認できる。
f:id:yukofeb:20160326120356p:plain

Service作成

最後にServiceを作成する。
Serviceとは、複数あるいは単独のTaskの集合体。

$ aws ecs create-service --service-name hubot --task-definition hubot:1 --desired-count 1

# 確認
$ aws ecs list-services --cluster hubot
{
    "serviceArns": [
        "arn:aws:ecs:ap-northeast-1:***:service/hubot"
    ]
}

GUIからも確認できる。
f:id:yukofeb:20160326120411p:plain

ここまで出来れば、すでにhubotは動く状態になっているはず。

CircleCI設定

次に、CircleCIから自動デプロイする設定を行う。
CircleCIからEC2へのアクセス設定についてはここでは省略するが、
CircleCIへの秘密鍵設定、EC2への公開鍵設定などが必要となる。
(方法はネット上に沢山あるのでそちらにお任せします。。)

機密情報への対応

CircleCIにデプロイしてもらうために全てのファイルはgithubにアップロードするが、
このままではSlackのトークンをレポジトリに公開することになってしまう。
これを隠蔽するためにCircleCIの変数機能を利用し対応する。

ということで、先ほど書いたスクリプトを一部修正する。

まずTask Definitionを定義したjsonファイルを手直し。
隠蔽したいSlackのトークンと、Pullするイメージのバージョンは環境変数から引っ張ってくる。
なので、あらかじめCircleCIの環境変数設定画面からHUBOT_SLACK_TOKEN環境変数として入力する必要がある。
CIRCLE_BUILD_NUMはCircleCIが入れてくれるので対応不要。

# Task Definition修正版
$ cat ecs_task_definition.json
[
  {
    "image": "yukofeb/hubot_in_docker:v_--CIRCLE_BUILD_NUM--",
    "name": "myhubot",
    "cpu": 10,
    "memory": 400,
    "essential": true,
    "entryPoint": [
      "bin/hubot",
      "-a",
      "slack",
      "-d"
    ],
    "environment": [
      { "name" : "HUBOT_SLACK_TOKEN", "value" : "--HUBOT_SLACK_TOKEN--" }
    ],
    "portMappings": [
      {
        "containerPort": 80,
        "hostPort": 80
      }
    ]
  }
]

# 変数を置き換えるスクリプト
# 以下のスクリプトを参考にさせていただきました
# https://github.com/noboru-i/backlog-to-chatwork/blob/master/bin/env2file
$ cat replace_variables.sh
#!/bin/bash

# Replace --***-- with $***(environment variables)
ORIGINAL_FILE=$1
REPLACED_FILE=$2

cp ./$ORIGINAL_FILE ./$REPLACED_FILE
cat $REPLACED_FILE | while read line
do
  if [[ $line =~ --([A-Z_]+)-- ]] ; then
    KEY=${BASH_REMATCH[1]}
    eval VALUE='$'${KEY}
    sed -i -e "s!${BASH_REMATCH[0]}!$VALUE!g" $REPLACED_FILE
  fi
done

CircleCIからのTask DefinitionとService更新

Task DefinitionとServiceをCircleCIから更新するためのスクリプトを作成する。

$ cat update_ecs.sh
#!/bin/bash

REPLACED_FILE=replaced_ecs_task_definition.json
MYSECURITYGROUP=***
MYIP=`curl -s ifconfig.me`
echo "MYIP is $MYIP"

# CircleCIの環境変数を利用してファイルを書き換える
bash ./replace_variables.sh ecs_task_definition.json $REPLACED_FILE

# awscliを使うための認証を行う
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
aws configure set region $AWS_REGION

# セキュリティ対策のため、都度ログインできるIPをセキュリティグループに登録している
aws ec2 authorize-security-group-ingress --group-id $MYSECURITYGROUP --protocol tcp --port 22 --cidr $MYIP/32

# Task Definition更新
# 結果はdevnullに捨てている
# (出力結果に環境変数が含まれているので、そのままではCircleCIのコンソールにそれらの情報が出力されてしまうため)
result=`aws ecs register-task-definition --family myhubot --container-definitions file://./$REPLACED_FILE` >/dev/null 2>&1

# 先ほどの出力結果から登録されたTask Definitionのrevision番号を取得する
revision=`echo $result | jq '.["taskDefinition"]["revision"]'`

# 登録されたTask Definitionを使ってServiceを更新する
aws ecs update-service --cluster hubot --service hubot --task-definition myhubot:$revision --desired-count 1

# 先ほど許可したログイン可能IP情報を削除する
aws ec2 revoke-security-group-ingress --group-id $MYSECURITYGROUP --protocol tcp --port 22 --cidr $MYIP/32

# いらないファイルを削除する
rm -rf $REPLACED_FILE

CircleCIに実行させるスクリプトに追加。
また先ほどのスクリプト内で、jsonパースのためにjqを使っているので、それもインストールする。

$ cat circle.yml
machine:
  services:
    - docker

dependencies:
  pre:
    - sudo apt-get install jq
    - sudo pip install awscli
  override:
    - docker build -t yukofeb/hubot_in_docker:v_$CIRCLE_BUILD_NUM .

test:
  override:
    - docker run -d yukofeb/hubot_in_docker:v_$CIRCLE_BUILD_NUM

deployment:
  hub:
    branch: master
    commands:
      - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
      - docker push yukofeb/hubot_in_docker:v_$CIRCLE_BUILD_NUM
      - bash ./update_ecs.sh

思ったよりも長くなったけど、これで完了!yukofeb/hubot_in_docker
これで快適なhubot生活が送れる(=゚ω゚)!!

References

How to Run HuBot in Docker on AWS EC2 Container Services - Part 1 - Philipp´s Blog
Slackと連携するHubotをDocker in AWSで構築 - Qiita
Amazon EC2 Container Service (ECS)を試してみた | Developers.IO Amazon EC2 Container Service(ECS)で静的Webサイトをデプロイする | Developers.IO