【AWS】CloudWatchのカスタムウィジェットからリソースを操作する(EC2開始/停止)

## はじめに

Lambdaで実装できるならCloudWatchのダッシュボード上で色々な操作ができそうなので、
EC2を開始/停止するLambda関数を書いてカスタムウィジェットに設定し、CloudWatchのダッシュボードからEC2の開始/停止をしてみる。
(Lambda書いてまでCloudWatchからEC2を操作する必要性は置いておいて)

### Lambda
サーバレスでコードを実行できるサービス。今回は[AWS SDK for Python(Boto3)](https://aws.amazon.com/jp/sdk-for-python/)を使う。

### CloudWatch
ダッシュボードに追加するウィジェットは折れ線グラフだったり円グラフだったり色々種類がある。その中にカスタムウィジェットというものがあり、Lambdaを実行し、実行結果を表示することができる。

:::note warn
カスタムウィジェット自体には料金は掛からないが、Lambda関数は実行した分従量課金される。
:::


## 1. Lambda関数を実装
以下2つを実装する。
* パラメータで指定したインスタンスの状態を表示し、開始/停止ボタンを表示する関数
* インスタンスを開始/停止する関数


### パラメータで指定したインスタンスの状態を表示し、開始/停止ボタンを表示する関数

Lambda関数にEC2 ec2:DescribeInstancesのポリシーが必要。

```diff_python
import json
import boto3

def lambda_handler(event, context):
   
    instance_name = event.get('Instance_Name')
    client = boto3.client('ec2')
    
    startbutton = '''
    <!DOCTYPE html>
    <a class="btn btn-primary">Start EC2</a>
    <cwdb-action 
        display="widget"
        action="call" 
        endpoint="<インスタンスを開始/停止する関数のarn>"
    >    
    </cwdb-action>
    </html>'''
    
    stopbutton = '''
    <!DOCTYPE html>
    <a class="btn btn-primary">Stop EC2</a>
    <cwdb-action 
        display="widget"
        action="call" 
        endpoint="<インスタンスを開始/停止する関数のarn>"
    >    
    </cwdb-action>
    </html>'''
    
    responses = client.describe_instances(
        Filters=[
            {
                'Name': 'tag:Name',
                'Values': [
                    instance_name,
                ]
            },
        ],
    )
    
    
    for instances in responses['Reservations']:
        for instance in instances['Instances']:
            instance_state = instance['State']['Name']

    if instance_state == "running":
        return f"<h2>InstanceName:&nbsp;{instance_name}</h2>, <h2>State:&nbsp;{instance_state}</h2><br>{stopbutton}"
    else:
        return f"<h2>InstanceName:&nbsp;{instance_name}</h2>, <h2>State:&nbsp;{instance_state}</h2><br>{startbutton}"
    
```
htmlでStartボタン、Stopボタンの定義をしておいて、インスタンスの状態を見て表示するボタンを変えている。
インスタンスの状態の場合分けはrunningとそれ以外にしたけど、もっと丁寧に場合分けした方がベターかなと思う。


### インスタンスを開始/停止する関数

関数にec2:StartInstancesとec2:StopInstancesのポリシーが必要
```diff_python
import boto3

def lambda_handler(event, context):    
    instance_name =  event['widgetContext']['params']['Instance_Name']

    client = boto3.client('ec2')

    responses = client.describe_instances(
        Filters=[
            {
                'Name': 'tag:Name',
                'Values': [
                    instance_name,
                ]
            },
        ],
    )

    for response in responses['Reservations']:
        for instance in response['Instances']:
            state =instance['State']['Name']
    
            if  state == "stopped":
                client.start_instances(
                    InstanceIds=[
                        instance['InstanceId'],
                    ],
                )
                return f'<h3>Successfully Started</h3>'
                
            elif state == "running":
                client.stop_instances(
                    InstanceIds=[
                        instance['InstanceId'],
                    ],
                )
                return f'<h3>Successfully Stopped</h3>'            
    
            else:
                return f'<h3>You don\'t have Running/Stopped Instances</h3>'
```

呼び出したLambda関数でもウィジェットで設定したパラメータを使いたいので、
instance_name =  event['widgetContext']['params']['Instance_Name']でインスタンス名を取ってきている。


## 2. CloudWatchのカスタムウィジェットの設定

CloudWatchのダッシュボードを開き、`最初のウィジェットを作成`か右上の`+`ボタンをクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458266/a78faaa1-6ba7-a5e8-42f1-8124b3f1c566.png)

カスタムウィジェットを選択し、`次へ`をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458266/a20e4fa8-2b72-10d5-5f85-75050ed5fd27.png)

プルダウンでは何も選択せず、`次へ`をクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458266/76e208e2-548d-63c8-08a4-8c027172fc1d.png)

プルダウンからLambda関数を選択するかARNを入力。関数のパラメータがあればここで設定する(JSON or YAML)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458266/a855b5d6-4357-a3ec-7a51-d72a61d2c9dd.png)


設定するとこんな感じで表示される。Sampleインスタンスは停止しているので、Start EC2ボタンが表示されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458266/d4223980-ed1d-1266-efad-f8729da1db5a.png)





## 3. 動かしてみる

Start EC2ボタンをクリックしてみると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458266/d4223980-ed1d-1266-efad-f8729da1db5a.png)

インスタンスを起動/停止する関数が実行された
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458266/b8216879-529a-7f99-1dda-393289583c40.png)

インスタンスも起動される
![インスタンス.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458266/5878848e-3b29-b966-e2e0-5db04930a264.png)

ダッシュボード側でウィジェットを更新すると、インスタンスの状態もrunningに変わっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458266/44789821-f1e8-e4e6-761d-a4945f83980f.png)


再度インスタンスの状態を表示する関数に戻りたいなと思ってSuccessfully Startedの画面にbackボタンを実装してみたけど、ボタンを押してみたら以下エラーが出た。
ウィジェットをリフレッシュすると状態表示をする関数のところに戻れるので、backボタンの実装は今後の課題とする。

`The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.`

## 4. 参考サイト

* AWSのカスタムウィジェットサンプルコード

https://github.com/aws-samples/cloudwatch-custom-widgets-samples/tree/main/samples

* ボタン実装の参考
DevelopersIO「[アップデート]CloudWatch dashboardに任意のLambda関数の結果を表示できるcustom widgetsが使えるようになりました」

https://dev.classmethod.jp/articles/cloudwatch-dashboard-custom-widgets/

* cwdb-actionの公式ドキュメント

https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/add_custom_widget_dashboard_interactivity.html#add_custom_widget_dashboard_cwdb


* 呼び出したLambda関数にパラメータを渡すときの参考
DevelopersIO「CloudWatch dashboard custom widgetsのcwdb-actionで実行するLambda関数にパラメーターを渡したいときはどうするの?」

https://dev.classmethod.jp/articles/how-to-pass-rapams-to-lambda-functions-triggered-by-cloudwatch-dashboard-custom-widgets-cwdb-action/



## おわりに
コードを書ければダッシュボードで色々できそうな感じ

今後の課題
* 呼び出したLambda関数から呼び出し元のLambda関数を呼び出す方法