Sumerian+Lambdaで簡易EC2モニタを作ってみた

# はじめに

AWS Sumerianは、他のAWSサービスと連携したAR/VRのコンテンツを作成することができ、そこが大きな魅力の一つでもあります。
そこで今回は、SumerianとLambdaを直接連携させる形で簡易的なEC2インスタンスのモニタリング機能（＋停止機能）を実現するVRアプリケーションを作成してみます。

![sumerian-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533054/7e29d710-2be2-01fc-91d0-b79dfc79cfa6.png)

# 必要なもの

- AWSアカウント
- ウェブブラウザ（開発および実行環境として。ChromeやFirefoxなど）

## 構築要素

- **EC2インスタンス：** 監視/制御の対象となるEC2インスタンス
- **Lambda関数：** EC2インスタンスの情報取得やシャットダウンを実行する関数
- **Cognito IDプール：** Sumerianに一時認証情報を払い出すためのIDプール
- **IAMロール：** Lambda関数やSumerianのスクリプトでAWSのAPIリクエストを実行するためのロール
- **Sumerianシーン：** 簡易EC2モニタ本体（ウェブブラウザからアクセスするVRアプリケーション）

![sumerian-lambda-arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533054/47406921-6fe9-8c0b-3c76-a0b038ad4a33.png)

## Step 1: EC2インスタンスの作成

AWSマネジメントコンソールでEC2の管理画面に移り、任意のEC2インスタンスを1つ作成します。

## Step 2: Lambda関数の作成

### 関数1： EC2インスタンスの情報取得処理

1. Lambdaの管理画面に移り、以下の内容でLambda関数を作成します。SumerianのスクリプトはJavaScriptで実装するので、今回はLambda関数もJavaScriptで実装することにしました。
   - オプション： **一から作成**
   - 関数名： **GetEc2Info**
   - ランタイム： **Node.js 12.x**
   - 実行ロール： **基本的な Lambda アクセス権限で新しいロールを作成**（デフォルトのまま）

2. 関数のコードを以下の内容で書き換えます。

    ```javascript:index.js
    'use strict';
    const AWS = require('aws-sdk');
    AWS.config.update({region: 'ap-northeast-1'});
    const ec2 = new AWS.EC2({apiVersion: '2016-11-15'});

    exports.handler = (event, context, callback) => {
        ec2.describeInstances(event, function(err, data) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, data);
            }
        });
    };
    ```

3. IAMの管理画面に移り、Lambda関数の作成時に作られたIAMロール **GetEc2Info-role-ランダム文字列** を選択します。

4. EC2インスタンスの情報参照リクエスト（ec2:describeInstances）を実行できるよう、このロールにAWS管理ポリシー **AmazonEC2ReadOnlyAccess** をアタッチします。

5. Lambdaの管理画面に戻り、関数 **GetEc2Info** で以下のテストイベントを作成し、テストを実行します。関数の実行が成功すると **詳細** にEC2インスタンスの情報が表示されます。

    ```json
    {
      "InstanceIds": ["＜EC2のインスタンスID＞"]
    }
    ```

### 関数2： EC2インスタンスの停止処理

1. Lambdaの管理画面に移り、以下の内容でLambda関数を作成します。
   - オプション： **一から作成**
   - 関数名： **StopEc2Instance**
   - ランタイム： **Node.js 12.x**
   - 実行ロール： **基本的な Lambda アクセス権限で新しいロールを作成**（デフォルトのまま）

2. 関数のコードを以下の内容で書き換えます。

    ```javascript:index.js
    'use strict';
    const AWS = require('aws-sdk');
    AWS.config.update({region: 'ap-northeast-1'});
    const ec2 = new AWS.EC2({apiVersion: '2016-11-15'});
    
    exports.handler = (event, context, callback) => {
        ec2.stopInstances(event, function (err, data) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, data);
            }
        });
    };
    ```

3. IAMの管理画面に移り、Lambda関数の作成時に作られたIAMロール **StopEc2Instance-role-ランダム文字列** を選択します。

4. EC2インスタンスの停止リクエスト（ec2:stopInstances）を実行できるよう、このロールにAWS管理ポリシー **AmazonEC2FullAccess** をアタッチします。

5. Lambdaの管理画面に戻り、関数 **StopEc2Instance** で以下のテストイベント（GetEc2Infoで作成したテストイベントと同じ）を作成し、テストを実行します。関数の実行が成功するとEC2インスタンスが停止します。

## Step 3: Cognito IDプールの作成

1. Cognitoの管理画面に移り、**IDプールの管理** で以下の内容で新しいIDプールを作成します。
   - IDプール名： **SumerianEC2Monitor**
   - 認証されていないIDに対してアクセスを有効にする： **チェックを入れる**

