多段CNAMEとパフォーマンス、およびAWS Route53のエイリアスレコードについて

概要

CNAMEレコードの設定値(参照先1)に、別の箇所でCNAMEとして設定されたドメイン名を指定することはできるか。できたとして、問題は無いのか気になったので調べた。

言葉にするとわかりにくいが、下記のような設定が可能かどうかだ。

web2.example.con. IN CNAME web1.example.com. 
web1.example.com. IN CNAME www.example.com.
www.example.com. IN A  192.0.2.1

“web2″のCNAMEで”web1″が指定され、”web1″のCNAMEで”www”が指定されるよな、CNAMEが連鎖する設定を「多段CNAME」とこの記事では呼ぶ。2

そして、多段CNAMEの短所を補うAWS Route53のエイリアスレコード(Aliasレコード)について最後に紹介する。

結論

  • 多段CNAMEの設定は可能
  • ただし、パフォーマンスに問題があるため避けたほうがよい。使う場合も多段の階層を深くしない
  • AWSでは、エイリアスレコード(Aliasレコード)を使える場合は使ったほうがよい

多段CNAMEは可能か

直感的に多段CNAMEは問題を起こしそうな気がしなくもないが、実際のところどうなのか。

DNSの基本的な仕様はRFCは1034と1035にまとめられているが3、いずれも多段CNAMEを禁止する記述は見当たらなかった。
下記の『CNAMEとパフォーマンス』でも軽く触れたが、RFC 1034に書かれているネームサーバとリゾルバの動作を見ても、問題なさそうだ。

ただし、多段CNAMEは循環的な参照を作りうる4ので、キャッシュDNSサーバ側でなんらかの制限が必要になる。
(BINDでは再帰問い合わせの回数に制限があるらしい)

CNAMEの循環的な参照については、RFCでも触れられている。

The amount of work which a resolver will do in response to a
client request must be limited to guard against errors in the
database, such as circular CNAME references, and operational
problems, such as network partition which prevents the
resolver from accessing the name servers it needs. While
local limits on the number of times a resolver will retransmit
a particular query to a particular name server address are
essential, the resolver should have a global per-request
counter to limit work on a single request.
(RFC 1034)

深すぎる多段CNAMEは、キャッシュDNSによる問い合わせが打ち切られる可能性があるため、避けるべきといえる。

(余談)CNAMEにおける制限

CNAMEに関する制限事項といえば、よく知られた話だが「CNAMEは他のリソースデータと共存できない」というものがある。

If a CNAME RR is present at a node, no other data should be present;
(RFC 1034より引用)

この件に関してはRFC 1912が詳しく、このRFC内の例を用いると、下記の設定が許可されない。(NOT ALLOWED)

           podunk.xx.      IN      NS      ns1
                           IN      NS      ns2
                           IN      CNAME   mary
           mary            IN      A       1.2.3.4

podunk.xx.にCNAMEを指定すると同時に、NSとしても指定しているため、「no other data should be present」に反している。

修正するには、下記のようする。

           podunk.xx.      IN      NS      ns1
                           IN      NS      ns2
                           IN      A       1.2.3.4
           mary            IN      A       1.2.3.4

CNAMEをAレコードに変更している。

CNAMEとパフォーマンス

CNAMEをつかうことで、問い合わせのパフォーマンスが低下する。

なぜなら、キャッシュDNSサーバはCNAMEの応答を受け取ったとき、その受け取ったドメイン名についての問い合わせを始めから実行する必要があるためだ。

つまり、キャッシュDNSサーバが”www.example.com” のAレコードを問い合わせている最中で、「www.example.com の正規名はwww.example.org である」と知った場合、今までの問い合わせを打ち切って”www.example.org” の問い合わせを実行する必要がある。

通常の2回分の問い合わせが行われることになり、あきらかに問い合わせの効率が下がる。
さらにいうと、CNAMEの段数が増えれば増えるほど、問い合わせの回数は増えていく。

このときの動作の詳細は、RFC 1034の『5.5.2 Aliases』を読むとよい。

ただし、CNAMEを多段に用いてもパフォーマンスの低下が少ないケースもある。それは、自身のゾーン内、もしくは委譲先のゾーンでCNAMEが解決される場合だ。
この点については、同じくRFC 1034の『4.3.2. Algorithm』を読んでほしい。

AWS Route53のエイリアスレコードについて

エイリアスレコード(Aliasレコード)とは

エイリアスレコードは、CNAMEとよく似た機能を提供するが、その弱点をカバーする。

まず、エイリアスレコードを知らない人のために、AWS公式ドキュメントから引用する。

Amazon Route 53 は “エイリアス” レコードを提供します (Amazon Route 53 固有の仮想レコード)。[中略]エイリアスレコードは CNAME レコードのように機能するので、DNS 名(example.com)を別の「ターゲット」DNS 名(elb1234.elb.amazonaws.com)にマッピングできます。それらはリソルバーに表示されていないという点で、CNAME レコードと異なります。リソルバーには、A レコードと結果として生じるターゲットレコードの IP アドレスだけが表示されます。

(AWS Route53ドキュメント、『よくある質問』より引用)

先ほど述べた通りだが、キャッシュDNSのサーバがCNAMEの応答を受け取ったときの動作は、返されたドメイン名に対する問い合わせを再度実行する。
これが問い合わせのコストの増加となり、問い合わせのパフォーマンス(主にクライアントへ応答を返す時間)が悪化してしまう。

しかし、エイリアスレコードを使うことで、Aレコードと同じコストでCNAMEと同様の機能を使うことができる。

エイリアスレコードとCNAME

ELBを作成すると、my-loadbalancer-1234567890.us-west-2.elb.amazonaws.comのようなドメイン名がAWSから発行される。

このドメイン名は人間にとってわかりにくいため、”my-web.example.com” のようなドメイン名を別名でつけることがある。

このとき、CNAMEを使った場合、キャッシュDNSサーバは”my-web.example.com” の問い合わせ中にCNAMEとして”my-loadbalancer-1234567890.us-west-2.elb.amazonaws.com” を受け取り、”my-loadbalancer-1234567890.us-west-2.elb.amazonaws.com”の問い合わせをルートドメインから行うことになる。

