DynamoDBのデータからcsvを作ってS3に上げるLambda関数

経緯

仕事でDynamoDBを使うことになり、その中で「日次でDynamoDBのデータをtxtファイルにエクスポートして所定のS3バケットに置く(日ごとにファイルを分ける)」という要件があり、やり方を調べて実装してみた。

全体設計

CloudWatchEventを使ったよくある日次処理の構成。
Dynamoの項目が日付をもっているためその日付を条件にその日エクスポートするデータをQueryしてくる。
構成.jpg

調べたところ他にDynamoDB Streamをトリガにする方法があるっぽいが、テーブルが更新されるたびにデータを取ってきてファイルにアペンドするより1日の終わりにバッチ一発でやったほうがコスト的にもよくね?と思ったので今回の方法をとった。
このあたり実際のところはどうなのかは分からないので、詳しい方は是非おしえてください( ^)o(^ )

テーブル

こんな感じ。
DynamoではQueryの条件にできるのはkey項目のみらしい。
ところが条件に指定したかったdateがkey項目ではなかったためdateをキーにしたインデックス(GSI)を作成した。

id(Hash key) count(Range Key) date
a1 1 20170101
a1 2 20170101
a1 3 20170101
b1 1 20170102
b1 2 20170102
c1 1 20170102

Lambda関数

こんな感じ。
IAMロールの設定とかは割愛。いい感じにやりました。

index.js
'use strict'
const AWS = require('aws-sdk');
const moment = require('moment');
const fs = require('fs');

AWS.config.update({ region: 'ap-northeast-1' });

const docClient = new AWS.DynamoDB.DocumentClient();
const s3 = new AWS.S3();

exports.handler = function (event, context, callback) {

    let params = {
        TableName: "dynamo_toS3",
        IndexName: "date-count-index",
        KeyConditionExpression: "#date = :date",
        ExpressionAttributeNames: {
            "#date": "date"
        },
        ExpressionAttributeValues: {
            ":date": moment().format('YYYYMMDD') //バッチ実行日に作成されたレコードのみ取得
        }
    };

    docClient.query(params).promise().then(data => {

        let logArray =[];

        data.Items.forEach(element => {

            logArray.push(JSON.stringify(element));

        });

        return new Promise(function(resolve, reject) {
      
       //Lambdaでは一時的なファイルの出力先に/tmpが使える
            fs.writeFile('/tmp/tmp.txt', logArray.join('n'), function(err) { 
                if (err) reject(err);
                else resolve(data);
            });
        });

    }).then(() => {

        let s3Params = {
            Bucket: "my-bucket",
            Key: `log_${moment().format('YYYYMMDD')}.txt`,
            Body: fs.readFileSync('/tmp/tmp.txt')
        }

        return s3.putObject(s3Params).promise();

    }).then(data => {

        console.log(data);

    })
    .catch(err => {

        console.log(err);
    })

};

まとめというか所感

普段RDBに慣れ親しんでいる身からすると、Dynamoはやや癖があるように感じる。
こちらの記事にもある通りSDKを使う場合はAWS.DynamoDBよりもAWS.DynamoDB.DocumentClientを使った方が良い。
次はDynamoDB Streamをトリガにしたものも試してみたい。

続きを読む

Lightsailインスタンスのスナップショット管理

概要

Lightsailインスタンスのスナップショット管理の自動化について検討してみる。検討する機能は2つ。

  1. Lightsailインスタンスのスナップショットを取得する
  2. Lightsailインスタンスのスナップショットを削除する

いずれも設定したスケジュールに従って自動実行する。これにより、何世代かのスナップショットを残しつつ、無尽蔵にたまらないよう古いものはガーベージされる。

構成

CloudWatch EventsとLambdaの組み合わせで実現する。スナップショット作成用、スナップショット削除用でそれぞれCloudWatch EventのルールとLambdaファンクションを作成する。

実装

まずはLambdaファンクションを2つ作成する。

スナップショット作成

lambda_function.py
# -*- coding: utf-8 -*-
import boto3
import os
import datetime

client = boto3.client('lightsail')

#Lightsailのインスタンス名。Lambdaファンクションの環境変数から受け取る
name = os.environ['instancename']

def lambda_handler(event, context):

    #スナップショットファイル名に付加するタイムスタンプ
    timestamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')

    try:
        #スナップショットの作成
        response = client.create_instance_snapshot(
            instanceSnapshotName = name + '-' + timestamp,
            instanceName = name
        )
    except Exception as e:
        print(e)
        raise e

Lightsailのインスタンス名はLambdaファンクションの環境変数としてセットする。

スナップショット削除

lambda_function.py
# -*- coding: utf-8 -*-
import boto3
import os
import datetime

client = boto3.client('lightsail')

#Lightsailのインスタンス名。Lambdaファンクションの環境変数から受け取る
name = os.environ['instancename']

def lambda_handler(event, context):

    #Lightsailのスナップショットのリストを取得
    response = client.get_instance_snapshots()

    #インスタンス名でフィルタ
    filterdList = filter(lambda x: x['fromInstanceName'] == name, response['instanceSnapshots'])

    #作成時刻でソート
    sortedList = sorted(filterdList, key=lambda x:x['createdAt'])

    #削除対象が存在する場合
    if len(sortedList) > 0:
        try:
            #最も古いスナップショットを削除
            response = client.delete_instance_snapshot(
                instanceSnapshotName = sortedList[0]['name']
            )
        except Exception as e:
            print(e)
            raise e

