URLパラメータで指定したEC2の起動・停止

## 最小限の手順でサクッと構成したい

<font color="IndianRed">【2023/2/9更新】※API Gatewayを使わない方法を赤で追記しました。コードの見直しもしています。</font>

**「EC2の起動とか停止とかをURLアクセスでできないかね。URLパラメーターでサーバー名を指定して（Nameタグで指定して）起動する、みたいな。」**
という要望は、EC2の起動・停止を手動で運用している環境で生じていると思います。
実現する投稿は多くあるけれども、セキュリティや拡張性も考えて実装している例が多く（まあそうするのが当然なので）、その結果として難しくなってしまい、やっぱコンソールで手動操作でいいやとあきらめてしまうことも考えられます。
ちょっともったいないですね。

そこで、~~API Gateway + Lambda を使用して~~ <font color="IndianRed">API Gatewayを使わずLambdaのみで</font> **できるだけ少ない手順で** 実装してみます。
入力値や変更する選択肢は全て書き起こしてみます。
・EC2は全て ap-northeast-1（東京リージョン）にある前提です。

※AWSでEC2の”起動”はローンチのことを指しますが、ここでは最初のセリフのように”起動”を一般的な使われ方（AWSでは”開始”に当たる）で使っています。

では進めましょう。

## １．IAMポリシーの作成

IAM　＞　ポリシー　＞　ポリシーを作成 ボタン
ビジュアルエディタは使いません。"ビジュアルエディタ" タブの右にある "JSON" タブを押します。
以下をコピーしてペーストします。

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

- ポリシー名：**ec2startstop** (適当な名前を付けます)

## ２．IAMロールの作成

IAM　＞　IAMロール　＞　ロールを作成 ボタン
- ユースケースの選択：**Lambda**
- アタッチするポリシー：**ec2startstop** (さっき作ったもの)、**AWSLambdaBasicExecutionRole**
- ロール名は **ec2startstop** とします。（ポリシー名と同じ名前でもエラーになりません）

## ３．Lambdaの作成

Lambda　＞　関数　＞　関数を作成 ボタン

