Serfを使用したNatサーバの冗長化

Serfを使用してNatサーバの冗長化構成を構築したので、メモ。

Serfとは

Serfとは、オーケストレーションを実現できるツールで、Serf間でクラスタを組み、クラスタのメンバーの状態変化時にスクリプトを実行したりできる。
管理サーバを特に必要とせず導入できるため、比較的導入は簡単である。
監視単位としては、ノード単位の監視しかできず、サービスレベルでの監視をしたい場合はConsulで実現できる。

www.serfdom.io

Serfを利用するケースとしては、以下のようなものがある

・WEBサーバ増減に伴うロードバランサーへの登録、解除

・サーバ増減に伴うDNSの更新

Natサーバの冗長構成

今回はAWSにてNatサーバの冗長構成を構築した。

MultiAz環境にてそれぞれのAZにNatサーバを配置し、各WebサーバはそれぞれのAZのNatサーバに通信するように、NatサーバはActive/Active構成とした。

冗長構成の仕組みとしては、2台のNatサーバにインストールしたSerfでクラスタを組み、WebサーバのサブネットのDefaultGWをNatサーバになるようにRouterを設定。
以下構成図の場合、サブネットごとに紐づけるルートテーブルを分けている(WEB1⇒Nat1にルーティング、WEB2⇒Nat2にルーティングするルートテーブル)
障害時には、それぞれのルートテーブルをスクリプトで向きを切り替え、正常のNatサーバ側へ通信するような構成にした。

構成図は以下のとおり。

f:id:hangyoun:20151103005358p:plain

障害時の挙動は、serf.confにて設定する。
以下例では、メンバーのFail,Leave時にスクリプトを実行している。

{
  "tags" : {
    "role" : "nat"
  },
  "event_handlers" : [
    "member-failed=/etc/serf/route-change.sh",
    "member-leave=/etc/serf/route-change.sh"
  ],
  "reconnect_timeout" : "1h",
  "reconnect_interval" : "10s",
  "enable_syslog" : true
}


スクリプトの中で、1台のNatサーバが障害時にもう1台のNatサーバにリクエストがいくように、Routerの向き先を変更する。

Router_A="AAAA"
Router_B="BBBB"
dest_cidr_block="0.0.0.0/0"

instance_id=`curl -s http://169.254.169.254/latest/meta-data/instance-id`
nic_id=`aws ec2 describe-instances --filters "Name=instance-id,Values=$instance_id" | jq '.Reservations[].Instances[].NetworkInterfaces[].NetworkInterfaceId' | sed -e 's/"//g'`


for routeid in $Router_A $Router_B
do
  aws ec2 replace-route --route-table-id $routeid \
                        --destination-cidr-block $dest_cidr_block \
                        --network-interface-id $nic_id
done

起動時のクラスタ組み込み

再起動などの場合に、毎回Natサーバを手動でクラスタに組み込むのは嫌なので、自動で組み込むように設定。

SerfはMulticastでのオートディスカバリによる自動組み込みをサポートしているが、AWS環境ではMulticastを使用できないとのことなので、
今回はinitスクリプトの中でタグを利用し、クラスタの自動組み込みをすることにした。

各NatサーバのTagに、Serf-joinタグを付与し、タグからNatのIPアドレスを検索する。
そして、検索したIPアドレスをSerf joinでメンバー登録する。
以下例でタグからサーバのIPアドレスを取得している。