この例では一番古いスナップショットを問答無用で削除する。「n世代は確実に残す」という考慮をしたい場合は、作成時刻の新しい順でソートしてn+1番目以降を削除する、といったロジックにする必要がありそう。

CloudWatch Events

CloudWatch Eventsのルールを2つ作成する。「イベントソース」は「スケジュール」を選択し、任意のスケジュールを設定。ターゲットとしてさきほど作成したLambdaファンクションを指定。その他のオプションはデフォルトのままでOK。

続きを読む

GuardDutyのイベントをSplunkで検索・可視化

re:Invent 2017で発表されたGuardDutyですが、Splunkでそのデータを取り込んで分析できるとのことなので、さっそく試して試してみました。

記事(英語)
Splunk Announces New Integrations With Amazon Kinesis Firehose and Amazon GuardDuty

Amazon GuardDutyって?

Amazon GuardDuty – 継続したセキュリティ監視と脅威の検知

(抜粋)

GuardDutyは 脅威情報を含む複数のデータストリームから、悪意のあるIPアドレス、デバイスドメインを認識し、あなたのAWSアカウントで悪意のある、もしくは不正な行動があるか特定するために学習します。VPC Flow Logs、CloudTrail のイベントログ、DNS ログを集め組み合わせることにより、GuardDuty は非常に多くのことなったタイプの危険性のある、悪意のある行動を検知します。

なるほど、AWSリソースの脅威を機械学習で発見するんですね。

Splunkって?

https://www.splunk.com/
ログ分析のソフトウェア。
あらゆるマシンデータをインデックスして検索や可視化、アラート通知や分析ができるっていう優れモノ。

設定してみた

5つのステップで設定できます。
1. SplunkにAppインストール
2. Splunk HTTP Event Collector有効化
3. Amazon GuardDuty有効化
4. AWS Lambdaでテンプレから関数作成
5. AWS CloudWatchでGuardDutyとLambdaを設定したルールを作成

ということで、設定方法を書いていきます。

Splunk設定

まずはデータの受け口であるSplunkの設定から
App入れてHTTP Event Collector (HEC)有効化するだけです。

Appインストール

このApp↓をSplunkインスタンスにインストールしましょう。
AWS GuardDuty Add-on for Splunk

(補足)Appインストール方法

Splunkにログインした後、左側のメニューにある歯車アイコンをクリック
Screen Shot 2017-12-14 1.31.38 PM.png

上記リンク先からAppをダウンロードして ファイルからAppをインストール からインスコ、もしくは、 他のAppを参照 からGuardDutyを検索してインスコ
Screen Shot 2017-12-14 1.35.35 PM.png

データ入力設定

Appインストール完了後、ログイン後のトップ画面に aws_guardduty というAppが追加されています。
Screen Shot 2017-12-14 1.38.53 PM.png

早速 aws_guardduty に移動
Screen Shot 2017-12-14 1.41.51 PM.png

まだ何もデータが入ってきていない状態なので、データ受け取りとしてHTTP Event Collectorを設定します。

右上の 設定 から データ入力 をクリック
Screen Shot 2017-12-14 1.42.50 PM.png

HTTPイベントコレクタ をクリック
Screen Shot 2017-12-14 1.44.07 PM.png

別の記事でHECの設定方法は書いたので、これ以降の手順は割愛します。こちらを参照ください。
https://qiita.com/kikeyama/items/515d65906537239e04d2#splunk%E3%81%AE%E8%A8%AD%E5%AE%9A

(注意)ソースタイプは aws:cloudwatch:guardduty を選択してください。

設定後のトークンはどこかにコピペしておいてください。

GuardDuty設定

AWSコンソールからAmazon GuardDutyに行って有効化。

今すぐ始める をクリック
Screen Shot 2017-12-14 1.15.38 PM.png

GuardDutyの有効化 をクリック
Screen Shot 2017-12-14 1.16.47 PM.png

GuardDuty設定は完了
Screen Shot 2017-12-14 1.20.55 PM.png

今はまだ空っぽですけど、とりあえずGuardDutyの設定はこれでおしまいです。

Lambda設定

まずは 関数の作成
これでSplunkにHTTPでイベントをPOSTするインターフェースを作ります。
Screen Shot 2017-12-14 1.49.39 PM.png

設計図 (Blueprints) を選択して、検索画面に splunk と入力して検索
Screen Shot 2017-12-14 1.51.43 PM.png

Splunk Logging を選択
Screen Shot 2017-12-14 1.53.08 PM.png

下にスクロールすると環境変数の設定があるので、こちらにSplunkのHECエンドポイントURLとトークンを設定
Screen Shot 2017-12-14 1.58.09 PM_mosaic.png

で、名前をつけて保存

その後、作成した関数を編集して sourcetype の値を aws:cloudwatch:guardduty に上書き

