syntax

2014年1月29日水曜日

appサーバ(Rails)周りの準備

アプリそのものはプログラマーさんが制作していて、私のほうでは作ったアプリをサーバ上で動かす環境とデプロイ周りを担当します。
以前にもRails環境のサーバをいくつか構築したことがあり、過去を思い出してみると、

  • Lighttpd+ fcgi(ライティーー!)
  • Apache + Mongrel
  • Apache + Passenger
  • nginx + Unicorn

こんな流れでやってました。今回も「何か違うものでやりたい!」と思ってnginx + Rubinius + Pumaあたりに目をつけたのですが、軽くアプリを動かした時点で色々と動かなかったので、すぐに撤退してNginx+Unicornに戻りました。

Rubyの実装は、rbenvでRuby2.1(立ち上げ最初は2.0)をビルドしてそれを使います。
実はappサーバの中にもnginxを建ててnginxとunicorn間はUnixドメインソケット通信するようにします。
上流にnginx、ELBがいるのにapp内にもnginx建てるのも野暮ったいのですが、app内のnginxで何か制御したい場合があるかもということで立ち上げます。

アプリが起動するまでにやった作業の流れは以下の感じです。(事前にrbenvでRuby2.1が動いてるとします)
rbenv、nginx、Unicornのセットアップは参考の記事がたくさんありますのでここでは書きません。
bundlerでのgem管理が基本ですが、bundlerとeyeはgem installでいれておきます。

ソースをgitからチェックアウトできるようにする

  • appサーバのec2-userの公開鍵をgitosisに登録する。

環境変数を設定する

  • RAILS_ENVとか

デプロイする

  • 手元のローカルマシンからbundle exec cap staging deploy

 unicorn起動する

  • 上のデプロイのタスクに含まれている

プロセスを確認する、ブラウザで見てみる

  • psしたりログ見たり、ブラウザでたたいてみたり。。

特に書くことがありませんでした^^;
デプロイ関連の記事で色々書くことになると思います。
環境変数の設定は、この案件だけでやってそうなことなので書いておきます。
環境変数セットにEC2のUserDataを使用しています。マシン起動時に下記の変数がされます
(UserData自体の設定はインスタンスを作るときに下記のパラーメータを与えてやってます)

user data

RAILS_ENV=staging
SERVER_ROLES=staging-app
STAGE=staging
  • RAILS_ENV Rails内で色々参照される環境変数
  • SERVER_ROLES このマシンがどういう役割をするのか(web,app,resque専用とか) 。このマシンではunicornを動かすのかresqueを動かすのかなどこの変数で決めます。
  • STAGE cap STAGE ~~で指定するステージと一致(大体STAGE=RAILS_ENVなのですが、もしかしたらstaging環境を2,3個作ったりするかもしれないので区別できるように変数を用意します)

Written with StackEdit.

nginxでのプロキシ設定

nginxに来たアクセスをインターナルなELBにフォワードしてappサーバ群に振り分けてもらいます。

インターネット
|
nginx
|
ELB
|
appサーバ群

nginxのプロキシ先にELBを指定している場合このような問題がおきますので、参考記事のように設定しておきます。

http {
...
    resolver 127.0.0.1 valid=10s;
...
    server {
        location / {
            proxy_pass http://staging-elb-1.example.internal$request_uri;
        }
    }
}

staging-elb-1.example.internalのIPが変更された場合、ngxinの再起動なしで変更後のほうにプロキシするようになりました。
なおupstream内にホスト名を書いた場合は上記resolverを書いてもnginx起動時にしか名前解決をしないようです。
なのでupstreamを使うのは今のところ見合わせています。(でもupstreamでkeepaliveを使いたいところでもあります)

Written with StackEdit.

アクセスログのフォーマットをltsv形式にする

nginxが出力するアクセスログをfluentdで取り込み、それをElasticSearchやMongoDBに集約する予定なので、取り込みやすいようにログフォーマットをltsv形式にします。
必要そうな項目を一通り羅列しました。

upstream_statusはappsサーバからのステータスコード、upstream_addrはappsサーバのIP、opensocial_viewer_idはユーザのopensocial_viewer_id(取れる場合)も取得しています。