aws ec2 describe-instances --filters "Name=tag-key,Values=serf-join" "Name=tag-value,Values=true" | jq '.Reservations[].Instances[].PrivateIpAddress' | sed -e 's/"//g'`

AWS環境でのRedis構築

今回RedisをActive/Stanbyで構築したため、備忘録としてブログに残したいと思います。

今回検証した環境は以下。

・Redis 3.0.2

参考資料: Redis Sentinel Documentation – Redis

 システム構成としては、Active/Stanbyそれぞれ1台構成。

Redisの設定

RedisをActive/Stanby構成をするには、Slave側に一行以下設定をする。

slaveof masterのIp port

Redis Sentinelの動作

 Redis sentinelは、Redisサーバの監視機能、Failoverの機能を提供する管理サーバみたいなもの。実際に、公式ドキュメントでもRedisでの冗長構成の際は、このSentinelを使うのを推奨している。

複数のSentinelがRedisのマスターを監視しており、設定された閾値(Quorum)以上のSentinelがRedisマスターDownと判断した際に、RedisマスターはStatusがOdown(停止)となる。

フェイルオーバーの動作としては、以下のようになる。

・マスターにて障害が発生

・SentinelがマスターのDownを検知(Sdown) 

複数Sentinel間でイベントを通知し、閾値以上のSentinelがSdownと判断した場合、RedisマスターのStatusをOdownに遷移する。

・過半数以上(Sentinelが5台の場合、3台以上)のSentinelがDownと判断した場合はFailoverが実行される。

 

Sentinelの設定ファイルの以下設定がマスターDownの検知の設定となる。(以下例では、2台以上Sentinelがダウンの同意をすれば、マスターはOdownとなる)

sentinel monitor mymaster redis1 6379 2

検証するまでは、上の設定で2を設定すると2台以上が同意すれば、フェイルオーバーまでするものと思い込んでたが、実際はダウン判定とフェイルオーバーの設定は上記動作の流れで記載した通り、別ものだった。

Redisのフェイルオーバーの設計

 RedisMasterに障害が発生した際、上記のSentinelによりFailoverの条件を満たしていれば、Redis自体のFailoverが実行され、SlaveがMasterに遷移する。

しかし、Redisを使用するアプリケーションから見たときには、接続先は障害が発生している旧Masterとなるため、アプリケーション側でRedisの接続先を変更する必要がある。

アプリケーション側のRedis接続先を変える方法はいくつかあるが、今回はVipとAwsのVirtualRouterの変更により、接続先を変更する。

 

 ※ネットワーク構成

f:id:hangyoun:20151010181646p:plain

RedisにそれぞれVipである192.168.10.10のIPアドレスを持たせて、アプリケーション側はRedisの接続先IPをVipである192.168.10.10にする。

Virtual Routerにて、192.168.10.10への通信はRedis MasterのInstanceへルーティングする設定をする。(※Natサーバと同じで、Source/Destのcheckを無効にしておく必要がある)

 

※Routerの定義(Sample)

送信先ターゲット
192.168.10.10(※Redis のVip) RedisMasterのInstanceId

 

そしてMasterに障害が発生した際は、Sentinelの機能により、Failover後にスクリプトを実行できるため、スクリプトの中でVirtual Routerの192.168.10.10の通信を新MasterにするようにAPIを使用して変更する。(aws ec2 replace-route)

また、スクリプトは全Sentinelが動作しているサーバで実行されるため、何度実行されても同じ結果になるようにする必要がある。

そうすることで、アプリケーション側のRedis接続先の切り替えを可能としている。

 

AWSのネットワーク設計ついて

現在仕事でWEBサービスAWSで構築しているため、備忘録程度にそのことについて書こうとおもいます。

今回はAWSVPCのサブネットとセキュリティグループについて、実際に業務で触ったので、その際のネットワークの設計方針に関して書いていきたいと思います。

VPCのサブネット設計方針

 VPCのサブネットを分ける考え方として、以下方針でサブネットを設計しました。

  • Availability Zone
  • 外部(インターネット)へ出る際のルーティング

※私が構築している環境では、Private固定IPアドレスを設定しないため考慮しませんでしたが、固定IPアドレスを使用する場合は、DHCPを使用するサブネットと固定IPのサブネットを分ける必要があると思われます。

以下に大枠のネットワーク構成図を記載します。

ネットワーク構成図

f:id:hangyoun:20150712222857p:plain

 

MultiAZ環境

 AZに関しては、AZレベルの障害を考慮しMultiAZ環境としています。MultiAZにすることにより、AZ間のレイテンシが発生します。(※上記図はざっくり書きましたが、SubnetはAZを跨げないので、Subnetの数は各AZに3つずつ、計6個必要となります)

ルーティング設計

サブネットの数に関しては、ルーティングの設計をどうするかにより変わってきます。私の環境では、以下の3つの区分でサブネットを分けています。

・Front層:外部へのInbound/outbound通信可能なサブネット(GatewayがInternetGateway)

・App層:外部へはProxyを経由してoutbound通信可能(GatewayProxyサーバ

・DB層:外部とは通信不可(外部へ出るGatewayが設定されてない)

基本的には、Publicサブネット(インターネットへ通信可能)とPrivateサブネット(インターネットへ通信不可)の2つで、環境によっては、NatやProxyサーバを使うところでは、3つになります。

Natサーバなどを使うかどうかは、システム要件とあとは好みによってもわかれると思います。NatやProxyを使う要件としては、例えば外部の会社と連携しており、向こうでこちらのGlobalIPアドレスをファイアウォールで通信可能にしているなど、GlobalIPアドレスを固定しないといけないケースなどがあると思います。

そういったシステム要件がなく、単純にYumなどでインターネットへ通信したいだけならば、PublicIPアドレスを各EC2に付与しても通信できるため、Natを使うかどうかは好みの問題なると思います。

 

 セキュリティの設計方針について

 セキュリティの考え方としては、以下方針で設計しました。

  • セキュリティは基本的にセキュリティグループを使用
  • 各セキュリティグループはInboundで制御し、Outboundは使用しない(似たような設定が重複し、設定が複雑になり修正漏れが発生するので)
  • セキュリティグループの種類としては、全インスタンス共通のDefaultグループと各サービスごとのセキュリティグループで構成
  • 外部からAWSのサーバへのSSH接続は、Bastion(踏み台サーバ)を経由

 セキュリティグループの使用

AWSにはセキュリティグループのほかにもNetworkACL(サブネット単位でファイアウォールの制御)もありますが、基本的にインスタンスレベルで制御できるセキュリティグループを使用し、NetworkACLは使用しません。

また、セキュリティグループのSourceの制御は、基本的にSecurityGroupのIDを使用します。IPアドレスのレンジでも許可できますが、セキュリティグループの方が許可範囲をセキュリティグループを付与したインスタンスに絞れるためです。

 

SSH接続

AWS環境へのSSH接続は、Bastion(踏み台サーバ)を経由して行います。

そうすることで、セキュアな構成になります。また、AWS環境へSSH接続の必要のないときには、Bastionサーバを停止することにより、外部からはSSH接続できず、セキュリティの向上にもつながります。(※一応AZ障害を考慮し、各AZに踏み台サーバ設定していますが、片方のAZの踏み台サーバは障害が発生しないと使用しないため、常時停止しています。)踏み台サーバのセキュリティグループは、社内からのGlobalIPアドレスのみを許可し、その他へのサーバへはBastionからのみ接続許可とする方針としています。