index.js
    // Advanced:
    // Log event with user-specified request parameters - useful to set input settings per event vs token-level
    // Full list of request parameters available here:
    // http://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTinput#services.2Fcollector
    logger.logEvent({
        time: Date.now(),
        host: 'serverless',
        source: `lambda:${context.functionName}`,
        sourcetype: 'aws:cloudwatch:guardduty',
        event: event,
    });

CloudWatch設定

ルールを一個作りましょう
Screen Shot 2017-12-14 2.08.15 PM.png

サービス名は GuardDuty 、ターゲットは Lambda関数 から、先ほど作成したLambda関数を選択
Screen Shot 2017-12-14 2.09.28 PM.png

あとは名前をつけて保存

以上、すべての設定は完了!

GuardDutyイベントを検索

ということで、しばらく待つとGuardDutyデータがSplunkにインデックスされてきました。

Screen Shot 2017-12-14 2.16.08 PM_mosaic.png

ダッシュボード

GuardDuty Appには既成のダッシュボードがあるようです。

GuardDuty Examples からダッシュボードに移動してみましょう
Screen Shot 2017-12-14 2.19.22 PM.png

Screen Shot 2017-12-14 12.42.27 PM.png

脅威が検知されてしまったみたいですね・・・。
比較的シンプルなダッシュボードですが、可視化やモニタリングには十分かな、と。
運用してみて足りない部分は自前でダッシュボード作ってみよう。

最後に

どうやらダッシュボード内のテーブルをクリックすると、Splunk App for AWSにドリルダウンできるようです。

Kinesis FirehoseからSplunkにデータを流せるとのことですし、せっかくなので近日中にこのAppも設定してみて記事を書いてみようかなと思います。

続きを読む

ブラウザの自動操作で手軽さを追求したらAWS LambdaとPhantomJSの組み合わせにたどり着いた

はじめに

DMM.com #1 Advent Calendar 2017 の14日目を担当します、 @mesh1neko です。

普段はAWSやNode.jsをメインに扱っています :muscle:
今日はブラウザ操作を極力お手軽にやる組み合わせについて書きます。

▼このAdvent Calendar について
前日の記事は、 @canacel さんの ReduxにImmutable.JSを適用してみた
でした。
これは DMM.com #1 Advent Calendar 2017 – Qiita の14日目の記事です。

qiita.com
カレンダーのURLはこちら
DMM.com #2 Advent Calendar 2017 – Qiita
DMM.com #1 Advent Calendar 2017 – Qiita

tl;dr

お手軽さを追求するとlambdaとPhatomJSの組み合わせが楽

  • Lambdaの制限を頑張らなくても回避できる

対象読者

とにかくお手軽にブラウザ操作を自動化したい方向けです。

ブラウザの自動操作というとE2Eテストのようですが、
本記事は、いかに楽しつつ、ルーチンワークで楽するかを考えてる方向きとなります。
(きっとE2Eについては 16日目の @mt0m さんが書いてくれるはず…!)

モチベーション

ルーチンでやっている事務作業をできるだけ楽に解決したい

弊社では、MTG予定とMTG施設が社内システムで統合的に管理されています。
そのため、自分が設定したMTGはさておき、
MTGに召集された場合、社内システムをわざわざ開いて確認する必要がありました。

もちろん、当該社内システムにも通知機能はありますが、システムを開かないとその通知を確認できず
結局1日1回、google calendarにMTG予定を転記するという
マンパワー全開の方法にて参加忘れ防止を図っていました… :cry:

なんとか、この毎日の作業をなくして気持ちよく仕事を始めたい…!

お手軽な組み合わせ検討

さて、お手軽に毎日スケジュール実行させたいとなると
サーバもいらない・ログも残る…ということでAWS Lambdaとの組み合わせを考えていきたいと思います。

候補

まずは候補として以下を選んでみました。
選定理由に厳密な理由はなく、自分が把握している範囲で思いついたものを選んでいます。

候補 理由
GoogleChrome/puppeteer 最近のE2Eといえばpuppeteerだと話題があったので
grahcool/chromeless 少し前にgithubでトレンド入りしていた
segmentio/nightmare 去年くらいまではNightmareというイメージがあった
ariya/phantomjs ブラウザ操作の老舗的存在のイメージがあった

調査・比較

Lambdaとの組み合わせで、いかに手軽に実現できるかどうかを念頭におきつつ調査・比較してみました。

信頼性

単純にgithubのstar数で比較しました。

信頼性
GoogleChrome/puppeteer
grahcool/chromeless
segmentio/nightmare
ariya/phantomjs

star数で見ると調べた時点でpuppeteerが20,251、phantomjsが23,957。
puppeteerの勢いのすごさを実感しますね :innocent:

継続性

こちらは最近のcommit状況や開発元などを参考にしています。

継続性
GoogleChrome/puppeteer
grahcool/chromeless
segmentio/nightmare
ariya/phantomjs

puppeteerはChromeのDevToolの開発チームということで、日々の開発も活発なようです。
chromelessやnightmareはpuppeteerほどではありませんが、そこそこ開発が進んでいるようです。
一方でphantomjsは唯一のメンテナさんが対応stop宣言をされたことで、先は見えない状況です。
cf. https://groups.google.com/forum/m/#!topic/phantomjs/9aI5d-LDuNE

