matsuu : “MongoDB AtlasがGCPに対応。AWSとAzureにも提供済、どれも日本のリージョンに対…”

MongoDB AtlasがGCPに対応。AWSとAzureにも提供済、どれも日本のリージョンに対応してる。今後MongoDBを動かす場合はMongoDB Atlasでいいのでは。 / https://cloudplatform.googleblog.com/2018/03/expanding-MongoDB-Atlas-availability-on-GCP.html?linkId=49612463 Google Cloud Platform Blog: … 続きを読む

keystoneJSをnginxでデプロイしてaws上で動かす。

wordpressが使いたくなく、python大好きマンなのでmezzanineを使おうと思ったのですが、デプロイで頓挫したのでnodeJSでやろう。keystoneJSでやろうという次第です。

DDNSにはno-IPを利用しました。

keystoneJS official

前提

  • awsにインスタンスを作成済み
  • 上記インスタンスの3000番ポートを開けておく
  • 上記のインスタンスにssh接続出来る

諸々のインストール

sudo yum update -y

nodeJS


curl -L git.io/nodebrew | perl - setup
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
ebisennet$ source ~/.bash_profile

nodebrew install-binary v9.4.0

mongoDB

こちらの記事を参考にさせて頂きました。

sudo vim /etc/yum.repos.d/mongodb-org-3.2.repo

[mongodb-org-3.2]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/3.2/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-3.2.asc

sudo yum install -y mongodb-org

sudo service mongod start

sudo chkconfig mongod on

Yoeman

sudo npm install --global yo

keystoneJS

sudo npm install -g generator-keystone

no-IP

sudo yum-config-manager --enable epel
sudo yum install -y noip

nginx

sudo yum install nginx

forever

npm install -g forever

keystoneJSの実行

公式ページの通りに進めます。
htmlはpug,cssはsassがオススメです。

mkdir my-test-project
cd my-test-project
yo keystone

node keystone

この時点で[myIP]の3000ポートにアクセスするとページが見れるはずです。
example : http://x.x.x.x:3000

no-IPの設定

awsのドキュメントを参考に進めます。

sudo noip2 -C
sudo chkconfig noip on
sudo service noip start

nginxでデプロイ

sudo vi /etc/nginx/nginx.conf
nginx.conf
    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf; #この一行だけ加える
    ...
sudo vi /etc/nginx/conf.d/keystone.conf
keystone.conf
upstream node-sampleapp1 {
    server localhost:3000;
}

server {
    listen       80;
    server_name  http://DNSNAME/;
    proxy_redirect                          off;
    proxy_set_header Host                   $host;
    proxy_set_header X-Real-IP              $remote_addr;
    proxy_set_header X-Forwarded-Host       $host;
    proxy_set_header X-Forwarded-Server     $host;
    proxy_set_header X-Forwarded-For        $proxy_add_x_forwarded_for;
    location / {
        proxy_pass http://node-sampleapp/;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    root   /usr/share/nginx/html;
    }
}

foreverでデーモン化

forever start keystone.js

デバックモードを切る

vi my-test-project/.env

NODE_ENV=productionとする。

あとは、templatesやpublic,routesなどを編集して自分好みのスタイルに変更する。
また、記事の投稿はhttp://DNSNAME/keystone/signin より行う。

オチ

markdownのプラグインとか欲しいのでwordpressを始めようと思いました。

続きを読む

scrapy でクローラーを実装し、画像を収集してみる

AWS Rekognition を使う時にクローラーも使ってなんかできないかなと思い scrapy を利用してみました。とりあえず今回はドメインと画像収集のところまで。いかがわしいことには絶対利用しないでください

今回はスタートのページからどんどんリンクを辿り、ドメイン名のフォルダごとに、辿った時のページの画像を保存します。今度そのフォルダごとに画像を AWS Rekognition に投げて、そのドメインがどんなドメインなのかを画像から判別しようと考えています。

前提

  • scrapy 1.5.0
  • python3
  • scrapy インストール済み

参考サイト

Spider のコード

クローラーの肝となる部分です。参考サイトではCrawlSpiderクラスを継承して利用している場合が多かったです。そっちの方が大抵の場合は楽だと思います。

WebSpider.py
# -*- coding: utf-8 -*-
  import scrapy
  from tutorial.items import TutorialItem
  import re
  from scrapy.exceptions import NotSupported
  from urllib.parse import urlparse


  class WebSpider(scrapy.Spider):
      name = 'web'
      # 見つけたドメインを入れる
      tracked_domains = []
      # 全てを対象
      allowed_domains = []
      # 最初に見に行くサイト
      start_urls = ['http://XXXXXXXXXXXXX']

      # response を毎回処理する関数
      def parse(self, response):
          try:
              # データ処理
              # この関数内の処理が終わると続きを実行する
              # dataPipeline を利用した場合もここに戻って来る
              yield self.parse_items(response)

              # リンクを辿る
              for link in response.xpath('//@href').extract():
                  if re.match(r"^https?://", link):
                      yield scrapy.Request(link, callback=self.parse)
          except NotSupported:
              # GET のレスポンスが txt じゃなかった場合
              # Spiders の logging 機能が用意されているのでそれを利用
              self.logger.info("Raise NotSupported")

      # ドメインごとにページに表示された画像を保存する
      def parse_items(self, response):
          # domain の抽出
          url = response.url
          parsed_url = urlparse(url)
          domain = parsed_url.netloc

          # 同じ Domain は一回しかチェックしない
          if domain in self.tracked_domains:
              return

          self.tracked_domains.append(domain)

          item = TutorialItem()
          item['domain'] = domain

          # title の抽出
          title = response.xpath(r'//title/text()').extract()
          if len(title) > 0:
              item['title'] = title[0]
          else:
              item['title'] = None

          # 画像 URL をセット
          item["image_urls"] = []
          for image_url in response.xpath("//img/@src").extract():
              if "http" not in image_url:
                  item["image_urls"].append(response.url.rsplit("/", 1)[0]
                         + "/" + image_url)
              else:
                  item["image_urls"].append(image_url)

          # item を返すと datapipeline に渡される
          return item