2. **Identify the IAM roles to use with your new identity pool** の画面ではそのまま **許可** をクリックします。 
 
3. **Edit identity pool** を選択してプールの詳細を確認します。
 
4. あとで使用するために、 **AWS 認証情報の取得** に表示されている **Identity pool ID** の値を書き留めます。

5. IAMの管理画面に移り、CognitoのIDプール作成時に作られたIAMロール **Cognito_SumerianEC2MonitorUnauth_Role** を選択します。

6. Lambda関数の呼び出しリクエスト（lambda:InvokeFunction）を実行できるよう、このロールにAWS管理ポリシー **AWSLambdaRole** をアタッチします。

## Step 4: Sumerianシーンの作成

### Sumerianシーンの作成

1. Sumerianの管理画面（ダッシュボード）に移り、**Create new scene** をクリックし、以下の内容でシーンを作成します。
   - 名前： **EC2Monitor**     

2. 右側のペインの **AWSの設定** コンポーネントで、 **Cognito Identity Pool ID** の入力欄に書き留めていた **Identity pool ID** の値を入力します。

### HTMLエンティティ（情報表示パネル用）の作成

EC2インスタンスの情報を表示するための2DのHTMLドキュメントを配置し、EC2インスタンスの情報を表示する処理を実装します。

1. 画面上部の **＋Create Entity** をクリックし、2D Shapesの **HTML**を選択します。

2. 右側のペインの **HTML Entity** コンポーネントで、分かりやすい名前に変更します。
   - Name： **Panel**

3. **Transform** コンポーネントで位置を調整します。あとで作成するBoxエンティティの上に配置されるようにYの値を変更します。
   - Translation（Y）： **1.5**

4. **HTML** コンポーネントで **Open in Editor** をクリックします。

5. Sumerianのテキストエディタが表示されます。HTMLファイルを以下の内容に置き換え、**Save** をクリックします。

    ```html
    <style>
    .left{ text-align:left;}
    .right{ text-align:right;}
    .center{ text-align:center;}
    
    .box{
        background:rgba(192,192,192,0.2);
        color:#FFF;
        float:left;
        width:400px;
        height:160px;
        margin:10px;
        padding:15px;
        font-family:Arial, Helvetica, sans-serif;
        font-size:18px;
        font-weight:normal;
    }
    
    .fourcorners{
        -moz-border-radius: 15px;
        -webkit-border-radius: 15px;
        -khtml-border-radius: 15px;	
        border-radius: 15px;
    }
    </style>  
    
    <section class="box fourcorners">
        <p>Instance ID:  <span id='InstanceId'></span></p>
        <p>Instance Type:  <span id='InstanceType'></span></p>
        <p>Instance State:  <span id='InstanceState'></span></p>
        <p>Launch Time:  <span id='LaunchTime'></span></p>	
    </section>
    ```

### Boxエンティティの作成

EC2インスタンスに相当する立方体を配置し、EC2インスタンスの状態や情報を立方体（色）とパネル（文字）に反映する処理を実装します。

1. Sumerianのシーン画面に戻り、画面上部の **＋Create Entity** をクリックし、**Box**（立方体）を選択します。

2. 右側のペインの **Box** コンポーネントで、分かりやすい名前に変更します。
   - Name： **Instance**

3. **Transform** コンポーネントで向きを調整します。Y軸を45度回転させてみます。
   - Rotation（Y）： **45**

4. 右側のペインの一番下にある **＋Add Component** をクリックし、**State Machine** を選択します。

5. ペインに追加された **State Machine** コンポーネントの中にある **＋** をクリックします。

6. 画面中央の下側に表示されているペインで、**Show current state for:** で **Instance** が選択されていることを確認し、このエンティティの状態とアクションを追加していきます。
   - 状態1：初期状態
     - Name： **Init**
     - 追加するアクション1： **AWS SDK Ready**
     - 追加するアクション2： **Transition**
   - 状態2：EC2インスタンスの情報取得処理実行
     - Name： **Execute**
     - 追加するアクション： **Execute Script**
   - 状態3：ループ処理のウェイト状態
     - Name： **Wait**
     - 追加するアクション： **Wait**
     - Waitの設定（Time）： **15**（秒）

7. 状態の間を以下のようにつなげます。
![InstanceBehavior.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533054/aa59575b-ea42-9db4-bdcc-1791b4d9b255.png)

8. **GetInfo** の状態を選択し、右側のペインに追加されている **Execute Script** アクションの設定で、鉛筆マークのアイコンをクリックします。