一方でエイリアスレコードを使った場合、CNAMEの応答は行われず、一連の”my-web.example.com” の問い合わせの中で、”my-loadbalancer-1234567890.us-west-2.elb.amazonaws.com” のAレコードが返される。

エイリアスレコードを使用するための条件

なぜこのようなことができるかというと、参照先のドメイン名の解決をRoute 53内部で行なっているためだ。

したがって、エイリアスレコードを使用するには2つの条件を満たす必要がある。

  • ユーザはRoute 53を使うこと(そもそもエイリアスレコードはAWSの拡張機能)
  • 参照先が、AWSの限定されたサービスであること(CloudFront, ELB, 静的ウェブサイトとしてのS3など)

エイリアスレコードを使った場合のデメリットは特に無いので、使用できるケースであれば積極的に使うべきだ。

エイリアスレコードの詳細については、Route 53のドキュメントの『エイリアスリソースレコードセットと非エイリアスリソースレコードセットの選択』を読んでほしい。

参照先として指定できるドメイン名についても、このドキュメント内に書いてある。

(余談)パフォーマンスではない、エイリアスレコードのメリット

私が思うエイリアスレコードの最大のメリットはパフォーマンスだが、AWSのドキュメントでは触れられていない。

『よくある質問』には下記のように書いてある。

静的なウェブサイトをホスティングするよう設定されている CloudFront ディストリビューションおよび S3 バケットには、CNAME を使用する代わりに、CloudFront ディストリビューションまたは S3 ウェブサイトバケットにマッピングされる “エイリアス” レコードを作成することを推奨します。エイリアスレコードには 2 つの利点があります。1 つは、CNAME とは異なり、エイリアスレコードは zone apex (例えば、www.example.com ではなく example.com) に対しても作成できることです。もう 1 つは、エイリアスレコードへのクエリは無料であることです。

また、AWS Trusted AdvisorでAWSの設定をチェックしたときに、エイリアスレコードを使える箇所でCNAMEを使っていた場合にワーニングになる。

参考文献

  1. RFC 1034 『DOMAIN NAMES – CONCEPTS AND FACILITIES

  2. RFC 1035『DOMAIN NAMES – IMPLEMENTATION AND SPECIFICATION』
  3. RFC 1912『Common DNS Operational and Configuration Errors』
  4. AWS Route53ドキュメント
  5. DNSのRFCの歩き方( http://dnsops.jp/event/20120831/DNS-RFC-PRIMER-2.pdf )
    • 記事を書いてから気づいたが、DNSのRFCを自力で読む前に目を通すと良さそう

  1. CNAMEは”canonical name”なので”正規名”などどしたほうが正確だが、わかりやすさを重視して”参照先”とした 

  2. 「多段CNAME」をググるとそれなりにヒットするので、ある程度一般的な言葉かもしれない 

  3. 関連するRFCは他にもある. 参考文献の3を参照 

  4. 「web.example.com -> www.example.com -> web.example.com …」のような循環的な参照 

続きを読む

lambda入門(Node)③ – API Gatewayを使ってslackからのリクエストをlambdaで受けられるようにする

第3回になりました。

過去のはこちら。

今回は、ようやくやりたいことに近づいて来まして
slackからのリクエストをlambdaで受けられるようにしたいと思います。

何か調べていくと、どうもAPI Gatewayを使うと良い感じぽい。
まずはAPI Gatewayについて予習を。

API Gateway

どんな役割をしてくれるのか

APIのエンドポイントとして待ち構える玄関として使える
現在はhoge/*のようなパスを/hoge/{proxy+}として設定できる
プロキシリソースなるものらしい
post, putの振り分けなどが簡単になった

課金体系

受信した API 呼び出しと、送出したデータ量に対して発生

serverless frameworkでの設定

serverless.yml
functions:
  bookStore:
    handler: books/store.store
    events:
      - http:
          path: books
          method: get
          cors: true

これだけ。
デプロイ実行すると

$ serverless deploy -v --stage dev
Serverless: Packaging service...
・
・
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1490662361327
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1490662361327
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeployment1490662361327
・
・
Service Information
service: testProject
stage: dev
region: ap-northeast-1
api keys:
  None
endpoints:
  GET - https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/books
functions:
  bookStore: testProject-dev-bookStore

エンドポイントが作られた。。!

レスポンスを送れるか試してみる

$ curl https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/books
{"message":"ok",...........}

うん、okそう、すごい。ymlに書くだけで何でも設定してくれちゃう。

Slackから応答させるようにする

ここで今回の本題。
作成したエンドポイントに対してスラックがリクエストを送って
slack上にメッセージが返ってくるようにする。

全体像

スクリーンショット 2017-04-29 15.17.57.png

やることとしては、リクエストパラメータを解析してデータを保存。
保存した結果を返す

book.js

'use strict';

const slackAuthorizer = require('../authorizer/slackAuthorizer');
const parser = require('../service/queryParser');

const bookSave = require('../useCase/book/save.js');

module.exports.book = (event, context, callback) => {
  const queryParser = new parser(event.body);
  const authorizer = new slackAuthorizer(queryParser.parseToken());

  /**
   * 認証
   */
  if (!authorizer.authorize()) {
    context.done('Unauthorized');
  }

  /**
   * @Todo ここで lambda function の振り分けを行いたい
   */
  bookSave(event, (error, result) => {});

  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'ok',
    }),
  };

  callback(null, response);
};

同期で処理を行いたいので asyncを使って処理をブロックごとに実行する

save.js

'use strict';

const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const bookTable = process.env.bookTable;

const uuidV1 = require('uuid/v1');

const webhookUrl = process.env.slack_webhook_url;
const request = require('request');

const dateTime = require('node-datetime');
const dt = dateTime.create();
const insertDate = dt.format('Y-m-d H:M:S');

const async = require('async');

const parser = require('../../service/queryParser');

