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 を継承したモデルは非常に便利で後続のデータ追加などでも利用したので、
一時的にモデルを作ります。
以下を参考にモデルを作ります。
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 で実行する方法を選択しました。
この一連の流れを Rake Task なりに書き起こして Batch に設定すると定期的に洗い替えができます。
ElasticSearch と Kibana で位置情報(geo_point)を扱う
ElasticSearch と Kibana で位置情報(geo_point)を扱う
前に MySQL 8 で GIS 関数を使ってみたが機能によってはインデックスが無効になってしまうものがあるためパフォーマンスの面で懸念をしました。
これらを解消すべく僕の中で白羽の矢が立ったのが 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 なエンドポイントで提供されています。
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 型の値の書き方は以下のパターンが有ります。
今回は 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"
}
}
}
}
}
}
ちなみに密集していた地域は大阪の動物園前付近のエリアでした。

kibana による可視化
先程の docker-compose.yml を元にサービスを立ち上げると以下の URL でマップを見ることができます。 http://localhost:5601/app/maps
まず Create Map を押すと以下のような地図が表示されます。

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

すると表示されます。

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

できてそうですね!
まとめ
ElasticSearch で位置情報を取り扱うことができました。
クラスタやマッピングについて詳しく学習するとパフォーマンスの面で目覚ましいことがわかるかもしれないです。
次回勉強してみようと思います。
また今回 map の部分しか使わなかった kibana ですが BI ツールとしても使えそうなのでその他の機能も深ぼっていきたいところです。
appium で Android の E2E テストを書く
appium で Android の E2E テストを書く
最近 Android 開発をちょこちょこしているのですが、フロントエンドの E2E テストみたいに自動でテストしてくれるものが Android にも無いかなー
と探していたところ appium というツールを見つけたので使ってみたというエントリーです。
appium
ネイティブ(モバイル)アプリのためのテスト自動化フレームワークです。
Android と iOS どちらにも対応しています。
特徴として自分の得意な言語でテストケースを書くことができます。
選べる言語は Java, Javascript, Python, Ruby, php などなど。
appium にはデスクトップアプリがあり、それを使うと簡単にテストケースを作ることができます。
以下からダウンロードすることができます。
ダウンロード後以下の画面が立ち上がるのでそのまま「Start Sever」で進みます。


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

このような画面が開くので「JSON Representation」に以下の内容を書き込みます。
{
"platformName": "Android",
"deviceName": "エミュレータの名前",
"platformVersion": "10",
"automationName": "Appium",
"app": "apk へのパス",
}
エミュレータの名前 は Android Studio でエミュレータを起動した状態で以下のコマンドを打つと得られます。
> adb devices List of devices attached XXXX device
XXXX の部分が名前です。
この状態で「Start Session」を押します。
すると以下のような画面が開きます。

この画面で要素をクリックして 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 のツールの中には QGIS や ArcGIS などがあります。
この 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_LongFromGeoHash と ST_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 店舗しかありませんでした。

まとめ
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)はこちら
GTFS(General Transit Feed Specification)とは
GTFS とは公共交通機関の時刻表と地理的情報に関するオープンフォーマットのことを言います。
GTFS と調べるとよく Google の乗り換え案内などがヒットするが Google は GTFS 形式で公共交通機関の情報を配信しているらしい。
GTFS には動的なものと静的なものが存在します。
動的なものはリアルタイムでバスや船の位置を把握することができるものです。
静的なものは単に時刻表で 1 日の位置がわかります。
GTFS は Zip ファイルに格納されたテキストファイルで構成します。
ファイルには停車地点、ルート、時刻などそれぞれの情報を表すものが分割されています。
ファイル一覧は以下のページに記載されています。
これらのファイルを用意すると GTFS として配信することが出来ます。
Deck.gl
言わずとしれた Uber 社のデータビジュアライズツールです。
Deck.gl は WebGL を使用するため 3D のデータ表現に長けています。
例えば ArcLayer を使うと飛行機の移動や人流データを表現することができます。
今回は船の移動を表現したかったので TripLayer を使いました。
この https://deck.gl/#/examples/overview を見るといろんな活用法が見れて想像力が駆り立てられますね。
実装
それでは実装していきます。
まずデータ集めですが、日本の公共交通機関は以下のサイトに集まっています。
ここからハッカソンのテーマであった沖縄の交通機関に関するデータを落としてきました。
あるファイルを 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 図を書くと以下のようになります。

