インフラブログ

とあるWEBサイトのインフラを構築運用するメモ

ChatOpsなデプロイ環境にしたい

新しい案件の話しがあり、インフラ構成、デプロイ周りをどうしようかと考えていました。 前までクラウドAWS一択でしたが最近はGCPなどにも興味があり、 「とりあえずDockerが動くクラウドならうちらが作るアプリも動くだろう」 というスタンスでDockerを積極的に使おうとしています。

Docker環境でのデプロイとなると、下記の手順になるでしょうか。

どこかにプッシュしたとか、なんらかのイベント発生をトリガとして他のサービスに通知したり、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           ポスト             ポスト
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

$ rails new sample_rails

$ 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 と互換ストレージ」選択。
      • アクセスキー、アクセスシークレットキー、バケット名を入力。

LDAP設定

ownCloudのユーザ認証にldap連携を使う。

  • 管理画面左上のアプリ>アプリ+>LDAP user and group backend>有効と進む。
  • 右上のadminから>管理>外部ストレージの項目に進む。
    • サーバー
      • 127.0.01 port 389
      • DN cn=admin,dc=example,dc=com
      • パスワード
    • ユーザーフィルター
      • posixAccount
    • ログインフィルター
      • LDAPユーザ名、メールアドレスにチェックする
    • グループフィルタ
      • posixGroup
      • それらのグループからのみ: dev
    • 詳細設定
      • 設定はアクティブですにチェックをする。
      • 接続テストをする。



    
    
  

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系のエラーの発生具合を時系列のグラフで見れるようにします。

f:id:ls-la:20140403150901p:plain

仕組みについてはこちらの記事が参考になります

設定

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;