小さなエンドウ豆

まだまだいろいろ勉強中

ActiveRecord でテーブルをダウンタイムなしで洗い替えする方法

ActiveRecord でテーブルをダウンタイムなしで洗い替えする方法

小ネタです。

こんな要件がありました。

「月一でこのテーブルをあるデータで洗い替えてほしい」

ActiveRecord を使うと delete_all してから insert すれば簡単なのですが、その間にデータへのアクセスはできずにサービスに影響を与えることになります。

調べてみると一時テーブルを用意しそのテーブルにデータを追加してから RENAME_TABLE で元のテーブル名にリネームしてあげるとダウンタイムほぼなしで洗い替えが可能だそう。

この手順を ActiveRecord を使って行う方法を記していきます。

ActiveRecord で一時テーブル作成

これはいつものでできます。

ActiveRecord::Base.connection.create_table('temporary_table', force: true) do |t|
  t.string :name, comment: '名前'
end

一時テーブルに対応するモデルの作成

コードベースで作成したためもちろん対応するモデルクラスがありません。

ActiveRecord::Base を継承したモデルは非常に便利で後続のデータ追加などでも利用したので、
一時的にモデルを作ります。

以下を参考にモデルを作ります。

qiita.com

klass = Class.new(ActiveRecord::Base) do |c|
        c.table_name = 'temporary_table'
end

Object.const_set('TemporaryTable', klass)

Rails ガイドにもありましたが、ActiveRecord::Base の table_name を書き換えるとテーブル名を上書きすることができます。
Object.const_set は Object モジュール配下に TemporaryTable という定数を klass という値で定義するって意味らしい。

Active Record の Bulk Insert(データの一括追加)

Rails 6 だと Bulk Insert は以下のようにします。

TemporaryTable.insert_all([{name: 'aaaa'}, {name: 'bbbb'}])

データが大量の場合

データが大量の場合は MySQL のコネクションエラーが出て一度に追加できない場合があります。
each_slice を使うと一度に登録する件数を絞ることができます。
例えば 100 件ずつに絞りたい場合以下のように書きます。

data = [{name: 'aaa'}, {name: 'bbb'}, ...]

data.each_slice(100) { |d| TemporaryTable.insert_all(d) }

ActiveRecord で RENAME_TABLE

今回は SQL をベタ書きして本番テーブルと作った一時テーブルをスワップさせます。

sql <<-"EOS"
  RENAME TABLE old_table TO t,  -- 1. 古いテーブルを t というスワップ用のテーブルに退避
    temporary_table TO old_table,  -- 2. 新しいデータの入った temporary_table を old_table にリネーム
   t TO temporay_table;   -- 3. 退避させていたテーブルを temporary_table にリネーム
EOS

# SQL 実行
ActiveRecord::Base.connection.execute(sql)

# 一時テーブルを削除
ActiveRecord::Base.connection.execute("DROP TABLE temporay_table")

ActiveRecord にも rename_table というメソッドがありますが、上記のような 1 つの SQL ではできなさそうだったため、
SQL で実行する方法を選択しました。

api.rubyonrails.org

この一連の流れを Rake Task なりに書き起こして Batch に設定すると定期的に洗い替えができます。

ElasticSearch と Kibana で位置情報(geo_point)を扱う

ElasticSearch と Kibana で位置情報(geo_point)を扱う

前に MySQL 8 で GIS 関数を使ってみたが機能によってはインデックスが無効になってしまうものがあるためパフォーマンスの面で懸念をしました。

h-piiice16.hatenablog.com

これらを解消すべく僕の中で白羽の矢が立ったのが ElasticSearch(ES)であります。

今回は ElasticSearch と Kibana を使って MySQL の記事みたく半径 1 km 圏内のマクドナルドを絞るクエリを書いてみようと思います。

ElasticSearch と Kibana

どちらも Elastic 社によって開発されたデータ分析用の OSS です。

ElasticSeach

分散型でオープンソースな検索・分析エンジンです。

と公式サイトに説明がありますが、扱えるデータとしてテキストや数値以外にも地理空間情報なども扱うことができます。
特徴としてはシンプルな REST API になっており開発者からすると非常に扱いやすい点と、
クラスタ構成が前提になっているためスケーラブルな点が挙げられます。

ElasticSearch を使うには以下の用語の理解が必要です。

  • ノード ... 一つのサーバーに該当
  • クラスタ ... ノードの集合体でトラフィックが増加した際にノードを増やすことで負荷分散することができる
  • インデックス ... RGB でいうデータベースのこと
  • タイプ ... テーブルのこと
  • ドキュメント ... 行のこと

