AWS LambdaでEC2インスタンスのStart/Stopを自動化してみる

#はじめに
本日がAdvent Calendarデビュー戦のhassaku_63です。普段は飯田橋にいます。
25日が最後とは知らず、うっかり同期につられて登録してしまい、今年のトリを飾ることとなってしまいました。
聖なる夜に私はひとり寂しくこの記事の執筆と格闘しております。。。

カレンダーのヘッドライン(?)にはバックアップの自動化、と書いていますがごめんなさい、嘘です。
今日はEC2のStart/Stopを自動化する実装の一例をご紹介します。

SlideShareにスライドをUpしています。こちらも併せてご参照あれ。
<iframe src="//www.slideshare.net/slideshow/embed_code/key/FjqwZGMnBsfukM" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> <div style="margin-bottom:5px"> <strong> <a href="//www.slideshare.net/ssuseredb34d/cloud-automator-56430278" title="Cloud automator的なものを実装してみる" target="_blank">Cloud automator的なものを実装してみる</a></div>

#背景
今回のネタ、そもそもは「サーバーレスアーキテクチャ」「Cloud Automator」「re:Invent」の3つが発端です。

##サーバーレスアーキテクチャ
話題になってますね。サーバーレスアーキテクチャ。
サーバーの運用から解放されるなんて、ステキですよね。

##Cloud Automator
<a href="http://cloudautomator.com/" target="blank">Cloud Automator</a>はバックアップなどのオペレーションを自動化するSaaSツールです。<a href="http://www.serverworks.co.jp/" target=blank">サーバーワークス</a>が提供しています。

Cloud Automatorでできること、その典型的なユースケースとしては↓のようなものがあります。

- EC2インスタンスを業務時間の間だけStartさせる
- EBSやRDSのバックアップを自動で

※ステマしても良いのですが、今回の目的はそこじゃないので商品紹介とかはしません。Cloud Automatorはあくまでネタのキッカケですので、あしからず

##re:Invent
「Python対応」と「スケジュールベースでの実行」が発表されましたね。
このニュースにテンションの上がった人も多いはず。
せっかくならこの2つを盛り込んだ何かを作りたい、と思っていました。

#今回やること
↑で紹介したCloud Automatorのユースケースの1つ、「EC2インスタンスのStart/Stopの自動化」をLambdaで実装してみます。
10:00にインスタンスを起動し、19:00に落とします。これを実装することで、うっかりインスタンスを消し忘れて高額請求、なんてことも避けられますね。