日本語記事

これはqiitaの投稿数でみています。

日本語記事
GoogleChrome/puppeteer
grahcool/chromeless
segmentio/nightmare
ariya/phantomjs

登場が早い分phantomjsの記事がやはり多かったです。
githubで話題になったわりに、chromelessの記事は少なかったのが意外でした。

セットアップ難易度

lambda上で実現する場合の難易度を比較しています。

セットアップ難易度
GoogleChrome/puppeteer ×
grahcool/chromeless ×
segmentio/nightmare ×
ariya/phantomjs

lambdaには容量の制限と、言語のバージョン縛りがある点で差がでました。
puppeteerは最低でも6.4以上が必要となり、Lambda上で動かすにはトランスパイルが必須となります。
また、phantomjs以外のヘッドレスブラウザをlambda上で動かすためには細工も必要です。

具体的にいうと、puppeteerとchromelessが使うheadless chromeやNightmareが使うElectronといった
ヘッドレスブラウザをAmazon Linux環境で動かすためには専用にビルドしてlambdaパッケージに含める必要があります。
しかし、このパッケージに含める際にも50Mの容量上限が問題となります…。

なんとか50M以内に抑えるか、実行時にS3から引っ張ってくるといった事例は既に事例があります。
cf. Lambda上でPuppeteer/Headless Chromeを動かすStarter Kitを作った – sambaiz-net
cf. dimkir/nightmare-lambda-tutorial

この点、PhantomJSは環境変数の指定だけで問題なくデプロイパッケージに含める(33Mほど)ことができました。

PHANTOMJS_PLATFORM="linux"
PHANTOMJS_ARCH="x64"

実行時間

lambdaで動かした場合の実行時間です。

実行時間
GoogleChrome/puppeteer
grahcool/chromeless
segmentio/nightmare
ariya/phantomjs

単純にコードの時間だけでみるとpuppeteerが一番だと思いますが、
さきに述べたヘッドレスブラウザをどこから調達するかで初回のオーバヘッドがかなり変わってきます。
S3から取得するパターンの場合、継続して実行したら早いけど、毎回最初の1回目が遅くなってしまいます。

コードの実行自体に時間はかかりますが、ヘッドレスブラウザ自体をパッケージに含められ
初回においても実行時間が安定しています。
今回の手軽さという用途に、より適しているとしてPhantomJSが他の候補より一歩リードしました。

書きやすさ

コード自体の書きやすさです。

書きやすさ
GoogleChrome/puppeteer
grahcool/chromeless
segmentio/nightmare
ariya/phantomjs

PhantomJS以外はどちらかというと、やりたいことを素直に書けます

pupper/chromeless/nightmare
    .goto('https://httpbin.org/forms/post')
    .type('input', 'body > form > p:nth-child(1) > label > input');

 一方でPhantomJSではそういったメソッドはないため、
evaluateで擬似的に値をセットするという、まわりくどい感じとなります。

phantomjs
yield page.open('https://httpbin.org/forms/post');
yield page.evaluate(function() {
    document.forms[0].custname.value = 'input';
});

evaluateのスコープ内に変数を持っていく場合も注意が必要です。

const name = 'hoge';
yield page.evaluate(function(arg1, arg2) {
    // この場合'hoge'は入らない
    //document.forms[0].custname.value = name;
    document.forms[0].custname.value = arg2;
}, name, 'hyooo');

比較まとめ

信頼性 継続性 日本語記事 セットアップ難易度 実行時間 書きやすさ
GoogleChrome/puppeteer ×
grahcool/chromeless ×
segmentio/nightmare ×
ariya/phantomjs

PhantomJSは今後のメンテナンスが状況がわからないため、この先ずっと使える保証はないですが、
当面は、お手軽さを追求するとlambdaとPhatomJSの組み合わせが楽そうだな、という結果になりました。
Lambdaの制限が緩和されるともっと楽にかける日がくるかもしれません :innocent:

Lambda上でのPhantomJSの使い心地

さて、冒頭で触れたGoogleカレンダーにMTG予定を転記する作業を自動化が今回の目的でした。

PhatomJS冗長な記述で実は大変なのでは…と思いきや

あっさりできました:innocent:

evaluateで大抵の操作をすることになるので記述がどうしても冗長にはなりますが、
ブラウザの開発コンソールで試したことを、そのままコードに書いて動かせる点は
いちいちAPIを調べなくてもよいので、その点でも楽さを感じました :smile:

なお、今回はgoogle-calendarへの情報登録にあたり
以下のライブラリを使わせていただきました。
こちらの解説は本筋がずれるので割愛しますが、こちらも比較的お手軽にカレンダーを操作できました。
https://github.com/yuhong90/node-google-calendar

※本家のライブラリではコールバックだらけになりそうだったので今回は見送りました。

社内ツールのスクレイピングになるため、ソースは公開できないのですが
実行結果のイメージだけ共有したいと思います!

$ yarn start
yarn run v1.3.2
$ node-lambda run -f deploy.env
lambda is started
use proxy:******
success