9. Sumerianのテキストエディタが表示されます。スクリプトの中の **enter()** の実装を以下の内容で書き換えます。 **＜EC2のインスタンスID＞** は作成したEC2インスタンスのものに置換します。

    ```javascript
    function enter(args, ctx) {
        const pendingColor = [1.00, 1.00, 0.00, 1]; // #ffff00 (yellow)
        const runningColor = [1.00, 0.65, 0.00, 1]; // #ffa500 (orange)
        const shuttingDownColor = [0.73, 0.33, 0.83, 1]; // #ba55d3 (mediumorchid)
        const terminatedColor = [0.29, 0.00, 0.51, 1]; // #4b0082 (indigo)
        const stoppingColor = [0.00, 0.75, 1.00, 1]; // #00bfff (deepskyblue)
        const stoppedColor = [0.80, 0.80, 0.80, 1]; // #cccccc (light gray)
    
        var lambdaParams = {
            FunctionName: args.lambdaGetEc2Info,
            InvocationType: 'RequestResponse',
            Payload: JSON.stringify({
                InstanceIds: ["＜EC2のインスタンスID＞"]
            })
        }
    
        ctx.worldData.lambda.invoke(lambdaParams, function(err, data) {
            if (err) {
                console.log("Lambda invoke error: " + err);
                ctx.transitions.failure();
            } else {
                try {
                    var payload = JSON.parse(data.Payload);
                } catch (err) {
                    console.log("parse error: " + err);
                    ctx.transitions.failure();
                }
     
                // Boxエンティティの表示色を更新
                var instance = payload.Reservations[0].Instances[0];
                var color = null;
                switch (instance.State.Code) {
                    case 0: // pending
                        color = pendingColor;
                        break;
                    case 16: // running
                        color = runningColor;
                        break;
                    case 32: // shutting-down
                        color = shuttingDownColor;
                        break;
                    case 48: // terminated
                        color = terminatedColor;
                        break;
                    case 64: // stopping
                        color = stoppingColor;
                        break;
                    case 80: // stopped
                        color = stoppedColor;
                        break;
                    default:
                        console.log("unknown code: " + instance.State.Code);
                        ctx.transitions.failure();
                }
                ctx.entity.setDiffuse(color);
    
                // パネル上の表示内容の更新
                var spanElement = null;
                spanElement = document.getElementById('InstanceId');
                spanElement.innerHTML = instance.InstanceId;
                spanElement = document.getElementById('InstanceType');
                spanElement.innerHTML = instance.InstanceType;
                spanElement = document.getElementById('InstanceState');
                spanElement.innerHTML = instance.State.Name;
                spanElement = document.getElementById('LaunchTime');
                spanElement.innerHTML = instance.LaunchTime;
    
                ctx.transitions.success();
            }
        });
    }
    ```

10. 同様に、スクリプトの中の **parameter** の値を以下の内容で書き換えます。 **＜AWSアカウントID＞** は使用しているAWSアカウントのものに置換します。

    ```javascript
    var parameters = [
        {type: 'string', key: 'lambdaGetEc2Info', 'default': 'arn:aws:lambda:ap-northeast-1:＜AWSアカウントID＞:function:GetEc2Info', description: 'Lambda Function GetEc2Info'},	
    ];
    ```

### HTMLエンティティ（停止ボタン用）の作成

EC2インスタンスを停止（シャットダウン）するためのボタンとして2DのHTMLドキュメントを配置し、クリックされたときの処理を実装します。

1. 画面上部の **＋Create Entity** をクリックし、2D Shapesの **HTML**を選択します。

2. 右側のペインの **HTML Entity** コンポーネントで、分かりやすい名前に変更します。
   - Name： **StopButton**

3. **Transform** コンポーネントで位置を調整します。
   - Translation（Y）： **-1.5**
   - Translation（Z）： **1.5**

4. **HTML** コンポーネントで **Open in Editor** をクリックします。

5. Sumerianのテキストエディタが表示されます。HTMLファイルを以下の内容に置き換え、**Save** をクリックします。

    ```html
    <style>
    .left{ text-align:left;}
    .right{ text-align:right;}
    .center{ text-align:center;}
    
    .btn-circle-3d {
        display: inline-block;
        text-decoration: none;
        background: #cc0000;
        color: #FFF;
        width: 80px;
        height: 80px;
        line-height: 45px;
        border-radius: 50%;
        text-align: center;
        font-weight: bold;
        overflow: hidden;
        box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.29);
        border-bottom: solid 3px #bd6565;
        transition: .4s;
    }
    
    .btn-circle-3d:active {
        -webkit-transform: translateY(2px);
        transform: translateY(2px);
        box-shadow: 0 0 1px rgba(0, 0, 0, 0.15);
        border-bottom: none;
    }
    </style>
    
    <section class="btn-circle-3d">
        <p>Stop</p>
    </section>
    ```