module.exports = (event, callback) => {

  const queryParser = new parser(event.body);

  const key = uuidV1();

  async.series([
    function(callback) {

      /**
       * データを保存
       */
      dynamoDB.put({
        'TableName': bookTable,
        'Item': {
          'id': key,
          'title': queryParser.parseText(),
          'insert_date': insertDate,
        },
      }, function(err, data) {
        callback(null, "saved");
      });
    },
    function(callback) {

      /**
       * 保存したものを取り出して
       * 結果を返す
       */
      dynamoDB.get({
        TableName: bookTable,
        Key: {
          id: key,
        },
      }, function(err, data) {
        if (!err) {
          const response = {
            text: ``${data.Item.title}` is Saved !!`,
          };
        
          /**
           * webhook でチャンネルにメッセージを返す
           */
          request.post(webhookUrl, {
            form: {
              payload: JSON.stringify(response),
            },
          }, (err, response, body) => {
            callback(null, 'getData');
          });
        }
      });
    },
  ], function(err, results) {
    if (err) {
      throw err;
    }
  });
};

slash commandから送られてきたパラメータをパースするために
query-stringモジュールを使い、ラップした

queryParser.js
'use strict';

const queryStringParser = require('query-string');

module.exports = class queryParser {

  constructor (queryString) {
    this.queryString = queryString;
  }

  parseToken () {
    return queryStringParser.parse(this.queryString).token;
  }

  parseText () {
    return queryStringParser.parse(this.queryString).text;
  }
};

slashコマンドの設定は省略します。
最初の api gatewayで設定されたエンドポイントのURLを
設定してあげればいいので

実際に動かすとこんな感じです。

4b4b9687608f58545ef2b4536a7166c2.gif

ソースは汚いかもしれませんが、saverlessフレームワークで
エンドポイント作成やdb、外部サービスの連携が簡単に出来ました

もっとキレイに書けるようにjsの筋力をつけていかねば。。笑

続きを読む

new relic で 一定期間が経過したレコードを削除する

new relic で “server not responding” になったあと、一定期間が経過したレコードを削除する

AWS EC2 で、インスタンスが Auto Scaling でterminateされた後も new relic 側でレコードが残り続けるので、Lambdaを使って定期的に削除するようにしました。

  • Lambda のランタイムは Node.js 6.10 を使用
  • 3秒以上かかるとtimeoutしてしまうので、timeoutの設定を10秒に増やしている

code

  • やっていることは、npm install moment request をして、curl的な処理をしているだけ
  • Lambdaにアップロードするzipには、メインのjsファイルと node_modules ディレクトリを格納
  • トリガは CloudWatch events に、週1で実行するルール追加(毎日とかでも良いかも)

index.js


var request = require('request');
var moment = require('moment');

var expireDays = process.env.expireDays || 7;
var apiKey = '【new relic で発行されたAPI KEY】';
var options = {
    url: 'https://api.newrelic.com/v2/servers.json',
    headers: {
        'X-Api-Key': apiKey
    }
};
var options_del = {
    url: 'https://api.newrelic.com/v2/servers/%s.json', // 実行時に上書き
    method: 'DELETE',
    headers: {
        'X-Api-Key': apiKey
    }
};

var now = moment();

function callback(error, response, body) {
    if (!error && response.statusCode == 200) {
        var info = JSON.parse(body);
        info.servers.forEach(function(item, index, arr) {
             if (item.reporting == false) {
                var m = moment(item.last_reported_at);
                if (now.diff(m, 'days') >= expireDays) {
                    // send DETETE request
                    console.log('*** deleting not reporting server from newrelic ***')
                    console.log(item);
                    options_del['url'] = 'https://api.newrelic.com/v2/servers/' + item.id + '.json';
                    // console.log(options_del);
                    request(options_del, callback_del);
                }
             }
        });
    } else {
        console.log('*** execute FAILED ***');
        console.log(body);
    }
}

function callback_del(error, response, body) {
    if (!error && response.statusCode == 200) {
        var info = JSON.parse(body);
        // console.log(info);
        var id = info.server.id;
        console.log('*** execute DELETE SUCCEEDED:' + id + ' ***');
    } else {
        console.log('*** execute DELETE FAILED ***');
        console.log(body);
    }
}

exports.handler = (event, context, _callback) => {
    console.log('*** start: deleting not reporting server from newrelic  more than '+ expireDays + ' days ago ***');
    request(options, callback);
    _callback(null, 'all done: delete records more than '+ expireDays + ' days ago.');
};

対象の日数は環境変数で変えられる

process.env.* で環境変数を参照できるので、これを使って。
expireDays で指定できる(指定なしの場合は7日で処理)ように。
あんまり変える需要はないかもですが

スクリーンショット 2017-04-28 17.27.27.png

see also

Feature Idea: Auto Delete Non-Reporting Servers – Feature Ideas / Feature Ideas: APM/UI – New Relic Online Technical Community

最初はこのワンライナーを、気がついたときに手動で行ってました。

続きを読む

AWS Lambdaをつかって、CloudWatchが監視した値を超えたら、slackに通知する仕組みをつくろう on Python

はじめに

社内で「らむだが〜らむだが」と話していて、元C#の僕からするとプログラミング言語の何かかな?と思っていましたが、違いましたね。(C#にはラムダ式というのがある。)

社内では、AWS Lambda上にnode.jsランタイムを配置して、slack通知していましたが、nodeのversionが古くてそろそろサポートやめるよ、と言われたので、Pythonに切り替えることにしました。(nodeより、Pythonのほうが知っている人が社内にいたので)

AWS Lambdaでは、よくあるような処理はテンプレートとして用意されているので、結構簡単にできます。手順をまとめてみます。

設定手順

おおまかに下記の手順でやります。

  1. Lambda関数作成
  2. SNSの作成とLambdaとの連携
  3. EC2とSNSの連携
  4. コーディング
  5. テスト

構成図

最終的にこんな感じになります。

aws.png

※ 構成図はdraw.ioを使って書いてみました

手順

1. Lambda関数の作成

今回のメインとなるLambda関数を作成しておきます。指示に従っていけば簡単に作れます。
作り直しや削除も簡単にできるのでご安心を。

サービス一覧から、Lambdaを選択。

①.png

Lambda関数の作成を開始します。

②.png

フィルターに、slackと入力するとslackの雛形ができますが、今回はブランクで作成します。

③.png

トリガーはあとで設定するので、からのままにしておきます。

④.png

2. SNSの作成とLambdaとの連携

SNS = Simple Notification Service。何らかの処理や状態変更を受取、メッセージにして通知してくれるサービスです。
メールや今回のようなLamdaへメッセージを通知できます。
サービス自体は、2017/04/06現在翻訳はされていませんが、簡単な英語なので問題ないと思います。

