EC2インスタンスをコールドスタンバイにしてみた

Serverlessにも憧れるけれど、いろいろな事情によりEC2を使っているパターンってまだまだ多いと思います。
そしてAWS利用料金の中でEC2インスタンスが占める割合って結構高いですよね。
だいたい1位EC2、2位RDS、3位以下なんかいろいろ・・・というところでしょうか。

EC2インスタンスの台数が結構増えてきて、それに伴い利用料金もうなぎ登りなのでケチケチ大作戦をすることにしました。
もともと検証環境とかビジネスタイムだけ起動していれば良いインスタンスに関しては、自動起動停止するようにしているのですが、本番環境とかでもリソースの使用量をみてインスタンスタイプを下げられないか、とか。
30%OFFぐらいになったらいいなぁ・・・なんて感じでやってます。

そんななか、コイツなんとかしたいなーと思ったのがFTPサーバー。
これもいろいろな理由により、SFTPではなくてFTPなのです。
ただし昼夜問わず、定期的にFTPされるので常時起動は必須。
さらにいうと、design for Failの原則に則って、AZ分けて2台起動しています。

・・・とはいっても1台無駄じゃん(´･_･`)

じゃあ普段は1台停止しとけばいいじゃん！
と思うでしょ？
で、起動している方に何かあったらどーやって停止しているインスタンスを起動させるの？

ということでやってみました。

# 前提条件  
- EC2インスタンスでFTPサーバ2台構成
- ELBは無し
- FTPアクセスはIPアドレスではなくドメイン名でのアクセス
- 1号機、2号機とあって、通常時は1号機がアクティブ、2号機がコールドスタンバイ(=STOP)
- EC2インスタンスはAvailabilityZoneを分けて、Multi-AZ構成
- 1号機になにかがあったら2号機が起動して2号機でFTPサービスを継続する
- 2号機になにかがあったら1号機が（ｒｙ
- FTP通信はLAN内のみ。インターネットからはこない
- FTPクライアント側は人間ではなくてシステム。他システムのバッチサーバとかからデータ連携みたいなイメージ
- 諸事情によりS3へのアップロードはできない

# 使うもの
- EC2
- Route53
    - hostzone
    - health checks
- CloudWatch
    - CloudWatchAlarm
- SNS
- Lambda
- IAM
    - IAM Role

# やること
1. EC2のCloudWatchAlarmsでEC2の起動状態をSNSに通知
2. SNS通知を受けたらLambdaキックしてもう片方のEC2インスタンスを起動
3. Route53でDNSフェイルオーバーさせてドメイン名とIPアドレスの紐付けを変更


![cold.png](https://qiita-image-store.s3.amazonaws.com/0/85659/8d3b7035-ee18-5c1a-dbfc-78d9ee646bca.png)
ね、なんかイケる気がするでしょ＼(^o^)／
# レシピ
## EC2
普通にインスタンスを2台作ってください。

## CloudWatch
CloudWatchコンソールでCloudWatchAlarmsを作ります。
メトリックスはStatusCheckFailedを選択してください。

![スクリーンショット 2016-10-08 20.44.19.png](https://qiita-image-store.s3.amazonaws.com/0/85659/4fde33bb-d944-1460-0f73-cf0ca6eddbd4.png)

ポイントとして、ActionsではNotificationをALARMの場合とINSUFFICIENTの場合の2つ作ること。
何かしらの理由でステータスチェックが失敗した場合はもちろんなのですが、EC2インスタンスがSTOPとなったときはEC2のAlarmStatusは"No Data"となって、CloudWatchAlarm的にはStatusCheckFailedとはならずにINSUFFICIENTとなるからです。

![スクリーンショット 2016-10-08 20.09.23.png](https://qiita-image-store.s3.amazonaws.com/0/85659/18ec8ccf-6e12-d769-ee01-79ac3b6e979e.png)

ちなみにCloudWatchAlarmsのActionsでNotificationではなくEC2Actionではアラームの状態によって以下の動作を選択できます。

![スクリーンショット 2016-10-08 20.19.08.png](https://qiita-image-store.s3.amazonaws.com/0/85659/c66f9766-548d-de94-9d0d-4b30311f0a4f.png)

ココでrebootとかrecoverとか選んでもいいのでは？と思いますよね。
・・・AZ障害も考慮するとそれだとちょっと弱いと思うので・・・というか何よりもLambdaを使いたかったから！ｗ
エンジニアの趣味嗜好を反映させるのって大切だと思うの。

CloudWatchAlarmsを作成したら入力したメールアドレスにSubscriptionの登録メールが届くはずなので承認しておきます。
たまに迷惑メールに入ってしまっていることもあるので、おかしいなと思ったら迷惑メールフォルダも確認しましょう。
承認するとCloudWatchコンソールで作ったAlarmのところに"Pending confirmation"と表示されているのが消えます。
これをEC2インスタンス2台分作成します。

## IAM
IAMコンソールでIAM Roleを作ります。
ManagedPoliciesでAWSLambdaExecuteをアタッチしましょう。
そしてEC2起動のみの権限としたいのでInline Policiesで「ec2:StartInstances」のみのポリシーを作りましょう。
Resourceでは「*（全て）」としましたが、対象リソースを絞っても良いと思います。

```json:InlinePolicies

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmtxxxxxxxxxxxx",
            "Effect": "Allow",
            "Action": [
                "ec2:StartInstances"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```

## Lambda
そしてここからがお楽しみのLambdaです。
select blue printはすっとばしちゃいましょう。
configure triggersで先程作ったSNS topicsを選びます。

![スクリーンショット 2016-10-08 20.59.11.png](https://qiita-image-store.s3.amazonaws.com/0/85659/2a7111fa-bdf5-5f90-b602-687f4b6d7254.png)

Configure Functionではもう片方のEC2インスタンスを起動させるコードを書きます。
ちなみに私はpythonにしました。jsむずい。
![スクリーンショット 2016-10-08 21.18.10.png](https://qiita-image-store.s3.amazonaws.com/0/85659/9bd0eb8a-720a-ef2b-a11d-99c932fe8fa7.png)

コードはこんな感じで。

```python:lambda_handler
def lambda_handler(event, context):
    # TODO implement
    import boto3

    ec2 = boto3.resource('ec2')
   
    #もう片方(＝落ちたときに起動させるほう)のEC2インスタンスID
    id = 'i-xxxxxxxx'
    
    instance = ec2.Instance(id)
    instance.start()
#return 0　←SNS通知をトリガーにする場合非同期実行となるため戻り値は無しで
```

Handlerにはコードに書いた関数名を指定します。
私はガチンコインフラ人でpython初心者のため、よくわかんないけどLambdaでpythonを動かすときはこうするらしいです。

> ※2016/10/9 追記：
> 記事公開後にとあるお方からこんなアドバイスをいただきました。
> 
> ```python:
> import boto3
> 
>     ec2 = boto3.resource('ec2')
> 
>     #もう片方(＝落ちたときに起動させるほう)のEC2インスタンスID
>     id = 'i-xxxxxxxx'
> ```
> 
> 
>> この部分、本当はhandlerの外に書いてもいい
>> Functionが読まれた時に、handler外にあるのが実行されて
>> そのあと、本当にkickされたときに、ハンドラーの関数が呼ばれる
>> ライブラリの初期化とかは、毎回やる必要なくて、最初の一回でいいから、外に出した方がベター
>> 繰り返し沢山呼ばれるやつは特にね
>> 今回のケースだと呼ばれる頻度少ないからどっちでもよい
>
> アプリエンジニアな人にはごく普通のことなのかもしれないけど、目からウロコでしたー
> 教えてくれてありがとうございます！
> なので、それにならうとこんな感じかなっと
> 
> ```python:lambda_handler
>     import boto3
> 
>     ec2 = boto3.resource('ec2')
>    
>     #もう片方(＝落ちたときに起動させるほう)のEC2インスタンスID
>     id = 'i-xxxxxxxx'
>     
> def lambda_handler(event, context):
>     # TODO implement
>     instance = ec2.Instance(id)
>     instance.start()
> #return 0　←SNS通知をトリガーにする場合非同期実行となるため戻り値は無しで
> ```

Roleの項目では先程作ったIAM Roleを選択します。

![スクリーンショット 2016-10-08 21.17.42.png](https://qiita-image-store.s3.amazonaws.com/0/85659/1273bc9b-0f47-63ca-3c4e-02eac791a6f2.png)

メモリも一番小さい１２８MBにしちゃいましょう、タイムアウトはデフォルトの３秒だとうまくいかなかったのでMAXの５分にしちゃったけど、もっと短くできるような気がしてます。
起動対象のEC2インスタンスはVPC内に作ってるけど、EC2インスタンス起動コマンドを実行できれば良いだけなのでNo VPCでOKです。
・・・というか、VPCを選んだらなんだかよくわかんなかったので・・誰か教えてください。。

ココまで来たらCreateしちゃいましょう。

できあがったFunctionでテストもできるのでSample Event TemplateでSNSを選んで「Save and Test」ボタンをクリックして上手くいくかやってみましょう。
うまく行ったら実際にEC2インスタンスを停止してもう片方が起動してくるか確認してみましょう。
OKであればもう１台分のLambdaも作りましょう。

## Route53
### Health Checks
Route53でDNSフェイルオーバーの設定をします。
まずはhealth checksを作ります。
以下のような感じで作ってください。
CloudWarchAlarmsは作ったものを指定しましょう。
ココでのポイントとしてCloudWatchAlarmsを作ったときと同様にHealth check statusのWhen the alarm is in the INSUFFICIENT stateという項目では「the status is unhealthy」を選択してください。
こちらも２台分作ってください。

![スクリーンショット 2016-10-08 21.30.00.png](https://qiita-image-store.s3.amazonaws.com/0/85659/c1e4657b-0c99-ef2a-f41c-71c1d2f89251.png)

### Hosted zones

hosted zonesを作ります。
FTP通信はLAN内のみの通信となりますがDNSフェイルオーバーをするにはpublic通信ができないといけないのでpublic zoneとして作成します。
zoneをpublic zoneにしていても、Aレコードでlocal IPアドレスを入れていたらそのアドレスで返ってくるし、DNSがインターネット側の名前解決ができるようになっていれば、そのクエリ結果でアクセスできちゃうんです。
これが結構便利で、同じドメイン名で社内アクセス専用とインターネットアクセス用のレコードの管理をわざわざpublic zoneとprivate zoneの２つに分けなくても同じhosted zoneで管理できちゃうんです。

そしてレコードセットを２つ作ります。
２つのIPアドレスに同じ名前のAレコードを設定します。
Routing PolicyはFailoverとします。

１号機用のは以下のような感じで、Failover Record TypeはPrimary, health check AssosiateではRoute53 health checksで作成したヘルスチェックを指定します。

![スクリーンショット 2016-10-08 21.46.37.png](https://qiita-image-store.s3.amazonaws.com/0/85659/9aff77fb-c681-d598-b16a-01210acbd8f9.png)


２号機用のは以下のように、今度はFailover Record TypeはSecondaryとします。
レコード名は１号機と同じものにします。

![スクリーンショット 2016-10-08 21.49.05.png](https://qiita-image-store.s3.amazonaws.com/0/85659/cb32dda2-ccf7-4268-430e-b1c2d1b29a35.png)

１号機と２号機のIPアドレス、health checksを入れ違いにしないように注意しましょう。

# フェイルオーバーさせてみる
これで、１号機が起動、２号機が停止状態で１号機が落ちたら２号機が起動してきて、次に２号機が落ちたら１号機が上がってきてnslooｋupの結果もそれぞれ起動してきた方のIPアドレスが返ってくるはずです。

試したところ・・・切替えが完了してDNSフェイルオーバーしたアドレスが返ってくるまで約１０分かかっちゃいました。。。
CloudWatchAlarmsのthresholdやAレコードのTTL、Lambdaのタイムアウト値をもっと短くしたらもっと短縮できるのかなー。
それか、１０分程度の断なら許容範囲としてリランで対応できるようにしてしまうかｗ

FTPサーバだけではなく、バッチサーバとかでも１０分程度の断なら許容できるけど長時間止まってるのはマズいってサーバならアリかもですね。