Amazon LightsailでもAWS Lambdaで運用して(◜◡◝)できた話

# はじめに

ある案件で、社内のユーザーが使用する数台のサーバを提供しているものがありまして
その案件のテストサーバで先ごろ特殊な要件が出てきました。（以降、G案件と記載します）

`Global IPアドレス変更に関するプログラム側への影響をテストをしたい`

具体的には、期間限定で定期的にGlobal IPを変更することが要件になります。
G案件はAmazon Lightsail（2016年末から日本リージョンに提供されたVPSサービス）で構築されています。

EC2であればLambdaで色々手を入れられるので、こうした細工は問題なくできそうに思いましたが、
AWS SDKがLightsailに対応しているか分からなかったので調査するところから始めました。

結果としては、SDKもLightsailに必要十分に対応していたので、必要なことはすべて実装できて
:smiley:な結末となりました。

ここでは、ハマった点も含めハンズオン形式で紹介してみようと思います。

# どうやって実装する？

**Lightsailでは静的なGlobal IPアドレスがアカウントあたり5つしか確保できない仕様**があるので、
エフェメラルで設計するのが通常です。（むしろこの仕様が元で今回の要望が出てきたわけです！）

つまり、サーバを再起動してもGlobal IPアドレスは変わりませんが、サーバを一度停止させると
IPアドレスが解放され次回の起動時に別のGlobal IPアドレスが割当てされることになります。

よって、変更時にサーバが短時間ダウンすることを許容するとして、

-----
1. 影響のない時間に各サーバを停止させる
2. 数分後に各サーバを起動させる  
（必要なサービス群などは自動で起動するよう、あらかじめ仕込んでおく）

-----

というだけの簡単な処理で実装することができます。
本件ではユーザーもサーバログインが必要なので、変更期間中もGlobal IPアドレスを確認できるように、
Lightsailの画面で情報が確認できるだけの制限IAMアカウントも用意しておくことにします。

# 対象とシナリオ

- 対象  
G案件のテストサーバということで下記の名前のサーバ5台をLightsailインスタンスで用意します。

```
test-g01
test-g02
test-g03
test-g04
test-g05
```

- シナリオ  
各サーバは週1回、下記の日時にて停止/起動を行うことで運用することとします。

```
停止曜時　：　日曜日　22:55
起動曜時　：　日曜日　23:00
```

# Lambdaファンクション設定①（インスタンス起動）

AWSのアカウントにサインインし、[サービス] > [Lamda] > [関数] と開きます。

![01a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/9f4f436d-eeef-a663-e5d1-52ddb9c8ac20.png)

[関数の作成] をクリックして今回のファンクションを作成開始します。

![02a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/19971f0b-e233-b2bd-ecb8-6b7912fdaa53.png)

まず、ファンクションが使用するロールを定義します。
[ロール] > [カスタムロールの作成] をクリックします。

![03a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/b64f7717-cba3-0a44-0e16-081676837b9f.png)

自動的にIAMロールを作成する画面に飛ぶので、ロール名を`als-instances-op`と入力したうえで
[ポリシードキュメントを表示] > [編集] とクリックして、下記のjsonを記述します。

```als-instances-op.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "lightsail:*"
      ],
      "Resource" : "*"
    }
  ]
}
```

lightsailのActionはもう少し細かく制御してもいい気もしますが、Lambdaファンクション以外では
使用しないロールなので、セキュリティも特に問題ないでしょう。

入力ができたら [許可] を押してロールを作成します。

元の画面に戻ったら、以下のように設定して [関数の作成] をクリックします。

![04a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/8fddbcda-fd0f-1e5b-8e0d-3f39f8325d78.png)

| Factor | Value |
|:----------:|:----------:|
| **名前** | als-instances-start |
| **ランタイム** | Python 2.7 |
| **ロール** | 既存のロールを選択 |
| **既存のロール** | als-instances-op |

作成された関数の画面に遷移したら、【関数コード】の項目を探して早速コードを書いていきましょう。

**Lambdaでは現在さまざまな言語がサポートされるようになりましたが、**
**実装したいことに応じて特性に合わせたものを選択すると良いと思います。**
**今回は純粋なインフラ運用なので、計算処理などに向いたPythonが良いのではないでしょうか。**

Python自体シンプルに書けるのと、やりたいことも単純なのでとても短いコードでできます:relaxed:

![05a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/23b3b63e-2e55-d42e-e8ec-0ca2330136fc.png)

まず、ファイル名は`als-instances-start.py`として、
ハンドラ名を`als-instances-start.lambda_handler`に、そして以下のようなコードを書きます。

```als-instances-start.py
import boto3

lightsail = boto3.client('lightsail')
instances = ['test-g01', 'test-g02', 'test-g03', 'test-g04', 'test-g05']

### lambda_handler.
def lambda_handler(event, context):
    start_instance(instances)

### definition of 'start_instance' function.
def start_instance(lists):
    for name in lists:
        log = lightsail.start_instance(
            instanceName=name
        )
        print 'started instance: ' + name
        print log
```

続けて、万一台数が増えた場合に実行時間が延びるといけないので「タイムアウト」は少し延ばしておきます。
とりあえず`8秒`くらいならまず大丈夫でしょうか。

そのまま [保存] して作成したファンクションを一旦保存します。

![06a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/addf9c51-5484-408f-9840-67ddaf2a9a1b.png)

保存ができたら、[テスト] をして実際にインスタンスが起動できることを確認しましょう。
**※ 予め全てのインスタンスは落としておいてください。**

![08a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/e693bd03-3577-b9b0-4bf0-d20073d8edf4.png)

テストをするためには、テストイベントが必要なので自動で作成画面が立ち上がります。