SNSを作成し、subscriptionをくっつけます。

Subscriptionsを作成する。

AWS_SNS.png

先程作成したLambda関数を選択します

AWS_SNS.png

3. EC2(Cloud Watch)とSNSの連携

EC2の画面から、アラームを設定します。

EC2_Management_Console.png

4. コーディング

各種設定を行います

設定するものは下記です。
* Lambda関数名
* コード
* 環境変数
* 関数ハンドラーとロール
etc

今回は下記のように設定してみました。

Lambda_Management_Console.png

@コード

lambda_handler.py
from __future__ import print_function

import boto3
import json
import logging
import os

from urllib2 import Request, urlopen, URLError, HTTPError

# slackの設定
SLACK_CHANNEL = os.environ['SLACK_CHANNEL']
HOOK_URL      = os.environ['HOOK_URL']

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))

    alarm_name  = message['AlarmName']
    description = message['AlarmDescription']
    new_state   = message['NewStateValue']
    reason      = message['NewStateReason']

    if new_state == 'OK':
        emoji = ":+1:"
    elif new_state == 'ALARM':
        emoji = ":exclamation:"

    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': "*%s %s: %s*n %sn %s" % (emoji, new_state, alarm_name, description, reason)
    }

    req = Request(HOOK_URL, json.dumps(slack_message))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

@環境変数+あるふぁ

Lambda_Management_Console_1.png

5. テスト

Lambdaの画面から、下記を実施します。

  1. アクション→テストイベントの設定→テストイベント入力
  2. 保存してテスト

手前味噌ですが、テストデータはこちらを参考にしていただければと。
* 参考:AWS Lambdaをつかった、CloudWatch監視 -> slack通知の際のテストデータ

実行結果

sandboxチャンネルに無事通知が飛びました。絵文字も入っていますね。

Slack_-_eversense.png

おわりに

簡単なプログラムでslack通知が実現できました。適切にメトリクス設定して、快適なAWSライフを。(通知しても、見をとしてしまったりしたら意味ないけれど…)

なお、Lambdaからは同一リージョンで作成したSNSしか設定できないようなので、リージョンをまたぐ場合は各リージョンのSNSをからLambdaを選択してあげます。

当初、Lambdaの環境変数を利用しようと思いましたが、base64周りでエラーが出てしまったので使えていません。これ使えると、Lambda関数が簡単に使いまわせるので次の機会にはきちんと対応しようと思います。

続きを読む

LambdaでRDSチュートリアル

AWSのAPI GatewayとLambdaとRDSを用いて、RDSに保存されているデータを外部に公開するAPIを作成することを想定して、まずは、LambdaからRDSに接続する

想定するテーブル構造

# テーブル名: link_clicks
| カラム名   | description       | 
| path      | urlのパス         |
| clicks    | リンクのクリック数   |
| stat_date | 集計日             |

1. Lambda関数を作成する

lambdaを用いて、RDS(mysql)に接続して、クエリパラメータ(path)に応じた情報を引き出して返すものプログラムをpythonで作成する

1.1 pythonパッケージ(api)を作る

mkdir api

1.2 pythonの依存パッケージをインストールする

mysqlへの接続のためにPyMySQLをインストールする

pip install PyMySQL -t api

1.3 メイン関数を記載する

メイン関数(handler)記載ファイルと設定ファイルを用意する
(YOUR_XXXは置換する)

api/api.py
# -*- coding: utf-8 -*-
import sys
import logging
import rds_config
import pymysql
import datetime as DT
import json

#rds settings
rds_host  = rds_config.db_host
name = rds_config.db_username
password = rds_config.db_password
db_name = rds_config.db_name


logger = logging.getLogger()
logger.setLevel(logging.INFO)

try:
    mysql_client = pymysql.connect(rds_host, user=name, passwd=password, db=db_name, connect_timeout=5)
except:
    logger.error("ERROR: Unexpected error: Could not connect to MySql instance.")
    sys.exit()

logger.info("SUCCESS: Connection to RDS mysql instance succeeded")

def handler(event, context):
    """
    This function fetches content from mysql RDS instance
    """

    path = event["path"]

    query = "select sum(clicks) from link_clicks where path = '%(path)s' group by path" % dict(path=path)
    logger.info(query)

    results = []
    with mysql_client.cursor() as cur:
        cur.execute(query)
        for row in cur:
            clicks = int(row[0])
            results.append({"clicks": clicks})

    return json.dumps(results)
api/rds_config.py
db_host = "YOUR_HOST"
db_username = "YOUR_USERNAME"
db_password = "YOUR_PASSWORD"
db_name = "YOUR_DB_NAME"

1.4 lambdaにプログラムをアップロード

deploy.sh
# zip作成
(cd api && zip -r - *) > api.zip

# create lambda function
## YOUR_ROLEにはlambda-vpc-execution-roleを指定すること
aws lambda create-function \
--region YOUR_REGION \
--function-name api  \
--zip-file fileb://api.zip \
--role YOUR_ROLE \
--handler api.handler \
--runtime python2.7 \
--vpc-config SubnetIds=YOUR_SUBNET_IDS,SecurityGroupIds=YOUR_SECURITY_GROUP_ID

# update lambda function
aws lambda update-function-code \
--region YOUR_REGION \
--function-name  api  \
--zip-file fileb://api.zip

2. Lambda関数を確認する

2.1 Lambda関数がアップロードされたことを確認する

以下にアクセスして、apiファンクションが追加されたことを確認する
https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/functions?display=list

apiファンクションが追加されてる.png

2.2 テストイベントを設定する

actions > configure test event からテストイベントを登録する
設定したものは以下の通り

{
  "path": "/11111"
}

testイベント設定.png
テストイベントとしてpathを設定.png

2.3 テストする

上記画像の save&test をクリックしてテストする
テスト結果が表示される.png

最後に

以上の手順で、lambda => RDSに接続して、データを参照できるようになりました
詳細はawsのドキュメントにも書いてあるので、そちらを参考にしてください
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/vpc-rds-create-lambda-function.html

続きを読む

簡単に仮想のS3を作成してAWSLambdaとS3サービスの連携をローカル環境でテストする

事前準備

実行する前にEclise用のAWSツールキットを導入しておいてください。導入手順は以下のリンクにご参考をお願いします。

