AWS NATインスタンス構築とSerfによる冗長化

2015.12.17追記:マネージドNATゲートウェイというサービスがリリースされました。

AWS VPCでプライベートサブネット内に起動したインスタンスは、インターネットと通信することができません。外部リポジトリやAmazon S3等の各種サービスも利用するためには、NATインスタンスを用意して外部との通信を中継する必要があります。

このためにAWSではNATインスタンス用のオフィシャルなAMIが提供されているのですが、わざわざ専用のインスタンスを稼働させるのは勿体ないので、ヴェッテルでは管理用サーバにNATインスタンス機能を持たせて併用しています。またNATインスタンスがSPOFにならないように障害が発生したら、自動的にフェイルオーバーするような冗長化も行っています。

ここでは、オフィシャルなAMIを利用せずに独自でNATインスタンスを構築する方法と Serf による冗長化の方法を紹介したいと思います。

NATインスタンスの構築

まずはAMIを使わない独自NATインスタンスの構築ですが、これは configure-pat.sh がポイントなだけでそれほど難しくありません。当然ですが、NATインスタンスはパブリックサブネット上にあり、Public IPもしくはElastic IPアドレスが割り当てられている必要があります。

1. configure-pat.sh を /usr/local/sbin/ に設置
$ sudo mv configure-pat.sh /usr/local/sbin/
$ sudo chmod 755 /usr/local/sbin/configure-pat.sh
2. /etc/rc.local に最後の2行を追加
$ sudo vi /etc/rc.local

#!/bin/sh
#
# This script will be executed *after* all the other init scripts.
# You can put your own initialization stuff in here if you don't
# want to do the full Sys V style init stuff.

touch /var/lock/subsys/local

# Configure PAT
/usr/local/sbin/configure-pat.sh
3. 再起動もしくは configure-pat.sh を実行して設定を反映
$ sudo /usr/local/sbin/configure-pat.sh
4. Route Tablesの設定を追加

VPCコンソールにて、 [Route Tables]を開きます。[Routes] タブで [Edit] をクリックし、[Destination] ボックスに [0.0.0.0/0] と指定します。次に、[Target] リストから NAT インスタンスのインスタンス ID を選択して、[Save] をクリックします。

5. 「Source/Dest. Check」の無効化

Amazon EC2コンソールからNATインスタンスを選択して、[Actions]-[Networking]-[Change Source/Dest. Check] をクリックします。以下の画面が表示されるので [Yes, Disable] をクリックして無効にします。


6. SecurityGroupの設定

必要に応じて適切なSecurityGroupを設定します。公式ドキュメントを参考にしてください。

これで無事にNATインスタンスを構築することができました。
しかし、NATインスタンスが1台だけだとこの部分がSPOFになってしまうので、もう一台NATインスタンスを用意して監視を行い、何かしらの原因で通信ができなくなった場合に2代目のNATインスタンスに切り替えるように設定します。

Serfによる冗長化

Serfはサービスディスカバリーやオーケストレーション、障害検出のためのツールでVagrantの開発者であるMitchell Hashimoto氏により開発が進められています。Goで書かれており、軽量で設置も簡単です。
ここでは、2台のNATインスタンスをそれぞれ nat1(10.0.0.1), nat2(10.0.0.2) として設定します。

1. インストール (nat1, nat2)
$ wget https://dl.bintray.com/mitchellh/serf/0.6.3_linux_amd64.zip
$ unzip 0.6.3_linux_amd64.zip
$ sudo cp serf /usr/local/bin/
2. TCP/UDPの7946番ポートを開放 (nat1, nat2)
3. 設定ファイルの設置 (nat1)
$ vi /etc/serf.conf

{
  "node_name": "nat1",  ← ノード名
  "tags": {
    "role": "nat",
    "routetable": "rtb-123456789"  ← Route Table IDを指定
  },
  "start_join": [
    "10.0.0.2"  ← 相手のIPアドレスを指定
  ],
  "event_handlers" : [
    "member-failed,member-leave=/home/ec2-user/bin/replace-nat.sh >> /var/log/serf-event.log 2>&1"
  ]
}

EC2ではマルチキャストDNSによるオートディスカバリはできないようなので、少々ダサいですが相手ノードのIPアドレスを指定しています。

4. 設定ファイルの設置 (nat2)
$ vi /etc/serf.conf

{
  "node_name": "nat2",  ← ノード名
  "tags": {
    "role": "nat",
    "routetable": "rtb-123456789"  ← Route Table IDを指定
  },
  "start_join": [
    "10.0.0.1"  ← 相手のIPアドレスを指定
  ],
  "event_handlers" : [
    "member-failed,member-leave=/home/ec2-user/bin/replace-nat.sh >> /var/log/serf-event.log 2>&1"
  ]
}
5. 切り替えスクリプトの設置 (nat1, nat2)
$ vi /home/ec2-user/bin/replace-nat.sh