Kibana

ES にためたデータを可視化するためのツールです。

環境構築

ES, Kibana それぞれ docker イメージが存在するのでそれらを使って環境構築していきます。
以下が docker-compose.yml です。

version: "3.3"

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.6.1
    environment:
      - discovery.type=single-node
      - cluster.name=docker-cluster
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    ports:
      - 9200:9200
    volumes:
      - es-data:/usr/share/elasticsearch/data
  kibana:
    image: docker.elastic.co/kibana/kibana:7.6.1
    ports:
      - 5601:5601

volumes:
  es-data:
    driver: local

ES のコンテナを複数に増やすとクラスタ構成を組むことが可能なようですが今回はスキップします。

ES 上の操作

構築した ES にインデックスを登録します。
インデックスの登録も RESTful なエンドポイントで提供されています。

www.elastic.co

curl -H "Content-Type: application/json" -XPUT 'http://localhost:9200/mc' -d @mc.json

mc というのが index 名です。
mc.jsonマッピングといって RDB でいうスキーマのようなものです。 以下がその内容です。

{
  "mappings": {
    "properties": {
      "id": {
        "type": "integer"
      },
      "name": {
        "type": "text"
      },
      "address": {
        "type": "text"
      },
      "latidude": {
        "type": "double"
      },
      "longitude": {
        "type": "double"
      },
      "geo_location": {
        "type": "geo_point"
      }
    }
  }
}

geo_location というフィールドを用意し位置情報を管理します。

データの作成

データは前回も用いたマクドナルドの位置情報を使用しました。
マクドルドの情報を以下の用に整形します。

{"index":{}}
{"name":"西町店","address":"北海道札幌市西区西町北2-1-6","latitude":43.0773043276,"longitude":141.292534094,"geo_location":[141.292534094,43.0773043276]}

index には先程登録したインデックスの情報を書くのだがエンドポイントで指定が可能なのでここは空にします。
geo_point 型の値の書き方は以下のパターンが有ります。

www.elastic.co

今回は array 方式でいきました。
注意として緯度経度の順番が各方法で違うことが挙げられます。

データが揃ったら以下のエンドポイントを叩き登録していきます。

curl -H "Content-Type: application/json" -XPOST 'http://localhost:9200/mc/_bulk' -d @result.json

_bulk はデータを複数一括で登録するための機能となります。
result.json には全国のマクドナルドの位置情報が上記のフォーマットで記されています。

途中間違えたとなったらインデックスごと消すと良いでしょう。

curl -XDELETE 'localhost:9200/mc'

半径 1 km 圏内にある店舗

以下のようなエンドポイントで出すことができました。

curl -H "Content-Type: application/json" -XPOST 'http://localhost:9200/mc/_search' --data-binary @query.json

query.json の中身

{
  "query": {
    "geo_distance": {
      "distance": "1km",
      "geo_location": {
        "lat": 35.6812362,
        "lon": 139.764936
      }
    }
  }
}

結果