ー>AWS Toolkit導入手順

導入完成したらAWSプロジェクトがプロジェクト新規画面で出てくるはずです。

mavenは導入済みの前提です。

Screenshot from 2017-04-20 17-13-55.png

Lambdaファンクションを書く

  1. まずはAWS Lambda Java ファンクションのプロジェクト作成する.

Screenshot from 2017-04-20 17-19-41.png
- プロジェクト名 :S3EventTutorial
- パッケージ名:com.amazonaws.lambda.s3tutorial
以上のように必須な情報をいれておきまましょう。「完了」を押したらプロジェクトは作成されて一般的なプロジェクトフォルダは以下のようにみれます。
Screenshot from 2017-04-20 17-25-12.png

  1. S3をモックするように「s3mock_2.11」というライブラリをMavenで導入する。pomファイルに依存ライブラリを定義するだけでいいですので下のpomファイルを参考にして自分が作成したプロジェクトのpomを作成してみてください。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.amazonaws.lambda</groupId>
    <artifactId>s3tutorial</artifactId>
    <version>4.0.0</version>
    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.1.0</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>1.3.0</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk</artifactId>
            <version>1.11.119</version>
            <scope>compile</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.typesafe.akka/akka-http-experimental_2.11 -->
        <dependency>
            <groupId>com.typesafe.akka</groupId>
            <artifactId>akka-http-experimental_2.11</artifactId>
            <version>2.4.11.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.typesafe.scala-logging/scala-logging_2.11 -->
        <dependency>
            <groupId>com.typesafe.scala-logging</groupId>
            <artifactId>scala-logging_2.11</artifactId>
            <version>3.5.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/io.findify/s3mock_2.11 -->
        <dependency>
            <groupId>io.findify</groupId>
            <artifactId>s3mock_2.11</artifactId>
            <version>0.1.10</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>2.7.22</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.github.tomakehurst/wiremock -->
        <dependency>
            <groupId>com.github.tomakehurst</groupId>
            <artifactId>wiremock</artifactId>
            <version>2.6.0</version>
        </dependency>


    </dependencies>
</project>

ロカールにおいてあるmavenのリポジトリーにない依存ライブラリがあるかもしれないので一応プロジェクトのrootフォルダで「mvn package」をコマンドラインとして実行してみましう。そしてmavenはpomに定義されていたdependencyをダウンロードしてくれます。

  1. Lambdaファンクションのロジック
    作成してもらったLambdaFunctionHandler.javaを開いてロジックをかいてみましょう。アイデアは凄っく簡単です。

S3からファイルがアップロードされたというイベントがこられたら、イベントの内容を見てアップロードされたファイルをゲットしてコンソールでそのファイルを書き出すという作業です。コードみてみたらすぐ分かると思いますので説明しないですむ。


public class LambdaFunctionHandler implements RequestHandler<S3Event, Object> {

    private AmazonS3 s3Client;

    public LambdaFunctionHandler(AmazonS3 s3Client){
        this.s3Client = s3Client;
    }
    public LambdaFunctionHandler(){
        this.s3Client =  new AmazonS3Client(new ProfileCredentialsProvider());
    }

    private static void storeObject(InputStream input) throws IOException {
        // Read one text line at a time and display.
        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
        while (true) {
            String line = reader.readLine();
            if (line == null)
                break;
            System.out.println("    " + line);
        }
        System.out.println();
    }

    @Override
    public Object handleRequest(S3Event input, Context context) {
        context.getLogger().log("Input: " + input);

        // Simply return the name of the bucket in request
        LambdaLogger lambdaLogger = context.getLogger();
        S3EventNotificationRecord record = input.getRecords().get(0);
        lambdaLogger.log(record.getEventName()); // イベント名

        String bucketName = record.getS3().getBucket().getName();
        String key = record.getS3().getObject().getKey();
        /*
         * Get file to do further operation
         */
        try {
            lambdaLogger.log("Downloading an object");

            S3Object s3object = s3Client.getObject(new GetObjectRequest(bucketName, key));

            lambdaLogger.log("Content-Type: " + s3object.getObjectMetadata().getContentType());

            storeObject(s3object.getObjectContent());

            // Get a range of bytes from an object.

            GetObjectRequest rangeObjectRequest = new GetObjectRequest(bucketName, key);
            rangeObjectRequest.setRange(0, 10);
            S3Object objectPortion = s3Client.getObject(rangeObjectRequest);

            System.out.println("Printing bytes retrieved.");
            storeObject(objectPortion.getObjectContent());

        } catch (AmazonServiceException ase) {
            System.out.println("Caught an AmazonServiceException, which" + " means your request made it "
                    + "to Amazon S3, but was rejected with an error response" + " for some reason.");
            System.out.println("Error Message:    " + ase.getMessage());
            System.out.println("HTTP Status Code: " + ase.getStatusCode());
            System.out.println("AWS Error Code:   " + ase.getErrorCode());
            System.out.println("Error Type:       " + ase.getErrorType());
            System.out.println("Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            System.out.println("Caught an AmazonClientException, which means" + " the client encountered "
                    + "an internal error while trying to " + "communicate with S3, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message: " + ace.getMessage());
        }catch (IOException ioe){
            System.out.println("Caught an IOException, which means" + " the client encountered "
                    + "an internal error while trying to " + "save S3 object, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message: " + ioe.getMessage());
        }
        return record.getS3().getObject().getKey();
    }

}


書いたコードに対してのテストケースを作成しましょう

今回は実装したLambdaコードを注目しますのでLambdaFunctionHandlerTestを開いてテストケース作成します。まずはテストケースのコードに目を通してみましょう。


    private static S3Event input;
    private static AmazonS3Client client;

    @BeforeClass
    public static void createInput() throws IOException {
        input = TestUtils.parse("s3-event.put.json", S3Event.class);

        S3Mock api = S3Mock.create(8999, "/tmp/s3");
        api.start();

        client = new AmazonS3Client(new AnonymousAWSCredentials());
        client.setRegion(Region.getRegion(Regions.AP_NORTHEAST_1));

        // use IP endpoint to override DNS-based bucket addressing
        client.setEndpoint("http://127.0.0.1:8999");

    }

    private Context createContext() {
        TestContext ctx = new TestContext();

        // TODO: customize your context here if needed.
        ctx.setFunctionName("Your Function Name");

        return ctx;
    }

    @Test
    public void testLambdaFunctionHandlerShouldReturnObjectKey() {

        client.createBucket(new CreateBucketRequest("newbucket", "ap-northeast-1"));
        ClassLoader classLoader = this.getClass().getClassLoader();
        File file = new File(classLoader.getResource("file/test.xml").getFile());
        client.putObject(new PutObjectRequest(
                                 "newbucket", "file/name", file));

        LambdaFunctionHandler handler = new LambdaFunctionHandler(client);
        Context ctx = createContext();

        Object output = handler.handleRequest(input, ctx);

        if (output != null) {
            assertEquals("file/name", output.toString());
            System.out.println(output.toString());
        }
    }