#構成図
こんな感じ。
![構成図.png](https://qiita-image-store.s3.amazonaws.com/0/78417/3d9a4e64-d05a-ef15-16af-af3d7c7908d3.png)
現在のLambdaはスケジュールベースで起動できますので、それを利用します。
構成要素は以下の3つ。

- 一定時刻に起動して「イベント」の発生を伝えるためのLambda Function ... Function[1]とする
- イベント発生をレシーブして「アクション」を起動するためのSNS Topic
- 対象インスタンスのStart/Stop（または実行の通知）といった「アクション」を実行するLambda Function ... Function[2]とする

※右下にあるSNS Topicはなくても動くので、今回は扱いません。

#手順

1. IAM Role の作成
2. SNS Topic と Lambda Function の雛形を作成
3. SNS Topic にメール通知を実装
4. Lambda Function のイベントソース設定
5. Lambda Function を実装、テスト
6. Lambda の Event Source にタイマーを設定

## IAM Roleの作成
今回の構成では2種類のLambda Functionがあるので、それらに適切なIAM Roleを割り当てます。
Function[1]では _sns:Publish_ を、
Function[2]では _sns:Publish_ と _ec2:StartInstances_ , _ec2:StopInstances_ の権限をつけましょう。

今回はLambdaに付与するIAM Roleを作成するので、 **Role TypeにはLambdaを選択** しましょう。間違えないように。
![lambda_role-type.png](https://qiita-image-store.s3.amazonaws.com/0/78417/c6bc4ae7-269a-88a0-1ee7-c0a5ee9ffc47.png)


## SNS Topic と Lambda Function の雛形を作成
外側だけ先に作ってしまいます。
Function[1]を2つ（起動/停止時刻の発火用）、Function[2]（起動/停止の実行用）を1つ作成し、SNS Topicを最低1つ作成します。

Functionのコードは適当なテンプレートでも埋め込んでおきましょう。
IAM Roleの割り当ては先ほど作成したものを適用します。

Topicの **Subscriptionsには、Function[1]のArnを登録** しておいてあげましょう。

## SNS Topic にメール通知を実装
上で作ったTopicに、お好みで自分のメールアドレスも登録してあげると動作確認に使えます。
これは最悪なくても動きます。
Create Subscriptionにてメールアドレスを入力した後、"Confirm Subscription"すると確認用メールが行きます。文面に記載されたリンクを踏むとSubscription登録は完了です。

## Lambda Function のイベントソース設定
Lambda Functionのイベントソースを設定します。
まず時刻ベースで起動するFunction[1]の設定を行います。
<img width="768" alt="lambda-function_event-source.png" src="https://qiita-image-store.s3.amazonaws.com/0/78417/e517a049-4e92-f6a3-0749-66846c11695e.png">
Cronライクな書き方ができます。時刻は今のところUTCのみ。JSTの10:00、平日に起動したかったら上の図のようになります。

Function[2]も同じ要領です。
SNSのTopicを選ぶだけ。設定時に候補となるTopicをリストしてくれます。

## Lambda Functionを実装、テスト
コードの一例を挙げます。

```py

# EC2起動時刻を知らせるためのLambda Functionコードの例
import json
import boto3

sns_client = boto3.client("sns")

def lambda_handler(event, context):
    print("Received event: " + json.dumps(event, indent=2))

    # publish to SNS Topic ... 
    topic_arn = "my-topic-arn"
    message = dict(Action="start", InstanceIds=["instance-id"])
    param = dict(TopicArn=topic_arn, Subject="subject", Message=json.dumps(message))
    
    return sns_client.publish(**param)
```

"どのインスタンス" が "どのアクション(Start/Stop)"を実行すべきか、という情報をTopicに対して渡しています。パラメータの仕様は

```py
    message = dict(Action="start", InstanceIds=["instance-id"])
```

ここの記述が全てです。インスタンスID直打ちのコードなのが非常にナンセンスですが...気にせず行きましょう。
お次は実際にアクション（インスタンスのStart/Stop）を実行するFunctionのコード例です。

```py
import json
import boto3

ec2_client = boto3.client("ec2")

def lambda_handler(event, context):
    # Get parameter
    param = json.loads(event["Records"][0]["Sns"]["Message"])
    
    # Invoke action
    if param.get("Action", "") == "start":
        ret = ec2_client.start_instances(InstanceIds=param.get("InstanceIds", []))
    elif param.get("Action", "") == "stop":
        ret = ec2_client.stop_instances(InstanceIds=param.get("InstanceIds", []))

    return ret
```

パラメータの取得方法が汚いとか言わないで。。
_Message_ に入っているパラメータを見て、適切なインスタンスをStart or Stopさせます。
こんな感じで実装は完了です。

#課題とか反省点とか
いろいろありますが特に気になってることを挙げます。全部改善できれば現場投入しても使い物になるかも。

## インスタンスの指定が直打ち
必要なパラメータは外部から引っ張れるようにすべきですね。せめてタグ指定とかにしておけばよかったか。

汎用性を高める工夫としては、
『「今回作成したLambda Function」を作成するLambda Function』を実装して、そのFunctionをAPI Gatewayでラップして、POSTのパラメータで対象インスタンスを指定させるようにする・・・という手が考えられます。メタプログラミング的な。これはいつかやりたい。

## コード汚い
エラーハンドリングとか何も考慮してないですし。

## SNSのMessageについて
パラメータに渡す段階で一度JSONを文字列に変換してますが、確かJSONオブジェクトのまま渡せる仕様のはず。この辺は勉強不足です。

#まとめ
Lambdaを使ってEC2のStart/Stopを自動化してみました。
改善余地はたくさんありますし、そのうちアップデートをかけられればいいな、と思います。
以上。