start_urlsに設定したURLを元にクローラーが動きます。
私はそんなことはしませんが、ここにいかがわしいサイトを指定するとリンクを辿って新しいいかがわしいサイトのドメインが見つかるかもしれません。私はそんなことしませんが。

基本的にはparse()関数が scrapy のレスポンスごとに呼ばれて処理を行います。
今回は途中でparse_items()を呼び出し、まだ保存していないドメインであればフォルダを作成してそのページ上の画像を保存します。

settings.py で scrapy の設定を記述

parse_items()itemを return すると、ImagePipeline に渡されます。
その設定は以下の通りです。自作 Pipeline の説明は後述。

settings.py
  # 自作 pipeline に繋げる
  ITEM_PIPELINES = {'tutorial.pipelines.MyImagesPipeline': 1}
  # データの保存場所
  IMAGES_STORE = '/Users/paper2/scrapy/tutorial/imgs'
  # リンクを辿る深さを指定
  DEPTH_LIMIT = 5
  # LOG_LEVEL = 'ERROR'
  DOWNLOAD_DELAY = 3

pipelines.py で pipeline をカスタマイズ

デフォルトの ImagePipeline ですとドメインごとのフォルダを作成して、、、などといった追加の作業ができないので継承して自分で作成します。

pipeliens.py
# -*- coding: utf-8 -*-
from scrapy.pipelines.images import ImagesPipeline
import os
from tutorial import settings
import shutil


class MyImagesPipeline(ImagesPipeline):
    def item_completed(self, results, item, info):
        # DL できたファイルのパス
        file_paths = [x['path'] for ok, x in results if ok]

        # ドメインごとのフォルダに move
        for file_path in file_paths:
            img_home = settings.IMAGES_STORE
            full_path = img_home + "/" + file_path
            domain_home = img_home + "/" + item['domain']

            os.makedirs(domain_home, exist_ok=True)
            # DL した結果同じファイルのことがある
            if os.path.exists(domain_home + '/' + os.path.basename(full_path)):
                continue
            shutil.move(full_path, domain_home)

        # parse() の続きに戻る
        return item

これで完成です。

実際に回してみる

しばらく回すと色々なドメインから画像が集まりました。
Screen Shot 2018-01-20 at 20.07.47.png

うん?よくみたらいかがわしいサイトのドメインが混ざっているぞ、、、画像もいかがわしいものが、、、、ということで次回はこのいかがわしいドメインを取り除く (逆にそれだけ残す??)のを AWS Rekognition でやってみようと思います。

続きを読む

AWSとAzureとGCPを比較してみる – DB編

DBについて、AWSとAzureとGCPを比較してみました。

1. 新世代DB

AWS Azure GCP
新世代DB Aurora Cosmos DB Cloud Spanner
DBの種類 MySQL,
Postgresql
SQL (document DB),
MongoDB (document DB),
Gremlin (graph DB),
Azure Table(KVS),
Cassandra
オリジナルのリレーショナルDB
サーバレスか否か サーバあり サーバレス サーバレス
高可用性構成 / 負荷分散 Auroraレプリカ,
クロスリージョンレプリカ(MySQLのみ)
リージョン間フェイルオーバー,
予約済みスループット
リージョン内レプリケーション,
マルチリージョンレプリケーション
地理的範囲 リージョン(MySQLは別リージョンにレプリケーション可) グローバル グローバル
マルチマスター シングルマスター,
マルチマスター*
マスターになるリージョンは1個 マルチマスター

*プレビュー