テストのため、createInput関数でS3Mockのインスタンスを作成して起動します。このインスタンスはローカル環境の8999番ポートにバイドしてリクエストを待ちます。それに「/temp/s3」というフォルダを作成しておいてS3サービスのストレージを真似する。

一番大事なのはtestLambdaFunctionHandlerShouldReturnObjectKeyという関数の内容です。見るの通り、以下の作業を実装します。
– 「testbucket」を作成する。注意:Regionを指定するのは必須です(Regionの内容は別になでもいいですがなかったらjava.lang.NoSuchMethodError: com.amazonaws.regions.RegionUtils.getRegionByEndpoint(Ljava/lang/String;)Lcom/amazonaws/regions/Region;というErrorが出てきます。これはAWSのバグです)
– プロジェクトのしたにあるresourceフォルダに作成したfile/test.xmlを仮ストレージにアップロードする
– アップロードしたファイルを仮S3からダウンロードして内容をチェックする。

トリガーは「s3-event.put.json」で定義されているイベントの内容なので「s3-event.put.json」の内容にアップロードされたファイルの情報を反映しなければなりません


{
  "Records": [
    {
      "eventVersion": "2.0",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-1",
      "eventTime": "1970-01-01T00:00:00.000Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "EXAMPLE"
      },
      "requestParameters": {
        "sourceIPAddress": "127.0.0.1"
      },
      "responseElements": {
        "x-amz-request-id": "C3D13FE58DE4C810",
        "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigRule",
        "bucket": {
          "name": "testbucket",
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          },
          "arn": "arn:aws:s3:::mybucket"
        },
        "object": {
          "key": "file/name",
          "size": 1024,
          "eTag": "d41d8cd98f00b204e9800998ecf8427e"
        }
      }
    }
  ]
}

注意:bucket名とobjectのキーは一番大事です。見た内容の通りファイルはtestbuckにfile/nameというキーでアップロードされましたので応じてjsonの内容はそ言うことを表現される。

#終わり

ドラフトに説明しましたが不明なところがありましたらご相談をお願いします

続きを読む

Mastodonのrake mastodon:dailyをLambdaで定期実行する

ドキュメント
https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md
これやらないとこういうことが起きるので大事。
http://cryks.hateblo.jp/entry/2017/04/18/125351
まさに自分のサーバーで起きて大変だった…。

環境

前回のAWS EC2 Container Service
http://qiita.com/kawax/items/5307b58e549dd9cc3928

これでcronはどうやるんだろうと調べたけど結構簡単だった。

ECSタスク

aws-daily.yml

version: '2'
services:
  web:
    image: {image}
    env_file: .env.production
    command: bundle exec rake mastodon:daily
    mem_limit: 536870912
    ports:
      - "3000"
ecs-cli compose -f aws-daily.yml --project-name mastodon-daily create

Lambda

ランタイム:Node.js 6.10
ブランク関数で新規に作っていく。

トリガーで CloudWatch イベント - スケジュール を選択。
ルール名やルールの説明は適当なものを。
スケジュール式は rate(1 day)

ロールはカスタムロールの作成から新規ロールを作った後で
管理ポリシーAmazonEC2ContainerServiceFullAccessを付ける。
インラインポリシーoneClick_lambda_basic_executionも付いてるはず。
この辺は後でLambda 関数が実行できるポリシーを付ける。

コード

clusterとtaskDefinitionを自分のものに書き換える。

var AWS = require('aws-sdk');
var ecs = new AWS.ECS();

exports.handler = (event, context, callback) => {
    var params = {
        cluster: "mastodon", 
        taskDefinition: "ecscompose-mastodon-daily"
    };
    ecs.runTask(params, function(err, data) {
        if (err) console.log(err, err.stack); // an error occurred
        else     console.log(data);           // successful response
    });
};

テストしてみてエラーが出てなければトリガーを有効化して終わり。
エラーが出てる場合はロールを確認する。

続きを読む

AWSIoT ラージデータアップロードパターン実装

AWSIoT ラージデータアップロードパターン実装

このBlackbeltセミナー の「ラージデータアップデートパターン」を実装しようと思ったら、未熟ゆえに結構色々詰まったので、やりかたをまとめときます。

image

AWSIoT Thing作成

このへんは普通に作るだけなので、適当に流します。

デバイス名はdevice00, 以下のようなPolicyを作成し、attachしておきます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:Connect",
      "Resource": [
        "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:client/device00"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "iot:Publish",
      "Resource": "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topic/device00/token/req"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topicfilter/device00/token/res"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Receive",
      "Resource": "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topic/device00/token/res"
    }
  ]
}

また、証明書のダウンロードもおこなっておきます。

IAMロールの設定

この後、LambdaでToken発行処理とデバイスからのS3へのアップロード処理を作成するのですが、
関数作成時に該当処理の実行権限をもつIAMロールが必要になるので、先に作っておきます。

S3アップロード処理ロール

デバイスに渡すSTSに許可する権限を付与したロールを”s3-role”として定義します。
ここでは、任意バケット以下にPutObjectのみをおこなえる権限を与えておきます。