6. 右側のペインの一番下にある **＋Add Component** をクリックし、**State Machine** を選択します。

7. ペインに追加された **State Machine** コンポーネントの中にある **＋** をクリックします。

8. 画面中央の下側に表示されているペインで、**Show current state for:** で **StopButton** が選択されていることを確認し、このエンティティの状態とアクションを追加していきます。
   - 状態1：初期状態
     - Name： **Init**
     - 追加するアクション1： **AWS SDK Ready**
     - 追加するアクション2： **Transition**
   - 状態2：ユーザーの操作待ち
     - Name： **WaitForAction**
     - 追加するアクション： **Mouse Up / Touch end**
   - 状態3：EC2インスタンスの停止処理実行
     - Name： **Execute**
     - 追加するアクション： **Execute Script**
   - 状態4：ループ処理のウェイト状態（連続操作を避けるための便宜的な仕組みとして設けました）
     - Name： **Wait**
     - 追加するアクション： **Wait**
     - Waitの設定（Time）： **60**（秒）
     
9. 状態の間を以下のようにつなげます。
![StopButtonBehavior.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533054/d2ab648b-0e54-b49a-9a48-849c2204e755.png)

10. **GetInfo** の状態を選択し、右側のペインに追加されている **Execute Script** アクションの設定で、鉛筆マークのアイコンをクリックします。

11. Sumerianのテキストエディタが表示されます。スクリプトの中の **enter()** の実装を以下の内容で書き換えます。 **＜EC2のインスタンスID＞** は作成したEC2インスタンスのものに置換します。

    ```javascript
    function enter(args, ctx) {
        console.log('enter()')
    
        var lambdaParams = {
            FunctionName: args.lambdaStopEc2Instance,
            InvocationType: 'RequestResponse',
            Payload: JSON.stringify({
                InstanceIds: ["＜EC2のインスタンスID＞"]
            })
        }
    
        ctx.worldData.lambda.invoke(lambdaParams, function(err, data) {
            if (err) {
                console.log("Lambda invoke error: " + err);
                ctx.transitions.failure();
            } else {
                ctx.transitions.success();
            }
        });
    }
    ```

12. 同様に、スクリプトの中の **parameter** の値を以下の内容で書き換えます。 **＜AWSアカウントID＞** は使用しているAWSアカウントのものに置換します。

    ```javascript
    var parameters = [
        {type: 'string', key: 'lambdaStopEc2Instance', 'default': 'arn:aws:lambda:ap-northeast-1:＜AWSアカウントID＞:function:StopEc2Instance', description: 'Lambda Function StopEc2Instance'},	
    ];
    ```

    

### テスト実行

1. 左側のペインで **EC2Monitor** を選択します。
2. 右側のペインで **EC2Monitor** のコンポーネントにある **View Scene** をクリックします。
3. ウェブブラウザのタブが開き、作成したシーンが実行されます。

# 実行イメージ
作成したシーンを公開すると、PCやスマートフォンのブラウザからアクセスできるようになります。
これはiPhone（Safari）からアクセスしたものです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Sumerian+Lambdaで簡易EC2モニタを作ってみた <a href="https://t.co/jlj0Dws2Lt">pic.twitter.com/jlj0Dws2Lt</a></p>&mdash; marudai (@marudai86882629) <a href="https://twitter.com/marudai86882629/status/1210185217014583298?ref_src=twsrc%5Etfw">December 26, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


アプリケーションの開始直後に（PCの）AWSマネジメントコンソールからEC2インスタンスを起動しています。EC2インスタンスの状態に合わせてパネル上の **Instance State** の値とBoxエンティティの表示色が変わっています。
起動が完了したあと、アプリケーション上の **Stop** ボタンをタップしてシャットダウンしています。

# まとめ
Lambda関数や連携するAWSサービスを変えると様々なことを実現できます。エンターブライズでの利用も含め、AR/VRアプリケーションの用途が拡がるのではないでしょうか。

今回は初めて自分のユースケースでSumerianを使ったアプリケーションを作ってみましたが、日本語の情報が比較的少なめなので情報収集に少し苦労しました。困ったときは、[Sumerian Slack チャネル](http://slack.sumerian.world/) で検索や質問をしてみるとヒントが得られるかもしれません。また、今回の記事もSumerianとAWSサービスの連携で困ったときの参考になれば幸いです。

# 参考にしたもの
https://docs.aws.amazon.com/ja_jp/sumerian/latest/userguide/sumerian-guide.pdf
https://github.com/TankAndDozer/AmazonSumerian-DynamoDB-Lambda