xxx ログイン
loggedin done
<------------------------------>
終日            【セミナー】SYSxxx
14:30-15:00     相談 [施設A]
15:00-16:00     PF: v1.0.0 比率変更1%→20%
18:00-18:10     社内:daily-scrum
<------------------------------>
lambda will ended with success
Success:
"done success"
finally is started
lambda is closed
inserted event:...

さいごに

PhantomJSでお手軽にブラウザ操作を自動化することで、
あまり頑張らずに、ルーチンワークをlambdaに代行させられるようになりました。

puppeteerやchromlessなど、さらにお手軽にできる情報がありましたら
ぜひ情報を教えていただけますと幸いです!

明日は @daichiii さんの記事です!

続きを読む

AWS Lambdaによるサーバーレス構成でのCacheとCashを考える

はじめに

Serverless Advent Calendar 2017の12日目の投稿です。

ここでは、AWSベースのサーバーレスアーキテクチャで、主にFaaS(Function-as-a-Service)の利用を前提としますが、データキャッシュ(Cache)が必要な場合にどのように実現するのか、その技術解とお金(Cash)の関係について整理してみました。

サーバーレスアーキテクチャにしたは良いけど

サーバーレスアーキテクチャをベースにすることで、処理数に応じてリソースの最適化を図れるので、スモールスタートしやすく、またマイクロサービス化しやすいので、積極的に検討するようにしています。

ただ、よく自社で課題になるのが、データキャッシュに関する処理。
例えば、何かしらのIDを持つイベントを受け取り、それに対して名称を付与するようなケース。AWS Lambdaでデータを処理して、DynamoDBにデータを保存するような場合に、どこで名称のデータを保持するかが問題になります。

lambda-cache.png

DynamoDBは、スループット課金のため、あまり高頻度なRead/Writeをするには向いていません。もちろん、それだけの投資が行えるサービスであれば良いのですが、ID->名称の変換だけで、それだけのコストが発生するとなると、なかなか費用帯効果が得にくいケースが多いのではないでしょうか?
しかも、DynamoDBのRead処理は、レイテンシがそこまで高速ではなく(といっても数十~数百msですが)、システムのパフォーマンスに制約が発生するので、あまりこのような用途には向かないでしょう。

ということで、普通はインメモリでのキャッシュを利用することに至りますよね。

キャッシュパターン

ここでは、以下を前提に考えます。

  • 料金は、東京リージョンでのオンデマンドとして計算する。

    • データ転送量は除く。
    • 1ドル=115円で計算する。

ElastiCache(Redis)

AWSにおけるキャッシュサービスといえば、まずはElastiCacheですよね。ElastiCacheのエンジンは、MemcachedとRedisがサポートされていますが、私はRedis派です。キャッシュとして保持できるデータもHash/List/Setなどの色々な型をサポートしており、対応できる幅が広く、レプリケーションにも対応しているためです。

ElastiCacheを利用する場合は、以下のような要件の場合に向きます。

  • 高頻度なWrite/高頻度なRead
  • Writeスルー1

ただ、コストはそれなりに高いです。

インスタンスタイプ スペック 単位時間コスト 1ヶ月のコスト
cache.t2.medium 2vCPU、3.22GiB $0.104/h $76.13(約8,800円)
cache.m4.xlarge 4vCPU、14.28GiB $0.452/h $330.87(約38,100円)

さらに注意が必要なのは、上記は 1ノード である、ということです。ElastiCache(Redis)は、Multi-AZでの冗長化構成に対応していますが、追加のノードはレプリカノードとなるので、利用する場合は単純に2倍のコストがかかります。加えて、非同期レプリケーションとなるので、データのロストを避けるために同一AZにもレプリカノードを配置するとなると3~4倍のコストがかかることになります。

DynamoDB Accelerator(DAX)

いつもElastiCacheを使うかどうかで悩むことが多かったのですが、2017年の4月にDAXが発表され、新たな選択肢ができました。
DAXを利用することで、DynamoDBにデータを保持しつつも、数ミリ秒~マイクロ秒での低レイテンシでの処理が実現可能になります。

DAXを利用する場合は、以下のような要件の場合に向きます。

  • 高頻度なRead
  • Writeスルー1

大事なポイントは、DAXのタイトルにあります。

Amazon DynamoDB Accelerator(DAX) – Read heavyなワークロード向けインメモリ型キャッシュクラスタ

そう、Read heavy であることです。DAXでは、DynamoDBに書き込んだデータを読み込む際にキャッシュされるので、2回目以降のアクセスなどが高速化されます。書き込み自体が高速化されるわけではないので、頻繁にキャッシュを更新するような場合には、書き込みスループットの設定値を上げる必要が出てきてしまいます。

DAXを利用する場合のコストですが、ElastiCache(Redis)よりは1GiBあたりのコストは低くなります。

インスタンスタイプ スペック 単位時間コスト 1ヶ月のコスト
dax.r3.large 2vCPU、15.25GiB $0.322/h $235.71(約27,100円)

DAXでも、可用性向上のためには、3ノード以上でのクラスタ構成での運用が推奨されています。
ただ、DAXの場合、データ自体はDynamoDBに保持されているので、DAXノードがダウンしても、データ自体はロストせずに済みます。

