ChatOpsなデプロイ環境にしたい
新しい案件の話しがあり、インフラ構成、デプロイ周りをどうしようかと考えていました。 前までクラウドはAWS一択でしたが最近はGCPなどにも興味があり、 「とりあえずDockerが動くクラウドならうちらが作るアプリも動くだろう」 というスタンスでDockerを積極的に使おうとしています。
Docker環境でのデプロイとなると、下記の手順になるでしょうか。
- リリースしたいアプリのDockerイメージを作る
- Dockerレジストリにプッシュする
- オーケストレーションツールが新しいコンテナを作る
- WEBリバースプロキシが古いコンテナから新しいコンテナにつなぐ
どこかにプッシュしたとか、なんらかのイベント発生をトリガとして他のサービスに通知したり、APIを叩いたりとサービス間の連携ができることが最近多いので、上記の手順も流れるように行えるでしょう。 Dockerイメージを作るより前の工程がいくつかありますが、これらも同様に自動化できるので一連の流れでデプロイできそうで、図にしてみたらこんな感じになりました。
- デプロイの流れ
local hipchat hubot github circleci docker kubernetes staging registry aws ecsとか? production deploy --------> pr, merge --------> test ---------> docker push ----------> push notify ------------> docker run --------------> container 切り替え
Dockerイメージを作る前に hipchatやgithubなど使っていますが以下のところにメリットがあると思っています。
- hipchatでdeployの内容が流るとデプロイのタイミングや内容をメンバーで共有しやすい
- githubのプルリクエスト、マージは今回リリースされる内容がレビューしやすい
- circleciは、まぁテスト通ってないのにリリースはできないので^^;
RedisのHyperLogLogを試してみる
AWSのElasticCacheのredisが2.8.19になりHyperLogLogというのをサポートしたとのことなので、どんなものか試してみました。 HyperLogLogの利用法としてユニークユーザの推移に便利と記事でよく紹介されているようで、 今回のお試しでもwebサイトに訪問した人のユニークユーザが大体どのくらいかを簡単にわかるようにしてみます。
HyperLogLogについてはこちらの記事が参考になりました。
nginxのインストール
webサイトはnginxでサービスするものとして、nginxの中でredisのコマンド叩くことになるのでredis2-nginx-moduleやらを追加してngxinのrpmを作成します。
wget http://nginx.org/packages/rhel/6/SRPMS/nginx-1.6.2-1.el6.ngx.src.rpm rpm -ivh nginx-1.6.2-1.el6.ngx.src.rpm git clone https://github.com/openresty/redis2-nginx-module.git git clone https://github.com/vkholodkov/nginx-eval-module.git git clone https://github.com/simpl/ngx_devel_kit.git git clone https://github.com/openresty/set-misc-nginx-module.git
nginx.specのconfigureのところに下記追加
--add-module=~/src/redis2-nginx-module \ --add-module=~/nginx-eval-module \ --add-module=~/ngx_devel_kit \ --add-module=~/set-misc-nginx-module \
ビルドしてインストール
rpmbuild -bb nginx.spec rpm -ivh RPMS/x86_64/nginx-1.6.2-1.amzn1.ngx.x86_64.rpm
nginxの設定
location内のredis2_queryでpfaddを叩きます。 セッションの中に含まれてるユーザIDなどをキーにしてredisに渡せばユニークユーザ数を推移ができるのですが、 今回はリクエストの度にnginx内でユーザID相当なものをランダムで生成します。 そして、このidをredisのpfaddコマンドでuuというHyperLogLogデータに追加していきます。
upstream staging-redis { server redisサーバのIP:6379 fail_timeout=0; keepalive 1024; } location / { eval $ret { set_random $uid 1 1000000000; redis2_query select 1; redis2_query pfadd uu $uid; redis2_pass staging-redis; } ... }
テスト
- pfcountでuuの異なり数を確認する(異なり数=ユニークユーザ数と判断する)
redis-cli > pfcount uu (integer) 0
- 10000回ほどアクセスしてみる
ab -c 100 -n 10000 http://staging/test.html
- もう1回pfcountで確認
redis-cli >pfcount uu (integer) 9998
pfcountは推定なので実際のアクセス数と同じにはなりません。 そもそもuidがランダム生成なので重複したuidが作られてるかもしれません。
パフォーマンス
redisのpfaddのパフォーマンスがどのくらいかをsetの場合とで単純に比べてみました。 上記のabのスループット結果です。特に違いがないようです。
- pfaddの場合
Requests per second: 4322.10 [#/sec] (mean) Time per request: 23.137 [ms] (mean)
- setの場合
Requests per second: 4138.47 [#/sec] (mean) Time per request: 24.164 [ms] (mean)
hubotを使ってあれこれする(mongodbをtailする)
サイトの50xエラー発生時になるべくリアルタイムで社内に周知する。
運用しているサイトで50xエラーが発生した際に、なるべくリアルタイムで社内に周知したいと考えていました。 nginxのログはcapped属性でmongodbにも集約しているので、これを常時tail監視してエラー検知時にアラートを発報するようにします。 さらにhubotのスクリプトとして動くようにすれば、hipchatにも周知しやすくなるので、今回はhubotのスクリプトにしました。(下記のサンプルではhipchatに流していませんが・・・)
簡単な仕様
- 動きとしては、mongoのカーソル使ってログを500系エラーのログのみtailするようにします。
- エラーかどうかはngxinログのupstream_statusのステータスコードで判定します。
- 該当データが発生した場合はonイベントで音を鳴らすようにします。音はafplayコマンドで指定のアラート音を鳴らすだけです。
- 瞬間的に大量の50xエラーが発生した場合、afplayコマンドが多重で大量に叩かれてプロセス生成できなくなるエラーが発生したので、afplayプロセス起動中は別のafplayが起動できないようにフラグ管理しておきます。
フラグはhubotのbrain(redis)で保持します。
scripts/mongo_tail.coffee
util = require('util') mongojs = require('mongojs') spawn = require("child_process").spawn; HOST = process.env.MONGO_HOST DB = process.env.MONGO_DB COLL = process.env.MONGO_COLL ALARM = "alarm_flag" FLAG_RESET_INTERVAL = 1 * 60 * 1000 SITE = "mysite" module.exports = (robot) -> cursor = {} db = {} robot.brain.set(ALARM, false) setInterval(()-> robot.brain.set(ALARM, false) , FLAG_RESET_INTERVAL) alarm = (site, doc) -> console.log("site:" + site) console.log(doc) unless robot.brain.get(ALARM) robot.brain.set(ALARM, true) cli = spawn("/usr/bin/afplay", ["/se/" + site + doc.upstream_status + ".mp3"]) cli.on 'exit', (code) -> robot.brain.set(ALARM, false) db = mongojs(HOST + '/' + DB, [COLL]) cursor = db[COLL].find({upstream_status:/50\d/}, {}, tailable: true timeout: false) cursor.on 'data', (doc) -> alarm(SITE, doc) return
hubotを使ってhipchat上であれこれする
hubot hipchat
やりたいこと
チャットアプリのhipchatとボットとして動くhubotを使って、下記の事をしてみようと思い、設定してみました。 社内にあるubuntuでhubotを起動して、hipchat上にボットとして常駐させるようにします。
運用するサイトに対する問い合わせが届いた際にチャットに通知する。
- 不具合の問い合わせが複数回来た場合は携帯端末にpush通知する。
- テキストスピーチで読み上げて社内のスピーカーで出力する
AWSで作業した履歴をチャットに通知する(履歴はCloudTrailで収集する)
処理の流れ
データ処理の流れは以下を想定しています。
- 問い合わせ
転送 ポスト 監視 読み上げ Gmail------>MTA------>hipchat<---->hubot--->シェル(open_jtalk) | |push通知 | 携帯端末
- AWSのログ
AWS ポスト ポスト cloudtrail ----------->hubot---------->hipchat sns
設定
下記は設定の記録です。
hipchat
部屋名をcontact,awsとして作成して、その部屋のRoom Notification Tokensも作成します。
hubot
社内のubuntuマシンにhubotをインストールしてひな形アプリも作成します。 hubotがログインの際に使うアカウントも作成して、起動確認まで行います。
$sudo apt-get install node $sudo npm install -g hubot coffee-script $sudo npm install -g yo generator-hubot $mkdir -p examplebot $cd examplebot $yo hubot [?] Owner: app [?] Bot name: examplebot [?] Description: (A simple helpful robot for your Company) hogehoge [?] Bot adapter: (campfire) hipchat $ cat hubot_env export PORT=5555 export HUBOT_HIPCHAT_JID=xxxx_xxxx@chat.hipchat.com export HUBOT_HIPCHAT_PASSWORD=xxxx export HUBOT_HIPCHAT_ROOMS=xxxx_contact@conf.hipchat.com,xxxx_aws@conf.hipchat.com $ source hubot_env $ ./bin/hubot -a hipchat
問い合わせの対応
hipchatに問い合わせ内容をPOSTする
Gmailに転送設定をつけてMTAサーバにメールを転送します。 その後、/etc/aliasesに設定されたスクリプトが起動してhipchatにpostするようにします。
* /etc/aliases contact: :include:/home/ec2-user/contact * /home/ec2-user/contact "| ruby contact.rb"
- /home/ec2-user/contact.rb
#!/usr/bin/env ruby # -*- encoding:utf-8 -*- require 'hipchat' require 'mail' token = "xxxxxxxxxx" room = "contact" sender = "contact" mail = Mail.new(STDIN.read) body = mail.body.decoded.encode("UTF-8", mail.charset) client = HipChat::Client.new(token, :api_version => 'v2') client[room].send(sender, body)
問い合わせ内容を読み上げる
contact部屋にメッセージが追加されたら読み上げソフトウェアであるopen_jtalkを起動するようなスクリプトをhubotに追加します。
- examplebot/scripts/contact.coffee
# Commands: # hubot contact # # Commands: # hubot contact - spawn = require('child_process').spawn module.exports = (robot) -> ROOM = ["contact"] # hipchatの部屋名 SENDER = ["contact"] # hipchatのRoom Notification Tokensで設定したlabelに該当 ENABLE = "contact_enable" # 読み上げるか読みあげないか(reidsのキー名) ERROR_COUNTER = "contact_error_counter" # 不具合報告のカウント数(reidsのキー名) REPORT_COUNT = 5 # この回数以上の不具合報告を受けたらpush通知 MEMBERS = "@foo @bar" #push通知する対象のhipchatユーザ correct = (a, b) -> return true if a.join(" ").match(///#{b}///i) return false robot.hear /^off/i, (msg) -> return unless correct(ROOM, msg.message.room) robot.brain.set(ENABLE, 0) msg.send("読み上げを無効にしました。") robot.hear /^on/i, (msg) -> return unless correct(ROOM, msg.message.room) robot.brain.set(ENABLE, 1) msg.send("読み上げを有効にしました。") # msg.match[1]] 問い合わせの本文 robot.hear /^(.*)$/i, (msg) -> return unless correct(ROOM, msg.message.room) \ and correct(SENDER, msg.message.user.name) \ and robot.brain.get(ENABLE) == 1 # 不具合報告のような問い合わせがREPORT_COUNT回届いた時にpush通知を試みる。 # 不具合報告が多い時はpush通知が連続されるのでトラブルとすぐに認識したい。 if /(エラー|サーバ|重い)/.test(msg.match[1]) cnt = robot.brain.get(ERROR_COUNTER) cnt ?= 0 if cnt > REPORT_COUNT # @fooをつけることでメンションとなり、push通知されるみたい。 msg.send(MEMBERS + " サーバトラブルのお問い合わせを受信") robot.brain.set(ERROR_COUNTER, (cnt++ % REPORT_COUNT)) # 必要ないものは読み上げない return if msg.match[1].length < 10 || /(NGワード)/.test(msg.match[1]) #読み上げ args = ['./bin/talk', "お問い合わせがきました。"+ msg.match[1]] cli = spawn('/bin/bash', args, {setsid: true}) cli.stderr.on 'data', (data) -> console.log('cli stderr: ' + data.toString())
- examplebot/bin/talk
#!/bin/sh # yukkuri #[ -f $yukkuri ] && echo $2 | $yukkuri | aplay # jtalk f=`mktemp` wav=`mktemp` score=$1 echo $2 > $f emp="normal" dic_dir="/var/lib/mecab/dic/open-jtalk/naist-jdic/" voice="/home/app/MMDAgent_Example-1.4/Voice/mei/mei_${emo}.htsvoice" open_jtalk -u 0.0 -jm 0.7 -jf 0.5 -a 0.55 -r 1.2 -m $voice -x $dic_dir -ow $wav $f [ -s $wav ] && aplay $wav exit 0
Docker環境でRailsを起動するところまで確認する
作業確認流れ
- プライベートなDocker registry(S3 backed)を用意する。
- registryのWEBUIを用意する。
- 手元のmacでdockerが動くようにする。
- 既存のrailsアプリをDockerイメージ化してregistryにpushする。
- Dockerまで動く状態の別の端末からRailsアプリのイメージをpullしてRailsを起動する。
プライベート registryを作る。
プライベートregistryを用意して、開発用のmacでdocker-registryをpullできるようにする。
下記、repos.example.comサーバで行う。
docker-registry
/etc/hosts
127.0.0.1 repos.example.com
- docker起動
#/etc/init.d/docker start
- registryコンテナ起動
$ sudo docker run \ -e SETTINGS_FLAVOR=s3 \ -e AWS_REGION=ap-northeast-1 \ -e AWS_BUCKET=example-docker-repos \ -e AWS_KEY=**** \ -e AWS_SECRET=**** \ -p 5000:5000 \ registry \
- registryコンテナをイメージ化
$ docker commit desperate_pare docker-regisrty-s3
desperate_pareはコンテナ起動時に付けられたコンテナ名
- タグをつけてpushする
$ docker tag 80078c2afa84 repos.example.com:5000/docker-regisrty-s3 $ docker push repos.example.com:5000/docker-regisrty-s3
docker-registry-frontend
ブラウザでregistryを閲覧検索できるようにする。
docker-front.sh
#!/bin/sh docker run \ -d \ -e ENV_DOCKER_REGISTRY_HOST=172.17.42.1 \ -e ENV_DOCKER_REGISTRY_PORT=5000 \ -p 8000:80 \ konradkleine/docker-registry-frontend \
HOSTは127.0.0.1ではだめでdocker registryのIPである172.17.42.1を指定する。
- リバースプロキシ設定追加
<VirtualHost _default_:443> ProxyPass / http://localhost:8000/ ProxyPassReverse / http://localhost:8000/ </VirtualHost>
- 確認
https://repos.example.com/#/repositories
開発用端末のセットアップ
TODO vagrant upでできるようにしたい
VirtualBoxインストール
boot2dockerインストール
$ boot2docker init
$ boot2docker up
$ boot2docker ssh
$ cat /var/lib/boot2docker/profile
DOCKER_TLS=no EXTRA_ARGS="--insecure-registry repos.example.com:5000 --insecure-registry 192.168.59.103:5000 --insecure-registry dockerhost:5000"
$ exit
$ boot2docker shellinit >> ~/.zshrc
export DOCKER_HOST=tcp://192.168.59.103:2375 unset DOCKER_CERT_PATH unset DOCKER_TLS_VERIFY
$ source ~/.zshrc
$ boot2docker restart
- VirtualBoxの設定からポートフォワードの追加
docker-registryをローカルのtcp:5000で立ち上げるため、ポートフォワードを追加する。
127.0.0.1:5000 -> 5000
docker-registryの取り込みと起動
$ docker run -d -p 5000:5000 repos.example.com:5000/docker-regisrty-s3:latest
registryを通してS3にテストイメージをpush
$ echo "FROM rails:onbuild" > Dockerfile
$ docker build -t localhost:5000/sample_rails .
$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE localhost:5000/sample_rails latest fa7eb42f096e 17 hours ago 933.4 MB
$ docker push localhost:5000/sample_rails
S3で確認する
別mac端末上でアプリをpullしてそのまま起動する。
前提
- boot2dockerが動いている。
- VirtualBoxの設定でtcp5000-tcp5000のポートフォワード設定がされている。
docker-registry起動
$ docker -run -p 5000:5000 repos.example.com:5000/docker-regisrty-s3 $ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b7f754807953 repos.example.com:5000/docker-regisrty-s3:latest "docker-registry" 16 seconds ago Up 11 seconds 0.0.0.0:5000->5000/tcp clever_bardeen
pull
$ docker pull localhost:5000/sample_rails
rails起動
$ docker run localhost:5000/sample_rails
[2014-12-19 07:06:37] INFO WEBrick 1.3.1 [2014-12-19 07:06:37] INFO ruby 2.1.5 (2014-11-13) [x86_64-linux] [2014-12-19 07:06:37] INFO WEBrick::HTTPServer#start: pid=1 port=3000
社内のクラウドストレージにownCloud(+AWS S3)を使う。
AWS S3の設定
アクセスキー、アクセスシークレットキーの作成
- S3でバケットexample-owncloudを作成。
- IAMでuser owncloud作成。AmazonS3FullAccessの権限を付ける。
- アクセスキー、アクセスシークレットキー作成
- ポリシーを下記に変更
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:*", "Resource": "*" }, { "Action": "s3:*", "Effect": "Allow", "Resource": ["arn:aws:s3:::example-owncloud","arn:aws:s3:::example-owncloud/*"] } ] }
インストール
wget http://download.opensuse.org/repositories/isv:owncloud:community/CentOS_CentOS-7/isv:owncloud:community.repo yum -y install owncloud service httpd restart mysql -uroot -p "create database owncloud"
セットアップ
ブラウザでセットアップ画面を開く
管理者ユーザ名、パスワードを決める。DBをmysqlにしてDBを指定する。 再度、管理者でログインする。
外部ストレージ(AWS S3)を追加
ストレージにAWS S3を使うので機能追加をします。
- 管理画面左上のアプリ>アプリ+>External storage support>有効と進む。
- 右上のadminから>管理>外部ストレージの項目に進む。
- ストレージを追加から「Amazon S3 と互換ストレージ」選択。
- アクセスキー、アクセスシークレットキー、バケット名を入力。
- ストレージを追加から「Amazon S3 と互換ストレージ」選択。
LDAP設定
ownCloudのユーザ認証にldap連携を使う。
RDSのリードレプリカ作成後にテーブルデータをプリロードする
レプリカ作成した後、全テーブルをプライマリキーで検索してテーブルデータをメモリにロードしておきます。
#!/bin/sh stage=$1 stty -echo echo -n "password for ${stage} ?> " read pass stty echo SLAVE="${stage}-db-slave-1.xxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com" USER="user" DB="db_${stage}" OPTS="-u${USER} -p${pass} -h ${stage}-db-master-1 $MYSQLOPTS" MYSQLSHOW="mysqlshow $OPTS" tables=$($MYSQLSHOW $DB | cut -d "|" -f 2 | sed "s/^ //g" | grep -v "^Name" | grep -v "^Database: " | grep -v "^\+---" | sed -e "1d") for table in $tables; do echo "$table" mysql -u${USER} -h ${SLAVE} -p${pass} ${DB} -e "select count(*) from ${table} where id between 1 and (select max(id) from ${table});" done exit 0
fluentd+Elasticsearch+kibanaでnginxのログを視覚化する
nginxのアクセスログをfluentdを経由してElasticsearchに保存します。 ログの視覚化にはkibanaを使います。
こんな感じで500系のエラーの発生具合を時系列のグラフで見れるようにします。
仕組みについてはこちらの記事が参考になります
設定
Elasticsearchはyumでインストールして起動するだけです。 fluentdの設定は以下のようになります。
webサーバのアクセスログをfluentd集約側サーバに流す
nginxのログはltsv形式で出力しています。
<source> type tail time_format %d/%b/%Y:%H:%M:%S %z path /var/log/nginx/staging.web.access.log tag staging.nginx.access pos_file /var/lib/td-agent/staging.nginx.access.pos format ltsv </source> <match staging.nginx.access> type copy <store> type forward <server> host staging-log-1 port 24224 </server> <server> host staging-log-2 port 24224 standby </server> buffer_type file buffer_path /var/lib/td-agent/staging.nginx.access.buffer retry_limit 18 flush_interval 3s flush_at_shutdown true </store> </match>
fluentdに流れてきたログをElasticsearchに流し込む設定
プラグインをインストール
#/usr/lib64/fluent/ruby/bin/gem install fluent-plugin-elasticsearch
/etc/td-agent/td-agent.conf
<match staging.nginx.access> <store> index_name adminpack type_name nginx type elasticsearch include_tag_key true tag_key @log_name host localhost port 9200 logstash_format true retry_limit 18 flush_interval 3s flush_at_shutdown true </store> </match>
mongodb構築
WEBサーバのアクセスログ、アプリのアクティビティログをmongodbで保存することにしました。 アクセスログの追跡をできるようにrockmongoというWEBユーザーインタフェイスも入れておきます。
レプリカセットを作る
PRIMARY,SECONDARY,ARBITERで3つのインスタンスを用意します。ARBITERはどちらがPRIMARYであるべきかを監視してるもののようです。 各インスタンスにmongodbをインストールした後、mongodを立ち上げます。レプリカセット名はrs0とします。
/etc/mongod.conf
replSet = rs0
次にmongoコマンドでシェルに入って、次のように初期化を行います。
config = { _id : "rs0", members : [ { _id : 0, host : "staging-mongo-1" }, { _id : 1, host : "staging-mongo-2" }, { _id : 2, host : "staging-admin-1", arbiterOnly : true } ] } rs.initiate(config);
rs.status()で状態を確認できます。
mongoidの設定
railsからは常にPRIMARYに接続するようにします。mongoidでは下記のように設定してPRIMARYにつなぐようにします。もしフェイルーオーバした場合でも、再起動とか必要なくそのまま新しいPRIMARYに接続してくれます。
config/mondoig.yml
staging: sessions: default: hosts: - staging-mongo-1:27017 - staging-mongo-2:27017 database: example_staging options: max_retries: 30 retry_interval: 1 read: primary
nginxのアクセスログの保存
fluentd経由でmongodbに書きこみます。
NginxでPOSTデータをログに出力する
POSTのデータもログにとっておいたほうが後の調査にも役立つだろうと思い、設定をしておきます。nginxの場合、プロキシしている前提になりますが、location内にもaccess_logを書くことで$request_bodyからPOSTデータが拾えるようです。一応リクエストのボディの最大サイズを少なく設定しておきます。
- log formatにrequest_bodyを追加
log_format ltsv 'time:$time_local\t' 'msec:$msec\t' ・・・ 'request_body:$request_body';
- location内にaccess_logディレクティブを書く
location / { ... access_log /var/log/nginx/staging.example.com.access_log ltsv; proxy_pass http://staging_puma; ... }
- リクエストボディのサイズを制限(デフォルトだと1M)
client_max_body_size 1000;