![09.PNG](https://qiita-image-store.s3.amazonaws.com/0/147015/4e1674cb-55dd-b84a-9582-448ca7fbb6d9.png)

ここではイベント名を`test01`として（何でもいいです）、下部の [保存] をクリックします。
作成した関数の画面に戻ったら、[テスト] をクリックして停止インスタンスが起動することを確認して下さい。

次にこのファンクションを実行するトリガーを設定します。
今回は定時起動なので、cronのスケジュール形式で設定します。

[CloudWatch Events]をクリックします。

![10a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/4efe1ac1-b5b8-e0c3-c769-2bc68a9b50bf.png)

【トリガーの設定】から以下のような設定をして、[追加] > [保存] と進めて設定を上書きします。

![11a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/b5ea8d84-c09e-d175-1168-92928aa79823.png)

| Factor | Value |
|:----------:|:----------:|
| **ルール** | 新規ルールの作成 |
| **ルール名** | rule-als-instances-start |
| **ルールタイプ** | スケジュール式 |
| **スケジュール式** | cron(00 14 ? * SUN *) |
| **トリガーの有効化** | ✔︎ |

保存された後に以下のようなトリガーが追加されていることを確認します。

![12a.png](https://qiita-image-store.s3.amazonaws.com/0/147015/8c29fbf3-c734-fefb-47e4-ccf9aab0d893.png)

これで、サーバ起動の設定は完了です。

なお、スケジュール式では**時刻をUTCで入力**しますので注意が必要です。
詳しい仕様については下記で紹介されています。
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html

# Lambdaファンクション設定②（インスタンス停止）

基本的には起動と同様の設定なので、詳しい設定は割愛してコードのみを記載します。

関数の名前を`als-instances-stop`として、各種の名前もそれに合わせて変えますが、
IAMロールは新しく作成せず`als-instances-op`を共用します。

```als-instances-stop.py
import boto3

lightsail = boto3.client('lightsail')
instances = ['test-g01', 'test-g02', 'test-g03', 'test-g04', 'test-g05']

### lambda_handler.
def lambda_handler(event, context):
    stop_instance(instances)

### definition of 'stop_instance' function.
def stop_instance(lists):
    for name in lists:
        log = lightsail.stop_instance(
            instanceName=name, 
            force=False
        )
        print 'stopped instance: ' + name
        print log
```

起動とほぼ同様の内容ですが、強制停止をするかどうかで以下の定義を1行追加しています。

```
force=False
```

あと当然ですが、CloudWatch Events のcronスケジュール式の設定値も以下のようになります。

`cron(55 13 ? * SUN *)`

# ユーザーが変更後のGlobal IPアドレスを確認するためのIAM

今回は「lightsail_general」というIAMユーザーを作成して、Policy Generatorで
Lightsailの読み取り権限のみを持つポリシーを生成しました。

本筋からずれるので、ここではユーザー設定の画面イメージと生成されたjsonだけ紹介します。

![13.PNG](https://qiita-image-store.s3.amazonaws.com/0/147015/b1f65cbd-b87f-2e36-7ebd-005f18b0cabb.png)

```policygen-lightsail_general-201712201925.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1513765439000",
            "Effect": "Allow",
            "Action": [
                "lightsail:GetActiveNames",
                "lightsail:GetBlueprints",
                "lightsail:GetBundles",
                "lightsail:GetDomain",
                "lightsail:GetDomains",
                "lightsail:GetInstance",
                "lightsail:GetInstanceAccessDetails",
                "lightsail:GetInstanceMetricData",
                "lightsail:GetInstancePortStates",
                "lightsail:GetInstanceSnapshot",
                "lightsail:GetInstanceSnapshots",
                "lightsail:GetInstanceState",
                "lightsail:GetInstances",
                "lightsail:GetKeyPair",
                "lightsail:GetKeyPairs",
                "lightsail:GetOperation",
                "lightsail:GetOperations",
                "lightsail:GetOperationsForResource",
                "lightsail:GetRegions",
                "lightsail:GetStaticIp",
                "lightsail:GetStaticIps"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```

# ハマったところ

### 1. ドキュメント探しに時間がかかった

Lambda経験が浅かったので、SDKのドキュメントが見つけられず地味に苦労しました。
他の方が同じ轍を踏まないよう、URLを書いておきます。（JSで書くことも検討したので併せて）

- Python
https://boto3.readthedocs.io/en/latest/reference/services/lightsail.html

- JS
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lightsail.html

### 2. LightsailのIAMの記載方法が特殊

IAMの権限が足りず、Lambdaファンクションのテスト実行がなかなか通らず苦労しました。

IAMでは、"Resource"で許可するリソースのカテゴリやリージョンまで絞るのがセオリーだと思います。
しかし、Lightsailコントロールの全権限を与えるユーザー定義として下記のような制御では権限が足りず
Lightsailのダッシュボード画面が403エラーで表示できませんでした。

```lightsail_all_NG.json
{
  "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lightsail:*"
            ],
            "Resource": [
                "arn:aws:lightsail:::*"
            ]
        }
    ]
}
```

原因までは探りませんでしたが、下記のようにすると大丈夫でした。

```lightsail_all_OK.json
{
  "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lightsail:*"
            ],
            "Resource" : "*"
        }
    ]
}
```

おそらくLightsailは、何らか他のAmazonリソースに依存しているのだと思います。

# おわりに

いかがでしたでしょうか。

Lightsail運用については、現時点（2017年末）ではさほど多くの情報を見つけることができませんでしたが、
EC2に近いようなことはだいたい実装できるのではないかという所感でした。

簡便に使用を始められることと、安さや課金体系の分かりやすさはLightsailの大きな魅力だと思います。
ほどほどの運用も敷けそうなので、スモールサービスなどに検討してみてはいかがでしょうか。