また、LambdaはPythonで実装派の自分としては残念なところなのですが、DAXはまだPythonには対応していません。そのため、DAXを利用する場合はJavaかNodeのSDKを利用する必要があります。

Lambda In-Memory

もうひとつは、適用条件は限られますが、Lambdaのインメモリを利用するパターンです。これは、Lambdaのインスタンスは再利用されるため、それを活用する方法です。
以下のような方法で実現しています。

  • キャッシュ対象のデータをLambda関数のグローバル変数に保持する。
  • インスタンスが新たに立ち上がった場合など、キャッシュがなければ、(DynamoDBやS3などから)データを取得する。
  • 一定時間を過ぎている場合、データを再取得する。

Lambdaのインスタンスの制御は、AWS任せなので、自分でコントロールはできません。
そのため、ゆるい一貫性(ある程度の時間を要して、書き込みしたデータがキャッシュに反映される)で問題が無い場合での利用に限定されますが、Lambdaの処理だけでシンプルに実現でき、コストも抑えられるので、条件が合えば強力な選択肢です。実際のプロジェクトで急遽DynamoDBへのアクセス負荷を減らすのに必要になった際に、役に立ちました。

Lambda In-Memory を利用する場合は、以下のような要件の場合に向きます。

  • 高頻度なRead
  • 呼び出し回数が少ない
  • ゆるい一貫性
  • キャッシュデータが大きくない

2017/11/30にLambdaの最大メモリ量が、3,008MB(=2.93GiB)に拡張されました(それ以前は、1,536MB)。そのため、2GiB程度までキャッシュが可能そうです(キャッシュを目的にして増えたわけではないでしょうが)。

キャッシュだけのコストを出すのは難しいですが、1回あたりのLambda処理が0.1秒として、1秒あたり10回の呼び出しを行う場合は、以下の程度のコストがかかります。

  • 合計コンピューティング(秒/日)= 10回/秒 x 0.1秒 x 86,400秒/日 = 86,400 秒/日
  • 合計コンピューティング(GB-秒/日)= 86,400 秒/日 × 1GB = 86,400 GB-秒/日
  • 1ヶ月のコンピューティング料金 = 86,400GB-秒/日 x 30.5日 × $0.00001667/秒 = $43.93(5,100円)

もちろん、呼び出し回数が多ければその分コストは大きくなりますが、元々Lambdaで処理する内容であり、呼び出し回数が少ない状況では都合が良さそうです。

おおよそのコードの内容ですが、以下のようにして、定期的にキャッシュが更新されるようにしています。

pytyon
import datetime
import json

_beacon_dict = {}
_updatetime = 0

def lambda_handler(event, context):
    update_beacon_dict()

    req_body = json.loads(event['body'])
    add_attributes(req_body)

    # データの保存
    ・・・

    return {}

def add_attributes(beacon_data):
    '''
    キャッシュデータを利用して、属性を付与します。
    '''
    global _beacon_dict

    beacon_id = beacon_data['beaconId']
    if beacon_id in _beacon_dict:
        # キャッシュからデータを取得してパラメータを追加
        beacon_data['name']  = _beacon_dict[beacon_id]

def update_beacon_dict():
    '''
    有効期限をチェックし、キャッシュデータの更新を行います。
    '''
    global _beacon_dict
    global _updatetime

    diff_time = 0
    if len(_beacon_dict) != 0:
        now = datetime.datetime.now()
        epoch_now = datetime.datetime_to_epoch(now)
        epoch_updatetime = datetime.datetime_to_epoch(_updatetime)
        diff_time = epoch_now - epoch_updatetime

    if len(_beacon_dict) == 0 or diff_time >= 5*60*1000:
        # キャッシュデータの更新
        ・・・
        _beacon_dict = new_dict
        _updatetime = datetime.datetime.now()

まとめ

キャッシュのパターンを整理すると、以下のようになります。
※費用は、あくまで目安です。インスタンスタイプやクラスタ構成などの条件で変わります。

方法 適用が向くケース 制限 1GiBキャッシュあたりのコスト
ElastiCache(Redis) ・高頻度なWrite
・高頻度なRead
Writeスルー
・可用性を上げるなら3ノード以上必要 1ノードの場合:
2,600~2,800円
3ノードクラスタの場合:
7,800~8,400円
DynamoDB Accelerator(DAX) ・高頻度なRead
・Writeスルー
・可用性を上げるなら3ノード以上必要 1ノードの場合:
1,800円
3ノードクラスタの場合:
5,400円
Lambda ・高頻度なRead
・呼び出し回数が少ない
・ゆるい一貫性
・キャッシュデータ2GiB以下(目安) 10回実行/秒
100ミリ秒/回
の場合:
5,100円

1GiBキャッシュあたりのコストで見ると、Lambdaでもそれなりの単価になりますね。上記はキャッシュする/しないに関わらず、Lambdaの実行にかかるコストのため単純な比較はできないですが、10回実行/秒以下の頻度の呼び出しであれば、メリットがありそうです。