{"took":18,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":4,"relation":"eq"},"max_score":1.0,"hits":[{"_index":"mc","_type":"_doc","_id":"yb9MAHEB7mQhkr5uWG6K","_score":1.0,"_source":{"name":"丸の内国際ビルヂング店","address":"東京都千代田区丸の内3-1-1","latitude":35.6770328213,"longitude":139.761160999,"geo_location":[139.761160999,35.6770328213]}},{"_index":"mc","_type":"_doc","_id":"yr9MAHEB7mQhkr5uWG6K","_score":1.0,"_source":{"name":"有楽町ビルヂング店","address":"東京都千代田区有楽町1-10-1","latitude":35.6750116772,"longitude":139.761805747,"geo_location":[139.761805747,35.6750116772]}},{"_index":"mc","_type":"_doc","_id":"HL9MAHEB7mQhkr5uWG-M","_score":1.0,"_source":{"name":"銀座インズ店","address":"東京都中央区銀座西1-2銀座インズ3内","latitude":35.6753447943,"longitude":139.765509864,"geo_location":[139.765509864,35.6753447943]}},{"_index":"mc","_type":"_doc","_id":"N79MAHEB7mQhkr5uWG-M","_score":1.0,"_source":{"name":"JR東京駅店","address":"東京都千代田区丸の内1-9-1東京駅一番街","latitude":35.6797146357,"longitude":139.767587012,"geo_location":[139.767587012,35.6797146357]}}]}}

MySQL のときと結果が一緒になっています。

四方のエリアで集計

ES では位置情報を集計することができます。
下記を参考に 5km 四方の四角内にどれだけの数のマクドナルドがあるか集計します。

curl -H "Content-Type: application/json" -XPOST 'http://localhost:9200/mc/_search?pretty' --data-binary @aggregate.json

aggregate.json の中身

{
  "size": 0,
  "aggs": {
    "1": {
      "geohash_grid": {
        "field": "geo_location",
        "precision": 5
      },
      "aggs": {
        "2": {
          "geo_bounds": {
            "field": "geo_location"
          }
        }
      }
    }
  }
}

ちなみに密集していた地域は大阪の動物園前付近のエリアでした。

f:id:h-piiice16:20200329084934p:plain

kibana による可視化

先程の docker-compose.yml を元にサービスを立ち上げると以下の URL でマップを見ることができます。 http://localhost:5601/app/maps

まず Create Map を押すと以下のような地図が表示されます。

f:id:h-piiice16:20200329085943p:plain

次に Add Layer を押し先程登録したインデックスを指定します。

f:id:h-piiice16:20200329090025p:plain

すると表示されます。

f:id:h-piiice16:20200329090052p:plain

上部にある検索バー?みたいなところに Kibana 特有のクエリを書くと表示する店舗を絞ることもできます。
例えばフォッサマグナ以東で佐渡ヶ島以南のマクドナルドを調べたいときは以下のようなクエリを入力します。

longitude >= 138.23553 and latitude <= 37.0670589

f:id:h-piiice16:20200329090312p:plain

できてそうですね!

まとめ

ElasticSearch で位置情報を取り扱うことができました。
クラスタマッピングについて詳しく学習するとパフォーマンスの面で目覚ましいことがわかるかもしれないです。
次回勉強してみようと思います。

また今回 map の部分しか使わなかった kibana ですが BI ツールとしても使えそうなのでその他の機能も深ぼっていきたいところです。

appium で Android の E2E テストを書く

appium で Android の E2E テストを書く

最近 Android 開発をちょこちょこしているのですが、フロントエンドの E2E テストみたいに自動でテストしてくれるものが Android にも無いかなー
と探していたところ appium というツールを見つけたので使ってみたというエントリーです。

appium

ネイティブ(モバイル)アプリのためのテスト自動化フレームワークです。
AndroidiOS どちらにも対応しています。
特徴として自分の得意な言語でテストケースを書くことができます。
選べる言語は Java, Javascript, Python, Ruby, php などなど。

appium にはデスクトップアプリがあり、それを使うと簡単にテストケースを作ることができます。
以下からダウンロードすることができます。

github.com

ダウンロード後以下の画面が立ち上がるのでそのまま「Start Sever」で進みます。

f:id:h-piiice16:20200321165532p:plain

f:id:h-piiice16:20200321165711p:plain

虫眼鏡のアイコンを押します。

f:id:h-piiice16:20200321170010p:plain

このような画面が開くので「JSON Representation」に以下の内容を書き込みます。

{
  "platformName": "Android",
  "deviceName": "エミュレータの名前",
  "platformVersion": "10",
  "automationName": "Appium",
  "app": "apk へのパス",
}

エミュレータの名前Android Studioエミュレータを起動した状態で以下のコマンドを打つと得られます。

> adb devices
List of devices attached
XXXX    device

XXXX の部分が名前です。
この状態で「Start Session」を押します。

すると以下のような画面が開きます。

f:id:h-piiice16:20200321174652p:plain

この画面で要素をクリックして tap, send_keys, clear で操作をしていきます。
tap はその名の通りボタンなどをタップする操作になります。
send_keys は EditText などに値を入力する操作で、clear は値を消す操作になります。

このように操作を加えていくとページ上部にコードが現れます。
これがテスト操作のためのコードになるのでコピーします。(言語はセレクトボックスから選べます)

そのコピーしたコードを以下のように RSpec に載せます。

require 'spec_helper'

desired_caps = {
  caps: {
    platformName:  'Android',
    platformVersion: '10',
    deviceName:    'xxxxxxxxxxxxxxxxxxx', # デバイスの名前
    app:           'yyyyyyyyyyyyyyyyyyyyyy' # apk の場所
  },
  appium_lib: {
    wait: 60
  }
}

describe 'Basic Android interactions' do

  before(:all) do
    @driver = Appium::Driver.new(desired_caps, false).start_driver
  end

  after(:all) do
    @driver.quit
  end

    it 'スケジュール画面での確認' do
          @driver.start_recording_screen
         ################ 以下ペーストした部分 ####################
          el4 = @driver.find_element(:id, "com.example.myscheduler:id/fab")
          el4.click
          el5 = @driver.find_element(:id, "com.example.myscheduler:id/dateEdit")
          el5.send_keys "2020/01/01"
          el6 = @driver.find_element(:id, "com.example.myscheduler:id/titleEdit")
          el6.send_keys "title"
          el7 = @driver.find_element(:id, "com.example.myscheduler:id/detailEdit")
          el7.send_keys "detail"
          el8 = @driver.find_element(:id, "com.example.myscheduler:id/save")
          el8.click
          el9 = @driver.find_element(:id, "com.example.myscheduler:id/snackbar_text")
          expect(el9.text).to eq('追加しました')
          @driver.back
          el10 = @driver.find_element(:id, "android:id/text2")
          expect(el10.text).to eq('title')
          ################ ここまで #############################
          @driver.stop_and_save_recording_screen './sample.mp4'
     end
end

@driver.start_recording_screen は動画を撮る際に要る構文です。
終わりに @driver.stop_and_save_recording_screen './sample.mp4' とすると mp4 形式で出力されます。

非常に便利ですね。

まとめ

appium はデスクトップアプリを使うと非常に簡単にテストケースを作ることができました。

この容易さに加えて AWS の Device Farm というサービスとの連携ができるそうです。
Device Farm を使うと iOS, Android のプラットフォームを越えたテストや Android 特有のいろいろな端末でのテストを同時に行うことができるらしいです。

Android E2E で調べると他にも候補がありましたが、これらの点から appium を選択してよかったと思いました。

MySQL8 の GIS 関数を使ってみる

MySQL8 の GIS 関数を使ってみる

MySQL では geometry 型という緯度経度をテーブルで扱うことができます。
これと組み合わせて MySQL8 から本格導入となった GIS 関数を使うと何ができるのか調べていきたいと思います。

GIS 関数とは

GIS とは地理情報システム Geographic Information System の略で、地理的位置を手がかりに位置に関する情報を持ったデータ(空間データ)を総合的に管理・加工し、視覚的に表示し高度な分析や迅速な判断を可能にする技術のことらしいです。
GIS のツールの中には QGISArcGIS などがあります。

この GIS ツールなどで使われる 2 点間の距離を求めるなど位置情報に関する操作を SQL ベースで利用できるようになるというイメージです。

今まで MySQL を使ってサービスで位置情報を扱うには緯度経度を float 型で保持し対象のレコードを絞ってからアプリケーションコードで位置情報に関する処理を行うしかありませんでした。
GIS 関数の対応によって絞り込みや集約を SQL レベルでできるようになることが期待できます。

MySQL8 の環境構築

ローカルには MySQL 5.7 をインストール済みで他のプロジェクトでも使うため Docker で構築していきます。

docker-compose.yml

version: "3"
services:
  db:
    image: mysql:8.0
    volumes:
      - db-store:/var/lib/mysql
      - ./logs:/var/log/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    environment:
      - MYSQL_DATABASE=geometry
      - MYSQL_USER=root
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=password
      - TZ="Asia/Tokyo"
    ports:
      - 13306:3306
volumes:
  db-store:

これで完成。

データの用意

geometry というデータベースが作られるのでここにテーブルを作成します。

CREATE TABLE mc (
  `id` INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(64) NOT NULL,
  `address` VARCHAR(128) NOT NULL, 
  `geohash` VARCHAR(32) NOT NULL
) ENGINE = InnoDB

geohash に位置情報が積めます。
Geohash とは緯度経度をとある文字列に置き換えたものです。
Geohash にすることによってデータ上文字列として扱うことができます。

次にデータを Insert します。

INSERT INTO mc (name, address, geohash)
    VALUES ('西町店', '北海道札幌市西区西町北2-1-6', ST_GeoHash(141.292534094, 43.0773043276, 12)),
    ('白石南郷通店', '北海道札幌市白石区南郷通2丁目南10-15', ST_GeoHash(141.400104165, 43.0446331934, 12)),
...

データにはマクドナルドの店舗情報をハックして取得した位置情報を使います。
ST_GEOHASH という GIS 用の関数が用意されており、経度、緯度の順番で引数に与えると geohash に変換してくれます。
変換した値をテーブルに追加していきます。

これで用意も完了。

GIS 関数を体験

一番遠い店舗順に取得

東京駅から一番遠いマクドナルドの店舗を算出します。

SELECT name, address,
st_distance(ST_GeomFromText('Point(139.764936 35.6812362)'),
  ST_GeomFromText(CONCAT('Point(', ST_LongFromGeoHash(geohash), ' ', ST_latFromGeoHash(geohash), ')'))
    ) as distance
    FROM mc
    ORDER BY distance desc
    LIMIT 1;

ここで大切なのが ST_LongFromGeoHashST_latFromGeoHash である。
名前の通りこれらの関数を使うと geohash から緯度経度へのデコードができます。
POINT() というのは後続の GIS 上のデータフォーマットで後続の関数の引数として使用されます。
この POINT からジオメトリオブジェクトに変換するのが ST_GeomFromText で、そのオブジェクト間の距離を算出するのが ST_DISTAMCE 関数です。

ちなみに指定している緯度経度は東京駅のもので一番通りマクドナルドは 石垣サンエー店 でした。

半径 1 km にある店舗

参考 qiita.com

SELECT
  id, name, address,
  ST_LENGTH(ST_GEOMFROMTEXT(
      CONCAT('LINESTRING(139.764936 35.6812362, ', ST_LongFromGeoHash(geohash),' ', ST_latFromGeoHash(geohash), ')')
  )) AS distance
FROM mc
group BY id
having distance <= 0.0089831601679492 -- 1 km という意味らしい
;

LINESTRING は先程の POINT と同じく GIS 上のデータ・フォーマットで ST_GEOMFROMTEXT でジオメトリオブジェクトに変換し、ST_LENGTH で長さを求めます。
これが 1 km 未満の店舗を表示するという簡単な SQL

ちなみに結果は以外で 4 店舗しかありませんでした。

f:id:h-piiice16:20200315205252p:plain

まとめ

MySQL で位置情報を扱ってみましたが、実践的な用途にも使えそうです。
ただ、関数によってはインデックスが無効化されたりするものがありパフォーマンスはあまり良くなさそうです。
PostGIS という PostgreSQL ベースの GIS プラグインがあるようでそちらのほうが集約関数に富んでるらしく実用的かもしれません。

既存のサービスでは MySQL を使ったものも多いため色々と改善してほしいものです。 GIS 関数に関しては開発中らしく、これからが楽しみです。

GTFS を Deck.gl のデータ形式に変換する

GTFS を Deck.gl のデータ形式に変換する

先日「Geospatial Hackers Program 沖縄」というハッカソンに出た際に GTFS という形式のデータを使った。
このデータ形式はバスなどの公共交通機関の時間表をオープンデータにする際の一般的なフォーマットらしい。

ハッカソンではこの形式のデータを Deck.gl の TripLayer に適応させ、地図上で移動を表現した作品を作りました。
ただその際に GTFS 形式のデータを Deck.gl 用に変換するのが面倒でした。
今回は GTFS についてまとめるのと同時に Deck.gl(TripLayer)用に変換するノウハウを紹介したいと思います。

ちなみにハッカソンは沖縄で開催されたこちらです。
リモート参加したのですが、楽しくできました。
ghp.connpass.com

作品(Github)はこちら

github.com

GTFS(General Transit Feed Specification)とは

GTFS とは公共交通機関の時刻表と地理的情報に関するオープンフォーマットのことを言います。
GTFS と調べるとよく Google の乗り換え案内などがヒットするが Google は GTFS 形式で公共交通機関の情報を配信しているらしい。
GTFS には動的なものと静的なものが存在します。
動的なものはリアルタイムでバスや船の位置を把握することができるものです。
静的なものは単に時刻表で 1 日の位置がわかります。

GTFS は Zip ファイルに格納されたテキストファイルで構成します。
ファイルには停車地点、ルート、時刻などそれぞれの情報を表すものが分割されています。

ファイル一覧は以下のページに記載されています。

developers.google.com

これらのファイルを用意すると GTFS として配信することが出来ます。

Deck.gl

言わずとしれた Uber 社のデータビジュアライズツールです。
Deck.gl は WebGL を使用するため 3D のデータ表現に長けています。
例えば ArcLayer を使うと飛行機の移動や人流データを表現することができます。

deck.gl

今回は船の移動を表現したかったので TripLayer を使いました。

deck.gl

この https://deck.gl/#/examples/overview を見るといろんな活用法が見れて想像力が駆り立てられますね。

実装

それでは実装していきます。
まずデータ集めですが、日本の公共交通機関は以下のサイトに集まっています。

tshimada291.sakura.ne.jp

ここからハッカソンのテーマであった沖縄の交通機関に関するデータを落としてきました。
あるファイルを zip でダウンロードしたあとに解凍すると先程のリファレンスに合ったようなファイルがありました。
ただ空のファイルも中にはあり、今回必要となるのが時刻とその時の緯度経度だけなので最終的に使ったファイルは「#」のついたものだけです。

.
├── agency.txt
├── calendar.txt
├── calendar_dates.txt
├── fare_attributes.txt
├── fare_rules.txt                    # 旅程(始点と終点を結ぶために使用)
├── frequencies.txt
├── routes.txt
├── shapes.txt
├── stop_times.txt                 # 時間
├── stops.txt                          # 停車位置
├── transfers.txt
└── trips.txt                          # 船便

これらの ER 図を書くと以下のようになります。

f:id:h-piiice16:20200224100431p:plain
ER 図

このように正規化されたテーブル構造になっているため SQL で join するのも一つの手だと思われますが、Python の DataFrame を使うと割と簡単に join できました。

ソースコード https://github.com/hiracky16/ghp-hackathon-team-conviction/blob/master/make_data/script.py

作るデータ形式は以下のようなものです。これが TripLayer で読むことのできる形式です。 https://raw.githubusercontent.com/uber-common/deck.gl-data/master/examples/trips/trips-v7.json

データの作成が終わったらあとはデータを読み込むだけです。
f:id:h-piiice16:20200224100856p:plain

まとめ

GTFS 形式のデータを扱ったのが初めてだったのでまとめてみました。
今回扱ったのは静的なデータだったためいつか動的なものも扱ってみたいと思いました。

今回データの変換には Python を使いましたが、GTFS → Deck.gl への適応はこれから多用すると外でも使えると思うので npm モジュールなどほしいなと思いました。
時間があれば自作してみようと思います。

AWS SQS + Lambda + SES でメール送信システムを作る

AWS SQS + Lambda + SES でメール送信システムを作る

Web サービスを運営しているとメールを送信したいタイミングが山程あります。
例えば、ユーザーの新規登録時、定期的に送信するメルマガ用途は多岐に渡ります。

メール送信の仕組みをモノリシックにサービスの一部として組み込むとパフォーマンスが低下してしまう恐れがあります。 そこでどのような設計にすれば悩んでいたところ、SQS をすすめられてため調査してみました。

また SES や SNS としった AWS による通知系のサービスと連携されることで完全マネージドな通知サービスとして構築を目指します。

Amazon Simple Queue Service(SQS)

名前の通りキューイングのサービスです。

キューには 2 種類あり、標準キューと FIFO キューが選べます。
2 つの違いは配信の順序です。
標準キューの配信順序はベストエフォート型で、配信は少なくとも 1 回行われます。
FIFO キューは、その名の通りメッセージが送信される順序のとおりに 1 回のみ確実に処理されるように設計されています。

また SQS にキューイングすることを Lambda のイベントとすることができます。
今回は SQS にイベントを登録し、それをフックにメールを送ります。

SQS を使う理由

SQS を使う理由として以下が挙げられます。

  • キューがあることによって非同期通信が有効になりパフォーマンス向上が見込める
    • 例えば会員登録時にメールを送ることを SQS にまかせて会員登録処理自体は別にすすめることが可能になる
  • キューに登録することによりデータの一貫性が保たれるため信頼性の向上につながる
  • 完全マネージドなサービスのため運用が楽

aws.amazon.com

Amazon Simple Email Service(SES)

クラウドベースの E メールサービスです。

使い方は簡単で、SES から送信されるメールの送信元メールアドレスを有効化したあとに SDK を使ってリクエストするとメールが送信できるようになります。

今回は Lambda からのリクエストを受けてメールを送信する役割を担います。

※ 注意 SES は東京リージョンではサポートされていないため他のリージョンで利用しなければならない。 また Lambda や Lambda のイベントとなるリソースも同じリージョンに存在しなければならない。 https://docs.aws.amazon.com/ses/latest/DeveloperGuide/regions.html#region-receive-email

Amazon Simple Notification Service(SNS

クラウドベースの通知サービスです。
SNS を使うとメールを送ることが可能です。

ただ送信元のアドレスが固定になってしまうため今回使用は見送りました。
(ちなみに no-reply@sns.amazonaws.com からのメールになります。)

SES と SNS では以下のような違いがあります。

違い SES SNS
東京リージョンでの使用 不可
送信元メールアドレス 可変 固定
HTML形式 不可
料金 1000通につき0.10USD 1000通まで無料,10万件あたり2USD

先程も書きましたが、今回の要件を SNS では満たせなかったことや今後の拡張性を考慮して SES を使用します。

実際にやってみる

改めて今回の構成です。

f:id:h-piiice16:20200215205412p:plain
構成

SQS は標準キューを選んで新しく登録します。
SES は先程言ったように送信元メールアドレスを有効化します。

次に Lmabda だが今回 Serverless Framework を使って実装してみます。

Serverless Framework

サーバーレスアプリケーションの構成管理ツールです。
yml 形式で連携するサービスを記述することが出来ます。
またデプロイをコマンドで行うことができるため非常に楽です。

Serverlass Framework に関しては詳しく記しませんが、プロジェクト内にある serverless.yml を以下のように書くと SQS をフックに lambda を呼び出すことができます。

functions:
  hello:
    handler: handler.mail
    events:
      - sqs:
          arn: arn:aws:sqs:ap-south-1:xxxxxx:node_mail

arn には作成したキューのものを記します。 events には他にも API Gateway や DynamoDB など他のリソースを書くこともできます。

Lambda 関数

実装は以下のような感じです。

import { SQSHandler, SQSRecord } from "aws-lambda";
import "source-map-support/register";
import * as AWS from "aws-sdk";
import nodemailer from "nodemailer";

const ses = new AWS.SES({
    accessKeyId: process.env.AWS_KEY_ID,
    secretAccessKey: process.env.AWS_KEY,
    region: "ap-south-1",
    apiVersion: '2010-12-01'
});

const transporter = nodemailer.createTransport({ SES: ses });

export const mail: SQSHandler = async (event: SQSEvent) => {
  const addresses: string[] = event.Records.map((r: SQSRecord) => {
    return r.body;
  });
  const params = {
    from: process.env.SEND_ADDRESS,
    to: addresses,
    subject: "Email Testing",
    html: "<h1>Title</h1>"
  };

  console.log(params);

  try {
    await transporter.sendMail(params)
  } catch (e) {
    console.log(e);
  }
};

SES のインスタンスを作成する際にはシークレットキーを設定する必要がありました。
TS で記述しているため mail という関数や SQS からのイベントの型を指定しています。

なぜ nodemailer を使うのか?

参考にした実装例では nodemailer をいうモジュールで SES のインスタンスをラップしてから使っていました。
その理由が以下のサイトに載っていたため挙げます。
簡単に言うとメール送信時にパラメータの指定の仕方がこちらの API のほうが簡単だそうです。
特に今回は要りませんでしが、ファイル添付する際など SES の標準 API を使うと煩わしいみたいです。

nodemailer.com

実行

SQS でイベントを発行することができます。
本来は EC2 から SQS にイベントを発行するというシチュエーションを想定していますが、GUI ベースでテストも可能です。
メッセージに送信先のメールアドレスを入力し登録するとメールが送られてきました!

f:id:h-piiice16:20200215224212p:plain
メール本文

html も解釈出来ていますね。

まとめ

まだ不十分ではあるがミニマムでメール送信サービスを作ることができた。
残タスクとして、SQS へのイベントの登録や各サービスで起きたエラーのハンドリング、バウンスメールへの対応などを考えなくてはいけません。

SQS や SES は初めて使うサービスだったがフルマネージドもあってか簡単に使うことができました。
実際に Web アプリケーションでメール機能を実装するよりこちらのほうがコストをかけずできると思います。
また運用、保守のコストも同じくかからないと思うのでおすすめです!

ソースコード

github.com

スタートアップのための AWS Fargate 入門

スタートアップのための AWS Fargate 入門

本記事は以下の内容を読んでより具体的な方法について書いたものです。

aws.amazon.com

そもそもコンテナが解決しているもの

アプリケーションを動かすにはコードだけでなく関連するコンポーネントが揃っている必要があります。
例えば Ruby on Rails の場合は以下が挙げられます。

このような依存関係を解決するための手段としてコンテナが利用されます。
アプリケーションの実行に必要な依存ブルをすべてコンテナの中にパッケージングし、開発環境から本番環境まで同一の環境で動作させることが出来ます。

https://d2908q01vomqb2.cloudfront.net/0286dd552c9bea9a69ecb3759e7b94777635514b/2019/08/07/docker2.png

パッケージングされたコンテナを実行するツールとしてデファクトスタンダードとなっているのが Docker です。
Docker Engine が動いている環境であればコンテナを実行することが出来ます。
Docker Engine の管理するスコープとしては単一ホストマシン上でのコンテナの動作になります。
それ以外の部分、例えばコンテナのオートスケーリングや複数ホストマシンにまたがるような配置、ローリングアップデートなどは別の仕組みとして実装する必要があります。

ローリングアップデートとは

同じ機能を持った複数のコンピュータで構成している場合のシステムをアップデートする手法の一つです。 システムの稼動状態を維持しながら、1台ずつ順番にアップデートを行っていきます。

コンテナオーケストレーション

AWS ECS や EKS といったサービスがこれに当たります。
複数のホストマシンにまたがるコンテナの配置やコンテナのアップデート、ロードバランサーへの紐づけなどを管理してくれます。
以下のイメージのようにこちらから API 経由で変更後の状態を支持することでツールがその状態になるように維持・動作します。
これを宣言的デプロイと呼ぶそうです。

https://d2908q01vomqb2.cloudfront.net/0286dd552c9bea9a69ecb3759e7b94777635514b/2019/08/07/docker3.png

コンテナオーケストレーションをを使うとオートスケールや複数のマシンでのコンテナ管理が簡単になり、開発者はコンテナの運用に集中できると思われるが、
実際はホストマシンそのものの管理や運用は依然として残っています。
コンテナが動くホストマシンは OS が動いていますし、Docker Engine が動いている必要があるからです。

コンテナ解決することは上記にも書きましたが、その一方でコンテナとホストマシンの両方を管理・運用しなければならないということになります。

AWS Fargate とは

上記のような二重管理を解決するのが AWS Fargate です。
ECS でコンテナを実行する際の起動タイプの一つで、EC2 と Fargate があります。 EC2 で実行したい際のイメージが以下です。
「どのホストマシン」で「どのコンテナ」を「いくつ起動する」かなどのハンドリングは、オーケストレーションツールである Amazon ECS から行うことが可能ですが、各ホストマシンの管理・運用業務は依然として残ります。

https://d2908q01vomqb2.cloudfront.net/0286dd552c9bea9a69ecb3759e7b94777635514b/2019/09/10/ecs1-2.png

一方 Fargate がこちらです。

https://d2908q01vomqb2.cloudfront.net/0286dd552c9bea9a69ecb3759e7b94777635514b/2019/09/10/fargate2-1.png

この画像ではホストマシンが見えなくなり、OS や Docker Engine, ecs-agent が抽象化され Fargate プラットフォームに隠蔽されていることを表しているようです。

他にも以下のようなメリットがあります。

クラスタの管理が不要(キャパシティ)

通常、EC2 等の仮想マシンクラスタを構築し運用する際には様々なことを考慮する必要があります。
AWS Fargate を利用すると、こういったことを意識することなくコンテナを実行することが出来ます。
コンテナ実行時に必要な CPU, メモリの組み合わせを選択するだけでよく、リソースの調達等は Fargate プラットフォーム側で行われます。

ホストマシンの管理が不要(セキュリティ等)

コンテナワークロードに限らず、通常 EC2 等の仮想マシンを運用する際は、 OS やミドルウェアのバージョンアップやセキュリティパッチの適用などをやらなければなりません。
Fargate プラットフォームの管理運用は AWS にて行うため、前述の OS や Docker Engine, ecs-agent 等のバージョンアップやセキュリティパッチの適用もこの中に含まれるため、行う必要はありません。

実践

AWS が用意しているチュートリアルをもとに気になった用語などをまとめていきます。

https://ap-northeast-1.console.aws.amazon.com/ecs/home?region=ap-northeast-1#/firstRun

ほとんどデフォルトのままで起動が可能でした。

Container definition

どんなコンテナを使うか宣言する部分。
チュートリアルでは Apache, Nginx, Tomcat とカスタムが用意されています。 今回は Nginx を選択。

カスタムにするとマシンスペックも選ぶことが出来ます。(CPU, メモリなど)

Task Definition

どのように起動させるかここで決めることが出来ますが、チュートリアルではすでに設定があるため必要ありませんでした。

Service

先程設定したタスク定義を何個実行して維持するか設定できます。
必要なタスクの数やロードバランサ(なし or ALB)を選ぶことができます。

Cluster

VPC, サブネットを設定します。 新規作成してくれるようなのでいじらずにそのまま作成へ。

これだけで Nginx に接続ができました。
後にタスクの数を 2 つにして起動し直し、2つにアクセスが振り分けれることも確認できました。

まとめ

  • コンテナを使うとアプリケーション実行に必要な依存関係をパッケージングしてくれる
  • コンテナを複数マシンで実行するためにンテナオーケストレーションツール(ECSやEKS)は必須
  • Fargate は ECS の起動モードの一つでホストマシンの管理が不要になる
  • Fargate のチュートリアルは雰囲気を味わうにはよい

今回はチュートリアルの内容をただやった実施しただけなので、次回は自作のコンテナを Fargate 上で実行するってことに挑戦してみます。