DBの種類ですが、Auroraは手堅くMySQLとPostgresql、Cosmos DBはバラエティーにとんでいてドキュメントDB・KVS・グラフDBとCassandra、Cloud SpannerはオリジナルのリレーショナルDBとなっています。
Cloud Spannerはクライアントライブラリが各言語(C#,GO,Java**, node.js**,PHP**, Python**, Ruby)に対し用意されていますが、ORMの対応が気になるところです。
**ベータ

Cosmos DBとCloud Spannerはサーバレスですが、Auroraはインスタンスタイプを指定してインスタンスを構築します。また、拡張機能というよりは別物として、サーバレスタイプのAurora serverless*がプレビュー中です。

高可用性構成と負荷分散ですが、Auroraはリージョン内ではリードレプリカが障害時にマスターに昇格することで対応しています。MySQL版はクロスリージョンレプリケーション構成を取ることができますが、リージョン間で自動フェイルオーバーする仕組みはありません。
また、Auroraはマルチマスター機能であるAurora Multi-Master*が現在プレビュー中ですが、リージョン間でも利用可能になる予定があるとアナウンスされています。リリースされればグローバルで高可用性と負荷分散が簡単に実現できそうです。
Cosmos DBは、1個の書き込みリージョンを持つリージョン間フェイルオーバー***の仕組みで高可用性を実現しています。
Cloud Spannerはリージョン内レプリケーションとマルチリージョンレプリケーションの仕組みで高可用性を実現しています。1つのリージョンで構築する場合は、3個のread-writeレプリカを保持します。複数リージョンで構築する場合は、2個のread-writeレプリカを保持する2個のread-writeリージョン(と場合によってread-onlyリージョン)で構成されます。

***Microsoftのドキュメントではregional failoverをリージョン内フェイルオーバーと訳していますが、意味合いはリージョン間フェイルオーバーなので、ここではそのように表記しています。

2. リレーショナルDB

AWS Azure GCP
MYSQL互換 MySQL
/ MariaDB
Azure Database for MySQL* Google Cloud SQL for MySQL
高可用性構成 Multi-AZ,
クロスリージョンリードレプリカ
フェイルオーバーレプリカ
負荷分散 リードレプリカ,
クロスリージョンリードレプリカ
リードレプリカ
Postgresql Postgresql Azure Database for PostgreSQL* Google Cloud SQL for PostgreSQL**
高可用性構成 Multi-AZ,
クロスリージョンリードレプリカ
リージョナルインスタンス**
負荷分散 リードレプリカ,
クロスリージョンリードレプリカ
リードレプリカ**
SQL Server SQL Server Azure SQL Database
高可用性構成 Multi-AZ アクティブgeoレプリケーション
負荷分散 アクティブgeoレプリケーション
Oracle Oracle
高可用性構成 Multi-AZ
負荷分散

*プレビュー
**ベータ

・各クラウド間での違い-その1

AWSのMulti-AZとGCPのリージョナルインスタンス(PostgreSQL)は、スタンバイ側はリードの機能がないので負荷分散には利用できませんが、GCPのフェイルオーバーレプリカ(MySQL)はリードの機能があるので負荷分散にも利用できます。

3. NOSQL

AWS Azure GCP
KVS・ドキュメント ElastiCache(Memcached, Redis),
DynamoDB
Redis Cache,
Cosmos DB(Azure Table, SQL, MongoDB)
Cloud Datastore,
Cloud Bigtable
グラフ Neptune* Cosmos DB(Gremlin)
Cosmos DB(Cassandra)

*プレビュー

Neptuneの現時点のプレビューのAWSマネジメントコンソール画面はAmazon RDSとよく似ています。また裏でAuroraと同じ仕組みを利用しているそうなので、ひょっとしたらCosmos DBみたいに、Auroraの一機能としてリリースされるかも知れません。

4. まとめ

現在は、MySQL・Postgresqlを利用したければAWSのAuroraかRDS、SQL Serverを利用したければAzureでしょうか。
ただ各クラウドのプレビュー・ベータ提供状況を見ていると、そのうち機能差は無くなるように思えます。

続きを読む

最適なデータベースを選択するために考慮するべきこと

こんにちは!少し前までProgateでエンジニアのインターンをさせていただいておりました、chisatonと申します。
Progateのアドベントカレンダーに参加させてもらえるだなんて思ってもみなかったので、とても嬉しい限りです。

今私はエンジニアとして別の会社で働かせていただいているのですが、今の部署とProgateではAWSを使用しているという点で共通点があるため、今回はAWSのサービス紹介も兼ねて、どんな時にどのデータベースを選択すべきなのか、についてまとめてみようかと思います。
元ネタは、最近参加させていただいたラスベガスでのAWS re:Inventの”Which Database to Use When?”というセッションになります。
こちらに動画が既にアップされていますので、興味がある方はこちらを見てみてください。

DB選択においてまず考慮すべき3つの項目

  • Shape
  • Size
  • Compute

Shape

Row Store

行指向データベースで使用されているストア方式です。MySQLなどでよく使われている有名なやつです。
例えばとある会社員の基本情報をまとめる時に1人ずつレコード単位でデータをまとめます。

スクリーンショット 2017-12-09 17.28.35.png

例えば上図のようなデータの場合、赤枠で囲われている(行ごとの)データを一つのデータとして保存します。
その為、新しい社員が入ってきた場合は、一行データを追加するだけで済みますが、逆にここから社内の平均年齢を出したい時は一度全てのデータをとってきた上で年齢の項目だけに着目し、平均値を計算しなければなりません。

Column Store

列指向データベースで使用されているストア方式です。後述するAWSのRedshiftなどではこの方式が採用されています。
上記の会社員の基本情報を再び例としますと、項目ごと(列ごと)にデータを保存します。

スクリーンショット 2017-12-09 17.28.35.png

この場合、新しく社員が入ってきた場合に、各項目ごとに保存しなければならないため、今回は「社員ID」、「名前」、「年齢」に対して書き込みが発生します。Row Storeと比べると書き込みの量が増えますが、一方で社員の平均年齢を計算をしたい時は年齢の項目だけを取り出して計算すればいいので、複雑な処理をせずに済みます。
また、列指向の場合、連続する同じデータを圧縮して管理出来るというのもメリットでしょう。

Key-Value Store

ユニークな一つのキーに足して、一つの値を紐付けて保存する方式です。
例えば社員のIDに対して社員の名前を一対一で対応付けるとすると、下図のようになります。

1 : 佐藤
2 : 鈴木
3 : 髙橋
4 : 田中
5 : 伊藤

こうすることで、社員IDを使えば社員の名前を取得することができます。
Column StoreやRow Storeと比べるとデータ自体を複雑な構造のものにすることはできませんが、この場合社員IDでインデックスを張っているため、constantな時間で値を取得でき、非常に高速です。
インメモリキャッシュのredisなどではこの方式を利用しています。

Document Store

よくNoSQLの代表格としてDocument Storeが紹介されることが多いです。いわゆるスキーマレスな(上記のRow Storeのように各レコードごとに各項目を用意していない)データベースです。

例えば、人の経歴についてのデータを保存するDBを考えてみましょう。
スキーマなDBに保存すると考えると、どんな項目が必要でしょうか?名前に年齢に最終学歴…他はどうでしょうか?
もしかしたら人によっては他の人にはないような項目の経歴があるかもしれません。実は留学していた。実は多言語喋れる。こんな時に役立つのがDocument Storeです。

example.json
{"name": "佐藤", "age": 22, "edu-background": "Hoge University"},
{"name": "鈴木", "age": 24, "edu-background": "Fuga University", "foreign-lang": ["English", "Spanish"]},
{"name": "髙橋", "age": 25, "edu-background": "Fuga University", "foreign-lang": ["English"], "written-book": ["Intro to DB"]},
{"name": "田中", "age": 33, "edu-background": "Foo University",  "written-book": ["Intro to Java", "Advanced Java"]},
{"name": "伊藤", "age": 31, "edu-background": "Bar University",  "study-abroad": ["America", "Germany"], "foreign-lang": ["English", "German"]},

このようなDocument Storeの代表格としてはMongoDBやAWSのフルマネージドサービスDynamoDBなどが挙げられます。

Graph Store

データ構造の勉強をしてれば必ず出てくるグラフ構造のストア方式です。
よくFBのような友人どうしのつながりなどのデータを保存する際に使われる事が多いです。

例えば、アプリの機能として友人になった人の友人をおすすめの友人として紹介する場合、BさんがEさんと友達になると、BさんにはCさんFさんが「友達かも」と紹介されるような感じです。この時、既にAさんとは友達なので、紹介対象には入りません。
これを例えばスキーマなDBで友人の友人を探そうとすると、友人かどうかの中間テーブルを参照するなど、そこそこ複雑な処理が発生します。
もちろん、全体から特定のデータを取得したい時にはとあるノードから次のノードへとひとつひとつ探査をしていく必要があるため、全探査をしなくてはならないという短所もありますが、他のストア方式では届かなかった痒いところに手の届くストア方式であるかもしれません。

Time-Series Store

時系列に保存、取得するストアです。
スキーマかスキーマレスかというよりかは、とある指定した時刻の間のデータが取れるようなstoreの事を指します。
例えばDynamoDBではrange keyなるものが存在し、保存されたtimestampを元に指定された時間の範囲内のデータを取ってきたりすることができます。
株価の動向を見る時などには、保存しておいた株のデータを1日単位や一週間単位で取得したりすることができます。

Size

Size at Limit

想定されるデータのサイズは有限のデータ(bounded)でしょうか、それとも無限に流れてくるデータ(unbounded)でしょうか。
社員のデータを保存するのでしたら、データの量はたかがしれています。10人かもしれませんし、大規模な会社でも2000万人ぐらいでしょう。これはboundedなデータです。
一方でユーザが訪れたサイトの情報を保存するとしたらどうでしょうか?1日一人あたり10サイト訪れる事を考えると、1日で日本だけでも、約1.2億 * 10 = 12億のデータを保存する必要があります。これはunboundedなデータに当てはまります。

Working Set & Caching

実際に保存したデータはどの程度使用するでしょうか。
仮に10年分のデータをDBに保存したとしても、もしかしたら使うデータは過去1年分のデータであり、9年分のデータは滅多に使用しないかもしれません。こんな時は1年分のデータのみキャッシュしてしまうというのも手かもしれません。

Retrieval Size

取得したいデータは一つだけですか。それとも同時に複数のデータとってきたいでしょうか。
状況にもよりますが、取得したいデータが一つだけならkey-value Storeが圧倒的に構造が単純で高速でしょう。

Partitionable & Monolithic

扱うデータは意味のある単位で区切ることができるでしょうか。
誰がどこにいるかというデータは、場所単位で分割できます。
一方で社員の給料明細は意味のある単位で区切るのがちょっと難しいデータかもしれません。
この違いによっては、データの保存を分割したりして高速化を図ることが出来るかもしれません。

Compute

Compute Functions

データを取り出す際の関数やクエリはどんなものが想定できるでしょうか。
データの保存の仕方によってはクエリや処理が簡単にも複雑にもなります。

Throughput

どの程度DBに対してwriteとread数を要求するでしょうか。
readやwriteのスケールについても考える必要があります。

Latency

どのくらいデータの取得に対して遅延を許容するでしょうか。
データの分析であれば多少遅くとも許容できるかもしれませんが、今アプリケーションを使ってくれているユーザーに対してデータの取得で時間をかけるわけにはいきません。

Rate of Ingestion

どのくらいの割合でデータが挿入されるでしょうか。
bounded/unboundedでの前述例であれば、社員数のデータ挿入の割合はたかがしれてますが、訪れたサイトの情報のデータの挿入であれば一秒間に幾つものデータを漏らすことなくDBに保存しなければなりません。

AWSのDB一覧

それでは上記の3項目を踏まえた上で、AWSのDBサービスを見ていきましょう。

  • Amazon RDS
  • Amazon DynamoDB & DynamoDB Accelerator(DAX)
  • Amazon ElasticCache(Redis & Memcached)
  • Amazon Neptune

Amazon RDS

リレーショナル・データベースです。様々なDBエンジンがあり、その選択も適切に選択する必要があります。
が、やはりAmazon Auroraがすごいですね!MySQLよりは少しお高いですが、それ以上に良い機能が満載です。

Auroraは作られるとまず3つのAZに2つずつレプリケーション(同じDBを複数の場所に複製)します。
そして、仮にマスタのDBが死んでしまってしても、どれか他のスレイブDBがフェイルオーバー(マスタに昇格)します。
そのため、readのスケールが向上するだけでなく、障害耐性に対しても強いDBと言えるでしょう。
また、Auroraのストレージは、10GB単位で勝手に増えるため、ストレージの容量を常に監視する必要もありません。

ちなみに、今回のKeynoteではAuroraのまさかのmulti master機能が発表されました。
筆者はこの発表を聞いた瞬間発狂したのですが、まさかwriteがスケールする日が来ようとは…!!

Amazon DynamoDB & DynamoDB Accelerator(DAX)

image.png

Document Store方式のNoSQLデータベースです。

DynamoDBも作られるとまず3箇所のAZにレプリケーションされます。
ストレージに関しては、勝手にパーティショニング(データの分割)が行われるため、気にする必要はありません。
そして、RDSとの大きな違いとしてreadとwriteのスループットをこちらで指定することができます。
そのため、RDSでは実現できないような膨大な量のデータのreadやwriteに対しても対応できます。

そしてDynamoDB Accelerator(DAX)はこのDynamoDBのデータをインメモリキャッシュしてくれるサービスになります。
二年ほど前のre:Inventで発表されて以来、未だサービスとして使うことができない(2017年12月執筆時)のが残念ですが、後々はデータの取り出しが高速化し、またDynamoDBへのreadが減ることはとてもうれしいですね!

Amazon ElasticCache(Redis & Memcached)

AWSのマネージドインメモリキャッシュサービスになります。
インメモリなので、膨大な量のデータのキャッシュには向いてませんが、RedisにしてもMemcachedにしてもkey-value Storeなので非常に高速にデータを返すことができます。
その他にもRedisにはsorted setといった追加したデータの値に応じてソートしてくれる機能や指定したキーに対して、指定した地理情報(緯度経度)を保存する機能などがあります。
Memcachedに関しては、ノードを追加することが出来るため、readもwriteもスケールさせる事ができます。

Amazon Neptune

Graph型のフルマネージドデータベースサービスです。こちらも今回のre:Inventにて新しく発表されました!

スクリーンショット 2017-12-09 19.57.57.jpg

スケールが可能で、膨大な量のデータに対してミリ秒のクエリを使う事ができます。

こちらも3つのAZに足して6つのレプリケーションがなされるため、データを失う心配がほぼありません。
(筆者もこのへんはあまり詳しくはありませんが) 公式サイトによると、
Apache TinkerPop や W3C の RDF など一般的なグラフモデルと、TinkerPop Gremlin や RDF SPARQL など関連するクエリ言語をサポート
しているらしいので、既にどこかでGraph型のDBを使用されている方でしたら、比較的容易に扱えるかもしれません。

まとめ

以上、表にまとめると以下のようになります。
image.png

データ分析のためのAWSのサービス一覧

データを保存したら、次はデータの分析が必要になるかもしれません。DBサービスと共に分析のためのサービスも見てみましょう。

  • Amazon Athena
  • Amazon Redshift & Redshift Spectrum
  • Amazon Kinesis Analytics
  • Amazon Elasticserach

Amazon Athena

S3にあるファイルに対してSQLのようなクエリを投げることが出来るサービスです。
テーブル結合みたいなこともできるとか。
私の部署ではログの分析のするためにRedshiftを用いているのですが、いちいちログデータをRedshiftの方へコピーする作業が必要なため、現在Athena(またはRedshift Spectrum)を使おうかと検討中です。
分析した結果のデータを再び3Sへファイルとして保存したりできます。
クエリのページが完全にBigQuery

Amazon Redshift & Redshift Spectrum

フルマネージドなデータウェアハウスです。
このRedshiftは大量のデータをクラウド上に保持し、それに対してSQLライクなクエリでデータの解析、分析を行います。
イメージとしましてはAthenaの分析データがS3にファイルとして上がっているのに対して、Redshiftではクラウド上に分析しているデータを保存しているといった感じです。
また、Redshift Spectrumを使えば直接S3のファイルに対してクエリを投げる事ができます。こちらはAthenaとは違ってSpectrum専用のクラスタが複数立ち上がって処理を行ってくれます。

別記事ではありますが、Redshiftを既に使っている人向けのパフォーマンスチューニングの記事も書きましたので、よろしければこちらも参考にしてください。
Amazon Redshiftでより高速な処理を実現する

Amazon Kinesis Analytics

Kinesis Analyticsは取得したデータに対してすぐに分析や解析をしたい時に役立ちます。
unboundedな(無限に流れている)データに対して基本的に収集はストリーム処理をするかと思いますが、そのストリームデータに対してSQLで処理を行います。
他にもデータの変換を行ったり、またタイムウィンドウを設定することができるので、どのくらいの時間間隔でクエリを走らせるのかを自由に設定することができます。

Amazon Elasticsearch

Elasticsearchといえば全文検索のイメージが強いですね。
Elasticsearchはログデータをキャプチャし、それと同時にそのデータに対してうまくインデックスを張ります。
そのため、ログデータに対して非常に高速にクエリの結果を返すことができます。
全文検索に関しても、同じようにうまく独自のアルゴリズムでインデックスを張ることで、検索に対応しています。

まとめ

以上、表にまとめると以下のようになります。
image.png

全体まとめ

長くなってしまいましたが、以上、re:Inventで聞いてきたセッションについてまとめさせてもらいました。
普段どのDBを使うべきかを話すことってあまりないかと思うのですが、比較的サービスの根幹をなすものなので、上記知識はとても重要なものであると思います。
プレゼンターも言っていましたが、

Which Database to Use When?

ではなく

Which Database s to Use When?

であるべきである、と主張していました。

どんなサービスを提供するかにもよりますが、より良いサービスを届けるために、適材適所なDBや分析サービスを選択していくことがとても大切だなぁと感じた講義でした。

続きを読む

docker-lambdaでAWS Lambda環境をお手軽に動かす

Lambdaを使ってみて

お疲れ様です。本記事は、AWS Lambda Advent Calendar 2017の3日目の記事になります。

さて、Lambdaをがっつり本番環境で動かしていますという話もチラホラ聞きますが、私はLambdaを使い始めて約1年半、まだいまひとつガッツリ本番な気分になっていません。

その理由の一つがローカル開発環境問題です。ありきたり!
少し複雑なコードを書くとなると、デバッグしながらになるのでローカルマシンの使い慣れたエディタでやりたいところ。
ではささっと環境構築してみましょう。

ローカル開発環境の整備

その :one: 各言語のSDKをインストール

Lambdaで使える言語は今のところnode.js, Python, C#, Java(今後Golang, .NET)がありますが、MacでLambdaの開発環境を構築するとなると、

  • nodejs用

    $ npm -g install aws-sdk
    
  • Python用

    $ pip install python-lambda-local
    
  • Java用
    AWS Toolkitをインストール

以下略

当然全言語を使うわけではないですが、用途によって適正言語を使おうとすると、それぞれランタイムが必要。これらを今後バージョン管理していくとなると、若干見通しが暗く。。

その :two: 必要なライブラリのインストール

さらに、モジュールをImportしたりすると、それらもそれぞれbuild && デプロイパッケージ(要はZIPで固めて)をLambdaにアップロードするという二手間作業の発生

その :three: invalid ELF header対策

Lambdaは実際にはEC2のAmazon Linuxを立ち上げて動くことになるので、macOS上でビルドされたライブラリでは動かないケースが有る。いわゆる invalid ELF header 問題である。
じゃあちょろっとEC2インスタンス立ち上げて、ビルドしてパッケージングすれば解決。

もうローカル環境じゃなくなっちゃった!

2016年まではこのツラみがありましたが、今はもうAmazon Linux Dockerイメージが出た!
これでEC2インスタンスを立ち上げるのはなんとか回避できるようになりました。

その :four: 実行&デプロイ(ざっくり)

  • nodejs用
    メイン処理を実行するファイル lambda_handler.jsを作る(ファイル名は任意で)

    $ node lambda_handler.js
    

    実装が完了したら、それらを固めてデプロイするためのgulp.taskを作る

  • Python用
    メイン処理を実行するファイル lambda_handler.pyを作る(ファイル名は任意で)

    $ python-lambda-local -l lib/ -f handler -t 30 lambda_handler.py event.json
    

以下略再び

というか、 サーバのプロビジョニングの必要がないのがLambdaの売りなので、 もうだいぶ前の時点で既に本末転倒なんじゃないか感があったのに気づかないふりをしていた自分。。

もっとこうスマートに何か!

こういった状況の一つのソリューションとしてServerless Frameworkがあります。

こちらメリットとしては、

  • slsコマンドでビルドやデプロイなど大抵のことが一発完了
  • 設定もymlでパッケージ管理しやすい

という点がありますが、一方で

  • npmインストールして使う以上、結局管理対象が増えるのでは?
  • AWSの管理者権限が付与されたIAMロールが必要
    (なので、所属組織によっては開発者全員が使うのは難しいかも)
  • 上述のネイティブライブラリ問題は据え置き

といった点が個人的には引っかかりました。

ここまでで既に勘の良い人はお気づきかと思いますが

Dockerで管理する

「これDockerで全部やれるんじゃないか?」

先程このへんで触れたAmazon Linux Docker Image、これに諸々入れる分にはローカルのマシンを基本汚さずに済む。
そして、開発が終わったらコンテナごと廃棄ドボンしてスッキリ。

あとはまた、Lambdaでの開発が必要なときにそのつど docker buildするわけですが、うまいことDockerfileが保守されていれば理想的ですよね。

何より普段Dockerでアプリケーション開発してるんなら、Dockerでまとめてしまうのが見通しがよい。

そんなLambda用のDockerイメージを作れば良い

と思いましたが、lambci/docker-lambdaというLambda用Dockerイメージがありました。

Screen Shot 2017-12-04 at 2.50.05.png
https://speakerdeck.com/hihats/aws-lambdafalsejin-xian-zai?slide=18

LambciというOSSプロジェクトなので、自作より皆で保守していったほうが間違いない。
活発にアップデートされているので、Goへの対応なども楽しみです。

さっそく使ってみる

lambci/lambda:python3.6 イメージをベースにして実行

print_json.py
# 引数で渡されたJSONをそのまま出力するだけのPythonスクリプト
import json

def lambda_handler(event, context):
    print(event)
sample.json
  {
    "type": "products",
    "id": 123,
    "attributes": {
      "name": "Fun Toy",
      "description": "Toy for infants",
      "state": "in sale",
      "slug": "4b5366e5",
      "photo": "TOY.jpg"
    }
  } 

以下のように、(拡張子なしファイル名):関数名 をコマンドとしてイメージに渡してdocker runしてあげるだけで、実行環境のbuild(イメージがなければpullも)から関数実行までやってくれます

$ docker run -v "$PWD":/var/task lambci/lambda:python3.6 print_json.lambda_handler $(printf '%s' $(cat sample.json))

Unable to find image 'lambci/lambda:python3.6' locally
python3.6: Pulling from lambci/lambda
5aed7bd8313c: Pull complete
d60049111ce7: Pull complete
216518d5352c: Pull complete
47aa6025d0bc: Pull complete
9a82bb1662ac: Pull complete
Digest: sha256:3663b89bd1f4c4d1a4f06a77fc543422c1f0cbfc3a2f491c8c7bdc98cf9cf0b6
Status: Downloaded newer image for lambci/lambda:python3.6
START RequestId: 6cf5f6b5-bf62-49de-aaa4-f05b1148b67e Version: $LATEST
{'type': 'products', 'id': 123, 'attributes': {'name': 'FunToy', 'description': 'Toyforinfants', 'state': 'insale', 'slug': '4b5366e5', 'photo': 'TOY.jpg'}}
END RequestId: 6cf5f6b5-bf62-49de-aaa4-f05b1148b67e
REPORT RequestId: 6cf5f6b5-bf62-49de-aaa4-f05b1148b67e Duration: 109 ms Billed Duration: 200 ms Memory Size: 1536 MB Max Memory Used: 19 MB 

実行ファイルとコマンドワンライナーでいけました :exclamation::sushi:

build用イメージ lambci/lambda:build-python3.6 をベースにして環境をbuild

Lambdaがデフォルトで扱ってくれないライブラリが必要な処理のケースでは、ビルド & デプロイパッケージングするためのイメージ(lambci/lambda:build-python3.6)をベースにDockerfileでゴニョゴニョする

FROM lambci/lambda:build-python3.6
ENV LANG C.UTF-8
ENV AWS_DEFAULT_REGION ap-northeast-1

WORKDIR /var/task
ADD . .

RUN /bin/cp -f /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
  pip install -r requirements.txt -t /var/task

CMD zip -9 deploy_package.zip search.py && \
  zip -r9 deploy_package.zip *

必要なモジュールはrequirements.txtに羅列

requirements.txt
twitter
pymongo
numpy
requests_oauthlib
pytz

ビルドしてみる

$ docker build -t twitter_search .

Sending build context to Docker daemon  107.5MB
Step 1/7 : FROM lambci/lambda:build-python3.6
 ---> a895020ff4f5

<中略>

Successfully installed certifi-2017.11.5 chardet-3.0.4 idna-2.6 numpy-1.13.3 oauthlib-2.0.6 pymongo-3.5.1 pytz-2017.3 requests-2.18.4 requests-oauthlib-0.8.0 twitter-1.18.0 urllib3-1.22  

必要なモジュールがdockerに入っていき、実行環境が整いました。

既に上のDockerfileのCMD文にsearch.pyとファイル名が書かれていますが、今回は 「Twitterで特定のキーワード検索した結果をmongodb(ローカルのDockerコンテナ)にINSERTする」 Lambda関数を用意。

search.py
import os
from twitter import *
from requests_oauthlib import OAuth1Session
from requests.exceptions import ConnectionError, ReadTimeout, SSLError
import json, datetime, time, pytz, re, sys,traceback, pymongo, pprint
from pymongo import MongoClient
from collections import defaultdict
import numpy as np
import csv

def lambda_handler(event, context):
    pp = pprint.PrettyPrinter(indent=4)
    mongo_host = os.environ['MONGODB_HOST']
    secrets = {
        'consumer_key': event['CONSUMER_KEY'],
        'consumer_secret': event['CONSUMER_SECRET'],
        'access_token': event['ACCESS_TOKEN'],
        'access_token_secret': event['ACCESS_SECRET']
    }
    s_pa = [
        secrets["access_token"],
        secrets["access_token_secret"],
        secrets["consumer_key"],
        secrets["consumer_secret"]
    ]
    query = event["KEYWORD"]

    t = Twitter(auth=OAuth(s_pa[0], s_pa[1], s_pa[2], s_pa[3]))

    client = MongoClient(mongo_host, 27017)
    db = client.twitter_db
    tw_collection = db.tweets
    metadata_collection = db.metadata

    while(True):
        results = t.search.tweets(q=query, lang='ja', result_type='recent', count=100, max_id=0)
        metadata_collection.insert(results['search_metadata'])
        if len(results['statuses']) == 0:
            sys.stdout.write("statuses is none. ")
            break
        for tw in results['statuses']:
            if tw_collection.find_one({'id': tw['id']}) is None:
                tw_collection.insert(tw)

        print(len(results['statuses']))
        if not 'next_results' in results['search_metadata']:
            sys.stdout.write("no more results. ")
            break
        since_id = results['search_metadata']['since_id']

secret.jsonにTwitterAPIのKEYやらmongoのホストアドレスやら検索したいクエリやらをぶちこみ :arrow_down: 実行する

$ docker run -v "$PWD":/var/task lambci/lambda:python3.6 search.lambda_handler $(printf '%s' $(cat secret.json))

START RequestId: afc3baf7-09d1-4407-85b7-e012e759053b Version: $LATEST
30 recorded
no more results. END RequestId: afc3baf7-09d1-4407-85b7-e012e759053b
REPORT RequestId: afc3baf7-09d1-4407-85b7-e012e759053b Duration: 2070 ms Billed Duration: 2100 ms Memory Size: 1536 MB Max Memory Used: 38 MB

とれました :exclamation::sushi::sushi:

注意するのは、ビルド用と関数実行用のイメージは別(`lambci/lambda:build-python3.6`はあくまでビルド&パッケージングだけで使う)ということ

無事実装が終わればパッケージング

$ docker run -v "$PWD":/var/task --name twitter_search twitter_search:latest
$ ls deploy_package.zip
deploy_package.zip

deploy_package.zipができていることが確認できました。

結論

Docker最高ですね。

LambdaのAdvent calendarなのにDockerアゲになってしまいました :bow:

参考記事

Serverless Frameworkのプラグインを利用した外部モジュールの管理
AWS Lambda 用の python パッケージをクロスコンパイルして serverless で deploy した話

続きを読む

AWS+Reactアプリ作成入門(DynamoDB編)

AWS+Reactアプリ作成入門(Cognito編)
AWS+Reactアプリ作成入門(S3編)
AWS+Reactアプリ作成入門(DynamoDB編)
AWS+Reactアプリ作成入門(IAM Role編)
AWS+Reactアプリ作成入門(ログイン後のAdmin編)

今回作成したアプリ ==>久喜SNS

 DynamoDBはAWSが提供するDBサービスです。MongoDBと比べるとちょっと癖がありますし、私も全体的な理解が十分ではないので、説明は今回アプリの利用に限定することとして、断定的に記述していきたいと思います。偏見や偏りがあるかもしれないという事ですが、ご容赦ください。

  1. テーブルの検索はqueryとscanがあります。
  2. scanは無条件に全テーブルをサーチしコストが高いので使わないことにします。
  3. queryはindexに対して検索条件を与えて検索するものです。
  4. テーブル作成時にprimary indexを設定するのが必須です
  5. indexキーは2つの項目を指定します。partition key とsort keyと呼びます(partition keyのみでもok)
  6. partition keyはテーブルを分割して、検索時に分割された領域だけをサーチするようにする、イメージですかね。
  7. query検索時にはpartition keyを指定して(equal条件)、sort keyでフィルタします。
  8. query検索時は自動的にsort keyでソートされます。
  9. primary keyでないものを条件として検索したい時は、secondary indexを明示的に作成します(課金される)
  10. secondary indexでも検索したい条件に合わせて、partition key とsort keyを定義します。

1.投稿のpost

 以下のようなコードで、画像掲示板のテーブルへを投稿します。

src/views/Admin.jsの一部
    var docClient = new AWS.DynamoDB.DocumentClient();
    var params = {
        TableName: tablename,
        Item:{
             identityId: identityId, // ★prime partition key
             email: _self.state.email,
             username: _self.state.username,
             filename: filepath,
             thumbnail: thumbnail,
             type: fileType,
             title: title,
             story: story,
             imageOverwrite: _self.state.imageOverwrite,
             mapUse: _self.state.mapUse,
             position: _self.state.position,
             uploadTime: uploadTime, // ★prime & secondary sort key
             uploadDate: uploadDate,
             partitionYear: partitionYear, //★secondary partition key
             refCounter: 0
        }
    };
    docClient.put(params, function(err, data) {
        if(err) {
            console.log("Err: table put :" +err);
        } else {
            console.log("Success: table put ok");
        }
    });

 docClient.put()で投稿をテーブルに挿入します。

 このテーブルは、トップ画面で最新投稿を検索するのと、管理画面で自分の最新投稿を検索する2つの種類の検索があります。どちらも最新順のリストを取得します。

1. 管理画面で自分の最新順の投稿リストを検索

primary indexを以下のキーで作成
partition key : identityId (文字列) 
sort key : uploadTime (数値)

 identityIdはユーザIDとして使っているもので、Cognitoでのログイン時にAWS.config.credentials.identityIdに値が設定されるものです。uploadTimeは投稿時間でunixtimeです。

2. トップ画面で最新順の投稿リストを検索

primary indexを以下のキーで作成
partitionYear: partitionYear (数値)
sort key : uploadTime (数値)

partitionYearはuploadTimeを年数字に変換したものです。2017とかの数字です。uploadTimeは投稿時間です。

ちなみに昔はAWS.DynamoDB()が使われていたようですが、検索結果に不要の型(SとかNとか)が含まれとても使いにくいので、ここではAWS.DynamoDB.DocumentClient()を使っています。

2.投稿削除

 以下のようなコードで、画像掲示板のテーブルから投稿を削除します。

src/views/Admin.jsの一部
    const docClient = new AWS.DynamoDB.DocumentClient();
    const params3 = {
        TableName: tablename,
        Key: {
           identityId: item.identityId, // ★partition key
           uploadTime: item.uploadTime  // ★sort key
        }
    };
    docClient.delete(params3, function (err, res) {
      if (err) {
          console.log("### delete table err:"+err); // an error occurred
      } else{
          console.log("### delete table ok"); // successful response
          if(addCallback) { //編集 => 削除 then 追加
              addCallback();
          }
      }
    });

 partition keyとsort keyを指定し、docClient.delete()で投稿を削除しています。

3.投稿編集

 編集は、古いものを削除して新しいものを挿入する、という考えで実装しています。上の削除のコードで、削除が成功した時にaddCallback()を呼んでいるのがそれに当たります。単に削除だけを行いたい場合はaddCallback=nullとしてこの関数を呼びます。

4.カウンター

 投稿のページにはそれぞれカウンターを設け参照数をカウントしています。投稿のpostで示したコードのrefCounterがそれに当たります。今回はReactのCounterコンポーネントを作成し、参照されるごとにアトミックにインクリメントするコードを書きました。

src/views/Counter.js
import React from 'react';
import AWS from "aws-sdk";
import appConfig from '../appConfig';
import {bucketname,tablename}  from '../appConfig';

//http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.NodeJs.03.html
export default class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        counter: 0
    };
  }

  componentWillMount() {
    const _self=this;
    const dynamo = new AWS.DynamoDB.DocumentClient();
    var params = {
        TableName:tablename,
        Key:{
            "identityId": this.props.identityId,
            "uploadTime": this.props.uploadTime
        },
        UpdateExpression: "set refCounter = refCounter + :val",
        ExpressionAttributeValues:{
            ":val":1
        },
        ReturnValues:"UPDATED_NEW"
    };
    console.log("Updating the item...");
    dynamo.update(params, function(err, data) {
        if (err) {
            console.error("Unable to update item. Error JSON:", JSON.stringify(err, null, 2));
        } else {
            console.log("UpdateItem succeeded:", JSON.stringify(data, null, 2));
            _self.setState( {counter: data.Attributes.refCounter} );
        }
    });
  }

  render () {
    return (
        <div>{this.state.counter}</div>
    );
  }
}

 Counterコンポーネントは親コンポーネントからidentityIdとuploadTimeを渡され、this.propsで参照しています。カウンターは dynamo.update()でアトミックにインクリメントされます。

 今回はこれで終わりです。次回以降にIAMのRoleについて述べたいと思います。

