Serverless Frameworkを使ってAWS LambdaにQRコードを返すAPIをデプロイする
サーバレスアーキテクチャ(AWS Lambda, Azure Functions, Google CloudFunctionsなど)の構成管理ができるServerless Frameworkを使ってみたかったので、任意の文字列をQRコード化して返すAPIを作ってデプロイしてみました。
環境は下記のとおりです。
本記事でわかること
- 使用するライブラリにネイティブバイナリが含まれている場合のLambdaの使い方
- APIがバイナリデータを返す場合のAPI Gatewayの設定方法
- python-qrcodeの使い方
- Binary Support for API Integrations with Amazon API Gateway | AWS Compute Blogの構成をServerless Frameworkで再現する方法(一部再現不可)
Serverless Frameworkのインストール
$ npm install -g serverless
$ sls create --template aws-python --path qrcode-service
python環境の構築
$ cd qrcode-service
$ pyenv install 2.7
$ pyenv virtualenv 2.7 qrcode-service
$ pyenv local qrcode-service
$ pip install -r requirements.txt
requirements.txtは下記の通り。
olefile==0.44
Pillow==4.0.0
qrcode==5.3
six==1.10.0
QRコードを返す関数の作成
受け取った文字列をQRコード化して、base64エンコードした結果を返す関数を作成します。
handler.py
import base64
from io import BytesIO
import os
import sys
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'vendor'))
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'vendor/PIL'))
import qrcode
def create(event, context):
params = event['query']
img = qrcode.make(params['text'])
bufferd = BytesIO()
stdout_buffer = getattr(sys.stdout, 'buffer', None)
img.save(bufferd)
image_base64 = base64.b64encode(bufferd.getvalue()).decode('utf-8')
return image_base64
serverless.ymlを書く
基本的にはこちらのブログの記事の構成を再現する内容です。
Binary Support for API Integrations with Amazon API Gateway | AWS Compute Blog
serverless.yml
service: qrcode-service
provider:
name: aws
runtime: python2.7
stage: dev
region: ap-northeast-1
package:
exclude:
- build_packages.sh
- Dockerfile
- requirements.txt
functions:
qrcode:
handler: handler.create
events:
- http:
path: qrcode
method: get
integration: lambda
request:
parameters:
querystrings:
text: true
passThrough: WHEN_NO_TEMPLATES
template:
image/png: |
#define( $loop )
{
#foreach($key in $map.keySet())
#set( $k = $util.escapeJavaScript($key) )
#set( $v = $util.escapeJavaScript($map.get($key)).replaceAll("\\'", "'") )
"$k":
"$v"
#if( $foreach.hasNext ) , #end
#end
}
#end
{
#set( $map = $input.params().querystring )
"query": $loop
}
ローカル実行で動作確認
Serverless Frameworkにはローカル実行する機能があるので、デプロイしなくても下記のようにして動作確認ができます。
$ sls invoke local -f qrcode -d '{ "query": { "text": "Kappa" } }' | sed 's/"//g' | base64 -d > kappa.png
$ open kappa.png
デプロイ用に外部ライブラリパッケージをまとめる
Lambdaで外部ライブラリを使う場合は外部ライブラリも含めてzipにする必要があります。 しかし、使用するライブラリにネイティブバイナリが含まれている場合は、MacでビルドしてしまうとDarwin用のモジュールになってしまってLambda上では動作しません。 今回は、MacもEC2もCPUは同じx64系ということで、amazonlinuxのDocker Imageを使ってMac上でビルドしてしまうことにします。
Docker Imageのビルド
Dockerfileは下記の通りです。
FROM amazonlinux
WORKDIR /work
RUN set -x && \
yum install -y python27-devel python27-pip gcc gcc-c++ cmake && \
yum install -y libtiff-devel libjpeg-devel libzip-devel freetype-devel \
lcms2-devel libwebp-devel tcl-devel tk-devel && \
pip install --upgrade pip
ビルドします。
$ docker build . -t qrcode-service
外部ライブラリのビルド
外部ライブラリをdocker container上でビルドします。
$ mkdir vendor
$ cp requirements.txt vendor
$ docker run -v "$(pwd)"/vendor:/work qrcode-service pip install -r requirements.txt -t .
AWSにデプロイする
Serverless Frameworkのコマンド一発で完了……といきたいところですが、Serverless Frameworkがバックエンドで使用しているCloudFormationがバイナリサポートの設定に対応していないらしく、コマンド一発で完了とはいきませんでした。 しかたがないので一部の設定は別途行います。
1. まずはデプロイする
下記のコマンドでデプロイを実行します。
$ sls deploy
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (6.26 MB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............................
Serverless: Stack update finished...
Service Information
service: qrcode-service
stage: dev
region: ap-northeast-1
api keys:
None
endpoints:
GET - https://u5uko0b1ne.execute-api.ap-northeast-1.amazonaws.com/dev/qrcode
functions:
qrcode: qrcode-service-dev-qrcode
上記の例だとu5uko0b1ne
がAPI IDになるので控えておきます。
デプロイが完了した時点でで下記のようにリクエストを送るとbase64エンコードされたまま結果が返ってくるのが確認できます。
$ curl -X GET -H "Accept: image/png" -H "Content-Type: image/png" "https://u5uko0b1ne.execute-api.ap-northeast-1.amazonaws.com/dev/qrcode?text=Kappa"
"iVBORw0KGgoAAAANSUhEUgAAASIAAAEiAQAAAAB1xeIbAAABhklEQVR4nO2YTW6DMBCFv6m9d26Qo5gb9Ky9AT5KDhAJ9kbThW3kkkrthvDnWSBiPoknZzR+PFH+rvDxDwga1ahGNapRe6ckl0W60QJjWek21XUJyquq6gDaA9JhVFVVf1Lv13UJaiw9Hu4x/wEidntdZ6bs4rfgJvviO/eq/mSUf1ike+cbL0uVvncKjACYKAB18+9V/bGpvPdBADDz+iRrvbFRpWQx2wVMXEItU1iRkg6QbpTU7dnb3/KDvas/KkXy8T1GtXdaXL1TxQ/z072qPzaV5r3gnoJ/2Ejo5pnjYjmJ96r+2FTaXWW06bOKfMxmky++30bXFahq5uQhM0CaPr2LbeasSeXdnQs/QH1pe78aRR2ZYbRea33/FqrOMY0SbpPgB6PSbavr1FRxMuMN/BcIgOCMEm7PkqrtVf25KFWN4B8idaq2va4zUssMmfA5QLgrkvK1jXRdiXKq2qfbSSqfs7WuM1OvPsfFdOBmy9l8zmrUMsf8tVqO2ahGNapRh6e+AZkLzhog49P2AAAAAElFTkSuQmCC"
2. バイナリサポートの有効化
API Gatewayがリクエストの送受信でバイナリデータを扱えるようにバイナリサポートを有効にします。
$ aws apigateway update-rest-api \
--rest-api-id u5uko0b1ne \
--patch-operations op=add,path=/binaryMediaTypes/image~1png
u5uko0b1ne
はそれぞれのAPI IDで置き換えるようにしてください。
3. API GatewayにLambda関数を呼び出す権限を与える
手動で作ると問題ないのに、CloudFormationで構成したときに上手く動かないので悩んでいたのですが、次の記事によるとCloudFormationで構成した場合は、AWS Consoleから権限を与える必要があるそうです。(他に方法はないのでしょうか?)
4. 再度のデプロイ
再度、デプロイします。
$ sls deploy
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (6.26 MB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............
Serverless: Stack update finished...
Service Information
service: qrcode-service
stage: dev
region: ap-northeast-1
api keys:
None
endpoints:
GET - https://u5uko0b1ne.execute-api.ap-northeast-1.amazonaws.com/dev/qrcode
functions:
qrcode: qrcode-service-dev-qrcode
APIを叩いてみると今度はバイナリデータが返ってくることが確認できました。
$ curl -X GET -H "Accept: image/png" -H "Content-Type: image/png" "https://u5uko0b1ne.execute-api.ap-northeast-1.amazonaws.com/dev/qrcode?text=Kappa"
�PNG
IHDR""u���IDATx��Mn�0����wn�����>J �FӅm�J���Y b>�'g4~<Q���ըF5�Q{�$�E��cY�6�u ʫ����aTUU�R��u j,=�1�"v{]g����&��;��d��X�{�K��w
��(u��U��A0��$k��Q�d1�L\B-SX���n������?*E��=F�wZ\�S��ӽ�?6�������H���b9����T�]e�鳊|�f�/��F��j��!3@�>��m�I�ݝ
?@}i{�E�a�^k}���1�n����nϒ��U�(U��"u����3R�
��9@�+��t]�r�ڧ�I*����3S�>��t�f��|�j�2��Z�٨F5�Q����
� ���IEND�B`�i
以上です。
リポジトリ
完成品が下記になります。
参考
Binary Support for API Integrations with Amazon API Gateway | AWS Compute Blog
python-qrcodeのマスク処理結果の評価方法が若干間違っている
lincolnloop/python-qrcode: Python QR Code image generator
このライブラリはマスク処理結果の評価方法が若干仕様と異なっているため、他のライブラリとはできあがったQRコードが違う結果になることがある。 間違っているのはQRコードの仕様書の次の部分。
1:1:3:1:1 比率パターンの前又は後ろに比率4の幅以上の明パターンが存在する。 qrcode_specification_ja
python-qrcodeの該当部分は下記になるが、1:1:3:1:1 比率パターンの出現自体を評価してしまっている。 本来は1:1:3:1:1 比率パターンの前か後ろに比率4の幅以上の明パターンが存在することを評価すべきである。
def _lost_point_level3(modules, modules_count): modules_range_short = xrange(modules_count-6) lost_point = 0 for row in xrange(modules_count): this_row = modules[row] for col in modules_range_short: if (this_row[col] and not this_row[col + 1] and this_row[col + 2] and this_row[col + 3] and this_row[col + 4] and not this_row[col + 5] and this_row[col + 6]): lost_point += 40 for col in xrange(modules_count): for row in modules_range_short: if (modules[row][col] and not modules[row + 1][col] and modules[row + 2][col] and modules[row + 3][col] and modules[row + 4][col] and not modules[row + 5][col] and modules[row + 6][col]): lost_point += 40 return lost_point
skip2/go-qrcode: QR Code encoder (Go)
では、下記のようになっており、1:1:3:1:1 比率パターンの出現だけではなく、その前後に比率4の幅以上の明パターンが存在するかどうかまできちんと評価している。
func (m *symbol) penalty3() int { penalty := 0 for y := 0; y < m.symbolSize; y++ { var bitBuffer int16 = 0x00 for x := 0; x < m.symbolSize; x++ { bitBuffer <<= 1 if v := m.get(x, y); v { bitBuffer |= 1 } switch bitBuffer & 0x7ff { // 0b000 0101 1101 or 0b10111010000 // 0x05d or 0x5d0 case 0x05d, 0x5d0: penalty += penaltyWeight3 bitBuffer = 0xFF default: if x == m.symbolSize-1 && (bitBuffer&0x7f) == 0x5d { penalty += penaltyWeight3 bitBuffer = 0xFF } } } } for x := 0; x < m.symbolSize; x++ { var bitBuffer int16 = 0x00 for y := 0; y < m.symbolSize; y++ { bitBuffer <<= 1 if v := m.get(x, y); v { bitBuffer |= 1 } switch bitBuffer & 0x7ff { // 0b000 0101 1101 or 0b10111010000 // 0x05d or 0x5d0 case 0x05d, 0x5d0: penalty += penaltyWeight3 bitBuffer = 0xFF default: if y == m.symbolSize-1 && (bitBuffer&0x7f) == 0x5d { penalty += penaltyWeight3 bitBuffer = 0xFF } } } } return penalty }
しかし、適用されるマスクパターンが間違っているぐらいではQRコードが読めなかったりはしないので実用上は問題なかったりする。 (読みにくくなったりするケースはあれど)
追記 2017/03/31
この件を修正するPull Requestが送られているようだ。
Laravel5.2とmysqlnd_msを併用するとSegmentation faultで死亡
透過的にRW Splitting
が実現できるという触れ込みのmysqlnd_ms
を既存アプリの構成に組み込んでみたところ、下記のようなログを残して死亡していた。
child pid 10815 exit signal Segmentation fault (11)
その原因の(ある程度)切り分けと対策メモ。
環境
- Laravel: 5.2.12
- mysqlnd_ms: 1.6
- php: 5.6.18
- mysqld: 5.6.29
原因
実はLaravel
はあまり原因とは関係なく、PDOのコンストラクタでPDO::ATTR_EMULATE_PREPARES => false
を渡すと死亡する模様。
そして、Laravel
のPDOのデフォルトオプションはPDO::ATTR_EMULATE_PREPARES => false
です。
対策
サーバサイドのプリペアドステートメントは諦めてPDO::ATTR_EMULATE_PREPARES => true
にする。
Laravel
だと下記のように設定する。
'mysql' => [ 'read' => [ 'host' => env('DB_HOST', 'localhost'), ], 'write' => [ 'host' => env('DB_HOST', 'localhost'), ], 'driver' => 'mysql', 'host' => env('DB_HOST', 'localhost'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', 'strict' => true, 'options' => [ PDO::ATTR_EMULATE_PREPARES => true, // <-- add ] ],
Twitter Kit for Androidを使って画像付きツイート
TwitterのAPIを使うのに、今までは非公式ライブラリであるTwitter4Jなどを利用しておりましたが、公式ライブラリであるTwitter Kit for Android
がリリースされましたので、このライブラリを使用して画像付きツイートを実現してみます。
環境は下記のとおりです。
課題
Twitter Kitで画像付きTweetをするには下記課題があります。
- まだライブラリがmedia/uploadに対応していない
- その上、statuses/updateにmedia_idsを渡すためのIFも用意されていない
解決策
1. まだライブラリがmedia/uploadを対応していない
自前で対応します。
Twitter KitでREST APIを使用するにはTwitterApiClient
を使用しますので、それにちなんで、media/upload対応したクライアントをTwitterUploadClient
とします。
2. その上、statuses/updateにmedia_idsを渡すためのIFも用意されていない
自前で用意します。
TwitterApiClient
を継承したClassを作成して対応します。
実装
- media/upload対応した
TwitterUploadClient
を実装する - media/uploadのレスポンスをマッピングする
MediaEntity
を実装する TwitterApiClient
を継承して、statuses/updateのmedia_idsに対応したIFを備えるMyTWitterApiClient
を実装する
使用例
TwitterUploadClient twitterApiClient = new TwitterUploadClient(Twitter.getSessionManager().getActiveSession()); TwitterUploadClient.MediaService mediaService = twitterApiClient.getMediaService(); String imageData = "/9j/4AAQSkZJRgABAQEAAQABAAD/2wBDAAcFBgYGBQcGBgYICAcJCxIMCwoKCxcQEQ0SGxccHBoXGhkdISokHR8oIBkaJTIlKCwtLzAvHSM0ODQuNyouLy7/2wBDAQgICAsKCxYMDBYuHhoeLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi7/wAARCAAgACADASIAAhEBAxEB/8QAGQABAQADAQAAAAAAAAAAAAAABgcAAwQF/8QALRAAAQMDAwIEBQUAAAAAAAAAAQIDBAAFEQYSIUFhEzEyUQcUgZGhNHLB0fD/xAAXAQEBAQEAAAAAAAAAAAAAAAAFBgME/8QAJBEAAQMDBAIDAQAAAAAAAAAAAQIDBAARIQUTMXFBkRQjgaH/2gAMAwEAAhEDEQA/AL7f73CsUAzJi/M7W20+pxXsKnErXt0lOEtLRGb6IQkE/Ums+Ja13K6OxAf0qQGv3EZP3yBRuxxYDMNiVcGfmHpAKkNLUQhtAUU5IGCSSD2AHeuL7pD2014pXTpOnNxlvP5Uk2tz1Ye/VIY+vrtEcCnVtym+qHEgH6EeVUnT18g3+AJkJR4O1xtXqbV7H++tTSBAsFzW40Lc00+2nfhClbVpzg8E8EZFKNKQoVquOITAaD42LwTzjkdf9mj3tSVBmphSAbqtY+M4B91s+uDMj7sZJBHX9zXha8iri6hW8oHw5SAtJ7gYI/A+9HJbAmR2Q0+2xIZBSPEB2OJJJ5IBwQSemDmrNfLREvUFUSUCOdyHE+ptXuKms7R2oYjpSw03MZzw42sJOO6T/GaRG9He3majZMVYWVIFwa1aQgmMZMl2Ql95SfCy2DsQMgkAkAqPA6YHemdibL1zQU+loFSj+BXHbbDcg03HSwI7aRgrcIyfc4FL7bAZgMeE3lSjytZ81GgUw5mpal86WLJTwOuMd5qgZCIsUMpyTz+81//Z"; mediaService.upload(imageData, new Callback<MediaEntity>() { @Override public void success(Result<MediaEntity> result) { MyTwitterApiClient twitterApiClient = new MyTwitterApiClient(Twitter.getSessionManager().getActiveSession()); MyTwitterApiClient.MyStatusesService statusesService = twitterApiClient.getMyStatusesService(); statusesService.update("upload test", null, result.data.mediaIdString, new Callback<Tweet>(){ @Override public void success(Result<Tweet> result) { } @Override public void failure(TwitterException e) { } }); } @Override public void failure(TwitterException e) { } });
Parse Android SDK1.8+でPush通知を無視する
Push通知をON/OFFにしたい。それをParseでするのに以前はPushService.setDefaultPushCallback
でnull
を渡すことで実現していましたが、ひさしぶりにParseを使ってみるとPushService.setDefaultPushCallback
がdeprecatedになっていたので代替の方法を探しました。
環境は次のとおり。
- Parse Android SDK1.8+
- gradle 2.2.1
ググって見つかったsubscribe/unsubscribe
を使ってchannel指定する方法だと、everyoneに配信したときに通知がでてしまう。ここのオペレーションは変えたくない。
そこで、Push通知を受け取るReceiverを継承してごにょごにょする方法を試したところ上手く行きました。
public class CustomReceiver extends ParsePushBroadcastReceiver { private static final String TAG = "CustomReceiver"; @Override public void onReceive(Context context, Intent intent) { Setting setting = ((AnyApplication)context.getApplicationContext()).getSetting(); if( setting.isEnabledReceiveNotification() ) { super.onReceive(context, intent); } else { abortBroadcast(); } } }
<receiver android:name="${applicationId}.CustomReceiver" android:exported="false"> <intent-filter> <action android:name="com.parse.push.intent.RECEIVE" /> <action android:name="com.parse.push.intent.DELETE" /> <action android:name="com.parse.push.intent.OPEN" /> </intent-filter> </receiver>
参考
MacでWifi共有で透過的にmitmproxy
mitmproxyをWifi共有を使って透過的proxyとして使うための手順メモです。 実施環境は下記の通り。
手順
1.mitmproxyのインストール
$ pip install mitmproxy
2.証明書を端末(iPhone/Android)にインストール
下記に配置されているのでメールなどを使って端末に送る。 添付ファイルをタップすれば自動的にインストールのためのアプリ選択画面が表示される。
$ ~/.mitmproxy/mitmproxy-ca-cert.pem
3.Wifi共有の設定
OS X側で、システム環境設定 -> 共有 -> インターネット共有からWifi共有の設定をする。 端末側はここで設定したWifiポイントにつなぐようにする。
4.Packet forwardingの設定
最後にport80/443に流れてくるパケットをmitmproxyにリダイレクトするようにする。 まずは、Wifi共有に使われているインターフェース名を調べる。
$ ifconfig
(たいてい、bridge0というような名前)
Packet forwardingを有効にする
$ sudo sysctl -w net.inet.ip.forwarding=1
Packet forwardingの設定を下記のような感じにする。
$ sudo vi /private/etc/pf.conf
scrub-anchor "com.apple/*" nat-anchor "com.apple/*" rdr-anchor "com.apple/*" rdr pass on bridge0 inet proto tcp from 192.168.2.0/24 to any port http -> 127.0.0.1 port 8080 rdr pass on bridge0 inet proto tcp from 192.168.2.0/24 to any port https -> 127.0.0.1 port 8080 dummynet-anchor "com.apple/*" anchor "com.apple/*" load anchor "com.apple" from "/etc/pf.anchors/com.apple"
pfを有効にする。
$ sudo pfctl -f /private/etc/pf.conf
$ sudo pfctl -e
mitmproxyを透過的プロキシモードで起動する。
$ mitmproxy -T --host
おしまい。
参考
GradleでAndroidアプリを起動するタスクを追加する
GradleのAndroid Pluginはapkをビルドしてインストールするところまではやってくれますが、アプリの起動まではやってくれません。 ということで、アプリの起動をやってくれるタスクを追加しましょう。
実行環境は下記のとおりです。
タスクの追加
下記の記述をbuild.gradleに追加します。
使い方
次のようにして実行します。(実際にはbuildTypeの種類だけタスクが追加されます)
$ gradle runDebug
$ gradle runRelease
参考
Custom Tasks hooking on to all flavors of a given buildtype
Thx
ichigotake様