#!/bin/sh
export AWS_CONFIG_FILE=/home/ec2-user/.aws/config

echo `date` "-- Other NAT heartbeat failed, taking over $SERF_TAG_ROUTETABLE default route to $SERF_SELF_NAME"

# Get this instance's ID
Instance_ID=`/usr/bin/curl --silent http://169.254.169.254/latest/meta-data/instance-id`

if [ -n $SERF_TAG_ROUTETABLE ]; then
  aws ec2 replace-route --route-table-id $SERF_TAG_ROUTETABLE --destination-cidr-block 0.0.0.0/0 --instance-id $Instance_ID
fi
6. Serfの自動起動設定 (nat1, nat2)
$ vi /etc/init/serf.conf

description "Serf agent"

start on started elastic-network-interfaces
stop on stopping elastic-network-interfaces

respawn
exec /usr/local/bin/serf agent \
    -config-file=/etc/serf.conf >> /var/log/serf.log 2>&1

ネットワークが利用可能になってから起動しないといけないので、start on started elastic-network-interfaces を設定しています。EC2のネットワークが有効になるタイミングなんですが、問題なく動いているのでおそらくこれで大丈夫だと思います。

以上で設定は完了です。Serfを起動して実際に動作確認をしてみましょう。

[nat1]$ sudo initctl start serf
[nat2]$ sudo initctl start serf

何も問題がなければ、お互いを認識しログに以下のように表示されるはずです。

[nat2]$ tailf /var/log/serf.log
==> Starting Serf agent...
==> Starting Serf agent RPC...
==> Serf agent running!
         Node name: 'special'
         Bind addr: '0.0.0.0:7946'
          RPC addr: '127.0.0.1:7373'
         Encrypted: false
          Snapshot: false
           Profile: lan
==> Joining cluster...(replay: false)
    Join completed. Synced with 1 initial agents

==> Log data will now stream in as it occurs:

    2014/12/29 08:34:28 [INFO] agent: Serf agent starting
    2014/12/29 08:34:28 [INFO] serf: EventMemberJoin: nat2 10.0.0.2
    2014/12/29 08:34:28 [INFO] agent: joining: [10.0.0.1] replay: false
    2014/12/29 08:34:28 [INFO] serf: EventMemberJoin: nat1 10.0.0.1
    2014/12/29 08:34:28 [INFO] agent: joined: 1 nodes
    2014/12/29 08:34:29 [INFO] agent: Received event: member-join

membersコマンドで確認してみます。

$ serf members
nat2  10.0.0.2:7946  alive  routetable=rtb-123456789,role=nat
nat1  10.0.0.1:7946   alive  role=nat,routetable=rtb-123456789

nat1とnat2が表示され、問題なく認識されていることが分かります。

それではいよいよ nat1 を停止してフェイルーバー機能が想定通りに動作するか試してみましょう。ここでは、serfを停止していますが、実際にサーバを shutdown してしまっても構いません。

[nat1]$ sudo initctl stop serf
2014/12/29 08:24:45 [ERR] memberlist: Push/Pull with nat1 failed: write tcp 10.0.0.1:7946: connection refused
2014/12/29 08:24:47 [INFO] memberlist: Suspect nat1 has failed, no acks received
2014/12/29 08:24:49 [INFO] memberlist: Suspect nat1 has failed, no acks received
2014/12/29 08:24:50 [INFO] memberlist: Suspect nat1 has failed, no acks received
2014/12/29 08:24:52 [INFO] memberlist: Suspect nat1 has failed, no acks received
2014/12/29 08:24:52 [INFO] memberlist: Marking nat1 as failed, suspect timeout reached
2014/12/29 08:24:52 [INFO] serf: EventMemberFailed: nat1 10.0.0.1
2014/12/29 08:24:53 [INFO] memberlist: Suspect nat1 has failed, no acks received
2014/12/29 08:24:53 [INFO] agent: Received event: member-failed
2014/12/29 08:25:00 [INFO] serf: attempting reconnect to nat1 10.0.0.1:7946

member-failedイベントが発火し、Route Tablesが書き換わっているのが分かります。

Route Tablesを書き換えることでプライベートサブネットから外部への通信がnat2経由に切り替わります。

以上、負荷の低いサーバを待機系のNATインスタンスとして設定することで、可用性を保ったままコストも最小限に抑えることができます。これで年末年始も安心して休めますね。



このエントリーのはてなブックマーク (-)