キャッシュの更新や呼出をどう行うかによって、どの方法が良いかは変わりますが、処理の特性とコストを踏まえて、キャッシュの方式を選択できると良いと考えています。


  1. Writeスルー:キャッシュに書き込んだデータが、整合性を保って読み取り可能になる。 

続きを読む

動画を探して自動ツイートしてくれるPython製botをAWSに載せてみた(後編)

TL;DR

  • YouTubeから動画を拾ってTweetするbotをPythonで開発し、AWS Lambdaに載せてみました
  • 全2記事です。後編のこちらでは、主にAWS Lambdaでのデプロイ・運用にフォーカスします
    • Pythonプログラムのパッケージングとアップロードに際しハマった知見を共有します
    • AWS CloudWatchと連携し、指定時間での自動ツイートを実現します
  • 前編はこちらです

AWS Lambda

AWS Lambdaとは

AWS Lambda はサーバーをプロビジョニングしたり管理しなくてもコードを実行できるコンピューティングサービスです。…コードが実行中でなければ料金はかかりません。

「一定の時刻に起動してツイート」さえしてくれればいいようなbotを動かすには、常時起動のサーバは必須ではありません。Lambdaのようなプラットフォームは今回のユースケースにうってつけと言えます。

関数をアップロードしておき、任意のイベントによって関数をトリガーするのがLambdaの基本的な使い方です。実際にやってみます。

ハマる、ハマる

が、実際には色々困りました…。順番に知見を共有します。

パッケージング

デプロイパッケージの作成 (Python)

Lambda 関数を作成するには、最初に Lambda 関数デプロイパッケージ (コードと依存関係で構成される .zip ファイル) を作成します。

ということで、プロジェクトのディレクトリに依存関係をインストールし、ZIP化する必要があります。pipのオプションでライブラリのインストール先は指定できるので、プロジェクトディレクトリに移動して…

$ pip3 install -r requirements.txt -t ./lib

ところが、エラーで失敗。

DistutilsOptionError: must supply either home or prefix/exec-prefix — not both

HomeBrewでPython導入しているとpip3 install -tが失敗する

らしいです。StackOverflowに同様の問題がありました。

ただ、回避の手段も回答されてます。ホームディレクトリに.pydistutils.cfgという名前のファイルを作って、以下の設定(というか、空のprefixを指定するハック)を書けば通るようになります。

pydistutils.cfg
[install]
prefix=

パッケージングの自動化について

pip installができるようになったのはいいとしても、増えた依存モジュールを再インストールして、もう一回.zipの中にモジュールを入れて…とか毎回手作業でやるのは辛いです。パッケージ管理ツールを導入して自動化できないかなあ、と調べたのですが色々動きが激しいようで。

ライブラリの配布について | Python Snippets

この辺りはベスプラを知りたいところではあります。追うのが大変そうだったのと、今回は大した規模でもないのでシェルスクリプトでなんとかすることにしました。

package_lambda.sh
#!/usr/bin/env bash

rm lambda.zip

cd src
pip3 install -r requirements.txt -t ./lib
zip ../lambda.zip *.py
cd lib
zip -r ../../lambda.zip *

src配下には自分で書いたPythonスクリプト群を配置しています。src/lib配下に依存モジュールをインストールし、それぞれ順番にZIPの直下に詰めてます。結論としてこれで大丈夫だったので先に進みます。

ハンドラ関数の定義

AWS Lambdaのコンソールに移ります。関数を作成、ランタイムには”Python3.6″を選びます。コードエントリで「.ZIPファイルをアップロード」を選び、作ったZipをアップしましょう。

スクリーンショット 2017-12-10 22.55.08.png

さて、次は画面右側の「ハンドラ」で処理の起点となる関数の名前を指定します。公式の説明では、

関数の filename.handler-method 値。たとえば、「main.handler」は、main.py で定義されたハンドラーメソッドを呼び出します。

すなわち[ファイル名].[関数名]とすれば良いので、前回作った感じだとmain.mainかなー、とか指定してテスト実行すると…

スクリーンショット 2017-12-10 23.08.36.png

落ちます。

ハンドラ関数の引数の数が間違っていると落ちる

"errorMessage": "main() takes 0 positional arguments but 2 were given"

Lambda 関数ハンドラー (Python)

まあドキュメントを読めという話でお恥ずかしいのですが、引数が合ってないわけですね。イベントハンドラであるところのLambda関数はeventとcontextを受けるのが基本ですから、その形に沿ったハンドラ定義が必要です。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tube_crawler import TubeCrawler
from tweeter import Tweeter
import config

def main():
    t = TubeCrawler()
    movies = t.movies_from_query("Hybrid Rudiments")
    chosen = t.choose(movies)

    tw = Tweeter()
    tw.reply(config.REPLY_TO, chosen)
+   return chosen

+def lambda_handler(event, context):
+   result = main()
+   return { 'tweetedURL': result }

if __name__ == '__main__':
    main()

あとは、コンソールでmain.lambda_handlerをハンドラに指定すればOKです。せっかくなので実際にツイートされた動画のURLを呼び出し元に返す仕様にしてみました。

定期実行

デプロイはできたので、次は定時実行の仕組みを作ります。

スクリーンショット 2017-12-11 22.05.53.png

