AWS Cloud9でLambdaアプリの開発をしたり共同編集をしてみた

この記事はAmazon Web Services Advent Calendar 2017の9日目の記事になります。

結構気になっていたAWS re:Invent 2017での発表があります。

AWS Cloud9

です。実際どの程度便利なのか、使ってみました。

環境作成

利用開始をしようとすると、環境作成のページに飛ばされました。まずはAWS Cloud9が動く環境を準備する必要があるんですね。(そういえばオリジナルのCloud9もそうだったような。。。)

スクリーンショット 2017-12-05 19.31.16.png

とりあえずテスト環境を作成します。
テスト環境なので、設定はデフォルトで。30分以上放置してたら作成したインスタンスを止めてくれるとか、気が利いてますね!

スクリーンショット 2017-12-05 19.48.41.png

環境作成の直前に推奨の設定やらが案内されましたが、とりあえず無視します。

スクリーンショット 2017-12-05 19.52.29.png

環境が作成されるまでしばし待ち。。。(しかし画面かっこいいな)

スクリーンショット 2017-12-05 19.53.49.png

コーディングしてみる

環境が整えばいよいよコーディングです。が、色々と便利そうなビューになってます。
タブでファイルが分割できたり、

スクリーンショット 2017-12-05 20.29.42.png

ターミナルがあったり。これもうVisual Studio Codeと変わらないな。

スクリーンショット 2017-12-05 20.30.45.png

ちなみに実態はEC2インスタンスなので、上記のようにコマンドでライブラリをインストールすることができます。実際にインストールすると、左側のフォルダツリーにリアルタイムに構成が反映されます。

スクリーンショット 2017-12-05 20.30.58.png

で、コーディングはエディタのキャプチャの通り書いてみました。GoogleにRequestを飛ばす一般的な処理です。

実行してみる

もちろんこのままだとファイルパスがマッチせずに実行時にインポートエラーが発生するので

スクリーンショット 2017-12-05 20.30.31.png

こんな感じでライブラリをインストールするようにして、

$ mkdir lib
$ pip install requests -t ./testrequests/testrequests/lib/

ソースと同じディレクトリにライブラリをインストールするようにします。

スクリーンショット 2017-12-06 7.30.06.png

コードも少し修正しました。

import sys
import os

sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib'))

import requests

def lambda_handler(event, context):
    res = requests.get("http://www.google.co.jp/")
    return res.status_code

これでうまく動きそうです。

もう一回実行してみる

スクリーンショット 2017-12-06 7.30.36.png

きちんと結果が得られました。無事動いたようです。
ただ、1点気をつけないといけないのは、あくまでこれはEC2インスタンス上のエディタで実行した、というだけなので、実際のLambdaには何も保存されていません。

ちなみに今回はPythonを使ったのでデバッグはサポートされていませんが、Node.jsやPHP、Goであればデバッグ実行もできるようです。これは便利。

スクリーンショット 2017-12-07 1.05.31.png

最初にファンクションを作成時、Lambdaは登録されるのですが、デプロイするまでは中身は初期化されたままになっています。(当然ではあるのですが、忘れやすいかもこれ)

スクリーンショット 2017-12-07 1.13.48.png

Lambdaでファンクションを選択しても、先ほど書いたコードは存在していません。デプロイをするとLambdaに実際に反映されるようになります。

デプロイをしてLambdaに更新を反映する

Cloud9上からLambdaへのデプロイを行います。やり方はとても簡単で、ファンクションリストから該当のファンクションを右クリックして「Deploy」を選択するだけ。正直Serverless Frameworkなどを使うよりずっと簡単です。

スクリーンショット 2017-12-07 1.16.42.png

これだけの操作でLambdaが更新されます。

共同編集をしてみる

他にアカウントを作成して、共同編集を試してみました。テスト用に「test」というアカウントを作成します。画面右上の「Share」より共有設定を開きます。

スクリーンショット 2017-12-07 1.22.19_dec.png

するとこんな感じの共有用URLが表示されますので、これを別アカウントのブラウザで開いてみます。が、共有の設定をしないともちろんアクセスできません。400で怒られます。

スクリーンショット 2017-12-07 1.23.02_dec.png

共有設定で自分以外に共有したいIAMユーザーのアカウントを追加すれば設定は完了です。

スクリーンショット 2017-12-07 1.23.21.png

testユーザーでアクセスすると、共有設定画面のユーザーのステータスがOfflineからOnlineに変更になります。

スクリーンショット 2017-12-07 1.23.47.png

あとはお互いに編集しているポイントがリアルタイムに反映されていくので、とても便利ですね。キャプチャだけだとなんのことかわかりづらいですが、自アカウントで編集したものがリアルタイムでtestユーザーの画面にも反映されます。

スクリーンショット 2017-12-07 1.24.44.png

スクリーンショット 2017-12-07 1.25.00.png
※キャプチャ右上のアカウントの並びが異なるので、それぞれ異なるユーザーの画面だということは見てわかるかな(^^;。

実際にAWS Cloud9を使ってみての感想

思った以上にできることが多いので、かなり可能性を感じました。自分は今仕事の関係上ミャンマーにいるのですが、多少ネットが遅くてもリアルタイムにインターネットにアクセスできれば、開発していて不便だと感じることはありませんでした。実際に開発したパッケージをデプロイしようとした際には、ローカル10MBなどの重いファイルをアップロードするために何分も待たされる、というのもCloud9ならなさそうですし、ありがたい新サービスです。

続きを読む

AWS LambdaからAmazon Elasticsearch Serviceにつないでみる

この投稿は、AWS Lambda Advent Calendar 2017の初日の投稿になります。

初日なので簡単なのをば!

Elasticsearchは今までローカルやプライベートネットワーク上にインストールして使うことが多く、マネージドサービスを使ったことがなかったので、今回Amazon Elsaticsearch Serviceを使ってみました。

で、色々なサイトを参考させてもらい、設定〜インデックス作成まで実施したので、その備忘録を載せます。

準備

準備ですが、クラスタを作成するだけです。当然t2.smallで作成。Elasticsearchのバージョンは5.5を選択しました。他はデフォルトの設定です。

スクリーンショット 2017-11-19 18.52.14.png

Lambdaからアクセスしてインデックスを作成してみる

AWS Lambdaに適当な関数を作成して、とりあえずアクセスしてみます。こちらを参考にさせてもらいました。

Lambda から elasticsearch service に何かする [cloudpack OSAKA blog]

このコードでアクセスするためには、STSのAssumeRolests:AssumeRoleがIAM Roleに付与されている必要があります。ポイントとなるポリシーの設定はこんな感じです。(もちろんテスト的な設定なので、本来はきちんとアクセス権限を設計しましょう)

        {
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "*"
            ],
            "Effect": "Allow"
        }

ソースコードはこんな感じで動きました。ENDPOINT、REGION、ROLE_ARN、S3_BUCKET、S3_OBJECTは環境変数からの定義となります。

import os
import sys

import boto3

sys.path.append(os.path.join(
    os.path.abspath(os.path.dirname(__file__)), 'lib'))
from elasticsearch import Elasticsearch, RequestsHttpConnection
from requests_aws4auth import AWS4Auth

ENDPOINT = os.environ['ES_ENDPOINT']
REGION = os.environ['REGION']
ROLE_ARN = os.environ['ROLE_ARN']
S3_BUCKET = os.environ['S3_BUCKET']
S3_OBJECT = os.environ['S3_OBJECT']


def run(event, context):
    es_client = connect_es(ENDPOINT)

    if event['method'] == "create-index":
        s3 = boto3.client('s3')
        index_doc = s3.get_object(Bucket=S3_BUCKET, Key=S3_OBJECT)['Body'].read()
        print(index_doc)
        create_index(es_client, event['index'], index_doc)
        return "Success"
    return es_client.info()

def connect_es(es_endpoint):

    print('Connecting to the ES Endpoint {0}'.format(es_endpoint))
    credentials = get_credential()
    awsauth = AWS4Auth(credentials['access_key'], credentials['secret_key'], REGION, 'es', session_token=credentials['token'])

    try:
        es_client = Elasticsearch(
            hosts=[{'host': es_endpoint, 'port': 443}],
            http_auth=awsauth,
            use_ssl=True,
            verify_certs=True,
            connection_class=RequestsHttpConnection)
        return es_client
    except Exception as E:
        print("Unable to connect to {0}".format(es_endpoint))
        print(E)
        exit(3)