http {
    ....
    log_format ltsv 'time:$time_local\t'
                    'msec:$msec\t'
                    'remote:$remote_addr\t'
                    'forwardedfor:$http_x_forwarded_for\t'
                    'method:$request_method\t'
                    'uri:$request_uri\t'
                    'status:$status\t'
                    'size:$body_bytes_sent\t'
                    'referer:$http_referer\t'
                    'ua:$http_user_agent\t'
                    'reqtime:$request_time\t'
                    'cache:$upstream_http_x_cache\t'
                    'runtime:$upstream_http_x_runtime\t'
                    'host:$host\t'
                    'upstream_addr:$upstream_http_x_server_addr\t'
                    'upstream_status:$upstream_status\t'
                    'session:$session\t'
                    'set_cookie:$sent_http_set_cookie\t'
                    'opensocial_viewer_id:$opensocial_viewer_id\t';
    ...
    server {
    ...
        access_log  /var/log/nginx/staging.example.web.access.log ltsv;
    ...
    }
}

Written with StackEdit.

2014年1月28日火曜日

nginxでソーリーページを返す(メンテ以外の場合)

nginxでソーリーページを返す(メンテ以外の場合)

以下の状況の場合はnginxでエラーハンドリングを行います。

  • リバースプロキシしてappsからの応答が一定以上ない場合のタイムアウト
  • アプリからのステータスコードが40x,50xの場合

nginxが40X,50Xのステータスコードでレスポンスを返すと、プラットフォーム側で用意しているエラー画面が表示される場合があります。
これを見た時プラットフォーム側のエラーなのか、こちらのサーバ側のエラーなのか判断つきません。
40x,50xのエラー時でもプラットフォームには200を伝えてこちらの用意したソーリーページを表示するようにします。

ステータスコードを書き換えたうえで事前に用意した専用のコンテンツを返すには下記のように設定します。
appsからのレスポンス待ちを5000msに設定しています。超えた場合は504となります。

server {
...
  proxy_read_timeout 5000ms;
  proxy_connect_timeout 5000ms;
  proxy_intercept_errors on;
  error_page 400 =200 /system/400.html;
  error_page 403 =200 /system/403.html;
  error_page 404 =200 /system/404.html;
  error_page 500 =200 /system/500.html;
  error_page 502 =200 /system/502.html;
  error_page 503 =200 /system/503.html;
  error_page 504 =200 /system/504.html;
...
}

書き換わったステータスコードがnginxのログにも出力されるので、アクセスログからエラー状況の調査ができなくなります。
対策としてログフォーマットに$upstream_statusを追加して、こちらのステータスコードでエラー状況を判断するようにします。

メンテ中の場合、nginxでメンテ画面を返す。

メンテ中のメンテ画面

Capistranoでのメンテナンス切り替えはmaintenance.htmlファイルを配置するかどうかというものなので、nginx側で下記のように設定してメンテ画面を表示するのが一般的かと思います。

こういう感じ

if (-f $document_root/system/maintenance.html) {
  return 503;
}
error_page 503 @503;
location @503 {
  rewrite ^(.*)$ /system/maintenance.html break;
}

ただ、メンテ中であっても動作確認のためにテスターなど特定のユーザがアクセスした場合は、メンテ画面にしないでアプリに素通ししたい場合もあります。
nginxのifの制御に癖があったりするので(ifの中にifを書いたら怒られた)、結局以下の様な設定になってしまいました。

set $maintenance  "";
#メンテファイルがあった場合
if (-f $document_root/system/maintenance.html) {
  set $maintenance  "maintenance";
}
#gadget.xmlへのアクセスの場合は$maintenanceを上書きする(メンテ画面を返さないようにする)
if ($request_uri ~ ^\/gadget\.xml) {
    set $maintenance  "xml_${maint}";
}
#特定のIP(会社とか)からのアクセスだったら変数maintenanceを上書きする。(メンテ画面を返さないようにする)
if ($http_x_forwarded_for ~ ^1\.1\.1\.1) {
    set $maintenance  "office_${maintenance}";
}
#特定のopensocial_viewer_idだったら変数maintenanceを上書きする。(メンテ画面を返さないようにする)
if ($opensocial_viewer_id = "12345678" ) {
    set $maintenance  "office_${maintenance}";
}
#$maitenanceがmaintenanceのままだったらメンテナンス画面を返す。
if ($maintenance = "maintenance"){
  rewrite ^(.*)$  /system/maintenance.html break;
  break;
}

Written with StackEdit.

2014年1月27日月曜日