実際には、バケット以下のデバイス名のフォルダにのみアップロード可能にする予定ですが、それは
STS発行時にロール権限を制限する形で加えるので、ここではバケット以下への書き込み権限を与えます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::[YOUR_BACKET_NAME]/*"
            ]
        }
    ]
}

STS発行処理ロール

“s3-role”のロール権限を付与したSTSを作成するLambda関数に与えるロール権限を
“create-token”ロールとして作成します。

Lambda関数を実行するので、既存ポリシーのアタッチで”AWSLambdaBasicExecutionRole”を付与します。

また、作成したSTSをAWSIoTの機能でPublishするので、そのための権限も付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "iot:Publish"
            ],
            "Resource": [
                "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topic/device00/token/res"
            ]
        }
    ]
}

そして、STSにs3-roleの権限を付与するための権限も追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::[YOUR_ACCOUNT_ID]:role/s3-role"
        }
    ]
}

さらに、s3-role側でcreate-tokenロールを信頼しAssumeRoleを許可する旨の設定をおこなう必要があります。

s3-roleの設定画面で「信頼関係」タブを選択し、「信頼関係の編集」をおこなってエディタで
JSONの”Statement”要素の配列に以下を追加します。

{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::[YOUR_ACCOUNT_ID]:role/create-token"
  },
  "Action": "sts:AssumeRole"
}

STS作成とデバイスへのトークン送信処理

STSを発行し、AWSIoT経由で指定のTopicにTokenをpublishするLambda処理を作成します。

今回はスライドの構成にしたがってTopic経由のpublishにしましたが、STSを使った通信が頻繁に
発生することが想定される場合は、Lambdaをスケジュール起動にしてDeviceShadowを用い定期的に
Pushで通知するほうがいいかもしれません。

import boto3
import json

def lambda_handler(event, context):

    sts = boto3.client('sts')
    iotData = boto3.client('iot-data')

    thingName=event['name']

    token = sts.assume_role(
        RoleArn="arn:aws:iam::[YOUR_ACCOUNT_ID]:role/s3-role",
        RoleSessionName=thingName,
        Policy=json.dumps({
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "",
                    "Effect": "Allow",
                    "Action": [
                        "s3:PutObject"
                    ],
                    "Resource": [
                        "arn:aws:s3:::large-data-upload-pattern/"+thingName+"/*"
                    ]
                }
            ]
        })
    )

    iotData.publish(
        topic=thingName+'/token/res',
        qos=1,
        payload=json.dumps({
            "AccessKeyId": token['Credentials']['AccessKeyId'],
            "SecretAccessKey": token['Credentials']['SecretAccessKey'],
            "SessionToken": token['Credentials']['SessionToken']
        })
    )

eventオブジェクトのメンバー’name’からデバイス名を取得していますが、ここにデバイス名をセットするための
設定は次項でおこないます。

AWSIoTでSTS発行リクエストを受ける

AWSIoTで”*/token/req”のTopicにトークン発行リクエストがpublishされたら、上記のLambdaを起動する
Ruleを作成します。

“Rule query statement” が以下になるように設定し、ActionにはLambda起動を選択して先述の
関数を指定してください。

SELECT topic(1) AS name FROM '+/token/req'

ここまでの設定で、AWSIoTコンソールの”Test”から、”device00/token/req”にメッセージ(空でよい)を
送信するとTokenが”device00/token/res”にpublishされることが確認できます。

あとは動作確認です。

STSを取得してみる

下記サンプルスクリプトを実行すると、”device00/token/req”にトークン発行リクエストを送信し、
“device00/token/res”からトークンを取得して標準出力に表示します。

from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
import json
import time

def cb(client, userdata, message):
    print("got message")
    print(message.payload)

host="[IOT_ENDPOINT_DOMAIN]"
rootCAPath = "[ROOT_CA_FILE_PATH]"
certificatePath = "[CERTIFICATE_FILE_PATH]"
privateKeyPath = "[PRIVATE_KEY_FILE_PATH]"

myAWSIoTMQTTClient = None
myAWSIoTMQTTClient = AWSIoTMQTTClient("device00")
myAWSIoTMQTTClient.configureEndpoint(host, 8883)
myAWSIoTMQTTClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath)

myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20)
myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1)
myAWSIoTMQTTClient.configureDrainingFrequency(2)
myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10)
myAWSIoTMQTTClient.configureMQTTOperationTimeout(5)

myAWSIoTMQTTClient.connect()
myAWSIoTMQTTClient.subscribe("device00/token/res", 1, cb)

time.sleep(2)

myAWSIoTMQTTClient.publish("device00/token/req", json.dumps({}), 1)

while True:
    time.sleep(5)

S3へのアップロード

取得したトークンを用いて、下記サンプルスクリプトで適当にS3にアップロードしてみます。

import boto3

AWS_S3_BUCKET_NAME = '[YOUR_BACKET_NAME]'

s3 = boto3.resource(
    's3',
    aws_access_key_id="[ACCESS_KEY]",
    aws_secret_access_key="[SECRET_KEY]",
    aws_session_token="[SESSION_TOKEN]"
)
bucket = s3.Bucket(AWS_S3_BUCKET_NAME)

obj = bucket.Object("device00/hogehoge")

response = obj.put(
    Body="hogehogehogehoge".encode('utf-8'),
    ContentEncoding='utf-8',
    ContentType='text/plane'
)

まとめ

まとめてみると大したことではないのですが、AssumeRoleとか権限まわりで色々詰まってよくわかんなくなるので大変でした。

続きを読む

初心者がどハマりしたAWS IoTのRuleの概要と使い方まとめ

AWS IoT使ってますか!

Webコンソールが分かりづらかったり新UIになったりで、
まだまだ黎明期感を感じるAWS IoTですが、最近ようやく僕もはじめました。
ただ毎回何かやる度にハマって学んでを繰り返してる感じで、
毎日ぐぬぬってなってます(でもこういう感覚っていいですよね!)。

で、今回もRuleっていう概念をお勉強してハマったんですが、
せっかくなのでハマったことも含めて概要とか使い方まとめてみました。
僕みたいな誰かの役に立てれば幸いです:sweat_smile:

まずAWS IoTのRuleとは!

AWS IoT内の指定したTopicに更新があった場合に、
LambdaやSNSなど外部のサービスをキックすることができるAWS IoTの重要なサービス!

例えば予め指定したデバイスShadowのTopic更新を受けて、
それを元にLambdaでクラウド側にデータを保存したり、
Shadowを再更新してあげたりなどいろいろできます。

正直このRuleを上手く使えないとAWS IoTで出来ることが全然広がりません :joy:

作ってみよう