- オプション：**一から作成**
- Lambda関数名：**ec2startstop** (他のリソースと同じ名前でもエラーになりません）
- ランタイム：**Python 3.x**
- デフォルトの実行ロールの変更－既存のロールを使用する： **ec2startstop**

関数の作成ボタンを押す

コード（元のコードを消して、以下のコードをコピペして上書きします。）

``` lambda_function.py
import boto3
import re

ec2 = boto3.client('ec2')

def lambda_handler(event, context):

    ret_data =  {
        'isBase64Encoded': False,
        'statusCode': 200,
        'headers': { 'Content-Type': 'text/html' }
    }
    access_ok = False

    if isinstance(event.get('requestContext', {}).get('http', {}).get('sourceIp'), str):
        for ip_prefix in ['1', '2', '3', '4', '5', '6', '7', '8', '9' ]:
            if event['requestContext']['http']['sourceIp'].startswith(ip_prefix):
                access_ok = True
                break

    if not access_ok:
        # アクセス元のIPアドレスがプレフィックスのリストのいずれかと一致しなければ中断
        return dict( { 'body': 'Source IP permission error' }, **ret_data )

    instance_ids = []
    param = event.get('queryStringParameters')

    if param == None or not re.match(r'^(start|stop):[^:]+:[^:]+$', param.get('command', '')):
        # URLパラメーターの最小限のチェックでエラーがあれば中断
        return dict( { 'body': 'URL parameter error' }, **ret_data )

    # URLパラメータ（例：command=start:Name:ServerA01）からコロン区切りで切り出す
    start_stop, tag_key, tag_value = param['command'].split(':')[0:3]

    try:
        ret_describe = ec2.describe_instances(
            Filters=[ { 'Name': 'tag:'+tag_key, 'Values': [tag_value] } ]
        )
    except Exception as e:
        return dict( { 'body': f'API error (Exception: {str(e)})' }, **ret_data )

    # 指定したタグキーと値の組合せに合致するEC2のインスタンスIDをリスト化する
    for rsv in ret_describe['Reservations']:
        for inst in rsv['Instances']:
            instance_ids.append( inst['InstanceId'] )

    ret_data['body'] = f"{param['command']} is valid. "

    if instance_ids == []:
        # 該当EC2が見つからなかったので中断
        ret_data['body'] += 'instance not found error'
        return ret_data
    elif len(instance_ids) >= 7:
        # 該当EC2が多すぎるので中断（セーフティネット）
        ret_data['body'] += 'too many instances error'
        return ret_data

    try:
        if start_stop == 'start':
            ec2.start_instances( InstanceIds = instance_ids )
        else:
            ec2.stop_instances( InstanceIds = instance_ids )

        ret_data['body'] += f'{start_stop}_instances called {str(instance_ids)}'

    except Exception as e:
        ret_data['body'] += f'API error (Exception: {str(e)})'

    return ret_data
```

----

<font color="IndianRed">[2023/2/9] 上記のコードは、主に4点を更新しました。
・<font color="IndianRed">正規表現を更新（絞り込みが粗かったため）</IndianRed> ```r'(start|stop):.*:.*'``` → ```r'^(start|stop):[^:]+:[^:]+$'```</font>
・<font color="IndianRed">アクセス元IPのプレフィックスで許可するようにコードを追加</font>
　<font color="IndianRed">（上記コード内では ```['1', '2', '3', ・・・, '9' ]``` として全てのIPを許可にしています。適宜修正ください）</font>
・<font color="IndianRed">API実行時のExeptionを取得して中断する</font>
・<font color="IndianRed">セーフティネットとしてインスタンスが7個以上の場合失敗させる</font>
　<font color="IndianRed">（上記コード内では ```>= 7``` としています。適宜修正ください）</font>

----

- 一般設定 － タイムアウト：3秒 → **10秒** に延ばしておきます
- <font color="IndianRed">関数URL － 「関数URLを作成」ボタンを押す</font>
    - <font color="IndianRed">認証タイプ：**None**</font>

<font color="IndianRed">関数URL「 https:～xxxxx.lambda-url.xxxxx.on.aws/ 」のリンクが生成されたことを確認します。</font>

## ４．Lambdaのテスト

<font color="IndianRed">関数URL「 https:～xxxxx.lambda-url.xxxxx.on.aws/ 」のURLにアクセスします。
Webブラウザに ```URL parameter error``` が返ってくればOKです。
以下も合わせて実施してもよいです。</font>

コードが正しく動作するか一応テストします。
テストタブで、テストを作成します。

``` json
{
    "requestContext": { "http": { "sourceIp": "12.34.56.78" } },
    "queryStringParameters": { "command":"start:Name:dummy" }
}
```

<font color="IndianRed">コードの修正に合わせてテストデータに ```$.requestContext.http.sourceIp``` とダミーのIPを追加しています。</font>
上記をインプットのテストデータとして、変更を保存します。
テストボタンを押します。結果に ```instance not found``` があればOKです。
```dummy``` を存在するEC2のNameタグに変更して保存します。（ServerA01 が存在するなら dummy を ServerA01 に）
再度テストボタンを押します。結果に ```start_instances called [i-xxxxxxxxxxxxxxx]``` があればOKです。


## ５．API Gatewayの作成

<font color="IndianRed">関数URLを使う場合は、この API Gatewayの作成は不要です。一応、引用の形式で残します。</font>

> API Gateway　＞　APIを作成　＞　REST API
> プロトコル： **REST**
> 新しい API の作成： **新しい API**
> API名： *ec2startstop*
> APIの作成ボタンを押します。
> 
> アクション－メソッドの作成 を押すとブランクのボックスが現れるので **GET** を選択
> **GET** をクリックすると GET - セットアップ 画面になります。
> 統合タイプ： **Lambda 関数**
> Lambda プロキシ統合の使用： **チェックを入れる**
> Lambda 関数： *ec2startstop*
> 保存ボタンを押します。
> Lambda 関数に権限を追加する　のダイアログでOKボタンを押します。
> 
> アクション－APIのデプロイ を押すとダイアログが表示されます。
> デプロイされるステージ： **[新しいステージ]**
> ステージ名： **s**　※何でもいいのですがURLの一部に入ってきますのでそのつもりで
> デプロイ ボタンを押します。
> 
> ステージエディタの画面で
> URL の呼び出し: https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s
> が表示されます。
> xxxxxxxx はランダムで割り当てられます。
> 
> /s の後にパラメータを付与しないままURLをWebブラウザーで呼び出してみましょう。
> **URL parameter error**
> と表示されたらOK。API Gatewayを経由してLambdaが実行されたことになります。

## ６．URLの組み立て

URLの後ろに
?command=＜start または stop＞:＜タグのキー名＞:＜タグの値＞
を付与してURLを組み立てます。

例：
Nameタグの値が ServerA01 のEC2を起動・停止する場合
~~https:～xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s?command=start:Name:ServerA01~~
~~https:～xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s?command=stop:Name:ServerA01~~
<font color="IndianRed">https:～xxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/?command=start:Name:ServerA01
https:～xxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/?command=stop:Name:ServerA01</font>
Envタグの値が Development のEC2をまとめて起動・停止する場合
~~https:～xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s?command=start:Env:Development~~
~~https:～xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/s?command=stop:Env:Development~~
<font color="IndianRed">https:～xxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/?command=start:Env:Development
https:～xxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/?command=stop:Env:Development</font>

## ７．実行

組み立てたURLでアクセスします。
EC2の起動・停止ができたでしょうか。

-----

この方法で実装されたURLは、学習のためと割り切ってください。
~~セキュリティも何もありません。~~ <font color="IndianRed">送信元IPによるアクセス制御が可能です。</font>
URLが意図せずに第3者に漏れてしまったら、、、勝手にOSを停止されたりして大変ですので。