Quick build an OpsWorks Stack with HA-NAT Layer

参考
=======================
[[VPC]NATインスタンスを冗長構成にしてみた](http://d.hatena.ne.jp/j3tm0t0/20120814/1344971491)

目的
=======================
* Immutable Infrastructure
* High-Availability な NAT インスタンスレイヤーを持つ OpsWorks Stack を、スクリプト一発で構築する

動機
----------------------

OpsWorks は Immutable Infrastructure を実現するにはとても良いソリューションで、レイヤー毎にレシピを設定しておけば、あるサーバーが死んだ時にも数クリックで代替インスタンスを用意できるという魅力的な特徴を持っている。
(とはいえインスタンスセットアップにとても時間が掛かるので、時間としては数分～十数分はみておかなければいけないが)

しかしながら、VPC でアプリケーション環境作りを行うと、必然的に下記のようなネットワークを構築することになる。

![what_is_nat.png](https://qiita-image-store.s3.amazonaws.com/0/41678/05ccb63d-9a9d-f21a-7cad-6b0732e29e39.png)
(出典: http://docs.aws.amazon.com/ja_jp/AmazonVPC/latest/UserGuide/VPC_NAT_Instance.html)

[上記参考サイト](http://d.hatena.ne.jp/j3tm0t0/20120814/1344971491) によれば、

> Wizardによる標準構成のVPCにおいてNATインスタンスはSPOFであり、インスタンス障害や、単一AZの障害、AZ間接続障害によっても両AZのインターネット接続性が損なわれる可能性がある。

これには全く同意する。
そこで同サイト紹介の下記の **方針**

> そこで、NATインスタンスをAZ毎に用意し、障害発生時にfailoverする仕組みを考えてみた。
> 
> ### 方針
> 
> * なるべくAWSの１リージョン内で完結し、別リージョンや外部に監視用ホストなどをおかない
> * 瞬間的にフェイルオーバーはおこなわず、毎分ごとにインターネット上のターゲットIPへの疎通を確認し、障害と思われる状態になったらフェイルオーバーを行う
> * 疎通が回復したと思われた場合には、元の状態に戻す

を踏襲したネットワークを組んでみたが、いかんせんルートテーブルの設定やらスクリプトのインストール(というよりパラメータ設定)やらがめんどくさい。たすき掛けの heartbeat という特徴も相まって、初期の構築はもとより、いざ NAT インスタンスが死んだ際、沸騰した頭で果たして正常に代替インスタンスを用意できるだろうかという不安が生じる。

そこで、自動 failover 機能を備えた NAT インスタンスレイヤーを持つ OpsWorks Stack を、スクリプト一発で作成できるスクリプトを書いてみた。

ソースリポジトリ
-----------------------
https://github.com/yuki-takei/dupsworks

ネットワーク概観
--------------

#### 定常状態

<a target="_blank" href="https://raw.githubusercontent.com/yuki-takei/dupsworks/master/docs/images/02_working_correctly.png">
  <img src="https://raw.githubusercontent.com/yuki-takei/dupsworks/master/docs/images/02_working_correctly.png"/>
</a>

that has **4 subnets**

* public subnet 1
* private subnet 1 (connectable to the Internet due to nat1)
* public subnet 2
* private subnet 2 (connectable to the Internet due to nat2)

#### Heartbeat 機構

<a target="_blank" href="https://raw.githubusercontent.com/yuki-takei/dupsworks/master/docs/images/03_heartbeat.png">
  <img src="https://raw.githubusercontent.com/yuki-takei/dupsworks/master/docs/images/03_heartbeat.png"/>
</a>

nat1 と nat2 が相互に死活監視を行う。

#### Failover (nat2 死亡時)

<a target="_blank" href="https://raw.githubusercontent.com/yuki-takei/dupsworks/master/docs/images/05_nat2_failure.png">
  <img src="https://raw.githubusercontent.com/yuki-takei/dupsworks/master/docs/images/05_nat2_failure.png"/>
</a>

nat1 は nat2 の死亡を検知し、本来は nat2 が NAT すべき「private segment AZ 2」のデフォルトゲートウェイを、自身(nat1)に設定する。

#### Auto Recovery (nat2 復旧時)

nat1 は nat2 の復旧を検知し、「private segment AZ 2」のデフォルトゲートウェイを元の nat2 に設定する。

DupsWorks は何を行うか？
-----------------------

OpsWorks 管理下に上記環境を自動構築する。
具体的には、以下のプロセスを順に行う。

1. create VPC
1. create 4 subnets
1. create an OpsWorks stack
1. create OpsWorks layers
  1. admin layer (for gateway instances)
  1. nat layer
1. set permissions (optional)
1. create OpsWorks instances
  1. 1 admin instance
  1. 2 NAT instances
1. start NAT instances
1. configure route
  1. public subnets -> internet gateway
  1. private subnets -> nat instances
  1. checking heartbeat route
1. configure NAT instances
  1. disable Source/dest. check.
  1. set '1' to net.ipv4.ip_forward using sysctl
  1. configure iptables and enable IP Masquerading
1. install scripts(check heartbeat and failover) to NAT instances.

NAT インスタンスの設定に関しては、[chef-vpcnat](https://github.com/weseek/chef-vpcnat) の機能に依存している。

下準備
=======================

OpsWorks 用 IAM Role の作成
--------------------------

OpsWorks で Stack を生成する際は、Stack が AWS に関する様々な操作を行うのに必要な「IAM Role」と、生成されるEC2インスタンスに関連づけられるデフォルトのロールである「Default IAM Instance Profile」とを指定できる。マネジメントコンソールではこれは Optional になっていて Stack を作る時に自動生成することも可能だが、DupsWorks が使う API(boto) からは arn としての値の指定が必須なので、予め作っておく。


1. OpsWorks Stack を仮作成  
<a target="_blank" href="https://qiita-image-store.s3.amazonaws.com/0/41678/62852d40-a61b-d9cb-449d-1e523962c34a.png">
  <img width="200px" src="https://qiita-image-store.s3.amazonaws.com/0/41678/62852d40-a61b-d9cb-449d-1e523962c34a.png" alt="Add Stack.png"/>
</a>
2. IAM で arn を確認して控えておく
  * 「aws-opsworks-service-role」の「Role ARN」  
  <a target="_blank" href="https://qiita-image-store.s3.amazonaws.com/0/41678/e39922bd-5d49-53c8-4006-46b9330dcb31.png">
    <img width="200px" src="https://qiita-image-store.s3.amazonaws.com/0/41678/e39922bd-5d49-53c8-4006-46b9330dcb31.png" alt="IAM Management Console_service_role.png"/>
  </a>
  * 「aws-opsworks-ec2-role」の「Instance Profile ARN(s)」  
  <a target="_blank" href="https://qiita-image-store.s3.amazonaws.com/0/41678/332dc833-af38-4377-3fb3-f9634582cc88.png">
    <img width="200px" src="https://qiita-image-store.s3.amazonaws.com/0/41678/332dc833-af38-4377-3fb3-f9634582cc88.png" alt="IAM Management Console_instance_profile.png"/>
  </a>


Build
======

Requirements
-------------
* Python 2.7

clone と 依存パッケージのインストール
---------------------------------------------

```shell:Shell
$ git clone https://github.com/yuki-takei/dupsworks.git
$ cd dupsworks
$ pip install -r requirements.txt
```

コンフィグファイルの作成と編集
---------------------------

1. `settings.cfg.example` をコピーして `settings.cfg` を作成
1. 編集する  
基本的に `[PersonalSettings]` セクションだけでよい

```ini:settings.cfg
[PersonalSettings]
vpc_name = MyVPC
vpc_cidr                    = 10.0.0.0/16
vpc_subnet_az1_public_cidr  = 10.0.0.0/24
vpc_subnet_az1_private_cidr = 10.0.128.0/24
vpc_subnet_az2_public_cidr  = 10.0.1.0/24
vpc_subnet_az2_private_cidr = 10.0.129.0/24
region                      = ap-northeast-1
vpc_subnet_az1              = ap-northeast-1c
vpc_subnet_az2              = ap-northeast-1a
stack_name = MyStack
stack_service_role_arn = arn:aws:iam::111111111111:role/aws-opsworks-service-role
stack_default_instance_profile_arn = arn:aws:iam::111111111111:instance-profile/aws-opsworks-ec2-role

#    [[stack_permissions]]
#        [[[hoge]]]
#            iam_user_arn = arn:aws:iam::111111111111:user/hoge
#            allow_ssh = True
#            allow_sudo = True

(snip)
```

### 確認事項

* ``vpc_cidr`` に設定した値と同じ CIDR を持つ VPC が存在していないことを確認
* `stack_service_role_arn` には、  
下準備で控えた「IAM Role」の arn (aws-opsworks-service-role の Role ARN) をセット
* `stack_default_instance_profile_arn` には、  
下準備で控えた「IAM Role」の arn (aws-opsworks-ec2-role の Role ARN) をセット
* サブセクションの `stack_permissions` は、インスタンスを立ち上げた際に ssh や sudo を許可したいユーザーを予め登録できる

いずれも Stack 生成後でもマネジメントコンソールから設定可能だが、最低限 stack_service_role_arn に十分な権限がないと、正常に動作しないので注意(少なくとも DupsWorks のスクリプトは走りきらない)。


スクリプトの実行
---------------

```shell:Shell
$ export AWS_ACCESS_KEY_ID="Your AWS Access Key ID"
$ export AWS_SECRET_ACCESS_KEY="Your AWS Secret Access Key"
$ python build_stack.py
```

```shell:output
VPC has been created : vpc-f1263d93
OpsWorks Stack has been created : 75c8a5fb-dd53-4f04-96ea-cfd0d3ee09d0
OpsWorks Layer 'Admin Server' has been created : d88ac6f3-a8db-4658-9bbc-f606e11b7d3f
OpsWorks Layer 'NAT Server' has been created : 18f6bc7f-0942-4589-9433-e8b1ae86b4c6
1 admin instance has been created : c872f092-dbbe-4434-a9c3-97a500b7b6df
2 nat instances has been created : adb8a5a0-c9f8-48f8-915d-b84299dfee1f, 72d657f6-2fc3-498b-a9ed-bd483c8e2266
checking whethere security groups had been created...
Security Groups '['AWS-OpsWorks-Custom-Server', 'AWS-OpsWorks-Default-Server']' found.
starting instance : adb8a5a0-c9f8-48f8-915d-b84299dfee1f
starting instance : 72d657f6-2fc3-498b-a9ed-bd483c8e2266
retrieving EC2 Instance ID... (this might take several minutes)
retrieved. EC2 Instance ID is : i-a9b954b0
retrieving EC2 Instance ID... (this might take several minutes)
retrieved. EC2 Instance ID is : i-0c24460a
installed custom_json to Stack : {
    "vpcnat": {
        "az": {
            "ap-northeast-1a": {
                "enabled": 1,
                "opposite_primary_nat_id": "i-a9b954b0",
                "opposite_rtb": "rtb-b8e8f2da",
                "target_via_checking_nat": "8.8.8.8",
                "target_via_inetgw": "google.co.jp"
            },
            "ap-northeast-1c": {
                "enabled": 1,
                "opposite_primary_nat_id": "i-0c24460a",
                "opposite_rtb": "rtb-bee8f2dc",
                "target_via_checking_nat": "8.8.4.4",
                "target_via_inetgw": "google.co.jp"
            }
        },
        "ipmasq_src": "10.0.0.0/16"
    }
}
creating route to NAT... (this might take several minutes)
[WARN] Instance with state 'pending' is not valid for this operation. (will retry after 5 sec...)
[WARN] Instance with state 'pending' is not valid for this operation. (will retry after 5 sec...)
[WARN] Instance with state 'pending' is not valid for this operation. (will retry after 5 sec...)
[WARN] Instance with state 'pending' is not valid for this operation. (will retry after 5 sec...)
[WARN] Instance with state 'pending' is not valid for this operation. (will retry after 5 sec...)
[WARN] Instance with state 'pending' is not valid for this operation. (will retry after 5 sec...)
[WARN] Instance with state 'pending' is not valid for this operation. (will retry after 5 sec...)
[WARN] Instance with state 'pending' is not valid for this operation. (will retry after 5 sec...)
[WARN] Instance with state 'pending' is not valid for this operation. (will retry after 5 sec...)
creating route to NAT... (this might take several minutes)
creating route to NAT... (this might take several minutes)
creating route to NAT... (this might take several minutes)
```

1～2分で構築完了。  
[WARN] は特に気にしなくて良い。スクリプトの進捗に対して Security Group の生成が遅れていたり、インスタンスの立ち上がりが遅れていたりしているようなケースでリトライを行った時に出る。

* * * * *

<a target="_blank" href="https://qiita-image-store.s3.amazonaws.com/0/41678/2e32a05c-c04c-0c36-f036-bd81166a8e0f.png">
  <img width="400px" src="https://qiita-image-store.s3.amazonaws.com/0/41678/2e32a05c-c04c-0c36-f036-bd81166a8e0f.png" alt="Instances_booting.png"/>
</a>

<a target="_blank" href="https://qiita-image-store.s3.amazonaws.com/0/41678/362e808c-77c1-b7d0-3d8f-817901d6ae42.png">
  <img width="400px" src="https://qiita-image-store.s3.amazonaws.com/0/41678/362e808c-77c1-b7d0-3d8f-817901d6ae42.png" alt="MyStack.png"/>
</a>

<a target="_blank" href="https://qiita-image-store.s3.amazonaws.com/0/41678/7e9d9bbe-44c2-d26e-1b28-11f99c8f96ef.png">
  <img width="400px" src="https://qiita-image-store.s3.amazonaws.com/0/41678/7e9d9bbe-44c2-d26e-1b28-11f99c8f96ef.png" alt="Layers_NAT_Server.png"/>
</a>

ただこの時点ではまだインスタンスが立ち上がりきっていないのでしばし待つ。t1.micro だと15分は見ておいた方がいい。インスタンスタイプを変えたい場合は、`[OptionalSettings]` セクションをいじる。


Built !
=======

### heartbeat 用ルート確認

```shell:nat1
[yuki@nat1 ~]$ traceroute -m 1 8.8.4.4
traceroute to 8.8.4.4 (8.8.4.4), 1 hops max, 60 byte packets
 1  nat2 (10.0.1.138)  2.576 ms  2.688 ms  2.712 ms
```

```shell:nat2
[yuki@nat2 ~]$ traceroute -m 1 8.8.8.8
traceroute to 8.8.8.8 (8.8.8.8), 1 hops max, 60 byte packets
 1  nat1 (10.0.0.242)  2.493 ms  2.596 ms  2.637 ms
```

### heartbeat 確認

定常状態。
大体30秒に1度チェックを行う。

```shell:nat1
[yuki@nat1 ~]$ sudo tail /var/log/messages
Apr 22 22:09:01 nat1 /opt/heartbeat/check.sh: pinging to google.co.jp via Internet Gateway...
Apr 22 22:09:03 nat1 /opt/heartbeat/check.sh: SUCCESS: could ping google.co.jp via Internet Gateway
Apr 22 22:09:04 nat1 /opt/heartbeat/check.sh: current NAT instance of rtb-bee8f2dc is i-0c24460a
Apr 22 22:09:04 nat1 /opt/heartbeat/check.sh: pinging to 8.8.4.4...
Apr 22 22:09:06 nat1 /opt/heartbeat/check.sh: SUCCESS: could ping 8.8.4.4 via NAT instance in opposite zone.
Apr 22 22:09:06 nat1 /opt/heartbeat/check.sh: the script has done.
```

### failover 実験

nat2 に対し Enable Source/Dest. Check して、
故意に他ホストからのパケットを受け取らないようにすると…

```shell:nat1
[yuki@nat1 ~]$ sudo tail /var/log/messages
Apr 22 22:11:01 nat1 /opt/heartbeat/check.sh: pinging to google.co.jp via Internet Gateway...
Apr 22 22:11:03 nat1 /opt/heartbeat/check.sh: SUCCESS: could ping google.co.jp via Internet Gateway
Apr 22 22:11:04 nat1 /opt/heartbeat/check.sh: current NAT instance of rtb-bee8f2dc is i-0c24460a
Apr 22 22:11:04 nat1 /opt/heartbeat/check.sh: pinging to 8.8.4.4...
Apr 22 22:11:06 nat1 /opt/heartbeat/check.sh: FAILED: could NOT ping 8.8.4.4 via NAT instance in opposite zone.
Apr 22 22:11:06 nat1 /opt/heartbeat/check.sh: switching route to secondary(this) NAT instance.
Apr 22 22:11:06 nat1 /opt/heartbeat/check.sh: aws ec2 --region ap-northeast-1 replace-route --route-table-id rtb-bee8f2dc --destination-cidr-block 0.0.0.0/0 --instance-id i-a9b954b0
Apr 22 22:11:07 nat1 /opt/heartbeat/check.sh: the script has done.
Apr 22 22:11:31 nat1 /opt/heartbeat/check.sh: pinging to google.co.jp via Internet Gateway...
Apr 22 22:11:33 nat1 /opt/heartbeat/check.sh: SUCCESS: could ping google.co.jp via Internet Gateway
Apr 22 22:11:34 nat1 /opt/heartbeat/check.sh: current NAT instance of rtb-bee8f2dc is i-a9b954b0
Apr 22 22:11:34 nat1 /opt/heartbeat/check.sh: pinging to 8.8.4.4...
Apr 22 22:11:36 nat1 /opt/heartbeat/check.sh: FAILED: could NOT ping 8.8.4.4 via NAT instance in opposite zone.
Apr 22 22:11:36 nat1 /opt/heartbeat/check.sh: continue to use secondary(this) NAT instance
Apr 22 22:11:36 nat1 /opt/heartbeat/check.sh: the script has done.
```

対向である「MyVPC private segment AZ2」の Route Table ``rtb-bee8f2dc`` の ``0.0.0.0/0`` に対し、インスタンス ``i-a9b954b0``(nat1) へのルートを設定している。

### revert 実験

nat2 に対し Disable Source/Dest. Check して、
復旧した状態にすると…

```shell:nat1
[yuki@nat1 ~]$ sudo tail /var/log/messages
Apr 22 22:16:31 nat1 /opt/heartbeat/check.sh: pinging to google.co.jp via Internet Gateway...
Apr 22 22:16:33 nat1 /opt/heartbeat/check.sh: SUCCESS: could ping google.co.jp via Internet Gateway
Apr 22 22:16:34 nat1 /opt/heartbeat/check.sh: current NAT instance of rtb-bee8f2dc is i-a9b954b0
Apr 22 22:16:34 nat1 /opt/heartbeat/check.sh: pinging to 8.8.4.4...
Apr 22 22:16:36 nat1 /opt/heartbeat/check.sh: SUCCESS: could ping 8.8.4.4 via NAT instance in opposite zone.
Apr 22 22:16:36 nat1 /opt/heartbeat/check.sh: reverting route to primary(opposite) NAT instance.
Apr 22 22:16:36 nat1 /opt/heartbeat/check.sh: aws ec2 --region ap-northeast-1 replace-route --route-table-id rtb-bee8f2dc --destination-cidr-block 0.0.0.0/0 --instance-id i-0c24460a
Apr 22 22:16:37 nat1 /opt/heartbeat/check.sh: the script has done.
```

``i-a9b954b0``(nat2) へのルートが再設定された。


代替 NAT インスタンス準備手順
===========================

障害が発生したインスタンス/ゾーンを把握する
---------------------------------------
実は [chef-vpcnat](https://github.com/weseek/chef-vpcnat) によってインストールされるheartbeatスクリプト( ``/opt/heartbeat/check.sh`` ) には、[Amazon SNS](http://aws.amazon.com/jp/sns/) を利用して障害通知を行う機能が備わっている。

が、自分で実装した部分ではないので割愛する。

代替インスタンスの作成
---------------------

1. OpsWorks 上で NAT Server レイヤーにインスタンスを作成する
  1. Subnet は、障害が発生したインスタンスと同じものを選択
  1. OS は、Amazon Linux を選択  
    (jq をインストールできれば他の OS, カスタムAMIでも構わない) 
1. start
  (インスタンスIDが付与される)
1. Stack の設定ページから、Custom JSON を編集する
  1. NATインスタンスに障害が発生しているゾーン **ではなく**、もう一方のゾーンの「opposit_primary_nat_id」に、新しいインスタンスのインスタンスIDを入れる
1. 新しいインスタンスに対して、``vpcnat`` レシピを実行する

ルーティングテーブルとインスタンスの紐付け
-------------------------------------

Amazon VPC の Route Tables ページへアクセス。

### heartbeat チェック用のルートの切り替え
1. NATインスタンスに障害が発生しているゾーン **ではなく** 、もう一方のゾーンの public segment を選択
1. 「Routes」タブで、ルート設定
1. 8.8.x.x/32 が宛先のルートを削除
1. 新たなルートを追加、先ほど削除した 8.8.x.x/32 と新しいインスタンスのインスタンスIDを紐付ける

…手順多いな。これも一発でリプレースできたらベターなんだけど。
fork 歓迎。


CloudFormation ではできないの？
==============================

OpsWorks 管理にこだわらなければ可能。

RoutaTable の設定は NAT インスタンスの EC2 の ID か、またはインターフェースの ID を必要とする。しかし OpsWorks 管理化のインスタンスは、作成時はただのメタデータが作られるだけで、EC2インスタンスは作成されない(start させて初めて生成される)。

また、OpsWorks での管理を前提とすると、NAT インスタンスが起動し、setup フェーズのレシピが実行される時には、Stack のCustom Json に vpcnat の設定が既に入っている必要がある。こちらも NAT インスタンスの EC2 の ID が必要。

鶏が先か卵が先か…を CloudFromation で解決する事はできない。


ハマったとこメモ
==============

Debian 派な自分としては当然ディストリは Ubuntu を指定したい。しかし OpsWorks が 14.04 に対応するまでは、JSON Parser である jq パッケージをインストールできないので、heartbeat を行う NAT インスタンスは Amazon Linux を指定せざるを得ない。
(一応カスタムAMIを準備して backports を sources.list に入れれば可能ではあるけど気持ち悪いよね)

そこで以下のような設定にしたのだが…

* Stack のデフォルト OS 指定: Ubuntu 12.04 LTS
* 「NAT Server」レイヤーの packages: traceroute,jq
* NAT インスタンス生成時の OS 指定: Amazon Linux

これだと OpsWorks から例外が返ってくる。

```shell:Shell
boto.opsworks.exceptions.ValidationException: ValidationException: 400 Bad Request
{u'message': u'Packages: jq is not a valid Ubuntu 12.04 LTS package name', u'__type': u'ValidationException'}
```

つまり、インスタンス生成時の OS 指定がなんであれ、 **インストールパッケージが存在するかどうかの Validation 対象 OS はStack のデフォルトに指定した OS になる** 。

これが OpsWorks 自体の仕様なのか、 Boto を利用した場合のみ起こる事象なのかは調べてない。

そんなわけなので、`settings.cfg` の `[OpsWorks]` セクションの中の OS 指定パラメータは、変えるとたぶん動かなくなる。