このように正規化されたテーブル構造になっているため 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
データの作成が終わったらあとはデータを読み込むだけです。

まとめ
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 にまかせて会員登録処理自体は別にすすめることが可能になる
- キューに登録することによりデータの一貫性が保たれるため信頼性の向上につながる
- 完全マネージドなサービスのため運用が楽
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 を使用します。
実際にやってみる
改めて今回の構成です。

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

html も解釈出来ていますね。
まとめ
まだ不十分ではあるがミニマムでメール送信サービスを作ることができた。
残タスクとして、SQS へのイベントの登録や各サービスで起きたエラーのハンドリング、バウンスメールへの対応などを考えなくてはいけません。
SQS や SES は初めて使うサービスだったがフルマネージドもあってか簡単に使うことができました。
実際に Web アプリケーションでメール機能を実装するよりこちらのほうがコストをかけずできると思います。
また運用、保守のコストも同じくかからないと思うのでおすすめです!
ソースコード
スタートアップのための AWS Fargate 入門
スタートアップのための AWS Fargate 入門
本記事は以下の内容を読んでより具体的な方法について書いたものです。
そもそもコンテナが解決しているもの
アプリケーションを動かすにはコードだけでなく関連するコンポーネントが揃っている必要があります。
例えば Ruby on Rails の場合は以下が挙げられます。
- アプリケーションのソースコード
- ランタイム: Ruby 2.x
- 依存ライブラリ: rails, rmagick etc ...
- 依存パッケージ: ImageMagick, etc ...
このような依存関係を解決するための手段としてコンテナが利用されます。
アプリケーションの実行に必要な依存ブルをすべてコンテナの中にパッケージングし、開発環境から本番環境まで同一の環境で動作させることが出来ます。
パッケージングされたコンテナを実行するツールとしてデファクトスタンダードとなっているのが Docker です。
Docker Engine が動いている環境であればコンテナを実行することが出来ます。
Docker Engine の管理するスコープとしては単一ホストマシン上でのコンテナの動作になります。
それ以外の部分、例えばコンテナのオートスケーリングや複数ホストマシンにまたがるような配置、ローリングアップデートなどは別の仕組みとして実装する必要があります。
ローリングアップデートとは
同じ機能を持った複数のコンピュータで構成している場合のシステムをアップデートする手法の一つです。 システムの稼動状態を維持しながら、1台ずつ順番にアップデートを行っていきます。
コンテナオーケストレーション
AWS ECS や EKS といったサービスがこれに当たります。
複数のホストマシンにまたがるコンテナの配置やコンテナのアップデート、ロードバランサーへの紐づけなどを管理してくれます。
以下のイメージのようにこちらから API 経由で変更後の状態を支持することでツールがその状態になるように維持・動作します。
これを宣言的デプロイと呼ぶそうです。
コンテナオーケストレーションをを使うとオートスケールや複数のマシンでのコンテナ管理が簡単になり、開発者はコンテナの運用に集中できると思われるが、
実際はホストマシンそのものの管理や運用は依然として残っています。
コンテナが動くホストマシンは OS が動いていますし、Docker Engine が動いている必要があるからです。
コンテナ解決することは上記にも書きましたが、その一方でコンテナとホストマシンの両方を管理・運用しなければならないということになります。
AWS Fargate とは
上記のような二重管理を解決するのが AWS Fargate です。
ECS でコンテナを実行する際の起動タイプの一つで、EC2 と Fargate があります。
EC2 で実行したい際のイメージが以下です。
「どのホストマシン」で「どのコンテナ」を「いくつ起動する」かなどのハンドリングは、オーケストレーションツールである Amazon ECS から行うことが可能ですが、各ホストマシンの管理・運用業務は依然として残ります。
一方 Fargate がこちらです。
この画像ではホストマシンが見えなくなり、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 上で実行するってことに挑戦してみます。



