パターン 4. WebUIからイベントの登録を行いたい

本パターンでは、WebアプリからREST APIへアクセスし、イベント登録を行うことができます。

イベント連携対象となるクラウドアプリと組み合わせてパッケージにすることで、イベントによるカスタムLambda間の連携を行うことができます。

WebアプリとREST APIへのアクセスは、それぞれCognitoによる認証が行われます。
Webアプリへアクセスする際に、IoTストアのアカウントを使用して認証を行います。

クラウドアプリケーション内の構成は以下の通りです。

パターン04

No.

リソース名

概要

1

CloudFront

WebクライアントからWebアプリへのアクセスする際のエンドポイントとなります

2

ユーザー認証用Lambda

Cognitoと連携してWebクライアントからWebアプリへのアクセス、およびAPI Gatewayへアクセスする際のユーザ認証処理を行います

3

Cognito

WebクライアントからWebアプリへのアクセス、およびAPI Gatewayへアクセスする際のユーザ認証を行います

4

S3(html+js)

WebクライアントがダウンロードしてアクセスするためのUIを格納します。

5

API Gateway

APIリクエストによりカスタムLambdaを実行します

6

カスタム処理

イベントアクセスLambdaを呼び出し、イベントデータを登録します

7

イベントアクセス

myiot-rel-event-access-lambda
DynamoDBへアクセスし、イベントデータの登録を行います

8

DynamoDB

イベントデータが格納されているデータベースです
イベントデータが登録されると、DynamoDBストリームによってイベント制御Lambdaが実行されます

9

イベント制御

イベントデータを受け取り、イベント連携対象のカスタムLambdaを実行します

10

イベント連携対象カスタム処理

イベントによって起動される連携対象のカスタムLambdaです

CloudFormationテンプレート例

本パターンにおけるCloudFormationテンプレートを作成します。
各項目についての設定の詳細はAWSのドキュメントを参照してください。
※yml/yamlファイルの場合に、IoTストアでは!GetAttなど、短縮形の構文で組み込み関数は使用できないため、Fn::GetAttのように完全名関数の構文で記述する必要があります。

テンプレート作成する際の注意事項として以下のコメント種別で説明をします。

コメント種別

内容

+

利用目的に応じて開発者側で適切な値の設定が必要な箇所を示しています

!

My-IoTが提供する共通リソースに関する記載のため変更禁止の箇所を示しています

*

その他の補足説明を示しています

yaml形式の場合の例

下記のファイル名で作成します。

  • cloudformation.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: An AWS function.
Resources:
  #************************************************************
  # Lambda Function
  # No.2 カスタム処理Lambdaのテンプレート例です
  #
  sipSamplePattern04lm:
    Type: 'AWS::Lambda::Function'
    Properties:
      #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
      # ソースコードの格納先はIoTストアで展開時に自動設定されるため記載しないでください
      # Code:
      #   S3Bucket: 
      #   S3Key: 
      #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
      Description: ''
      Handler: lambda_function.lambda_handler
      #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
      # ロールは変更しないでください
      #
      Role: 
        Fn::Sub: arn:aws:iam::${AWS::AccountId}:role/sip-sample-lambda-role
      #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
      Runtime: python3.7
      MemorySize: 128
      Timeout: 30
      #************************************************************
      # Lambdaのファンクション名は導入時にIoTストアにて一意の名称に変換されます
      # テンプレート内では任意の名称で構いません
      #
      FunctionName: 'sip-sample-pattern-04-lm'
      #************************************************************
      Environment:
        Variables:
        #************************************************************
        # カスタムLambdaが参照する環境変数と値を定義します
        #
          #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
          # 下記の項目は変更しないでください
          #
          # EVENT_ACCESS_LAMBDA: No.7 イベントアクセスLambda関数名を指定しています
          # 
          EVENT_ACCESS_LAMBDA: myiot-rel-event-access-lambda
          #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  #
  # Lambda Function
  #************************************************************

  #************************************************************
  # ApiGateway
  # No.3 API Gatewayのテンプレート例です
  #
  RestAPI:
    Type: AWS::ApiGateway::RestApi
    Properties:
      EndpointConfiguration:
        Types:
          - REGIONAL
      #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
      # 下記の項目は作成するクラウドアプリケーションに応じて変更してください
      #
      # Description: 説明文を記載してください
      # ※空文字はエラーとなります。不要の場合は「Description」を削除してください
      # Name: API名を記載してください
      #
      Description: 'sip-sample-pattern-04 RestApi'
      Name: 'sample-apigw'
      #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  RestAPIDeployment:
    Type: AWS::ApiGateway::Deployment
    Properties:
      Description: ''
      RestApiId:
        Ref: RestAPI
    DependsOn:
      - RestAPIMethod
      - RestAPIResource

  RestAPIStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      RestApiId:
        Ref: RestAPI
      DeploymentId:
        Ref: RestAPIDeployment
      StageName: 'dev'

  RestAPIResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      PathPart: '{proxy+}'
      ParentId:
        Fn::GetAtt: RestAPI.RootResourceId
      RestApiId:
        Ref: RestAPI

  RestAPIMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
      # 下記の項目は変更しないでください
      #
      # HttpMethod:        メソッド種別を指定しています
      # AuthorizationType: 認証種別を指定しています
      # Integration:       プロキシ統合に関する設定を指定しています
      #
      HttpMethod: ANY
      AuthorizationType: NONE
      RequestParameters:
        'method.request.path.proxy': true
      Integration:
        CacheKeyParameters:
          - 'method.request.path.proxy'
        CacheNamespace:
          Ref: RestAPIResource
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri:
          Fn::Sub: 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${sipSamplePattern04lm.Arn}/invocations'
      #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
      ResourceId:
        Ref: RestAPIResource
      RestApiId:
        Ref: RestAPI
  #
  # ApiGateway
  #************************************************************

  #************************************************************
  # LambdaPermission
  #
  LambdaPermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:InvokeFunction'
      Principal: 'apigateway.amazonaws.com'
      FunctionName: 
        Fn::GetAtt: sipSamplePattern04lm.Arn
  #
  # LambdaPermission
  #************************************************************