続きを読む

DockerでRocket.chatを構築し、hubotも連携させる

備忘録のために載せておきます。
※Dockerやhubotは他に詳しい記事があるので触れません。

Rocket.chatって?

Slackライクなチャットツール。
https://rocket.chat/

一年ほど前、コミュニケーションツールとしてチャットをプロジェクトで導入する際に
サーバインストール型のチャットツールを探してこれに行きつきました。
周りで誰も使ったことがありませんでしたが、とりあえずお試しで入れたのが経緯。

なんだかんだで1年半ほどプロジェクト内で運用しています。

Dockerレシピ

rocketchat:
  image: rocketchat/rocket.chat:latest
  environment:
    - MONGO_URL=mongodb://mongodb/rocketchat
    - ROOT_URL=http://localhost:80
  links:
    - mongodb
  ports:
    - 80:3000

hubot:
  image: rocketchat/hubot-rocketchat
  environment:
    - PORT=5080
    - ROCKETCHAT_URL=[任意のドメイン]:80
    - ROCKETCHAT_ROOM=
    - LISTEN_ON_ALL_PUBLIC=true
    - ROCKETCHAT_USER=[hubot用ユーザID]
    - ROCKETCHAT_PASSWORD=[hubot用パスワード]
    - BOT_NAME=[任意のhubot名]
    - EXTERNAL_SCRIPTS=hubot-help,hubot-seen,hubot-links,hubot-diagnostics,hubot-reddit,hubot-bofh,hubot-bookmark,hubot-shipit,hubot-maps,hubot-cron,hubot-jenkins-notifier
    - HUBOT_JENKINS_URL=[連携するjenkinsのURL]
    - HUBOT_JENKINS_AUTH=[jenkinsのアカウント:パスワード]
  volumes:
    - /usr/local/share/hubot/scripts:/home/hubot/scripts
    - /etc/localtime:/etc/localtime:ro
  links:
    - rocketchat:rocketchat
  ports:
    - 3001:5080