def create_index(es_client, index_name, index_doc):
 try:
  res = es_client.indices.exists(index_name)
  print("Index Exists ... {}".format(res))
  if res is False:
   es_client.indices.create(index_name, body=index_doc)
 except Exception as E:
  print("Unable to Create Index {0}".format("metadata-store"))
  print(E)
  exit(4)

def get_credential():
    client = boto3.client('sts')
    assumedRoleObject = client.assume_role(
        RoleArn=ROLE_ARN,
        RoleSessionName="Access_to_ES_from_lambda"
    )
    credentials = assumedRoleObject['Credentials']
    return { 'access_key': credentials['AccessKeyId'],
             'secret_key': credentials['SecretAccessKey'],
             'token': credentials['SessionToken'] }

コードを見れば分かる通り、S3にアクセスするため、権限の設定が必要になります。

S3に配置するJSONデータの準備

今回はS3にインデックスの設定を保持します。こんな感じのdata.jsonを配置します。この辺はAWS Solution Architectのブログを参考にします。

【AWS Database Blog】AWS Lambda と Pythonを使ってメタデータをAmazon Elasticsearch Serviceにインデクシング

{
    "dataRecord": {
        "properties": {
            "createdDate": {
                "type": "date",
                "format": "dateOptionalTime"
            },
            "objectKey": {
                "type": "string",
                "format": "dateOptionalTime"
            },
            "content_type": {
                "type": "string"
            },
            "content_length": {
                "type": "long"
            },
            "metadata": {
                "type": "string"
            }
        }
    },
    "settings": {
        "number_of_shards": 1,
        "number_of_replicas": 0
    }
}

アクセスしてみる

アクセスしたところ、無事インデックスが作成されました。sample-indexが増えているのがわかります。

スクリーンショット 2017-12-01 18.29.10.png

まとめ

今回初めてAmazon Elasticsearch Serviceを利用しましたが、準備段階の手間が省けているのはありがたいですね。またAWS上のサービスなので、Lambdaなどの別サービスからのアクセスも簡単でした。

おまけ

ちなみに最初、受け取ったElasticsearch ClientをそのままReturnしていたのですが、その時のエラーメッセージがこんな感じ。一見するとJSONで保持できない=パラメータが異なった?と受け取れます。

{
  "errorMessage": "<Elasticsearch([{'host': 'HOSTNAME.us-east-1.es.amazonaws.com', 'port': 443}])> is not JSON serializable",
  "errorType": "TypeError",
  "stackTrace": [
    [
      "/var/lang/lib/python3.6/json/__init__.py",
      238,
      "dumps",
      "**kw).encode(obj)"
    ],
    [
      "/var/lang/lib/python3.6/json/encoder.py",
      199,
      "encode",
      "chunks = self.iterencode(o, _one_shot=True)"
    ],
    [
      "/var/lang/lib/python3.6/json/encoder.py",
      257,
      "iterencode",
      "return _iterencode(o, 0)"
    ],
    [
      "/var/runtime/awslambda/bootstrap.py",
      110,
      "decimal_serializer",
      "raise TypeError(repr(o) + " is not JSON serializable")"
    ]
  ]
}

これ、原因が

    return es_client

es_clientをそのまま返していたから。

    return es_client.info()

とすれば動くのですが、こんなの気づけんって。。。せめてピンポイントでエラー発生行番号が出力されれば話は別ですが、不親切すぎなエラーメッセージ(^^;どこかで改善されることを期待っす。

続きを読む

Angular4 + Amazon Cognitoで認証画面作ってみた

いまさらながらAmazon Cognitoを使って認証画面を作ってみた備忘録。
せっかくなのでAngular4ベースで画面は作成。

amazon-cognito-identity-jsで認証

amazon-cognito-identity-jsを利用してみます。インストールは簡単。

$ npm install amazon-cognito-identity-js --save

ユーザプールを作成しユーザ情報を追加

ユーザプールを作成。設定はとりあえず全てデフォルトを選択しました。で、ユーザを追加。ただしAWSコンソールからユーザ追加をするとメールなどによる承認手続きが必要になってしまうので、手抜きのためにコマンドでユーザ追加と承認処理を実施。

$ aws cognito-idp sign-up --client-id <ClientId> --username <Username> --password <Password> --user-attributes Name=email,Value=<Mail Address>
$ aws cognito-idp admin-confirm-sign-up --user-pool-id <UserPoolId> --username <Username> 

無事ステータスが「CONFIRMED」のユーザが出来上がりました。(CONFIRMEDステータスでないと、APIでログインを試行してもVerifyしろ、というエラーが返ってきてしまう)

スクリーンショット 2017-10-18 19.25.38.png

画面作成の参考ページ

このページを参考に認証画面ぽいモノを作成しました。

http://www.fumiononaka.com/Business/html5/FN1704013.html

すでにバージョンの違いからパッケージの内容が若干変わっているところはありますが、特に問題なく作成することはできました。

それっぽい画面の作成

さくっとAngular4(なのかなこれ?)で作ります。

app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
app.component.ts
import { Component } from '@angular/core';
import { Http } from '@angular/http';
import { CognitoService } from './cognito.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [CognitoService]
})
export class AppComponent {
  userName = '';
  password = '';

  constructor(
    private cognitoService: CognitoService,
    private http: Http
  ) { }

  signIn() {
    this.cognitoService.signIn(this.userName, this.password);
  }
}

app.comoponent.html
<!--The content below is only a placeholder and can be replaced.-->
<div>
  <h1>
    Amazon Cognito Test login
  </h1>
</div>
<div>
  <div id="loginId">
    <div>
      <label>Login ID: </label>
    </div>
    <div>
      <input type="text" [(ngModel)]="userName" />
    </div>
  </div>
  <div id="loginPass">
    <div>
      <label>Password: </label>
    </div>
    <div>
      <input type="password" [(ngModel)]="password" />
    </div>
  </div>

  <div id="signIn">
    <button (click)="signIn()">Sign In</button>
  </div>
</div>

Cognito用サービスは、最低限サインインだけを実装しました。