と思ってCreateボタンを押してみてもぱっと見何がなんだかわかりません。
この初心者殺しめっ!!:knife:

ので、メモも兼ねて解説していきます。

Description

まぁただの説明なので省略

Using SQL version

一番新しいものを使えばいいと思います。

Rule query statement

ここに、これより下の欄で設定した結果が表示されます。
最終的には以下のようになりました。 ※可読性の為改行しています。

RuleQueryStatementの例
SELECT { "payload": *, "topicName": topic() } 
  FROM '$aws/things/+/shadow/update/accepted' 
  WHERE 
    indexof(topic(), '_foo_bar_baz_') > 0

細かい説明は下記で。

の前にまずは心構え

* はペイロード!

SQLでいうところのSELECT句の値となる場所にある * ですが、
AWS IoTのRuleだとこれは特別な意味を持っています。

非常に分かり辛いですが、 * だけだとTopicのペイロードがそのまま入ってくる感じで、
例えばSELECT句にこの * だけが指定されたRuleからLambdaがキックされた場合、
Lambda側で受け取れる event オブジェクトの中身はそのままTopicのペイロードと同一になります。

そして後述しますが、このSELECT句にはJSON形式でも記述できたりするので、
ペイロードそのものも、JSON内の1要素として扱うことができます。

関数を駆使する

正直 * でペイロードだけ取得するのでは若干情報が足りず扱いづらいです。
例えばそのRuleが実行されるにあたって、条件にマッチした複数のTopicから
どのTopicが呼んだものなのかを判別したりをペイロードの内容からだけでは実現できません。

そこでAWSが予め用意している便利な関数群があるので、これを使います。
先の例では topic() などがそうですね。これを利用すると、実際に実行されたトピック名を取得できるので、
ここから後でthing名なども抜き出すことができます。

ちなみにこの関数ですが下記にまとまっています。
但し、 日本語ページにはない情報が結構あります ので、必ず英語ページから探しましょう(めちゃくちゃハマりました…)。

Attribute

SELECT句の値となる場所です。
上記心構えでも説明しましたが、 * だとTopicのペイロードがそのままきます。
JSON形式での記述もでき、下記のような感じで書くことができます。

Attributesの例
{ "payload": *, "topicName": topic() }

先ほど利用した関数 topic() が使われていますね。
上記で実際にキックされたLambdaで event オブジェクトに入ってくる値が下記のようになります。

eventオブジェクトの中身
{
  payload: {
    state: {
      reported: { ... },
      desired: { ... },
      delta: { ... },
    }
  },
  topicName: '$aws/things/dev_foo_bar_baz_topic/shadow/update/accepted'
}

ペイロードが payload , トピック名が topicName というキーでラップされてちゃんとすべて渡されてきましたね! :thumbsup::sparkles:

Topic filter

ここで、どのトピックを対象にするのかの基本的な条件をMQTTのEndpointで指定します。
このEndpointは例えばデバイスシャドウなら各Thingの Interact -> MQTT 内で見つけられます。

TopicFilterの例
$aws/things/+/shadow/update/accepted

上記でも利用していますが、MQTTのTopic名には、MQTT専用のワイルドカードが利用できます。
# で前方一致、 + で部分一致という意味で、組み合わせも可能ですが、
文字列の途中で突然 + と使うことはできず / の区切りでのみ有効なことに注意しましょう( hoge+fuga みたいなのはダメ)。

Condition

上記TopicFilterだと、すべてのデバイスシャドウの update/accepted に対して反応してしまいます。
複数のプロダクトとかを1つのAWSアカウントで作成している時や、
その更新に対して関係ないデバイスに対しても一々反応して欲しくないですよね。
そこでConditionを書くと、ちょうど最初の Rule query statement の項に書かれている
SQLの WHERE 句として挿入されます。

Conditionの例
indexof(topic(), '_foo_bar_baz_') > 0

そしてここでもここでも関数が活躍しています。

indexof() は、第一引数で指定した文字列(カラム名ならその値)から、第二引数で指定した文字を検索し、
そのマッチしたIndexを返すというよくあるindexOf関数ですが、これを先ほどの topic() と組み合わせることで、
このRuleのトリガーとなったトピック名に _foo_bar_baz_ の名前が含まれている場合のみ、
Ruleを実行するというようなことが書けるようになります。便利ですね! :sparkles:

まとめ

正直これらの機能をフル活用しないとまともなRuleが書けない割に、
意外とまだWebに情報が転がってないので、
辛く厳しいAWSドキュメントの海を彷徨わないとなかなか正解に辿りつけずげんなりします。

というわけで僕は少なくとも最初にハマったのでメモとして残しておきます!

僕らのAWS IoTとの戦いはまだ始まったばかりだ…!! :boom::muscle:

続きを読む

EC2インスタンスの自動起動停止

自動起動

var AWS = require('aws-sdk'); 
AWS.config.region = 'ap-northeast-1';

function ec2Start(cb){
    var ec2 = new AWS.EC2();
    var params = {
        InstanceIds: [
            "i-0da881bbe333d6fbb"
        ]
    };

    ec2.startInstances(params, function(err, data) {
        if (!!err) {
            console.log(err, err.stack);
        } else {
            console.log(data);
            cb();
        }
    });
}

exports.handler = (event, context, callback) => {
    // TODO implement
    // callback(null, 'Hello from Lambda');
    ec2Start(function() {
        context.done(null, 'Started Instance');
    });
};

自動停止

var AWS = require('aws-sdk'); 
AWS.config.region = 'ap-northeast-1';

function ec2Stop(cb){
    var ec2 = new AWS.EC2();
    var params = {
        InstanceIds: [
            "i-0da881bbe333d6fbb"
        ]
    };

    ec2.stopInstances(params, function(err, data) {
        if (!!err) {
            console.log(err, err.stack);
        } else {
            console.log(data);
            cb();
        }
    });
}

exports.handler = (event, context, callback) => {
    // TODO implement
    // callback(null, 'Hello from Lambda');
    ec2Stop(function() {
        context.done(null, 'Stoped Instance');
    });
};

ロール

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "ec2:StopInstances",
                "ec2:StartInstances"
            ],
            "Resource": [
                "arn:aws:logs:*:*:*",
                "arn:aws:ec2:*"
            ]
        }
    ]
}

続きを読む