mongodb:
   image: mongo
   ports:
     - 27017
   volumes:
     - /srv/docker/mongodb/db:/data/db

一部解説

下記はJenkinsのジョブをhubot内から実行するための設定です。

    - EXTERNAL_SCRIPTS=hubot-help,hubot-seen,hubot-links,hubot-diagnostics,hubot-reddit,hubot-bofh,hubot-bookmark,hubot-shipit,hubot-maps,hubot-cron,hubot-jenkins-notifier
    - HUBOT_JENKINS_URL=[連携するjenkinsのURL]
    - HUBOT_JENKINS_AUTH=[jenkinsのアカウント:パスワード]

下記はRocket.chatのログはmongoDB内に格納されるため
コンテナを消してもログを残すためにマウントの設定を入れています。
ホスト側のパスはもちろん任意です。

   volumes:
     - /srv/docker/mongodb/db:/data/db

運用する中で起きた問題

ID管理

開発環境には他サービスも並行して動いており
導入したチャットでも新たにID管理するのは面倒だから何とかならない?
といったことがありました。

対処として、Rocket.Chatには特定サービスとのoAuth認証機能が備わっていましたので
今回のケースではバージョン管理として既に使用していたGitlabに集約することに。

キャプチャ.PNG

hubotコンテナが落ちる

hubotには導入したプラグイン次第で様々なコマンドを使用させることができます。
メンバーの利用状況を見ていると、通知機能(タスク登録)がよくつかわれていたようです。
Cron形式で登録することができ、お昼や定例などのアラーム代わりに使っているのが見受けられました。

ある日突然、hubotが反応しなくなりdocker ps -a で状態を見ると、hubotコンテナが落ちていました。
docker logs にてログを出力したところ、チャットの全ルームの過去ログが流れ出しました。

原因はおそらく、割り当てたメモリ枯渇ではないかなと推測しています。
ちなみにDockerはAWSのEC2上で起動しております。

対処として、docker rm [コンテナ名] でコンテナ削除を行い、docker-compuse up -d
で起動しました。
hubotが持っているログは不要なのでばっさり切り捨て。

ただこの方法だと、上記にありました通知機能で登録したタスクがすべてなくなってしまいますので
コアな運用に用いている場合はhubotコンテナもホストにマウントするなど、一工夫必要かなと思います。

続きを読む