Serverless Frameworkを使ってAWS LambdaにQRコードを返すAPIをデプロイする

サーバレスアーキテクチャAWS Lambda, Azure Functions, Google CloudFunctionsなど)の構成管理ができるServerless Frameworkを使ってみたかったので、任意の文字列をQRコード化して返すAPIを作ってデプロイしてみました。

環境は下記のとおりです。

本記事でわかること

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

上記の例だとu5uko0b1neAPI 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から権限を与える必要があるそうです。(他に方法はないのでしょうか?)

krisgholson/serverless-thumbnail: Recreate the thumbnail service described here .. https://aws.amazon.com/blogs/compute/binary-support-for-api-integrations-with-amazon-api-gateway/ .. using the serverless.com framework (and document some gotchas).

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

以上です。

リポジトリ

完成品が下記になります。

bagpack/serverless-framework-qrcode-service: Sample for Binary Support using API Gateway REST API with Serverless Framework.

参考

Binary Support for API Integrations with Amazon API Gateway | AWS Compute Blog

krisgholson/serverless-thumbnail: Recreate the thumbnail service described here .. https://aws.amazon.com/blogs/compute/binary-support-for-api-integrations-with-amazon-api-gateway/ .. using the serverless.com framework (and document some gotchas).

python-qrcodeのマスク処理結果の評価方法が若干間違っている

PythonQRコードを生成するのに便利なライブラリ

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

一方、golangQRコードのライブラリ

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が送られているようだ。

Change penalty rules. Small optimization. Add optional argument mask_pattern. by cryptogun · Pull Request #127 · lincolnloop/python-qrcode

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を使って画像付きツイート

TwitterAPIを使うのに、今までは非公式ライブラリであるTwitter4Jなどを利用しておりましたが、公式ライブラリであるTwitter Kit for Androidがリリースされましたので、このライブラリを使用して画像付きツイートを実現してみます。

環境は下記のとおりです。

課題

Twitter Kitで画像付きTweetをするには下記課題があります。

  1. まだライブラリがmedia/uploadに対応していない
  2. その上、statuses/updateにmedia_idsを渡すためのIFも用意されていない

解決策

1. まだライブラリがmedia/uploadを対応していない

自前で対応します。

Twitter KitでREST APIを使用するにはTwitterApiClientを使用しますので、それにちなんで、media/upload対応したクライアントをTwitterUploadClientとします。

2. その上、statuses/updateにmedia_idsを渡すためのIFも用意されていない

自前で用意します。

TwitterApiClientを継承したClassを作成して対応します。

実装

  1. media/upload対応したTwitterUploadClientを実装する
  2. media/uploadのレスポンスをマッピングするMediaEntityを実装する
  3. 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.setDefaultPushCallbacknullを渡すことで実現していましたが、ひさしぶりに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>

参考

disabling-parse-notifications

parse-android-disable-push-notifications

MacでWifi共有で透過的にmitmproxy

mitmproxyをWifi共有を使って透過的proxyとして使うための手順メモです。 実施環境は下記の通り。

手順

  1. mitmproxyのインストール
  2. 証明書を端末(iPhone/Android)にインストール
  3. Wifi共有の設定
  4. Packet forwardingの設定

1.mitmproxyのインストール

$ pip install mitmproxy

2.証明書を端末(iPhone/Android)にインストール

下記に配置されているのでメールなどを使って端末に送る。 添付ファイルをタップすれば自動的にインストールのためのアプリ選択画面が表示される。

$ ~/.mitmproxy/mitmproxy-ca-cert.pem

f:id:bagpack:20140204225038p:plain

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

おしまい。

 参考

mitmproxy公式

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様