コンソール画面の「トリガーの追加」から「CloudWatch Events」を選びます。トリガーのルールにcron式を選べば、今回やりたいことは実現できますね。

Rate または Cron を使用したスケジュール式

  • 日または週日の値は疑問符である必要がある
  • UTCしか使用できない(ので、日本時間に合わせてずらす)

あたりが注意点でしょうか。今回は平日の定時につぶやいて欲しいので、こんな式にしてみます。

cron(15 10 ? * MON-FRI *)

運用してみて

運用して1週間ほど様子を見てみましたが、無料利用の範囲内でおおむね問題なく動いております。
今度はTedみたいな勉強系の動画やらブログ記事やら拾わせても面白いかもしれませんね。

よかったこと

一気通貫して人が使えるサービスの形まで持って行くと、否応無しに広く技術をさらうことになります。インフラよりに苦手意識があったので半ば無理やりにでも触るのはいい経験になりました。

反省

  • YouTubeAPIとか使えばもっと楽に実装できたんじゃないか疑惑
  • パッケージングとAWS周りでハマりすぎた。アップロードが絡むあたりからはTry&Errorより前にドキュメントを読もう

お読みいただきありがとうございました。

リンク

続きを読む

はじめてのServerless ✕ Webpack ✕ TypeScript


このエントリーはaratana Advent Calendar 201712日目のエントリーです。

こんばんは!最近Google Home MiniAmazon echo dotを購入したはいいが置き場所に困っている蔭山です。
みなさんはどのような場所に置かれているのでしょうか。。。

前日は新卒エンジニアには決して見えない安定感をお持ちの猿渡くんの「NoSQLについて何か。」という記事でした!
NoSQL?あぁ、聞いたことはある。
みたいな僕でもわかりやすい記事でした!
最近AWSに興味が出始めたところでしたので、ぜひDynamoDBを使って軽い画像投稿サービスでも作ってみます!

さて今回はServerless ✕ Webpack ✕ TypeScriptの組み合わせで使えるように手順をまとめてみたいと思います!

動作環境

今回の動作環境は以下になります!

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G1114

$ node -v
v8.2.1

$ serverless -v
1.24.0

$ webpack -v
3.5.6

環境準備

nodeやserverlessのインストールについては下記記事を参考に。。。

とりあえずやってみる

まずはServerlessのプロジェクトを作ってみましょう

$ serverless create -t aws-nodejs -p hogehoge
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/fugafuga/hogehoge"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.24.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"
$ cd ./hogehoge

JSer御用達npmを使って必要なパッケージをダウンロードしましょう。

$ npm init
$ npm install --save-dev serverless-webpack serverless-offline ts-loader typescript webpack

インストールが終わり次第、各種設定を行います。
今回はTypescript -> ES2015へのコンパイルを目的に設定させていただきます。
細かい設定内容に関しては割愛します。

./serverless.yml
service: hogehoge

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1

plugins:
- serverless-webpack
- serverless-offline

functions:
  hello:
    handler: handler.hello
    events:
     - http:
         path: hello
         method: get
./webpack.config.js
module.exports = {
  entry: './handler.ts',
  target: 'node',
  module: {
    loaders: [{
      test: /\.ts$/,
      loader: 'ts-loader'
    }]
  },
  resolve: {
    extensions: ['.ts']
  },
  output: {
    libraryTarget: 'commonjs',
    path: __dirname + '/.webpack',
    filename: 'handler.js'
  }
};
./tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs"
  },
  "exclude": [
    "node_modules"
  ]
}

準備はできたので、次はTypeScriptでコーディングしてみましょう!

./handler.ts
export * from './src/ts/functions/hello';
./src/ts/functions/hello.ts
export function hello(event, context, callback): void {
  const response = {
    statusCode: 200,

    headers: {
    },

    body: JSON.stringify({
      "message": "Hello Serverless x Webpack x TypeScript!!"
    })
  };

  callback(null, response);
};

コードが書けたらローカル環境で動作確認

$ sls offline
・・・・・・・・・・・・・・・・
途中は割愛。m(__)m
・・・・・・・・・・・・・・・・
Serverless: Routes for hello:
Serverless: GET /hello

Serverless: Offline listening on http://localhost:3000

きちんと動作するか確認。

$ curl -X GET http://localhost:3000/hello
{"message":"Hello Serverless x Webpack x TypeScript!!"}

動作が問題なければ、早速デプロイしてみましょう!

$ sls deploy
・・・・・・・・・・・・・・・・
途中は割愛。m(__)m
・・・・・・・・・・・・・・・・
api keys:
  None
endpoints:
  GET - https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
functions:
  hello: hogehoge-dev-hello

デプロイが完了したようです。
では早速動作確認。

$ curl -X GET https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message":"Hello Serverless x Webpack x TypeScript!!"}

ちゃんと動きましたね!

最後に

無理やりTypeScriptを使った感が凄まじいですね。。。申し訳ありません><
僕個人がTypeScriptを使ったことがなかったため使ってみたかったんです

明日は新卒田村くんの「Ctagsで自由な翼を得たVimについて」です!
お楽しみに!

参考

主にこちらの記事を参考にさせて頂きました!ありがとうございますm(__)m

続きを読む