カスタム処理ソースコード例(Python)

下記のファイル名で作成します。

  • lambda_function.py

API Gateway経由で呼び出され、共通リソースのイベントアクセスLambda経由でイベントの登録を行います。

import boto3
import json
import logging
import os

# Loggerオブジェクトを取得し、表示するレベルを設定します。
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# イベントアクセスLambda関数名
EVENT_ACCESS_LAMBDA = os.environ.get('EVENT_ACCESS_LAMBDA')
# テナントID
TENANT_ID = os.environ.get('TENANT_ID')
# イベントキー
EVENT_KEY = os.environ.get('EVENT_KEY')

# エラーコード&メッセージ
ERR_INVALID_ARGUMENT = 400
ERR_INTERNAL_ERROR = 500

ERR_MSG_INVALID_ARGUMENT = {'message': 'Invalid Argument.'}
ERR_MSG_INTERNAL_ERROR = {'message': 'Internal error.'}


def is_empty_str(val):
    """値が空文字列か、又はNoneかを調べます。

    Args:
        val (str/NonType): 調べる値

    Returns:
        bool: 値が空文字列か、又はNoneなら True を返す
    """

    return not val if isinstance(val, str) else val is None


def make_response(code, body):
    """実行結果をJSON形式で返します。

    Args:
        code (int): ステータスコード
        body (str): 実行結果

    Returns:
        [dict]: 実行結果
    """

    # ==============================================================================
    # APIで返却するレスポンスには、オリジン間リソース共有(CORS)を有効にするために
    # 「Access-Control-Allow-Origin」をヘッダーに設定しています。
    # ここでは、すべてのドメインからのアクセスを許可するように設定しています。
    # ==============================================================================
    response = {
        'statusCode': code,
        'headers': {
            'Access-Control-Allow-Origin': '*',
            'Content-Type': 'application/json'
        },
        'body': json.dumps(body),
        'isBase64Encoded': False
    }

    return response


def event_register():
    """イベントアクセスLambdaを呼び出し、イベントの登録を行います。

    Returns:
        dict: 処理結果を返します。
    """

    # ==============================================================================
    # イベントアクセスLambdaを利用する際に必要な引数を設定しています。
    # 詳細については【PF仕様書】共通リソースの利用方法(API仕様など)
    # No.6 イベントデータの登録 を参照してください。
    # ここではイベント発火先のカスタムLambdaに引き渡すペイロードを設定しています。
    # ==============================================================================
    # イベント発火先のカスタムLambdaに引き渡すペイロード
    detail = {
        'eventName': 'test',
    }

    payload = {
        'method': 'register',
        'tenantId': TENANT_ID,
        'eventKey': EVENT_KEY,
        'detail': detail
    }

    try:
        # ==============================================================================
        # No.7 イベントデータの登録
        # 環境変数で定義されているイベントアクセスLambdaに
        # リクエストを実施し、結果を取得します。
        # ==============================================================================
        res = boto3.client('lambda').invoke(
            FunctionName=EVENT_ACCESS_LAMBDA,
            InvocationType='RequestResponse',
            Payload=json.dumps(payload)
        )

        response = json.loads(res['Payload'].read())

    except Exception:
        import traceback
        logger.error(traceback.format_exc())
        return make_response(ERR_INTERNAL_ERROR, ERR_MSG_INTERNAL_ERROR)

    # 処理結果を返します。
    return make_response(response['statusCode'], response['body'])