nginxで同一ユーザからの過度なアクセスを制限する

連打対策してるのをユーザにわかってもらえる程度の制限

今回運用するWEBサイトではユーザがほぼ同タイミングでPOSTのボタンを数回連打したり、もしくはツールなどを使って一定期間同じリクエストを延々と飛ばしてきたりする可能性が普通にあるらしく、ある程度の規制はいれておきたいと思います。

nginxで使えそうなものがないか色々と調べてみるとここで紹介されているlimit_req_zoneが使えそうです。

下記のように、httpディレクティブで何をキーにしてアクセス数をどの程度で制限するかを定義して、serverのlocation内でどの制限を適用するかを指定します。
制限を超えてアクセスしてきた場合、burst内であればhttp status 499,burstを超えた場合は503として扱い、専用のソーリーページを返すようにします。

http {
    limit_req_zone  $binary_remote_addr  zone=one:10m   rate=1r/s;
   # クライアント側IPをキーにして1秒間に1回までのアクセス上限とする。メモリ10Mを使用してoneという名前でセッション管理する。
    ...

    server {

        ...

        location /search/ {
            limit_req   zone=one  burst=5;
            # /searchには1秒に1リクエストまでの接続が許可される。
            # 1以上のリクエストが来た場合でも5リクエストまでは503エラーとせずに1r/sの割合でリクエストを処理していく。
        }

opensocial_viewer_idをキーにする

クライアント側のIP(binaryremoteaddr)WEBIPIPIPbinary_remote_addrでユーザを特定できません。

今回の案件ではそのまんまopensocial_viewer_idでユーザを区別することにします。このopensocial_viewer_idが必ずリクエストURLの中に含まれているのであればURLの中からopensocial_viewer_id=〜をすっぱ抜いてlimit_req_zone $opensocial_viewer_id zone=one:10m rate=1r/s;という感じで指定できるのですが、アプリの作りのほうで「そんなパラメータを毎回URLに付けません」ということになっているので、また一工夫必要です。

このRailsアプリではsession管理にRedisを使用することになっているのですが、このsessionの中にopensocial_viewer_idが保存されていましたので、nginxからこのopensocial_viewer_idを参照することにします。参照する際にセッションキーが必要ですが、これはcookie内の_session_idから取得可能です。

設定抜粋すると下記のようになります。

#参照するredisサーバ
upstream staging_redis {
  server staging-redis-1.example.interal:6379 fail_timeout=0;
  keepalive 1024;
}

server {
...
location / {
    ...
    #cookie内から_session_id抽出
    if ($http_cookie ~ _session_id=([0-9a-f]+)){ 
        set $session $1;
    }
    # _session_idをキーにしてredisサーバからセッション内容を参照して$redisに設定する
    eval $redis {
      #アプリの仕様でredisのindex=2にセッションを格納している
      redis2_query select 2;  
      redis2_query get example:staging:session:$session;
      redis2_pass staging_redis;
    }
    # セッション内容からopensocial_viewer_id部分を抽出して$opensocial_viewer_idに設定する。
    if ($redis ~ opensocial_viewer_id\x06:\x06EFI\x22\x0C([^\;]+)\x06){
      set $opensocial_viewer_id $1;
    }

    # oneというアクセス制限を適用する
    limit_req zone=one burst=2;
    ...
}
...
}

http {
    ...
    # $opensocial_viewer_idをキーとしたアクセス制限を定義する
    limit_req_zone $opensocial_viewer_id zone=one:10m rate=1r/s;
    ...
}

キーが空の場合は

設定した後で気づいたのですが、はじめてサイトに来た場合はsessionが作られてないのでsessionからopensocial_viewer_idを参照できません(空扱い)
。最初に来た人達に対して「キーが空」としてアクセス制限のテーブルを参照してしまうとすぐにアクセス超過となってしまいます。
空の場合の処理を調べてみたところ、キーが空の場合はアクセス制限処理がスルーされるようなので、とりあえず問題は起きなさそうです。

nginxでリバースプロキシする

リバースプロキシ上でやりたいこと

WEBサーバ(web)とアプリケーションサーバ(apps)をリバースプロキシ構成にします。WEBサーバは今回nginxを採用します。
WEBサイトの構築運用にあたってWEBサーバ側で行いたい要件が以下のようにあり、nginxに設定していきます。
nginxがどれくらい負荷に強いかわかっていませんが、リリース当初は1インスタンスで運用する予定です。頻繁にダウンするようであれば複数台構成に変更します。

  • インターネット側からのHTTPリクエストをappsにプロキシする
    • appsサーバの数が負荷に応じて動的に変わるので、nginxはappsサーバを束ねるELBにプロキシするようにします。
  • appsからの応答が5秒たっても無い場合、nginx側で強制的にソーリーページを返す
    • 5秒ルールというものがあるらしいです。
  • メンテ中はどんなリクエストにも一律同じソーリーページを返す

    • ただし社内の人など特定のユーザのアクセスの場合はアプリケーションサーバに通すようにします。
  • 同一ユーザによる短時間の大量アクセス、同時アクセスを制御する(連打対策)

    • システムの負荷も考慮して、アプリまで無駄なリクエストを通したくないという思いがあるため。
  • 過去のアクセスログを検索できるようにする
    • ユーザからの問い合わせがあったときに行動調査が後日でも行えるようにするためです(nginxだけ実現する要件ではありませんが、他の検索エンジン等を使って検索できるようにするために、nginx側でログの出力を調整します)

インストール

ngx_openresty1.5を使う

上記要件の設定を全部書くと内容が長くなるので記事を幾つかにわけます。この記事ではnginxインストールについて書きます。
$sudo yum install nginxでnginx 1.4が入るのですが、要件的に追加のモジュールをいれないといけないこともあり、モジュールてんこ盛りのngx_openresyを導入します。

バージョンについては、upstream内に記述したホスト名の名前解決がnginx起動時にしか行われない問題(これとか)があるのですが1.4でも問題が発生したので1.5系にします。

上記に書いた要件に同一ユーザによる短時間の大量アクセス、同時アクセスを制御する(連打対策)がありますが、今回のプロジェクトにおいてnginx上でこれを実現するためには、nginxがRedisサーバから指定の値を取得して比較、演算、分岐処理を行う必要があります。

redisに接続するモジュールはngx_openrestyに含まれているのですが、比較、分岐処理をするほうではnginx-eval-moduleのモジュールを使うので、モジュールを追加してからnginxのビルドが必要です。

手順

ngx_openrestyをダウンロードして解凍するとbundle/があるので、このディレクトリの中で$git clone https://github.com/vkholodkov/nginx-eval-module.gitしてから解凍ディレクトリに戻り、configure、make、make installします。ソフトウェア管理としてPaco使ってます。

configure時に他にも–add-moduleでnginx-eval-moduleを追加します。他にもオプション付与してますが下記参考にして下さい。

$./configure --prefix=/opt --with-http_stub_status_module  --with-http_realip_module  --add-module=./bundle/nginx-eval-module
$make
$sudo paco -D make install

一連のconfファイルは/etc/以下に移動しておきます。

$sudo mv /opt/nginx/conf /etc/nginx
$sudo ln -s /etc/nginx/conf /opt/nginx/conf

Written with StackEdit.

システムモニタのグラフ生成にMuninを使用する

Monitでシステムモニタ、プロセス監視も行うのですが、プラスでMuninも導入します。
Muninでは各種システムモニタ、グラフ生成、しきい値超え検知時の通知など行います。

導入するパッケージは各インスタンスに導入するmunin-nodeとmunin-nodeから情報を収集してグラフ生成(RRDtool)、アラート通知を行うmuninの2つで構成されます。
Muninはwatchと名づけたサーバに導入します。Muninでグラフ生成するノードの対象はEC2のNameタグに名前がついているものだけとします。
appサーバ、resqueサーバの中でオートスケールによって増えた分は対象としません。

対象ノードが増えてくるとMunin側でのグラフ生成に時間がかかるようで、色々対策されるかたもいるみたいです。この案件でもノード数が増えてきた場合は何かしらの対策をしたいと思いますが、とりあえずはそのまま使っていきます。

インストール

毎度のyumでいれるだけです。

# 全ec2インスタンス
$sudo yum install munin-node
# watchサーバ(グラフを生成する)
$sudo yum install munin

munin-nodeの設定

ACL

Muninが動いているサーバが各インスタンスのmunin-nodeに接続してくるので、munin-nodeの設定でmuninのIPを接続許可する設定が必要です。
今回はVPC内サブネットのネットワークアドレス全体を許可にしました。

/etc/munin/munin-node.conf

# A list of addresses that are allowed to connect.  This must be a
# regular expression, since Net::Server does not understand CIDR-style
# network notation unless the perl module Net::CIDR is installed.  You
# may repeat the allow line as many times as you'd like

allow ^127\.0\.0.1$
allow ^172\.31\.[0-9]+\.[0-9]+$

# Which address to bind to;
host *
# host 127.0.0.1

# And which port
port 4949

またmunin-node自体はtcp:4949でListenしているので、全セキュリティグループに対して下記のポリシーを追加します。
SecurityGroup=NETからのtcp:4949を受け入れる

プラグインを有効にする

色んなサービスのプラグインがデフォルトで用意されているので、サーバの中にそのサービスが動いていればmuninインストール後からすぐにモニタリングができるようになります。
どのプラグインが有効になっているかはmunin-node-configureで確認できるようです。
参考になった記事

試した所munin_statsというのが有効になっていないようなので有効にしてみました。シンボリックリンクを貼った後、munin-nodeのrestartをします。

$sudo munin-node-configure --shell
  ln -s '/usr/share/munin/plugins/munin_stats' '/etc/munin/plugins/munin_stats'
$sudo ln -s '/usr/share/munin/plugins/munin_stats' '/etc/munin/plugins/munin_stats'
$sudo /etc/init.d/munin-node restart

また、WEBサイトのヘルスチェックをMuninで行いたいと考えていたのですが、どんぴしゃなプラグインがありましたのでこれをそのままwatchサーバのmunin-pluginに導入します。
httpingとmuninでWebサーバのレスポンスをグラフ化した

munin側の設定

監視対象のノードを追加する

/etc/munin/munin.confで収集するノードを下記のように追記していきます。ノード毎にしきい値をデフォルト以外に設定することも可能です。

[example;]
  contacts systemalert

[example;watch-1] #watch-1で名前解決できないとだめ
    address 127.0.0.1
    use_node_name yes
    httping_staging.contacts httping
    load.load.warning 10
    cpu.user.warning  500
    load.load.critical 20
    cpu.user.critical  700

アラート通知設定

また、しきい値超の場合のアラートメール設定は、下記のようなものを任意の別ファイルにして設定しました。
アラートの内容、深刻度によってメールの送信先を分けてるようにします。

  • /etc/munin/munin-conf.d/munin-alert
contact.httping.always_send critical
contact.httping.max_messages 5
contact.httping.command mail -s "${var:worst} ${var:cfields} Munin http healthcheck" systemservicedown@watch.example.com

contact.systemalert.always_send critical
contact.systemalert.max_messages 3
contact.systemalert.command mail -s "${var:worst} ${var:cfields} ${var:group} ${var:host}" systemalert@watch.example.com

計画メンテナンス時などでサービスダウンしている場合に毎回ご丁寧にアラートメールを送ら無くても良いので、アラート通知を気軽にオン、オフにできるようにします。
上記のmunin-alertが/etc/munin/munin-conf.d/にあると通知オン、なければオフとなるので、このファイルを置いたり消したりするデプロイタスクを後ほど用意します。

グラフを見る

グラフはhttp://muinが動いているサーバ/muninで閲覧できます。
Muninを入れたサーバでApacheが動いているものとします。Muninを入れた時点で/etc/httpd/conf.d/munin.confが用意されます。
社内からのアクセスだったらパスワード認証なしでグラフを見れるようにして、社外であればベーシック認証というように変更します。

  • /etc/httpd/conf.d/munin.conf
AuthUserFile /etc/munin/munin-htpasswd
AuthName "Munin"
AuthType Basic
require valid-user

Satisfy Any
Order deny,allow
Deny from all
Allow from 127.0.0.1 172.31. 会社のIP

Written with StackEdit.

Monitでインスタンス内のシステムモニタ、プロセス死活監視する

各EC2インスタンスにMonitを導入して、CPU使用率、メモリ空き容量、Disk容量といった数値の監視とプロセスの死活監視を行います。例えば、Disk容量が90%超えを検知したら指定のメールアドレスに送信することもできます。
モニタをグラフで見る機能はないので、それについてはあとで記事にする予定のMuninを導入します。

インストール

$sudo yum install monitで入ります。
設定は/etc/monit.d/以下に.confを配置します。サーバの役割毎に監視したいプロセスなども変わってくるので、役割毎にファイルを分けて用意します。
base.confは全サーバ共通の監視項目、web.confはWEBサーバとしての監視項目、app.confはAPPサーバとしての監視項目・・・と言った具合です。
インスタンスがAPPサーバとして立ち上がった時は、base.confとapp.confを/etc/monit.d/下に配置されるようにします。

設定内容

confの例を書いておきます。こんなことを設定しています。

  • アラートの通知先と文面
  • cpuロードアベレージ、メモリ使用率、ディスク使用率
  • pidの存在でrsyslogdを監視
  • ps -efして”eye monitoring”がマッチするかどうかでeyeを監視
set alert systemalert@watch.example.com but not on { INSTANCE pid } with mail-format { 
  from: monit@$HOST
  subject: $SERVICE $EVENT $HOST
  message: 
Monit $ACTION $SERVICE at $DATE on $HOST: $DESCRIPTION.
}

set mailserver localhost  port 25

# System
check system system
group system
if loadavg (1min) > 12 then alert
if loadavg (5min) > 8 then alert
if memory usage > 90% then alert
if swap usage > 25% then alert
if cpu usage (user) > 95% then alert
if cpu usage (system) > 75% then alert
if cpu usage (wait) > 50% then alert

check filesystem rootfs with path /
  if space usage > 80% then alert

# syslogd
check process syslogd with pidfile /var/run/syslogd.pid
 start program = "/etc/init.d/rsyslog start"
 stop program = "/etc/init.d/rsyslog stop"
 if 5 restarts within 5 cycles then timeout
check file syslogd_file with path /var/log/messages

#eye
check process eye matching "eye monitoring"
  start program = "/etc/init.d/eye start"
  stop program = "/etc/init.d/eye stop"
  if 5 restarts within 5 cycles then timeout

monitの死活監視はどうしようとなりますね。upstartで制御というのが良いかもしれません。
参考になる記事

Written with StackEdit.

HAProxyでMySQL slaveの通信を分散する

初めにだらだらと概要を書く

MySQLの負荷分散として更新系のmaster、参照系のslaveでインスタンスを複数台用意することにします。slaveについては負荷に応じて台数を増減する予定です。

アプリからみてslaveサーバが何台あるか意識しなくてもいいようにHAProxyを用意します。
HAProxyがアプリとMySQLサーバの通信の中継役を行ってくれるので、アプリからは常にHAProxyだけ見えていればOKとなります。
HAProxy側で複数のslaveサーバに接続を分散してくれます。もしslaveサーバのどれかがダウンしてるときはそのサーバには接続を振らないようにもしてくれます。

また、「負荷に備えてslaveを増設したけど暖気が終わるまで接続を振りたくない」という場合に、設定を一時的に変更することでそのサーバに接続を振らないようにもできます。
接続数の重みづけの調整もできるので、じわじわと接続数を増やしていくということもできそうです。

当初MySQLだけでなくRedisの中継も行おうと考えていたのですが、RedisはElastiCache側でうまく分散してくれるようなので、MySQLのSlaveだけに使用することにします。

インストールと設定

インストールはyumですんなり入ります。
またコマンド経由でhaproxyを制御できるようにhaproxyctlを入れます。
こちらはgemでして、$gem install haproxyctl ; rbenv rehashでインストールします。

設定内容は以下の感じです
/etc/haproxy.cfg

global
  maxconn 4096
  user haproxy
  group haproxy
  daemon
        stats socket /tmp/haproxy level admin
        quiet
        nbproc 1
        log 127.0.0.1 local0 info

defaults
  log global
  mode  tcp
  retries 3
  option redispatch
        option tcplog
        option dontlognull
  maxconn 4096
        timeout connect 10s
        timeout client  10s
        timeout server  10s
        grace 1000

listen mysql-slave
  bind    :3307
  mode    tcp
  balance roundrobin
  option  mysql-check user haproxy
  server  slave1 staging-slave-1.example.internal:3306 weight 50 check port 3306 inter 5s rise 2 fall 2
  server  slave2 staging-slave-2.example.internal:3306 weight 50 check port 3306 inter 5s rise 2 fall 2
  server  slave3 staging-slave-3.example.internal:3306 weight 50 check port 3306 inter 5s rise 2 fall 2
  server  slave4 staging-slave-4.example.internal:3306 weight 50 check port 3306 inter 5s rise 2 fall 2
  server  master1 staging-master-1.example.internal:3306 check port 3306 inter 5s rise 2 fall 2 backup

MySQLのヘルスチェック

option mysql-check user haproxyという設定で、HAProxyがhaproxyユーザを使ってMySQLサーバに接続を試みてヘルスチェックを行います。
MySQLサーバ側で、haproxyユーザを作っておく必要があります。

grant usage on *.* to 'haproxy'@'%';
flush privileges;

運用中にやりそうな事

例えば、staging-slave-1を何らかの理由で使用したくないという場合は、sudo haproxyctl disable server mysql-slave/slave1で使用されなくなります。
復活させたい場合はsudo haproxyctl enable server mysql-slave/slave1です。

slaveを立ち上げたばかりなので少ない接続数から慣らしていきたいという場合は、他のslaveの重み(weight)より低い数値に変更します。
コマンドとしてはこんな感じです。
sudo haproxyctl set weight mysql-slave/slave1 1
weightの数値は0~256のようで、数が大きいほど接続数も多くなります。

なお、stats socket /tmp/haproxy level adminとしておかないとsetコマンドはpermission deniedで拒否されます。

Written with StackEdit.

各インスタンスのリゾルバを設定する

VPC内インスタンスの名前解決は下記の順に行います。

  1. /etc/hosts見る
  2. /etc/resolv.confに書かれたDNSサーバに問い合わせる

/etc/resolv.confは下記のように設定します。

options single-request-reopen
search example.internal
nameserver 127.0.0.1
nameserver 自前のDNSIP
nameserver AWSの用意したDNSIP

ローカルにdnsmasqをいれてDNSキャッシュする

127.0.0.1が何かというと、dnsmasqというDNSサーバで、各インスタンスのローカルで起動させておきます。
毎回DNSサーバに問い合わせしないようにDNSキャッシュ機能をするためです。dnsmasqにキャッシュがない場合は/etc/resolv.confに書かかれたIPにフォワードしてくれます。

dnsmasqの導入は$sudo yum install dnsmasqですんなり入ります。
ネガティブキャッシュ(名前解決できなかったということをキャッシュする)はしないようにします。

  • /etc/dnsmasq.conf
# If you want to disable negative caching, uncomment this.
no-negcache

キャッシュを全クリアしたい場合は$sudo /etc/init.d/dnsmasq force-reloadします。

インスタンス起動時に/etc/resolv.confが初期化されてしまう

最初/etc/resolv.confに上記の内容を直接ベタ書きしていたのですが、インスタンス再起動時に/etc/resolv.confが初期化?されてしまい
AWSの用意したDNSサーバのIPのみに書き換わっていたりしました。
DHCPクライアントなので/etc/resolv.confも設定されるのでしょう。

調べてたところ/etc/dhcp/dhclient-enter-hooksのファイルを用意していると、DHCP更新時の/etc/resolv.confの書き換え処理を再定義できるようです。

make_resolv_conf() {
  rscf=`mktemp /tmp/XXXXXX`;
  echo 'options single-request-reopen' > $rscf
  echo 'search example.internal' > $rscf
  echo 'nameserver 127.0.0.1' >> $rscf
  echo 'nameserver 172.31.4.75' >> $rscf
  echo 'nameserver 172.31.0.2' >> $rscf
  change_resolv_conf $rscf
  rm -f $rscf
}

今のところこれで正常に動作しているようです。

Written with StackEdit.

内部用の名前解決DNSを用意する

物理サーバでインフラ環境を作っていた時は、サーバに固定IPをつけたりネットワークアドレスも「10.1.1.0/24がAPPSセグメントで10.1.2.0/24がDBセグメントで・・・」と設計することがほとんどでした。
EC2を試した時はデフォルトでDHCPな環境だったので、AWSで構築するときはネットワークアドレス、IPアドレスをなるべく意識しなくても良いようにしたいと考えています。

$ssh staging-app-1のようにhost名でどのサーバかある程度識別できるようにはしたいので、自前で名前解決を用意することにします。
自分にとってはBINDでの構築が手っ取り早いので、即決でBINDを採用します。

このDNSサーバはVPC内のクライアントからDNS問い合わせを受け付けて、この案件用の内部ドメイン例えばexample.internalの名前解決を行います。
yahoo.co.jpのような他のドメインの問い合わせの場合はAWSで用意しているDNSにフォワードします。

/etc/named.conf一部

options {
  ....
  forwarders{
    172.31.0.2; 
  };
  ...
};

zone "example.internal" {
        type master;
        file "example.internal.db";
        allow-query { any; } ;
        //allow-transfer{ x.x.x.x; };
        //notify yes;
        //also-notify { x.x.x.x; };
};

いつものDNS運用ならゾーンファイルのレコード情報をたまに更新する程度なのですが、EC2環境の場合だと「インスタンスを新規に起動したからそのインスタンスの名前とIPを調べて、ゾーンファイルに書いて、namedリロードして、、、」なんてことを頻繁にやりそうです。そんな細かい作業を手で毎回やるのもしんどいのでスクリプト化します。

内容としてはVPC内にいるインスタンスのタグNameとプライベートIPを組み合わせたAレコードをゾーンファイルに書き出し、/usr/sbin/named-checkzoneでゾーンファイルに異常がなければnamedをリロードします。

AWS::EC2.new.instances.each {|ins|
  if ins.tags['Name'] && ins.private_ip_address.match(/\d+\.\d+\.\d+\.\d+/)
    p "#{ins.tags['Name']}   IN   A   #{ins.private_ip_address}"
  end
}

Written with StackEdit.

最初のec2インスタンスを作る

AWS management consoleのEC2 Dashboardから早速EC2インスタンスを一つ作り、これをテンプレートマシンと呼ぶことにします。
このテンプレートマシンに一通りのソフトウェア、設定を手でごりごり入れていきます。
アプリケーションサーバが1台ほしいという時になったら、このテンプレートマシンのAMIをベースに「アプリケーションサーバ」という環境変数的なものを与えてマシンを起動するとアプリケーションサーバとして立ち上がってくるというようにします。

色々調べているとChefを使って構成管理をする記事をよく見かけるので、「自分もやりたい!」と思いつつもすぐ「時間足りないかなぁ」となって、使うのは見送っていました。最近になってまた勉強する時間もできそうなのでChefにトライしたいところです。

インスタンス起動後は以下を進めます。手順詳細はここに書かずに、参考になった記事をリンクさせて頂きます。

yum updateする

/etcをgitで管理する

etckeeperを導入します。
手順参考

dateコマンド打った時に日本時間で表示されるようにする

cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

ruby環境準備

rbenv、ruby-build、ruby2.1(当時は2.0)を入れる
手順参考

シェルからAWSのコマンドを使えるようにする

このインスタンスのシェル上からAWSのAPIを叩けるようにします。
AWS-CLIパッケージはインストール済みだったので、与えたれたアクセスキー、シークレットアクセスキー等を設定します。
手順参考

$aws configure
AWS Access Key ID [****************]: ***
AWS Secret Access Key [****************]: *** 
Default region name [ap-northeast-1]: ap-northeast-1
Default output format [text]: text

すると~/.aws/configが生成されます。

[default]
aws_access_key_id = ***
aws_secret_access_key=***
output = text
region = ap-northeast-1

awsからはじまるコマンドが使えるようになります。
as-、ec2-、rds-から始まるコマンドは昔のもので今はawsに一新されたようです。

AWS SDK for rubyはRuby2.1を入れた後gem installで入れます。

このインスタンスのAMIを定期的に作成できるようにする

任意のインスタンスのAMIを作成するスクリプトを用意します。
AWS manegement consoleからAMI削除、AMI作成をするのが手間だったので、スクリプトにしました。
インスタンスの改造作業の前にスクリプトを実行したり、cronで定時バックアップするようにしておきます。

Ruby環境な会社なので、私も見習ってRuby使っていきます!

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require "aws-sdk"

AWS.config(YAML.load(File.read("config.yml")))

machine_names = [
  "template",
 ]

machine_names.each {|name|
  AWS::EC2.new.images.filter("name", name).each{|ami|
    p "deregister ami name: #{name}, ami_id: #{ami.id}"
    if ami.deregister
      p "success"
    else
      p "fail"
    end
  }
  AWS::EC2.new.instances.tagged("Name").tagged_values(name).each{|ins|
    p "create ami name: #{name}, ec2_id: #{ins.id}"
    new_ami = ins.create_image(name, {:description => Time.now.to_s, :no_reboot => true})
    if new_ami && new_ami.tag("template", :value => name) 
      p "success"
      p "ami.id: #{new_ami.id}"
    else
      p "fail"
    end
  }
}

Written with StackEdit.