cognito.service.ts
import { Injectable } from '@angular/core';
import * as AWS from 'aws-sdk';
import { CognitoUserPool, CognitoUserAttribute, CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';
import { environment } from '../environments/environment';

@Injectable()
export class CognitoService {
    userPool = null;
    constructor() {
        AWS.config.region = environment.region;
        const data = { UserPoolId: environment.userPoolId, ClientId: environment.clientId };
        this.userPool = new CognitoUserPool(data);
    }

    signIn(userName, password) {
        const userData = {
            Username: userName,
            Pool: this.userPool
        };

        const cognitoUser = new CognitoUser(userData);
        const authenticationData = {
            Username: userName,
            Password: password
        };

        const authenticationDetails = new AuthenticationDetails(authenticationData);
        cognitoUser.authenticateUser(authenticationDetails, {
            onSuccess: function (result) {
                alert('Sign in succeded');
            },
            onFailure: function (err) {
                alert(err);
            }
        });

        return;
    }
}

あとはこれらを使うための環境変数を記述すればOKです。

environment.ts
export const environment = {
  production: false,
  region: 'xxxxxxxxx',
  userPoolId: 'xxxxxxxxxxxxxxxxxxxxx',
  clientId: 'xxxxxxxxxxxxxxxxxxx'

};

結果はこちらに。

https://github.com/kojiisd/angular-cognito

アクセスしてみる

登録したユーザでアクセスしてみます。無事アクセスできました。

スクリーンショット 2017-10-18 19.31.46.png

アカウント情報を間違えたらもちろんアクセスできません。

スクリーンショット 2017-10-18 19.31.53.png

まとめ

だいぶ手抜きですが、Angular4ベースでCognitoを利用する画面を作成しました。Routerなどを実装できれば、それなりにCognitoを使ってのSPAが作成できそうなイメージも持てました。Angular4の勉強はもっとしないとなぁ。。。

続きを読む

DynamoDBの予約語一覧を無理やり動的に取得してみた

DynamoDBの予約語を対処するために一覧が欲しくなったのですが、いちいちプログラム内で定義したくなかったので、AWSの予約語一覧ページから一覧を取得するサービスを作りました。

前提

  1. AWSが公開しているDynamoDB予約語一覧のWebページ( http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html )をスクレイピングして予約語一覧を抽出します。
  2. 実装はAWS Lambda (Python3)、Webサービスとして動作させるためにAPI Gatewayを利用します。

結果

https://github.com/kojiisd/dynamodb-reserved-words

DynamoDB予約語一覧ページの確認

今回は以下のページをParseしたいと思います。

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/ReservedWords.html

HTMLを見てみると、Parseしたい箇所はcodeタグで囲まれているようで、しかもこのページ内でcodeタグが出現するのは1度だけのようです。

スクリーンショット 2017-10-15 18.03.18.png

これであればすぐにパースできそうです。

HTMLの取得とParse

BeautifulSoupを利用すれば、簡単に実装ができます。BeautifulSoupのインストールは他のページでいくらでも紹介されているので、ここでは省略します。

import sys
import os


sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib'))
from bs4 import BeautifulSoup
import requests

URL = os.environ['TARGET_URL']

def run(event, context):

    response = requests.get(URL)
    soup = BeautifulSoup(response.content, "html.parser")

    result_tmp = soup.find("code")
    if result_tmp == None:
        return "No parsing target"

    result = result_tmp.string

    return result.split('n');

とりあえず申し訳程度にパースできない場合(AWSのページがcodeタグを使わなくなった時)を想定してハンドリングしています。

API Gatewayを定義してサービス化

コードが書けてしまえばAPI Gatewayを定義してサービス化するだけです。

設定は非常に単純なので割愛で。

スクリーンショット 2017-10-15 18.12.11_deco.png

アクセスしてみる

実際にアクセスしてみます。

$ curl -X GET https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod
["", "ABORT", "ABSOLUTE", "ACTION", "ADD", "AFTER", "AGENT", "AGGREGATE", "ALL", "ALLOCATE", "ALTER", "ANALYZE", "AND", "ANY", "ARCHIVE", "ARE", "ARRAY", "AS", "ASC", "ASCII", "ASENSITIVE", "ASSERTION", "ASYMMETRIC", "AT", "ATOMIC", "ATTACH", "ATTRIBUTE", "AUTH", "AUTHORIZATION", "AUTHORIZE", "AUTO", "AVG", "BACK", "BACKUP", "BASE", "BATCH", "BEFORE", "BEGIN", "BETWEEN", "BIGINT", "BINARY", "BIT", "BLOB", "BLOCK", "BOOLEAN", "BOTH", "BREADTH", "BUCKET", "BULK", "BY", "BYTE", "CALL", "CALLED", "CALLING", "CAPACITY", "CASCADE", "CASCADED", "CASE", "CAST", "CATALOG", "CHAR", "CHARACTER", "CHECK", "CLASS", "CLOB", "CLOSE", "CLUSTER", "CLUSTERED", "CLUSTERING", "CLUSTERS", "COALESCE", "COLLATE", "COLLATION", "COLLECTION", "COLUMN", "COLUMNS", "COMBINE", "COMMENT", "COMMIT", "COMPACT", "COMPILE", "COMPRESS", "CONDITION", "CONFLICT", "CONNECT", "CONNECTION", "CONSISTENCY", "CONSISTENT", "CONSTRAINT", "CONSTRAINTS", "CONSTRUCTOR", "CONSUMED", "CONTINUE", "CONVERT", "COPY", "CORRESPONDING", "COUNT", "COUNTER", "CREATE", "CROSS", "CUBE", "CURRENT", "CURSOR", "CYCLE", "DATA", "DATABASE", "DATE", "DATETIME", "DAY", "DEALLOCATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DEFINE", "DEFINED", "DEFINITION", "DELETE", "DELIMITED", "DEPTH", "DEREF", "DESC", "DESCRIBE", "DESCRIPTOR", "DETACH", "DETERMINISTIC", "DIAGNOSTICS", "DIRECTORIES", "DISABLE", "DISCONNECT", "DISTINCT", "DISTRIBUTE", "DO", "DOMAIN", "DOUBLE", "DROP", "DUMP", "DURATION", "DYNAMIC", "EACH", "ELEMENT", "ELSE", "ELSEIF", "EMPTY", "ENABLE", "END", "EQUAL", "EQUALS", "ERROR", "ESCAPE", "ESCAPED", "EVAL", "EVALUATE", "EXCEEDED", "EXCEPT", "EXCEPTION", "EXCEPTIONS", "EXCLUSIVE", "EXEC", "EXECUTE", "EXISTS", "EXIT", "EXPLAIN", "EXPLODE", "EXPORT", "EXPRESSION", "EXTENDED", "EXTERNAL", "EXTRACT", "FAIL", "FALSE", "FAMILY", "FETCH", "FIELDS", "FILE", "FILTER", "FILTERING", "FINAL", "FINISH", "FIRST", "FIXED", "FLATTERN", "FLOAT", "FOR", "FORCE", "FOREIGN", "FORMAT", "FORWARD", "FOUND", "FREE", "FROM", "FULL", "FUNCTION", "FUNCTIONS", "GENERAL", "GENERATE", "GET", "GLOB", "GLOBAL", "GO", "GOTO", "GRANT", "GREATER", "GROUP", "GROUPING", "HANDLER", "HASH", "HAVE", "HAVING", "HEAP", "HIDDEN", "HOLD", "HOUR", "IDENTIFIED", "IDENTITY", "IF", "IGNORE", "IMMEDIATE", "IMPORT", "IN", "INCLUDING", "INCLUSIVE", "INCREMENT", "INCREMENTAL", "INDEX", "INDEXED", "INDEXES", "INDICATOR", "INFINITE", "INITIALLY", "INLINE", "INNER", "INNTER", "INOUT", "INPUT", "INSENSITIVE", "INSERT", "INSTEAD", "INT", "INTEGER", "INTERSECT", "INTERVAL", "INTO", "INVALIDATE", "IS", "ISOLATION", "ITEM", "ITEMS", "ITERATE", "JOIN", "KEY", "KEYS", "LAG", "LANGUAGE", "LARGE", "LAST", "LATERAL", "LEAD", "LEADING", "LEAVE", "LEFT", "LENGTH", "LESS", "LEVEL", "LIKE", "LIMIT", "LIMITED", "LINES", "LIST", "LOAD", "LOCAL", "LOCALTIME", "LOCALTIMESTAMP", "LOCATION", "LOCATOR", "LOCK", "LOCKS", "LOG", "LOGED", "LONG", "LOOP", "LOWER", "MAP", "MATCH", "MATERIALIZED", "MAX", "MAXLEN", "MEMBER", "MERGE", "METHOD", "METRICS", "MIN", "MINUS", "MINUTE", "MISSING", "MOD", "MODE", "MODIFIES", "MODIFY", "MODULE", "MONTH", "MULTI", "MULTISET", "NAME", "NAMES", "NATIONAL", "NATURAL", "NCHAR", "NCLOB", "NEW", "NEXT", "NO", "NONE", "NOT", "NULL", "NULLIF", "NUMBER", "NUMERIC", "OBJECT", "OF", "OFFLINE", "OFFSET", "OLD", "ON", "ONLINE", "ONLY", "OPAQUE", "OPEN", "OPERATOR", "OPTION", "OR", "ORDER", "ORDINALITY", "OTHER", "OTHERS", "OUT", "OUTER", "OUTPUT", "OVER", "OVERLAPS", "OVERRIDE", "OWNER", "PAD", "PARALLEL", "PARAMETER", "PARAMETERS", "PARTIAL", "PARTITION", "PARTITIONED", "PARTITIONS", "PATH", "PERCENT", "PERCENTILE", "PERMISSION", "PERMISSIONS", "PIPE", "PIPELINED", "PLAN", "POOL", "POSITION", "PRECISION", "PREPARE", "PRESERVE", "PRIMARY", "PRIOR", "PRIVATE", "PRIVILEGES", "PROCEDURE", "PROCESSED", "PROJECT", "PROJECTION", "PROPERTY", "PROVISIONING", "PUBLIC", "PUT", "QUERY", "QUIT", "QUORUM", "RAISE", "RANDOM", "RANGE", "RANK", "RAW", "READ", "READS", "REAL", "REBUILD", "RECORD", "RECURSIVE", "REDUCE", "REF", "REFERENCE", "REFERENCES", "REFERENCING", "REGEXP", "REGION", "REINDEX", "RELATIVE", "RELEASE", "REMAINDER", "RENAME", "REPEAT", "REPLACE", "REQUEST", "RESET", "RESIGNAL", "RESOURCE", "RESPONSE", "RESTORE", "RESTRICT", "RESULT", "RETURN", "RETURNING", "RETURNS", "REVERSE", "REVOKE", "RIGHT", "ROLE", "ROLES", "ROLLBACK", "ROLLUP", "ROUTINE", "ROW", "ROWS", "RULE", "RULES", "SAMPLE", "SATISFIES", "SAVE", "SAVEPOINT", "SCAN", "SCHEMA", "SCOPE", "SCROLL", "SEARCH", "SECOND", "SECTION", "SEGMENT", "SEGMENTS", "SELECT", "SELF", "SEMI", "SENSITIVE", "SEPARATE", "SEQUENCE", "SERIALIZABLE", "SESSION", "SET", "SETS", "SHARD", "SHARE", "SHARED", "SHORT", "SHOW", "SIGNAL", "SIMILAR", "SIZE", "SKEWED", "SMALLINT", "SNAPSHOT", "SOME", "SOURCE", "SPACE", "SPACES", "SPARSE", "SPECIFIC", "SPECIFICTYPE", "SPLIT", "SQL", "SQLCODE", "SQLERROR", "SQLEXCEPTION", "SQLSTATE", "SQLWARNING", "START", "STATE", "STATIC", "STATUS", "STORAGE", "STORE", "STORED", "STREAM", "STRING", "STRUCT", "STYLE", "SUB", "SUBMULTISET", "SUBPARTITION", "SUBSTRING", "SUBTYPE", "SUM", "SUPER", "SYMMETRIC", "SYNONYM", "SYSTEM", "TABLE", "TABLESAMPLE", "TEMP", "TEMPORARY", "TERMINATED", "TEXT", "THAN", "THEN", "THROUGHPUT", "TIME", "TIMESTAMP", "TIMEZONE", "TINYINT", "TO", "TOKEN", "TOTAL", "TOUCH", "TRAILING", "TRANSACTION", "TRANSFORM", "TRANSLATE", "TRANSLATION", "TREAT", "TRIGGER", "TRIM", "TRUE", "TRUNCATE", "TTL", "TUPLE", "TYPE", "UNDER", "UNDO", "UNION", "UNIQUE", "UNIT", "UNKNOWN", "UNLOGGED", "UNNEST", "UNPROCESSED", "UNSIGNED", "UNTIL", "UPDATE", "UPPER", "URL", "USAGE", "USE", "USER", "USERS", "USING", "UUID", "VACUUM", "VALUE", "VALUED", "VALUES", "VARCHAR", "VARIABLE", "VARIANCE", "VARINT", "VARYING", "VIEW", "VIEWS", "VIRTUAL", "VOID", "WAIT", "WHEN", "WHENEVER", "WHERE", "WHILE", "WINDOW", "WITH", "WITHIN", "WITHOUT", "WORK", "WRAPPED", "WRITE", "YEAR", "ZONE "]

うまくできました。

まとめ

思いつきで作成してみましたが、AWSのページが閉鎖されたりHTML構造が変更されない限り、仕様変更などは気にせずに使えそうです。

あとBeautifulSoup初めて使いましたが、かなり便利。

続きを読む

サーバ死活監視→メール送付をLambda、AmazonSNSで実現する

AWSを使ってのサーバの死活監視ってよくやると思うんですが、大体問題があった時の通知の飛ばし先はSlackだったりします。(無料だし)

とはいえ仕事で使おうとすると、Slackは見れない状況もあるかもしれないと思い、メールで通知を飛ばす、という観点でAWSを利用し実現してみました。

最終的に利用できるようにしたものは以下に登録してあります。

https://github.com/kojiisd/aws-server-monitor

サーバ監視からのメール送付までの仕組み

公開されているサービスに対してREST通信を行い、そのステータスコードを見るだけのシンプルな死活監視を行います。

ただし何も考えずに実装すると、エラーが起きてから回復するまで、定期実行のたびに何度もエラーメールを投げてしまう仕組みとなるため、管理する側としてはうっとおしくもあり(何回もメールが来る)コスト的にも嬉しくありません。またエラー発生時だけメールを飛ばす仕組みとすると、復旧したかどうかがわからず、管理者はヒヤヒヤすることになります。

そこで、現在のステータスをDynamoDBで管理するようにし、状態が変更されたタイミングでメールを送る(異常発生/復旧)という仕組みにしてみました。

監視対象のサーバ情報はS3から取得してくるようにします。

まずはサーバ死活監視とメール送付部分の作成

状態の監視は後ほど加えるとして、まずはメインとなるサーバ死活監視とメール送付のLambda実装です。Python3.6で実装しています。

サーバ死活監視処理

S3から監視対象のデータを持ってくるところと、サーバ監視を実施してエラーが発生したサーバの一覧を作成する処理です。Lambda実行の際の権限の割り振り忘れに注意。

def get_target_servers():
    s3 = boto3.resource('s3')
    obj = s3.Object(BUCKET_NAME, OBJECT_NAME)
    response = obj.get()
    body = response['Body'].read()
    return body.decode('utf-8')

def check_target_servers(target_json):
    data = json.loads(target_json)
    servers = data['servers']

    error_servers = []

    for server in servers:
        name = server['name']
        url = server['url']
        try:
            res = requests.get(url)
            if res.status_code != 200:
                error_servers.append(server)
        except Exception:
            error_servers.append(server)

    if len(error_servers) == 0:
        print("Successful finished servers checking")
    else:
        response = send_error(name, url, error_servers)
        print("Error occured:")
        print(response)
        print(error_servers)

S3上には以下のようなjsonデータを配置するようにします。

{
    "servers": [
      { "name": "googlea", "url": "http://www.google.coma" },
      { "name": "googleb", "url": "http://www.google.comb" },
      { "name": "google", "url": "http://www.google.com" }
    ]
}

上記を監視対象とすると、最後以外がアクセス失敗するので、エラーメールが飛ぶことになります。
以下の値は環境変数として定義しており、Lambda実行前に定義が必要です。

変数名 内容
S3_BUCKET_NAME S3の対象バケット名
S3_OBJECT_NAME S3の対象オブジェクト名
SNS_TOPICS_NAME SNSの送付対象トピック名
DDB_TABLE_NAME DynamoDBのテーブル名

メール送付処理

とりあえずこんな感じで書けばSNSは呼び出せるので、これをカスタマイズします。

import json
import boto3

sns = boto3.client('sns')

def lambda_handler(event, context):
    sns_message = "Test email"

    topic = 'arn:aws:sns:us-east-1:<ACCOUNT_ID>:<TOPICS_NAME>'
    subject = 'Test-email'
    response = sns.publish(
        TopicArn=topic,
        Message=sns_message,
        Subject=subject
    )
    return 'Success'

カスタマイズをしてこんな感じでメソッドにして組み込みました。

def send_error(name, url, error_servers):
    sns = boto3.client('sns')
    sns_message = "Error happens:nn" + json.dumps(error_servers, indent=4, separators=(',', ': '))

    subject = '[ServerMonitor] Error happens'
    response = sns.publish(
        TopicArn=SNS_TOPICS_NAME,
        Message=sns_message,
        Subject=subject
    )

    return response

死活監視でエラーが発生すると以下のようなメールを受け取れます。内容は質素ですが、とりあえずこれで良しとします。

Error happens:

[
    {
        "name": "googlea",
        "url": "http://www.google.coma"
    },
    {
        "name": "googleb",
        "url": "http://www.google.comb"
    }
]

DynamoDBを使った状態管理

さて、このままでも監視はできますが、エラー発生と復旧がわかった方が良いので、DynamoDBを使って状態管理をし、変化があった場合にメールを送付するように改修します。

「url」をプライマリキー、「name」をソートキー(レンジキー)として「server-monitor」というテーブルを作成します。
取得と実装はこんな感じになります。これをカスタマイズして今回の処理に適用します。

import boto3
import json
from boto3.dynamodb.conditions import Key, Attr


dynamodb = boto3.resource('dynamodb')
table    = dynamodb.Table('server-monitor')

def lambda_handler(event, context):
    #add_server()
    #get_server()

    return 'Finish operation'

def get_server():

    items = table.get_item(
            Key={
                 "url": "http://www.google.com",
                 "name": "google"
            }
        )

    print(items['Item'])

def add_server():
    table.put_item(
            Item={
                 "url": "http://www.google.com",
                 "name": "google",
                 "status": True
            }
        )    

カスタマイズして組み込んだ結果はこちらになります。

def check_status(url, name):
    status_ok = True
    try:
        items = dynamodb.Table(DDB_TABLE_NAME).get_item(
                Key={
                    "url": url,
                    "name": name
                }
            )
        status_ok = items['Item']['status']
    except:
        status_ok = None
    return status_ok

def add_server(url, name, status):
    dynamodb.Table(DDB_TABLE_NAME).put_item(
        Item={
                "url": url,
                "name": name,
                "status": status
        }
    )

死活監視後のエラー判定部分も、DynamoDBから現在の状況を確認してメール通知をする処理の追加が必要になります。こんな感じです。

def check_target_servers(target_json):
    data = json.loads(target_json)
    servers = data['servers']

    status_changed_servers = []

    for server in servers:
        name = server['name']
        url = server['url']
        status_ok = check_status(url, name)
        try:
            res = requests.get(url)
            if res.status_code != 200:
                if status_ok != False:
                    server['status'] = "Error"
                    status_changed_servers.append(server)
                add_server(url, name, False)
            else:
                if status_ok == False:
                    server['status'] = "Recover"
                    status_changed_servers.append(server)
                add_server(url, name, True)
        except Exception:
            if status_ok != False:
                server['status'] = "Error"
                status_changed_servers.append(server)
            add_server(url, name, False)

    if len(status_changed_servers) == 0:
        print("Successful finished servers checking")
    else:
        response = send_error(name, url, status_changed_servers)
        print("Error occured:")
        print(response)
        print(status_changed_servers)

動作確認

今までの実装でエラーが発生した時には「Error」と、復旧した時には「Recover」という内容のメールが送信されるようになっているはずです。動作確認をしてみます。

以前作成したWebページがS3上にあるので、アクセス権限を変更してテストしてみます。

Three.jsのかっこいいサンプルとAWSを連携させてみた

OKパターンで実行してみる。。。

メールは送信されず、DynamoDBにデータだけ追加されました。

スクリーンショット 2017-09-24 19.17.32.png

さて、アクセス権限を変更して、アクセスできなくしてから再度実行してみます。無事エラーが発生してメールが飛んで来ました。状態は「Error」です。

Server Status Changed happens:

[
    {
        "name": "s3-test",
        "url": "https://s3.amazonaws.com/xxxxxx/xxxx/css3d_periodictable.html",
        "status": "Error"
    }
]

DynamoDBのデータもエラーを表す「false」にstatusカラムが変更されています。

スクリーンショット 2017-09-24 19.21.23.png

もう一度実行しても、エラーメールは飛んでこないようです。無事状態判定をしてくれています。DynamoDBの値も変化なしです。(キャプチャだけだと何もわかりませんがw)

スクリーンショット 2017-09-24 19.23.43.png

それではページを復旧してみます。またアクセス権限を元に戻してアクセス可能にし、サーバ死活監視処理を実行してみます。

Server Status Changed happens:

[
    {
        "name": "s3-test",
        "url": "https://s3.amazonaws.com/xxxxxx/xxxx/css3d_periodictable.html",
        "status": "Recover"
    }
]

無事復旧メールが届きました。これで完成です。

まとめ

サーバの死活監視を、状態管理しながら実施してみました。今回はAWSのサービスを中心に実現しましたが、おかげでトータル数時間レベルで実現できました。かかるコストも抑えつつ、実際に使えそうなものがこれくらいスピーディーに作れてしまうのは、さすがAWSのマネージドサービス、といったところです。

[おまけ]ライブラリの管理

Lambdaでデプロイパッケージを作成する際、普通に実装すると外部ライブラリをプロジェクトのルートディレクトリに置くことになるので、あまり綺麗なパッケージ構成になりません。

そこで今回、こちらのページを参考にさせてもらいながら、別ディレクトリにパッケージを配置してく方法をとりました。

# pip install -U requests -t ./lib

で、Pythonにはこういう処理を追加する。

sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib'))

import requests

こうするだけでlib配下のパッケージを見に行ってくれるようになりました。これはディレクトリ管理することを考えるとかなり助かりました。実際serverlessコマンドでデプロイしても、正しく動きました。

続きを読む

Three.jsのかっこいいサンプルとAWSを連携させてみた

かっこいい画面が作りたかったので、やってみた備忘録。

やりたいこと

Three.jsがとてもかっこいいんですが、値の設定が静的なので、何とか動的に値を変えつつ画面に反映できないかな、、、と。このサンプルを使ってみます。

スクリーンショット 2017-05-14 19.08.58.png

イメージとしては、この画像で異常が検出されたところを赤くアラート表示したりできないかな、と。

実際の流れの整理

Githubからサンプルを持って来て、必要なものだけ抜き出します。

異常があったら表示する、という感じにしたいので、以前投稿したロジックを使います。

AWS LambdaでDynamoDBから取得した値に任意の集計をかける(グルーピング処理追加)

最新値を取得する中で、異常があったら対象の項目に変化をつけたいと思います。

とりあえずサンプルをAWS上で動かす

AWS上で動作させたいので、とりあえずこのサンプルをAWS上に乗せる必要があります。

  1. 必要なファイルの抽出(抽出結果などはこちらに。https://github.com/kojiisd/aws-threejs
  2. 適当な名前のバケットをS3に作成(今回は「aws-three」という名前にしました)
  3. ファイルのアップロード
  4. パーミッションの適用

パーミッションの適用は、とりあえず以下のコマンドで一括でつけました。実際の運用を考えると、もっと慎重にならないといけないところだとは思います。こちらのサイトを参考にさせてもらいました。

$ aws s3 ls --recursive s3://aws-three/ | awk '{print $4}' | xargs -I{} aws s3api put-object-acl --acl public-read --bucket aws-three --key "{}"

これで一旦サンプルがS3上で動くようになりました。

データの中身を理解する

実際にソースを読んで見ると、一つ一つのオブジェクトをどの様に格納しているかがわかります。

var table = [
    "H", "Hydrogen", "1.00794", 1, 1,
    "He", "Helium", "4.002602", 18, 1,
    "Li", "Lithium", "6.941", 1, 2,
    "Be", "Beryllium", "9.012182", 2, 2,
    :
    :

5つの要素で1セットとしてオブジェクトを表示している様です。元の実装がいいかどうかは置いておいて(^^;とりあえずこれを動的に変更できるようにします。

DBからの値をtableに反映する

データの構造自体はとても単純なので、1レコード5カラムのデータを持つテーブルを作成すれば、後々データの内容全てをDBから持ってくることも可能になりそうです。

DynamoDBで下記の様な単純なテーブルを作成します。もちろんAWS Consoleから作成可能ですが、Localで同じスキーマを作成したい場合はこんな感じで定義を流し込めばOK。

var params = {
    TableName: "test-three",
    KeySchema: [
        {
            AttributeName: "id",
            KeyType: "HASH"
        },
        {
            AttributeName: "timestamp",
            KeyType: "RANGE"
        }
    ],
    AttributeDefinitions: [
        {
            AttributeName: "id",
            AttributeType: "S"
        },
        {
            AttributeName: "timestamp",
            AttributeType: "S"
        }
    ],
    ProvisionedThroughput: {
        ReadCapacityUnits: 1,
        WriteCapacityUnits: 1
    }
};

dynamodb.createTable(params, function(err, data) {
    if (err) ppJson(err);
    else ppJson(data);
});

はい、無事出来ました。

スクリーンショット 2017-07-09 16.12.16.png

データの準備と投入

次にデータを流し込みます。こんな感じでpythonスクリプトでサクッと。実行する前に、クライアントのAWS認証設定が正しいことを確認してください。

data-insert.py
args = sys.argv

if len(args) < 4:
    print "Usage: python data-insert.py <FileName> <Region> <Table>"
    sys.exit()

dynamodb = boto3.resource('dynamodb', region_name=args[2])
table = dynamodb.Table(args[3])

if __name__ == "__main__":
    print "Data insert start."
    target = pandas.read_csv(args[1])
    for rowIndex, row in target.iterrows():
        itemDict = {}
        for col in target:
            if row[col] != None and type(row[col]) == float and math.isnan(float(row[col])) and row[col] != float('nan'):
                continue
            elif row[col] == float('inf') or row[col] == float('-inf'):
                continue
            elif type(row[col]) == float:
                itemDict[col] = Decimal(str(row[col]))
            else:
                itemDict[col] = row[col]
        print itemDict

        table.put_item(Item=itemDict)

    print "Data insert finish."

とりあえずこんなデータを用意しました。先ほどのスクリプトで投入します。

id,score,timestamp
"H",0,"2017-07-23T16:00:00"
"H",0,"2017-07-23T16:01:00"
"H",0,"2017-07-23T16:02:00"
"H",0,"2017-07-23T16:03:00"
"H",0,"2017-07-23T16:04:00"
"H",1,"2017-07-23T16:05:00"
"H",0,"2017-07-23T16:06:00"

無事投入できました。

$ python data-insert.py sample-data.csv us-east-1 test-three
Data insert start.
{'timestamp': '2017-07-23T16:00:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:01:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:02:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:03:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:04:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:05:00', 'score': 1, 'id': 'H'}
{'timestamp': '2017-07-23T16:06:00', 'score': 0, 'id': 'H'}
Data insert finish.

スクリーンショット 2017-07-23 17.33.30.png

API Gatewayの設定

絞り込み結果を得るためのLambdaを実行するRESTの口を設けます。API Gatewayを設定してPOST通信でデータが来た場合に該当のLambdaを実行するようにします。

スクリーンショット 2017-07-23 18.13.07_dec.png

画面からDynamoDBのデータを取得する

定期的に画面からDynamoDBのデータを取得します。今回はCognitoによるセキュアな通信は実装しません。他のページで色々と実装されている方がいるので、そちらを参考にして見てください。

処理の流れとしては、以下のような感じ。(S3からの取得処理が定期的に実行される)

S3 → API Gateway → Lambda → DynamoDB

API GatewayでSDK生成

API Gatewayの設定を以下にし、SDKを生成します。

  1. HTTPメソッドはPOSTを指定
  2. とりあえず認証などは今回はかけない

で、作成されたSDKを、本家のページを参考に埋め込みます。

API Gateway で生成した JavaScript SDK を使用する

以下のコードをScriptタグ部分に貼り付けました。

        <script type="text/javascript" src="../extralib/axios/dist/axios.standalone.js"></script>
        <script type="text/javascript" src="../extralib/CryptoJS/rollups/hmac-sha256.js"></script>
        <script type="text/javascript" src="../extralib/CryptoJS/rollups/sha256.js"></script>
        <script type="text/javascript" src="../extralib/CryptoJS/components/hmac.js"></script>
        <script type="text/javascript" src="../extralib/CryptoJS/components/enc-base64.js"></script>
        <script type="text/javascript" src="../extralib/url-template/url-template.js"></script>
        <script type="text/javascript" src="../extralib/apiGatewayCore/sigV4Client.js"></script>
        <script type="text/javascript" src="../extralib/apiGatewayCore/apiGatewayClient.js"></script>
        <script type="text/javascript" src="../extralib/apiGatewayCore/simpleHttpClient.js"></script>
        <script type="text/javascript" src="../extralib/apiGatewayCore/utils.js"></script>
        <script type="text/javascript" src="../extralib/apigClient.js"></script>

実際にAPI Gatewayにアクセスするスクリプトはこんな感じになります。

SDKを用いてAPI Gatewayにブラウザからアクセスしてみる

var apigClient = apigClientFactory.newClient();

var params = {
    // This is where any modeled request parameters should be added.
    // The key is the parameter name, as it is defined in the API in API Gateway.
    "Content-Type": "application/x-www-form-urlencoded"
};

var body = {

    "label_id": "id",
    "label_range": "timestamp",
    "id": [
    "H"
    ],
    "aggregator": "latest",
    "time_from": "2017-07-23T16:00:00.000",
    "time_to": "2017-07-23T16:06:00.000",
    "params": {
    "range": "timestamp"
    }
};


  var additionalParams = {
    // If there are any unmodeled query parameters or headers that must be
    //   sent with the request, add them here.
    headers: {
    },
    queryParams: {
    }
  };

  apigClient.rootPost(params, body, additionalParams)
      .then(function(result){
        // Add success callback code here.
        alert("Success");
        alert(JSON.stringify(result));
      }).catch( function(result){
        // Add error callback code here.
      });

パラメータ(body部)には「AWS LambdaでDynamoDBから取得した値に任意の集計をかける(グルーピング処理追加)」で必要になるJSONを渡します。

API GatewayにアクセスするにはCORSの設定が必要なので、AWS Consoleから設定します。この際Resourcesで変更したものはStaging環境に再デプロイをしないと追加で設定した項目は反映されないので注意(ハマった。。。)

「Enable CORS」から設定できます。

スクリーンショット 2017-09-10 10.56.00.png

実際に画面にアクセスすると、サーバから値が取れたことがわかります。(今回はAPI KEYなどはOFFにしていますが、実際に運用するとなったら、もちろん考慮が必要です。)

スクリーンショット 2017-09-10 11.03.04.png

スクリーンショット 2017-09-10 11.03.11_deco.png

無事値が取れました。ここまでくればもう一息です。

取得した値を元に画面に変更を加える

さて、先ほど取得できた値には「score」というものが入っています。この「score」値を元に画面のコンポーネントに変化を加えます。

持ってきたサンプルコードの中で色に影響を与えているのは以下のコードになります。

for ( var i = 0; i < table.length; i += 5 ) {

    var element = document.createElement( 'div' );
    element.className = 'element';
    element.style.backgroundColor = 'rgba(0,127,127,' + ( Math.random() * 0.5 + 0.25 ) + ')';

全てのコンポーネントに対してrgbaで値を与えているので、ここを変更します。とはいえ、そのまま変更しようとしてもinit()メソッドを実行すると追加でコンポーネントが描画されてしまうので、描画後に変更が可能なように以下のようなidの埋め込みを行います。

var element = document.createElement( 'div' );
element.className = 'element';
element.id = table[ i ];                        // ここが追加したところ
element.style.backgroundColor = 'rgba(0,127,127,' + ( Math.random() * 0.5 + 0.25 ) + ')';

これらの準備を前提として、処理の流れは以下とします。

  1. DynamoDBから取ってきたjson値をオブジェクトに変換。
  2. 一旦オブジェクト配列としてidとscore値を持たせるようにする。
  3. オブジェクト配列でループ処理を実装し、その中でscore値が「0」ではないidに対して、色の変更を実施する。

今回の準備を踏まえると、上記で対象となるのは「id: H」となります。DynamoDBからデータを取得するために、rootPost呼び出し後のコールバック処理を以下のように変更します。今回色変更の処理をシュッと実装するために、jQueryを読み込ませています。

apigClient.rootPost(params, body, additionalParams)
    .then(function(result){
        var resultObjArray= new Array();
        // Add success callback code here.
        var resultJson = JSON.parse(result.data);
        for (var index = 0; index < resultJson.length; index++) {
            var resultObj = new Object();
            resultObj.id = resultJson[index].id;
            resultObj.score = resultJson[index].score
            resultObjArray[index] = resultObj;
        }

        for (var index = 0; index < resultObjArray.length; index++) {
            var resultObj = resultObjArray[index];
            if (resultObj.score != 0) {
                $('#' + resultObj.id).css('background-color', 'rgba(255,127,127,' + ( Math.random() * 0.5 + 0.25 ) + ')')
            }
        }

    }).catch( function(result){
        // Add error callback code here.
        alert("Failed");
        alert(JSON.stringify(result));
    });

DynamoDBに格納されている値(検索期間の最新)を「1」に変更して画面を更新すると、無事赤くなりました。

スクリーンショット 2017-09-10 18.58.41.png

実行タイミングを任意にするためにボタンからのクリックイベントとして実装する

このままでもいいのですが、今のままだと最初のアニメーションが終わる前に色が変わるので、色が変わった感がありません。ですので一つボタンを追加してクリックしたら画面に値を反映する、という手法を取ろうと思います。

先ほどのrootPostを呼び出すプログラムを、画面に配置したボタンのイベントとして処理させます。

<button id="getData">Get Data from DynamoDB</button>

画面にはこんな感じでボタンを配置し、jQueryでイベントを登録します。

$(document).ready(function(){
    $("#getData").click(function() {
        apigClient.rootPost(params, body, additionalParams)
        .then(function(result){
            var resultObjArray= new Array();
            // Add success callback code here.
            var resultJson = JSON.parse(result.data);
            for (var index = 0; index < resultJson.length; index++) {
                var resultObj = new Object();
                resultObj.id = resultJson[index].id;
                resultObj.score = resultJson[index].score
                resultObjArray[index] = resultObj;
            }

            for (var index = 0; index < resultObjArray.length; index++) {
                var resultObj = resultObjArray[index];
                if (resultObj.score != 0) {
                    $('#' + resultObj.id).css('background-color', 'rgba(255,127,127,' + ( Math.random() * 0.5 + 0.25 ) + ')')
                }
            }

        }).catch( function(result){
            // Add error callback code here.
            alert("Failed");
            alert(JSON.stringify(result));
        });
    });
});

これで無事画面から操作することが可能になりました。ボタンを押すといい感じに「H」の要素が赤く光っています。

スクリーンショット 2017-09-10 23.08.28.png

まとめ

もともとかっこいいと思っていたThree.jsの画面をカスタマイズして監視画面(IoTとかのデバイス監視画面)のような機能にできないものか、と実装してみましたが、なんとかここまでたどり着けました。

DynamoDBからの取得間隔やIDの指定方法など、まだまだハードコーディングな部分はありますが、土台は出来上がったのであとはちょっとのカスタマイズで実際に使えそうなところまではイメージが持てました。元々の実装の仕組みも理解できたので、条件が合えば文言を変えるなども実現できそうです。

やっぱり業務で使うものもかっこいい画面でないとね、と思うこの頃です。
(GithubのコードはAPI GatewayのSDKさえ埋め込めば動くような作りにしています)

https://github.com/kojiisd/aws-threejs

続きを読む

AWS LambdaでDynamoDBから取得した値に任意の集計をかける(グルーピング処理追加)

以前の投稿の更新版です。前の仕組みでは一つのデータに対しての集計しかけられなかったのですが、それでは実運用上あまりに不便、と思ったので、指定した値でグルーピングできるようにしてみました。

AWS LambdaでDynamoDBから取得した値に任意の集計をかける

インプットデータのフォーマット変更

ほとんど使い方は以前のものと一緒ですが、以下の点だけ変えました。

  1. IDは配列形式([“sensor1”, “sensor2”])で指定するように仕様変更した。
  2. テーブル名を環境変数から取得するようにした。

IDは配列形式([“sensor1”, “sensor2”])で指定するように仕様変更した

こんな感じが最新のフォーマットです。

{
  "label_id": "id",
  "label_range": "timestamp",
  "id": [
    "sensor1",
    "sensor2"
  ],
  "aggregator": "latest",
  "time_from": "2017-04-30T22:00:00.000",
  "time_to": "2017-04-30T22:06:00.000",
  "params": {
    "range": "timestamp"
  }
}

IDの部分を配列にしました。こうすることで指定したIDの最新値を取得することが可能になります。
戻り値はこんな感じになります。

"[{"timestamp": "2017-04-30T22:05:00.000", "score": 0.0, "id": "sensor1"}, {"timestamp": "2017-04-30T22:06:00.000", "score": 1.0, "id": "sensor2"}]"

ちなみにDynamoDBにはこんな感じの値を用意していました。

スクリーンショット 2017-07-19 18.12.43.png

テーブル名を環境変数から取得するようにした

そのまんまです。 handler.pyの os.environ['TABLE'] の部分です。Lambda実行時に環境変数をこんな感じで指定してください。

スクリーンショット 2017-07-19 18.13.46.png

handler.py
import sys
import boto3
import json
import decimal
import os
from boto3.dynamodb.conditions import Key

from aggregator.lambda_aggregator import LambdaAggregator
from aggregator.latest_aggregator import LatestAggregator
from aggregator.max_aggregator import MaxAggregator
from aggregator.min_aggregator import MinAggregator
from aggregator.sum_aggregator import SumAggregator
from aggregator.avg_aggregator import AvgAggregator
from aggregator.count_aggregator import CountAggregator

dynamodb = boto3.resource('dynamodb')
table    = dynamodb.Table(os.environ['TABLE'])

aggregator_map = {}
aggregator_map['latest'] = LatestAggregator()
aggregator_map['max'] = MaxAggregator()
aggregator_map['min'] = MinAggregator()
aggregator_map['sum'] = SumAggregator()
aggregator_map['avg'] = AvgAggregator()
aggregator_map['count'] = CountAggregator()

def run(event, context):
    check_params(event)
    result = []

    for id in event['id']:
        res = table.query(
                KeyConditionExpression=Key(event['label_id']).eq(id) & Key(event['label_range']).between(event['time_from'], event['time_to']),
                ScanIndexForward=False
            )

        return_response = aggregator_map[event['aggregator']].aggregate(res['Items'], event['params'])
        result.append(return_response)

    return json.dumps(result, default=decimal_default)

def decimal_default(obj):
    if isinstance(obj, decimal.Decimal):
        return float(obj)
    raise TypeError

def check_params(params):
    if 'label_id' not in params or 'label_range' not in params or 'id' not in params or 'aggregator' not in params or 'time_from' not in params or 'time_to' not in params or 'params' not in params:
        sys.stderr.write("Parameters for label_id, label_range, id, aggregator, time_from, time_to and params are needed.")
        sys.exit()

ソース

こちらにコミットしています。(以前のものを更新)

https://github.com/kojiisd/lambda-dynamodb-aggregator

続きを読む

AWS LambdaからKintoneアプリケーションのレコードを更新してみる

AWS LambdaからKintoneのアプリケーションレコードを更新してみたのでその備忘録。

前提

  1. AWS Lambda Pythonを使います。
  2. LambdaはVPC内には配置しません。
  3. Kitoneのアプリケーションにはデフォルトで用意されている「備品在庫管理」を利用します。

Kintone側の設定

スクリーンショット 2017-05-28 17.05.06.png

手動で登録したこのレコード(ボールペン)をAWS Lambdaから変更したいと思います。

基本的にはこちらのサイトを参考にさせてもらいました。とてもわかりやすかったです。バージョンの違いなのか若干書き方が異なるところがあったりしましたが、なんとかなりました。

http://www.yamamanx.com/kintone-python-query-check/

Lambdaの実装

以下のような実装になります。一応メソッドによって処理を変更できるような作りにはしてはみましたが、実運用で使うならきちんと設計したほうが良いですね。

def kintone_operator(event, context):
    KINTONE_URL = "https://{kintone_domain}/k/v1/record.json"
    url = KINTONE_URL.format(
        kintone_domain=os.environ['KINTONE_DOMAIN'],
        kintone_app=os.environ['KINTONE_APP']
    )
    headers_key = os.environ['KINTONE_HEADERS_KEY']
    api_key = os.environ['KINTONE_API_KEY']
    basic_headers_key = os.environ['KINTONE_BASIC_HEADERS_KEY']
    basic_headers_value =os.environ['KINTONE_BASIC_HEADERS_VALUE']

    headers = {headers_key: api_key}
    headers["Content-Type"] = "application/json"
    if basic_headers_value != '':
        headers[basic_headers_key] = basic_headers_value

    if event['api'] == 'PUT':
        result = put_record(headers, url, event['data'])
    else:
        result = 'NO KEY'

    return result

def put_record(headers, url, data):
    response_record = requests.put(url, json=data, headers=headers)
    record_data = json.loads(response_record.text)

    return record_data

全て大文字の変数は環境変数としてLambdaで定義しました。API Keyなどを公開しなくて済むので非常に楽です。

実行時には以下のようなデータを送付しました。

{
  "api": "PUT",
  "data": {
      "app": 498,
      "id": 1,
      "record": {
          "文字列__1行_": {
              "value": "シャープペンシル"
          }
      }
  }
}

で、実行してみたところ、無事更新されたようです。

スクリーンショット 2017-06-06 19.45.48.png

まとめ

いろいろなサイトをベースにさせてもらい、更新機能を作ってみました。うまくAPI Gatewayやら他の機能やらと連携できれば、KintoneとAWSでどんどんとできることが増えてきますね。

続きを読む

AWS LambdaでDynamoDBから取得した値に任意の集計をかける

前回書いた記事「AWS LambdaでDynamoDBから取得した値の最新レコードを取得する」を、以下の集計にも対応させてみました。

  • 最新値 (latest)
  • 最大値 (max)
  • 最小値 (min)
  • 平均値 (avg)
  • 合計値 (sum)
  • 件数 (count)

前提&仕様

以下を前提&仕様として作成しました。

  1. DynamoDBのハッシュキーにID(文字列)、レンジキーに時刻(文字列)を指定しているものとする。
  2. データは時系列に格納されているものとする。
  3. 指定したカラムの集計を取るようにする。
  4. 集計の時間を指定できるようにする。
  5. 集計の種類は指定可能とする。
  6. ID、集計期間、集計対象のカラム、集計の種類はそれぞれJSONのインプットで指定可能とする。

結果

こちらに登録してあります。

https://github.com/kojiisd/lambda-dynamodb-aggregator

サンプルデータ

以下のデータをDynamoDBに入れました。これについて色々と操作をしようと思います。

スクリーンショット 2017-05-03 11.49.57.png

使い方

インプットデータの準備

以下のインプットデータが必要になります。

カラム名 内容
label_id DynamoDBで指定しているハッシュキー名
label_range DynamoDBで指定しているレンジキー名
id 集計したいIDの値
aggregator 集計種別。種別は最初に書いた6種類に対応
time_from 集計対象期間(開始)
time_to 集計対象期間(終了)
params [個別]集計時必要になるパラメータ

インプットデータの準備(個別部)

上述の「params」に該当する部分は以下の様なパラメータの準備が必要になります。

集計種別 必要な値
最新値(latest) range: レンジキー名(共通部で指定していますが、一応他に体裁を合わせました)
最大値(max) score: 集計対象のカラム名
最小値(min) score: 集計対象のカラム名
平均値(avg) score: 集計対象のカラム名
合計値(sum) score: 集計対象のカラム名
件数(count) score: 集計対象のカラム名

こんな感じで指定すれば実行可能になります。

とりあえず実行してみる

とりあえず動かしてみたい人は以下の様な感じで実行すれば結果が見れます。今回はSERVERLESS FRAMEWORK上で動かしています。

「sensor1」に対して指定した時間の中での:

最新値(latest)
$ sls invoke local -f run -d '{"label_id": "id", "label_range": "timestamp", "id": "sensor1", "aggregator": "latest", "time_from": "2017-04-30T22:00:00.000", "time_to": "2017-04-30T22:05:00.000", "params": {"range": "timestamp"}}'
"{"timestamp": "2017-04-30T22:05:00.000", "score": 0.0, "id": "sensor1"}"
最大値(max)
$ sls invoke local -f run -d '{"label_id": "id", "label_range": "timestamp", "id": "sensor1", "aggregator": "max", "time_from": "2017-04-30T22:00:00.000", "time_to": "2017-04-30T22:05:00.000", "params": {"score": "score"}}'
"{"timestamp": "2017-04-30T22:04:00.000", "score": 1.0, "id": "sensor1"}"

(同じ値がヒットした場合は新しい方のデータを取得する様にしています。)

最小値(min)
$ sls invoke local -f run -d '{"label_id": "id", "label_range": "timestamp", "id": "sensor1", "aggregator": "min", "time_from": "2017-04-30T22:00:00.000", "time_to": "2017-04-30T22:05:00.000", "params": {"score": "score"}}'
"{"timestamp": "2017-04-30T22:05:00.000", "score": 0.0, "id": "sensor1"}"
平均値(avg)
$ sls invoke local -f run -d '{"label_id": "id", "label_range": "timestamp", "id": "sensor1", "aggregator": "avg", "time_from": "2017-04-30T22:00:00.000", "time_to": "2017-04-30T22:05:00.000", "params": {"score": "score"}}'
"0.3333333333333333"
合計値(sum)
$ sls invoke local -f run -d '{"label_id": "id", "label_range": "timestamp", "id": "sensor1", "aggregator": "sum", "time_from": "2017-04-30T22:00:00.000", "time_to": "2017-04-30T22:05:00.000", "params": {"score": "score"}}'
"2.0"
件数(count)
$ sls invoke local -f run -d '{"label_id": "id", "label_range": "timestamp", "id": "sensor1", "aggregator": "count", "time_from": "2017-04-30T22:00:00.000", "time_to": "2017-04-30T22:05:00.000", "params": {"score": "score"}}'
"6"

仕組み

最新値

以前の記事と同様、指定したレンジキーの最大値を取るようにしただけです。

max(data, key=(lambda x:x[params['range']]))

最大値 / 最小値

max関数とmin関数が使えるようにこんな実装にしています。

max_aggregator.py
max(data, key=(lambda x:x[params['score']]))
min_aggregator.py
min(data, key=lambda x: x[params['score']])

平均値

後述の合計値と件数を使っての計算になります。

sum(map(lambda x: x['score'], data)) / len(map(lambda x: x['score'], data))

合計値

sum関数使ってサクッと計算したかったのでこんな感じです。

sum(map(lambda x: x['score'], data))

件数

対象カラムの長さをとっただけです。

len(map(lambda x: x['score'], data))

AWS Lambdaでも動くのか?

現状のserverless.ymlには権限周りの設定が足りなかったようなのですが、とりあえずデプロイ後に手動で権限を正しくセットしたらうまくいきました。

とりあえずデプロイは何も問題なく完了。

$ sls deploy 
Serverless: Packaging service...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (10.19 KB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............
Serverless: Stack update finished...
Service Information
service: lambda-dynamodb-aggregator
stage: dev
region: us-east-1
api keys:
  None
endpoints:
  None
functions:
  run: lambda-dynamodb-aggregator-dev-run

スクリーンショット 2017-05-03 12.20.13.png

動きはしました。(件数を取得しています)

スクリーンショット 2017-05-03 12.38.32.png

続きを読む

AWS LambdaでDynamoDBから取得した値の最新レコードを取得する

DynamoDBに値が入っている前提で、

  • とある時間の
  • あるIDにおける
  • 集計値

を取ろうする際の実装。(AWS Lambda Python 2.7を使っています)

前提

以下の前提とします。

  1. DynamoDBのハッシュキーにID(文字列)、レンジキーに時刻(文字列)を指定
  2. データは時系列に格納されているものとする。
  3. 指定したカラムの集計を取るようにする。
  4. とりあえず指定したIDで全件取得(時間による絞り込みはしない)

こんな感じのデータを想定。

id score timestamp
sensor1 0 2017-04-30T22:00:00.000
sensor1 0 2017-04-30T22:00:01.000
sensor1 1 2017-04-30T22:00:02.000
sensor1 0 2017-04-30T22:00:03.000
sensor2 1 2017-04-30T22:00:04.000

このうちsensor1の最新値を取るための実装をしてみます。結果としてscore:0が取得できるロジックを期待します。

実装

実行環境にはSERVERLESS FRAMEWORKを利用しました。こんな感じの実装でできました。

import boto3
import json
import decimal
from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource('dynamodb')
table    = dynamodb.Table('test')

def run(event, context):

    res = table.query(
            KeyConditionExpression=Key('id').eq("sensor1")
        )

    return_response = max(res["Items"], key=(lambda x:x["timestamp"]))

    return json.dumps(return_response, default=decimal_default)

def decimal_default(obj):
    if isinstance(obj, decimal.Decimal):
        return float(obj)
    raise TypeError

まずは最新値を求めるというところで手抜き感満載の実装ですが、Aggregate部分を別クラスにしてしまって少しインテリジェンスに選択できるようにすれば、一気に用途が広がる気がします。

実行結果は以下の通りです。無事取得できてますね。

$ sls invoke local -f run
"{"timestamp": "2017-04-30T22:03:00.000", "score": 0.0, "id": "sensor1"}"

続きを読む