# Lambda関数の処理の入口
def lambda_handler(event, context):

    # イベントアクセスLambdaを呼び出し、イベントの登録を行います。
    return event_register()

WebUI作成例

本パターンにおけるWebUIを作成します。
サンプルでは以下のファイル、フォルダ構成となります。

index.html
js(フォルダ)
└─ index.js

WebUI画面例(index.html)

パターン04_WebUi

<!DOCTYPE html>
<html lang="ja">
 <head>
  <meta charset="utf-8">
   <title>TOP画面</title>
   <meta name="viewport" content="width=device-width, initial-scale=1">  
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
  </head>
<body>
    <header>
      <h2 class="bg-secondary text-white text-center">イベント登録画面</h2>
    </header>
    <div class="Search area">
      <table>
        <tr>
          <td><button type="button" id="eventBtn" style="width: 140px">イベント登録</button></td>
        </tr>
      </table>
    </div>
    <div class="Response area">
      <table>
        <tr>
          <td><b> 実行結果:</b></td>
          <td><textarea id="result" style="width: 810px" rows="10"></textarea></td>
      </table>
    </div>
    <script type="text/javascript" src="js/index.js"></script>
</body>
</html>

WebUIソースコード例(index.js)

API Gatewayへのアクセスは、HTTPヘッダーにIDトークンを設定する必要があるため、window.onload()内でCookieからIDトークンの取得を行っています。

また、クラウドアプリのインストール時に生成されるconfig.jsonファイルからAPI Gatewayの情報を読み込み、API GatewayのURLを設定しています。

API Gatewayからステータスコード401が返却された場合は、IDトークンの再取得処理が必要となります。

IDトークンの再取得は、{WebUIアプリのURL}/refreshにajax等でアクセスすることで自動でCookieが更新されます。
Cookieの更新後、セッションストレージのtokenをCookieのidTokenで上書きする必要があります。

IDトークン再取得後も401が返却されてしまう場合は再度サインインが必要です。
HTMLの再読み込みなどを行い、サインイン画面に遷移させる処理が必要となります。

let configure = {};
let apiGatewayUrl = '';

window.onload = async function () {
    /**
     * API Gatewayへのアクセスに必要な情報を取得します
     */

    // IDトークンの取得を行います
    const idToken = document.cookie.split('; ').map((data) => {
        const _ = data.split('=');
        return { key: _[0], value: _[1] };
    }).find((data) => /\.idToken$/.test(data.key));

    // セッションストレージへ保存します
    sessionStorage.setItem('token', idToken.value);

    // config.jsonを読み込みます
    configure = await _getConfigure();

    // APIGatewayのURLを設定します
    apiGatewayUrl = configure.ApiUrl;
};

// イベント登録ボタンを設定します
const eventBtn = document.getElementById('eventBtn');

// ボタンイベントを登録します
eventBtn.addEventListener('click', onEventBtn);

/**
 * イベント登録リクエストを実行します
 */
async function onEventBtn() {
    // リクエストを送信します
    await sendRequest();
}

/**
 * APIGatewayへリクエスト送信します
 */
function sendRequest() {
    // レスポンス内容をクリアします
    $('#result').val('');

    // URL(APIGatewayエンドポイント + 任意のパス)を設定します
    const url = apiGatewayUrl + 'test';

    $.ajax({
        url: url,
        type: 'POST',
        headers: {
            'Authorization': sessionStorage['token'],
        },
        dataType: 'json',
        contentType: 'application/json',
        cache: false,
        timeout: 60000,
    }).then((_data) => {
        // 文字列型に変換します
        const text = JSON.stringify(_data);
        // レスポンス内容を表示します
        $('#result').val(text);
    }, (XMLHttpRequest, textStatus, errorThrown) => {
        // エラー内容を表示します
        const text = XMLHttpRequest.status + ' ' + textStatus + ' ' + errorThrown.message;
        $('#result').val(text);
    });
}

// config.json読み込み
function _getConfigure() {
    return new Promise((resolve, reject) => {
        $.ajax({
            url: `./config.json`,
            type: 'GET',
            dataType: 'json',
            contentType: 'application/json',
            cache: false,
            timeout: 60000,
        }).then((data) => {
            resolve(data);
        }, (XMLHttpRequest, textStatus, errorThrown) => {
            reject();
        });
    });
}