natインスタンスの冗長化

こんにちは。プラットフォームの小宮です。

他を冗長化してもnatインスタンスを冗長化してないと、
プライベートセグメントでHAしてるサーバ達がAWSのAPIサーバと通信できなくなって詰むなあと思いまして、
先人の皆さまの記事を参考にして以下のとおりにしました。

**・なんとなくどうするか検討
**

AmazonLinuxで作っちゃったので、たぶんHeartbeatとか入れづらいし、そもそもheartbaet使う必要ない気がする。
待機系から監視してaws的にルーティングテーブル挿げ替えるだけでよさそう。

待機系natインスタンスについて、
 作成しておかないと作成と起動とルーティングテーブル作るところもやらないといけないので切替時間が長くなる。
 インスタンス代がかかるけど起動もしたままがいいと思われ。

**・とりあえず既存のAmazonLinuxのNATインスタンスの設定をいじるところから
**
sudoできるようにしてみる

# usermod -G wheel user-op
# id user-op
uid=500(user-op) gid=501(user-op) groups=501(user-op),10(wheel)
# visudo
%wheel  ALL=(ALL)       ALL

コメントはずす。
他のインスタンスから渡ってsudoできるか確認する

あとnatインスタンスにpythonのツール入れておく

メール送信も設定
AmazonLinuxではsendmailが動いてる模様だった。

vi /etc/mail/submit.cf
D{MTAHost}[172.18.10.22]
Djnat01.hoge.com    ※nat02はそう変える
service sendmail stop
chkconfig sendmail off
yum install mailx
echo hoge |mail -s testkomi komiyay@xxxxx

ロケールを合わせる

mv /etc/localtime /etc/localtime.org
ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
date
Thu Nov 14 10:25:59 JST 2013
crontab -e
# Time Sync
0 * * * * /usr/sbin/ntpdate -bs 172.18.10.24
service ntpd stop
chkconfig ntpd off

カーネルチューニングを入れる

# cp -p /etc/sysctl.conf{,.org}
# vi /etc/sysctl.conf
vm.swappiness = 30
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 8192
net.ipv4.tcp_keepalive_intvl = 3
net.ipv4.tcp_keepalive_probes = 2
net.ipv4.tcp_keepalive_time = 10
# sysctl -p

・natインスタンスとそれをデフォルトゲートウェイにしたルーティングテーブルを別途作っておく

NATインスタンスはAMIをコピーして同じセグメントに作る

作った
i-005a5e02
Source/DestCheckをDisableにしないとRouterTableにルート設定する時の出口にできるインスタンスとして出てこない。

rtb-yyyyyyyy
というルーティングテーブルのDestination0.0.0.0/0(つまりデフォゲ)のターゲットを、
先ほどのインスタンスに指定してAddしておく
この時点でこのテーブルにAssosiationしてるサブネットは存在しない状態。

・とりあえずプライベート側のホストでyahooにpingしながらルーティングテーブルをawsのコマンドで挿げ替えてみる

マニュアルを見てみる

# aws ec2 replace-route-table-association help

SYNOPSIS
             replace-route-table-association
           [--dry-run | --no-dry-run]
           --association-id <value>
           --route-table-id <value>
       --association-id (string)
           The ID representing the current association  between  the  original
           route table and the subnet.

       --route-table-id (string)
           The ID of the new route table to associate with the subnet.

# aws ec2 describe-route-tables help
SYNOPSIS
             describe-route-tables
           [--dry-run | --no-dry-run]
           [--route-table-ids <value>]
           [--filters <value>]
--filters Name=string1,Values=string1,string2
   association.subnet-id

コマンドでのフェイルオーバーを確認する

参考:http://d.hatena.ne.jp/j3tm0t0/20120814/1344971491

active_rt=rtb-xxxxxxxx
standby_rt=rtb-yyyyyyyy
subnetid=subnet-zzzzzzzz
aws ec2 describe-route-tables --route-table-ids ${active_rt} --filters Name=association.subnet-id,Values=${subnetid}
            "Associations": [
                {
                    "SubnetId": "subnet-zzzzzzzz",
                    "RouteTableAssociationId": ";rtbassoc-7c535b1e",
                    "RouteTableId": "rtb-xxxxxxxx"
                },
association=`aws ec2 describe-route-tables --route-table-ids ${active_rt} --filters Name=association.subnet-id,Values=${subnetid}|grep -A 1 ${subnetid}|awk '{print $2}'|tail -1|sed -e 's/[",]//g'`

# aws ec2 replace-route-table-association --route-table-id ${standby_rt} --association-id $association
{
    "NewAssociationId": "rtbassoc-52767e30"
}

# aws ec2 describe-route-tables --filters Name=association.sub
net-id,Values=${subnetid}|grep -A 1 ${subnetid}|awk '{print $2}'|tail -1|sed -e
's/[",]//g'
rtbassoc-52767e30

確かにスタンバイルートに切り替わったことをマネジメントコンソールからも確認できた。
コマンド単体での切替試験は成功。yahooへのpingが途切れることはなかった。

・フェイルオーバーの条件を検討する

以下を参考に考えると裏側にいるホストが3台くらいyahooとかと疎通が通らなくなったらフェイルオーバとかでいいのではないか。
http://d.hatena.ne.jp/hirose31/20131105/1383623672

serf使うと楽なんじゃないのかな。調べてみる。
メッセージングツールSerfをEC2で使ってみる | Developers.IO
Serf を使ってみた - jedipunkz’ blog
https://dl.bintray.com/mitchellh/serf/
【Serf】v0.2.0 へのバージョンアップと、変わった所を確認してみた | Pocketstudio.jp log3
serf-muninでmunin-nodeの監視自動追加/削除 | Pocketstudio.jp log3
Serf+HAProxyで作るAutomatic Load Balancer - Glide Note - グライドノート

natの冗長化の参考:
NATインスタンス冗長化の深淵な話 - (ひ)メモ
suz-lab - blog: “High Availability NAT"の作成(CentOS6)
cloudpackブログ: (ELBとからめて)Hostヘッダでの振り分けをHAProxyでやってみる
NATインスタンスを冗長構成にしてみた - log4moto

serf入れてみたり調べてみたけど
結果的に新しすぎて情報が極端に少ないようなのでシェルスクリプトで頑張ることに。
(カスタムユーザイベントの例がいまいち少なくて今回の需要に合わない感じがした)

pacemakerでiptablesが落ちた時にF/Oというおなじみのパターンでもいいんだけど、
待機系からssh越しにpingして3つくらいのノードがダメならF/Oというほうが実情に即している模様。

cronで定期実行するかmonとかにするか。
⇒時間もないし簡単だからcronでいいや。

とりあえずpingだからrootで無くても大丈夫なような

natの公開鍵を裏にいるホスト全部の/home/user-op/.ssh/authorized_keysに登録する

$ ssh user-op@nfs02 ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=49 time=37.8 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=49 time=60.2 ms

こんな感じで確認は可能なのであとはどういうスクリプトを何でどう実行するのかを考えます

・フェイルオーバさせるスクリプトの実行をどうするか考える

完成したスクリプト↓

mkdir /opt/{bin,log}
vi /opt/bin/chk_backseg_ping.sh
------------------------
#!/bin/bash
#
# chk_backseg_ping.sh: 非公開セグメントのホストからpingチェックし失敗後NATのF/O を実施
# 依存関係:awsコマンド, /etc/hosts
# 更新履歴:20131114 - create komiyay
#
## variables
title=hoge
datetime=`/bin/date +%Y/%m/%d.%H:%M:%S`
nochk_time=30
up_time=`uptime|grep min|awk '{print $3}'|sed 's/,//g'`
mailto=xxxxx@xxxx.net
log=/opt/log/aws_error.log
backseg='172.18.20'
hostgroup=`grep $backseg /etc/hosts|egrep -v -a 'vip|sorry|^#'|awk '{print $2}'|perl -pe 's/\n/ /g'`
ssh_chkfile=/home/user-op/.ssh/authorized_keys
dir=`echo $(cd $(dirname $0); pwd)`
lockfile=$dir/nat_fo_complete
user=user-op
target1=8.8.8.8
target2=8.8.4.4
target3=yahoo.co.jp
pin_count=4
pin_int=0.5
pin_wait=3
ng_count=3
active_rt=rtb-xxxxxxxx
standby_rt=rtb-yyyyyyyy
subnetid=subnet-zzzzzzzz

## user defined functions
nat_failover() {
        echo `date +&quot;%Y-%m-%d %T&quot;` &quot;start replace-route-table-association&quot; &gt;&gt; $log
        eval `aws ec2 describe-route-tables|grep -A 2 ${subnetid}|egrep '(RouteTableAssociationId|RouteTableId)'|sed -e 's/[&quot;, ]//g'|awk -F : '{print $1&quot;=&quot;$2}'`
        if [ ${RouteTableId} = ${active_rt} ]
          then
            aws ec2 replace-route-table-association --route-table-id ${standby_rt} \
            --association-id ${RouteTableAssociationId} &gt;&gt; $log 2&gt;&amp;1
            result=`echo $?`
            echo ${result} &gt; ${lockfile}
            failstat=&quot;fail from ${active_rt} to ${standby_rt}, segment:${backseg}, status:${result}&quot;
          else
            aws ec2 replace-route-table-association --route-table-id ${active_rt} \
            --association-id ${RouteTableAssociationId} &gt;&gt; $log 2&gt;&amp;1
            result=`echo $?`
            echo ${result} &gt; ${lockfile}
            failstat=&quot;fail from ${standby_rt} to ${active_rt}, segment:${backseg}, status:${result}&quot;
        fi
}

mail_fail() {
        printf &quot;NAT_failover done. please check site.\n ${failstat}&quot; \
        |mail -s &quot;(${title}) nat_f/o_${datetime}&quot; ${mailto}
}

## main processing
### failover complete check
if [ -f ${lockfile} ];then
  exit
else
  :
fi

## Check invalid few minutes after startup
if [ -n &quot;$up_time&quot; ];then
 if [ $up_time -lt $nochk_time ];then
  exit
 else
  :
 fi
else
 :
fi

### ssh check
for i in $hostgroup
do
  ssh ${user}@${i} ls ${ssh_chkfile}
  result=`echo $?`
  if [ $result -eq 0 ];then
    chkhosts="$chkhosts $i"
  fi
done

### ping check
ng_hosts=0
for i in $chkhosts
do
  target=`printf "$target1\n$target2\n$target3"|sort -R|head -1`
  ssh ${user}@${i} ping -q -c ${pin_count} -i ${pin_int} -w ${pin_wait} ${target} > /dev/null 2>&1
  result=`echo $?`
  if [ ${result} -ne 0 ];then
    ng_hosts=`expr $ng_hosts + 1`
  fi
done

### fail over if the ng_hosts equal to or greater than the ng_count
if [ $ng_hosts -ge $ng_count ]
  then
    if [ -f ${lockfile} ];then
      exit
    else
      :
    fi
    nat_failover
    mail_fail
  else
    :
fi

exit 0
------------------------
chmod +x /opt/bin/chk_backseg_ping.sh

見ればわかるかもしれませんが敢えて解説すると
hostsからバックセグのチェックするホストを全部取得し、
pingの結果が3台以上のホストでNGな場合にnat02側のルートに再アソシエイトします。
一応sshの接続性チェックをしています。
pingのtargetも3つの中からランダム指定にしたので全滅でない限りは、
 targetに不通でGlobalに出れる状態でf/oはしないでしょう。たぶん。

NAT自体がAPIサーバに到達できなかった場合にはF/Oに失敗しますが、
 完了ロックファイルは作成されて異常終了ステータスが入るので再実行はされません。

定期的にstop/startとかする環境の場合はnatを最後に起動するなど起動順に気をつけないとだめそうで、
natが起動してないとHA組んでる裏側のサーバがAPI通信できなくなってしまうので、
uptimeが数分たたないとcheckが始まらないようにする等の分岐を入れたほうがいいかということで一応いれました。

・テスト結果

# time bash -x ./chk_backseg_ping.sh
~略~
+ ssh user-op@nfs01 ping -q -c 4 -i 0.5 -w 3 yahoo.co.jp
++ echo 0
+ result=0
+ '[' 0 -ne 0 ']'
+ for i in '$chkhosts'
++ head -1
++ sort -R
++ printf '8.8.8.8\n8.8.4.4\nyahoo.co.jp'
+ target=8.8.8.8
+ ssh user-op@nfs02 ping -q -c 4 -i 0.5 -w 3 8.8.8.8
++ echo 0
+ result=0
+ '[' 0 -ne 0 ']'
+ '[' 0 -ge 3 ']'
+ :
+ exit 0

real    0m19.274s
user    0m0.124s
sys     0m0.280s

pingの疎通が問題なかったためフェイルオーバーしなかった模様。
次はnat01のiptablesでもstopしてglobalにpingが通らない状態にしてf/oするか確かめてみる。

[root@nat01 ~]# service iptables status
Table: nat
Chain PREROUTING (policy ACCEPT)
num  target     prot opt source               destination

Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
num  target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
num  target     prot opt source               destination
1    MASQUERADE  all  -- 172.18.0.0/16        0.0.0.0/0

[root@nat01 ~]# service iptables stop
iptables: Flushing firewall rules:                         [  OK  ]
iptables: Setting chains to policy ACCEPT: nat             [  OK  ]
iptables: Unloading modules:                               [  OK  ]
[root@nat01 ~]# service iptables status
iptables: Firewall is not running.

適当なバックセグメントのサーバからpingうちながら実施。

# time bash -x ./chk_backseg_ping.sh
+ title=hoge
++ /bin/date +%Y/%m/%d.%H:%M:%S
+ datetime=2013/11/15.15:34:16
+ mailto=xxxxxxx@xxxx.net
+ log=/opt/log/aws_error.log
+ backseg=172.18.20
++ awk '{print $2}'
++ perl -pe 's/\n/ /g'
++ egrep -v -a 'vip|sorry|^#'
++ grep 172.18.20 /etc/hosts
+ hostgroup='lvs01 lvs02 cache01 cache02 db01 db02 web01 mov01 nfs01 nfs02 '
+ ssh_chkfile=/home/user-op/.ssh/authorized_keys
++++ dirname ./chk_backseg_ping.sh
+++ cd .
+++ pwd
++ echo /opt/bin
+ dir=/opt/bin
+ lockfile=/opt/bin/nat_fo_complete
+ user=user-op
+ target1=8.8.8.8
+ target2=8.8.4.4
+ target3=yahoo.co.jp
+ pin_count=4
+ pin_int=0.5
+ pin_wait=3
+ ng_count=3
+ active_rt=rtb-xxxxxxxx
+ standby_rt=rtb-yyyyyyyy
+ subnetid=subnet-zzzzzzzz
+ '[' -f /opt/bin/nat_fo_complete ']'
+ :
+ for i in '$hostgroup'
+ ssh user-op@lvs01 ls /home/user-op/.ssh/authorized_keys
/home/user-op/.ssh/authorized_keys
++ echo 0
+ result=0
+ '[' 0 -eq 0 ']'
+ chkhosts=' lvs01'
~略~
+ for i in '$hostgroup'
+ ssh user-op@nfs02 ls /home/user-op/.ssh/authorized_keys
/home/user-op/.ssh/authorized_keys
++ echo 0
+ result=0
+ '[' 0 -eq 0 ']'
+ chkhosts=' lvs01 lvs02 cache01 cache02 db01 db02 web01 mov01 nfs01 nfs02'
+ ng_hosts=0
+ for i in '$chkhosts'
++ head -1
++ sort -R
++ printf '8.8.8.8\n8.8.4.4\nyahoo.co.jp'
+ target=8.8.4.4
+ ssh user-op@lvs01 ping -q -c 4 -i 0.5 -w 3 8.8.4.4
++ echo 1
+ result=1
+ '[' 1 -ne 0 ']'
++ expr 0 + 1
+ ng_hosts=1
+ for i in '$chkhosts'
++ head -1
++ sort -R
++ printf '8.8.8.8\n8.8.4.4\nyahoo.co.jp'
+ target=yahoo.co.jp
+ ssh user-op@lvs02 ping -q -c 4 -i 0.5 -w 3 yahoo.co.jp
++ echo 1
+ result=1
+ '[' 1 -ne 0 ']'
++ expr 1 + 1
+ ng_hosts=2
~略~
+ for i in '$chkhosts'
++ head -1
++ sort -R
++ printf '8.8.8.8\n8.8.4.4\nyahoo.co.jp'
+ target=8.8.4.4
+ ssh user-op@nfs02 ping -q -c 4 -i 0.5 -w 3 8.8.4.4
++ echo 1
+ result=1
+ '[' 1 -ne 0 ']'
++ expr 9 + 1
+ ng_hosts=10
+ '[' 10 -ge 3 ']'
+ '[' -f /opt/bin/nat_fo_complete ']'
+ :
+ nat_failover
++ date '+%Y-%m-%d %T'
+ echo 2013-11-15 15:34:50 'start replace-route-table-association'
++ egrep '(RouteTableAssociationId|RouteTableId)'
++ sed -e 's/[&quot;, ]//g'
++ grep -A 2 subnet-zzzzzzzz
++ awk -F : '{print $1&quot;=&quot;$2}'
++ aws ec2 describe-route-tables
+ eval RouteTableAssociationId=rtbassoc-6a212908 RouteTableId=rtb-xxxxxxxx
++ RouteTableAssociationId=rtbassoc-6a212908
++ RouteTableId=rtb-xxxxxxxx
+ '[' rtb-xxxxxxxx = rtb-xxxxxxxx ']'
+ aws ec2 replace-route-table-association --route-table-id rtb-yyyyyyyy --association-id rtbassoc-6a212908
++ echo 0
+ result=0
+ echo 0
+ failstat='fail from rtb-xxxxxxxx to rtb-yyyyyyyy, segment:172.18.20, status:0'
+ mail_fail
+ mail -s '(hoge) nat_f/o_2013/11/15.15:34:16' xxxxxxx@xxxx.net
+ printf 'NAT_failover done. please check site.\n fail from rtb-xxxxxxxx to rtb-yyyyyyyy, segment:172.18.20, status:0'
+ exit 0

real    0m35.361s
user    0m1.392s
sys     0m0.456s

フェイルオーバするときの実行時間は35秒なので毎分実行しても大丈夫ではある。(ホストが増えすぎなければ
バックグラウンドで動かすと並列実行できるとか見かけたけどよくわからなかったので気にしないことにします。
(並列実行とかするとたぶんDoS攻撃ちっくになるので良ろしくないんじゃないのかとも思いました。)
負荷とか実行時間が気になる場合は、全部じゃなくてランダムに選んだ複数のホストでチェックするとかでもいい気がします。
変えるならこんな感じでしょうか。

hostgroup=`grep $backseg /etc/hosts|egrep -v -a 'vip|sorry|^#'|awk '{print $2}'|perl -pe 's/\n/ /g'`
↓
check_num=6
hostgroup=`grep $backseg /etc/hosts|egrep -v -a 'vip|sorry|^#'|awk '{print $2}'|sort -R|head -${check_num}|perl -pe 's/\n/ /g'`

何も考えずにcron登録すると複数回実行されることを防ぐためにロックファイルを作るようにしたので、
復旧するときにそれを手動で管理する必要があります。

ロックファイルがある状態でテストしてみると変数定義直後に終了します。

# time bash -x ./chk_backseg_ping.sh
~略~
+ pin_wait=3
+ ng_count=3
+ active_rt=rtb-xxxxxxxx
+ standby_rt=rtb-yyyyyyyy
+ subnetid=subnet-zzzzzzzz
+ '[' -f /opt/bin/nat_fo_complete ']'
+ exit

real    0m0.039s
user    0m0.000s
sys     0m0.012s

参考にしたスクリプトは以下URLに載ってるのです。
NATインスタンスを冗長構成にしてみた - log4moto

個人的に少し気にしているのはyahoo.co.jpにそんなにpingしていいのかどうか。
8.~もgoogleらしいので、まあいいことにします。
こんなの↓もあるようです。あとはjpじゃないyahooにpingしてみるという意見も。
pingチェックサイト(試験運用) - 学術情報ネットワーク(SINET4、サイネット・フォー)

・とりあえず2分おきにcron登録(スタンバイ機にて)

# crontab -e
# crontab -l
## nat check and failover script
*/2 * * * * /opt/bin/chk_backseg_ping.sh

後日修正した↓ので追記します。

# diff chk_backseg_ping.sh old/chk_backseg_ping.sh.20140210
31d30
&lt; current_ids=$dir/current_ids
36,41c35
>       aws ec2 describe-route-tables|grep -A 2 ${subnetid}|egrep '(RouteTableAssociationId|RouteTableId)'|sed -e 's/[", ]//g'|awk -F : '{print $1"="$2}' > $current_ids
>         source $current_ids
>       if [ -z "${RouteTableAssociationId}" ];then
>           printf "Failed to set the value of RouteTableAssociationId and RouteTableId..\n Nat-failover did not start. Maybe aws-api-server unreachable."|tee -a ${log}|mail -s "(${title}) nat_f/o_${datetime}" ${mailto}
>           exit
<       fi
---
>       eval `aws ec2 describe-route-tables|grep -A 2 ${subnetid}|egrep '(RouteTableAssociationId|RouteTableId)'|sed -e 's/[", ]//g'|awk -F : '{print $1"="$2}'`

以上、見ていただいてありがとうございました。