2017年4月30日 (日)

Android仮想デバイスとDocker開発環境をつなぐ

エンジニアの佐藤です。こんにちは。

Android開発を始めました。Android開発と言えば、Android Studioです。まずは正攻法でいこうと思いました。

ところで筆者は趣味のプログラミングのほとんどと、可能な限りの業務開発をクラウドの仮想デスクトップDockerコンテナ上で行なっています。技術が好きという個人的理由もありますが、常日頃慣れることで技術の細部まで知見が深まる効果を狙ってのものです。

ところがこのDocker仮想デスクトップ、Android開発と決定的に競合してしまうことがわかりました。問題はデバッグ環境となるAndroid仮装デバイス(AVD)です。理由は割愛しますが、つまるところAVDは、ハードウェアに直接インストールしてあるローカルOSでしか動かず、クラウド上では実行できないのです。

往生際の悪い筆者は、今度は(クラウド上の)Docker仮想デスクトップコンテナと、ローカルで実行されるAVDを接続することを考えました。今回はこの話を書かせていただきたいと思います。

以下がその目論見図となります。

Photo


ネットワークは以下のようになります。

Photo_3


AVDは通常、ポート5554と5555で着信待機しています(*1)。Android Studioはadbと呼ばれる仕掛け(コマンドプログラムとデーモンで構成)でAVD(あるいは実機)と通信しますので、この部分をSSHトンネルで仲介すれば、目論見が実現しそうです。

試してみましょう。

Docker仮想デスクトップサーバについては以下の筆者ブログを参考にします。
DockerでLinux仮想デスクトップサーバをつくる話

次にローカル環境にAndroid Studioをインストールし、AVDを一つ作成します。

Avdmanager_2


ここでは「Nexus_6_API_24」という名前で作成しました。作成したら、一旦Android Studioは終了します。

ここで例の「SSHトンネル」を仕掛けます。以下のコマンドは「SSH接続がある限り、リモートホスト(この場合はDocker仮想デスクトップコンテナ)で5554または5555ポートに向かって接続された場合、その接続をローカルの5554または5555ポートにフォワードする」という意味になります。これをローカル環境から実行します。(vncserverはリモートホストのホスト名)

$ssh user01@vncserver -R 5555:localhost:5555 -R 5554:localhost:5554

リモートホスト側で確認してみましょう。

$ netstat -ltnp
(中略)
tcp  0  0 127.0.0.1:5554  0.0.0.0:* LISTEN  -  
tcp  0  0 127.0.0.1:5555  0.0.0.0:* LISTEN  -

目論見通りフォワーディングされそうです。

リモートホストでAndroid Studioを起動します。

ローカル環境でAVDを起動します。筆者のMacOS環境では、ターミナルから以下のように入力します。

$ ~/Library/Android/sdk/emulator/emulator @Nexus_6_API_24

AVDが起動したら、リモートホストで実行しているAndroid Studioからデバッガーのアタッチが可能か見てみましょう。メニューを「Run -> Attach debugger to Android process」で選択すると、以下のダイアログが表示され、ローカル環境のAVDが接続されたことが確認できました。

Attach


インラインデバッガでデバッグしてみましょう。メニューを「Run -> Debug 'app'」と選択し、先ほど確認されたデバイスを選びます。

Debugstart


ブレークポイントでブレークできました!これでいつものやり方でAndroid開発をやっていけそうです。

Goal

手前のAVDがMacOSローカルで実行され、背景のAndroid Studioは仮想デスクトップクライアントで接続されたDocker上の環境で実行されています。

おわりに

ここまで読んでいただいてありがとうございます。「なんでわざわざ難しいことするの?」と思われた方も多いでしょう。普通は1台の開発マシンでやることをわざわざ分散しているのです。しかし、メリットもあると思います。

ローカルPCへのリソース投資が少なくて済む
クラウドとネットワークがどこでも使えるようになったので、ローカルPCへの固定費支出は抑え、なるべく従量課金のクラウドを使った方が安上がりです。今回紹介した手法は、開発設備の柔軟性を向上させます。

AVDに多量のリソースを割り当てられる
メモリ8GBの標準的なノートPCでは、メモリ4GBのAVDとAndroid Studioを同時実行するのは難しいでしょう。またAVDを2台以上起動したい場合も、今回の手法が役立つと思います。

今回ご紹介した手法が何かしらのお役に立てば幸いです。

(*1)以下のAndroid Debug Bridgeに詳しく書かれている
https://developer.android.com/studio/command-line/adb.html

2016年12月27日 (火)

UDP双方向通信を実装しやすくなったソラコムBeam

エンジニアの佐藤です。こんにちは。

今回は12月22日にソラコムBeamに追加された機能「SORACOM Beam のUDP、TCP エントリポイントでステータスコードを返さないオプション」についてです。

ソラコムBeamはSIMからUDP(HTTPやTCP、MQTTもある)で発信したデータをHTTPSリクエストに変換してくれるサービスです。(前回投稿「ソラコムBeamでUDP通信機器の暗号化を実装」を参照)

たいへん便利なサービスなのですが、今回のリリースによりさらに便利になったと思います。

今回の機能追加ではソラコムBeamの設定で「ステータスコードを省略」の設定ができるようになりましたが、さらにBeamから来たHTTPリクエストのレスポンスでHTTPヘッダに「Content-Type: application/octet-stream」を指定することにより、レスポンスBODYをSIM側へUDPでそのまま返信することができます。

この機能がなぜ重要なのか。筆者の知っているIoTデバイスは、いずれもUDPリモートコントロール機能を備えています。IoTデバイスは遠方に配置されるケースが多く、サーバーからUDPパケットを送信してデバイスの設定変更などを行う機能は、広範囲に実装されている定番機能なのです。そしてたいていの場合、そのパケットフォーマットはバイナリー形式で定義されています。

この類のデバイスをクラウドサービスにインテグレーションする場合は、通常SIM側のIPアドレスを固定し、回線全体をインターネットから隔離する「閉域ネットワーク」を用いることが考えられます。(そして事前登録されたIPアドレスに向かってサーバーからUDPパケットを投げる。)しかしながらこの方式の場合は回線費用が高く、相当の収益が見込めないと設置できないのが実情ではないでしょうか。

ソラコムBeamを使うと、小規模案件を安価に実装できます。IoTデバイスはサーバーへデータ送信する場合がほとんどでしょう。(そしてソラコムBeamを設定するとこのデータ送信はHTTPSリクエストに変換される。)サーバーはこのHTTPSリクエストに対し、Content-Typeをapplication/octet-streamに設定して今度は「デバイスを設定するUDPパケット」をHTTPレスポンスBODYに貼り付けて返信します。すると今度はbeam.soracom.io(のIPアドレスプールのどれか)からデバイスに向かってUDPパケットが送信され、デバイス設定が実行されます。

デバイスとソラコムBeamの間は秘密回線ですので、第三者がデータを見ることはできません。閉域ネットワークは不要。わずかなソラコムBeam料金とWebサーバーで、UDP双方向通信を実現できます。

ソラコムサンタよ、ありがとう。

Cloudblog20161227_01_2 図. ソラコムBeamからのHTTPリクエストに対して、デバイスに送信するバイナリデータ(青網掛け部分)を返信する。

2016年12月 7日 (水)

CodeBuildでgo言語のプロジェクトをビルドする際の注意事項

こんにちは。エンジニアの島袋です。

前回に引き続き、AWS CodeBuildの話になります。

CodeBuildでgo言語のプロジェクトをビルドする場合のハマりポイントに気づいたので、そちらを紹介します。

要点から先に言うと、以下です。

  • CodeBuildは/tmp配下にリポジトリをチェックアウトするため、ビルド対象がGOPATH配下にない
  • GOPATH配下にないとvendoringが有効にならないため、go build時に「パッケージが見つからない」と言われる

go buildがコケる??

前回のエントリではbuildspec.ymlのphase名をtypoしたせいでドハマリしてたんですが、その後typoを修正し、再度ビルドを実行すると、今度はgo buildでコケました。

エラーメッセージには「cannot find package〜」と表示されています。

依存しているパッケージが見つからないということですが、そんなはずはありません。

installフェイズでglideを使い、依存パッケージは全てvendorディレクトリの中にダウンロード済みです。

では何が駄目だったのでしょうか?

ビルド対象のプロジェクトがGOPATH配下になかったため、vendoringが有効になっていなかった

GOPATHとvendoringについて簡単に説明しておくと、GOPATHはgo言語を利用する際に設定が必要になる環境変数で、ユーザが任意のパスを指定する必要があります。

go言語でbuildコマンドを叩くと、buildに必要な依存パッケージをGOPATH/src配下から走査します。

(go get "パッケージ名" とコマンドを叩くと、GOPATH/src配下にパッケージをダウンロードできます)

ところが、この方法ではプロジェクト毎に利用するパッケージのバージョンを管理することができません。

(go getコマンドは常に最新版を取得します)

そこで追加された機能がvendoringです。

vendoringは有効な状態では、GOPATH/src配下を走査する前に、プロジェクトのルートディレクトリ直下のvendorディレクトリからパッケージを走査します。

上述のglideはvendorディレクトリ内に依存パッケージをダウンロードするパッケージマネージャです。

(rubyにおけるbundlerのようなものですね)

そんな便利なvendoringなんですが、ビルド対象のプロジェクトディレクトリをGOPATH/src配下に配置していないと有効になりません。

ところが、CodeBuildはビルド対象として指定したリポジトリを"/tmp/src257031055/src"といった感じで/tmp配下にチェックアウトしてしまうんですね。

当該ディレクトリ内のvendorディレクトリ内に依存パッケージをダウンロードし、go buildコマンドを叩いても、vendoringが有効になっていないため、GOPATH/src配下した走査してくれず、「cannot find package〜」と表示されてしまうわけです。

筆者含め、go言語利用者はGOPATH/src配下にディレクトリを作ってコードを書き始めるのが半ば常識化しているため、vendoringが無効になっていることに気づくのに時間を要しました。

回避策

とりあえずのワークアラウンドなんですが、以下のようにしました。

...省略...
  pre_build:
    commands:
      - glide install
      - mkdir -p $GOPATH/src/github.com/stk132/tsg
      - mv * $GOPATH/src/github.com/stk132/tsg/.
  build:
    commands:
      - cd $GOPATH/src/github.com/stk132/tsg && go build

はい。pre_buildフェイズでGOPATH/src配下にディレクトリを切って、CodeBuildがチェックアウトしてきたリポジトリの中身を全てコピーしています。

(これで当該ディレクトリの中ではvendoringが有効になります。)

そしてbuildフェイズでは当該ディレクトリに移動して、go buildを実行します。

(cdコマンドとgo buildをチェインしているのは、別の行にわけて実行すると、元のワーキングディレクトリに戻っているためです。多分Dockerfileのビルド時と同じ挙動??)

これ、もうちょっとスマートな解決方法ないですかね。。。。?

GOPATH=$GOPATH:pwd

とかして、ワーキングディレクトリをGOPATHに追加する方法も考えたんですけど、それだとビルド対象プロジェクト内のサブパッケージが走査できなくなるんですよね。

終わりに

よくよく考えると、jenkins等も同様の問題をはらんでいる気がするのですが、もしかしてgo言語のプロジェクトのCIを回している人にとっては今回のようなのは常識だったりするのでしょうか?

筆者は専らローカルで利用するCLIコマンドばかり作っているため、ビルドもローカルマシン上で行なっていたため、まんまとハマってしまいました。

2016年12月 5日 (月)

AWS CodeBuildに入門失敗した話

こんにちは。エンジニアの島袋です。

今回のエントリ内容については表題の通りです。

typoで日曜の夜をふいにしました。

要約すると以下です。

  • CodeBuildはbuildspec.ymlのphase名をtypoしてもエラーにはならない
  • phase名をtypoすると、phasesが全て無視される?

AWS CodeBuildとは

re:invent 2016で発表されたAWSの新しいサービスの1つで、コンテナベースのフルマネージドなCI環境を提供する的なやつです。

(CodeBuild単体だとビルドに特化してるから、CIとは言わないかな?)

ユーザが自分で管理するjenkinsなどと比較して、勝手にスケールしてくれるので、ビルドキューが詰まって長時間ビルド完了待ちをすることがないというのがウリのようですね。

(あと根本的にサーバー管理の必要がない)

詳細については

あたりを参考にしてください。

早速使ってみようとしたところ。。。

CodeBuildではビルド手順をyaml形式でbuildspec.ymlというファイル名で保存し、ビルド対象のプロジェクトのルートディレクトリ直下に配置する必要があります。

今回は筆者が作ったslackにコメント投稿するためのgo言語製のCLIコマンドをビルドしました。

buildspec.ymlは以下のようになります。

version: 0.1

environment_variables:
  plaintext:
    GOPATH: "/go"

phases:
  install:
    commands:
      - mkdir -p $GOPATH/{bin,src}
      - curl https://glide.sh/get | sh
  pre-build:
    commands:
      - echo prebuildstart
      - PATH=$PATH:$GOPATH/bin
      - echo prebuildend
  build:
    commands:
      - echo buildstrat
      - glide install
      - go build
  post_build:
    commands:
      - echo Build completed on `date`


artifacts:
  type: zip
  files:
    - oshirase

一応buildspec.ymlの記法について説明すると、以下のようになります。

  • environment_variables : 環境変数の設定
  • phases : 実際のビルド手順
    • install : ビルドに必要なコマンドなどのインストール
    • pre_build : ビルド前の作業
    • build : ビルド作業
    • post_build : ビルド後の作業
  • artifacts : zipにパッケージングするビルド成果物の指定

はい。pre_buildとするところをpre-buildとしてしまったんですね。

これに気づくのに二時間近く費やしました。

なぜそれほど時間がかかったかというと、phasesの中に誤ったphase名を記述しても、エラーにならず、ビルド完了となってしまったためです。

ビルドは完了しているにも関わらず、artifactsに指定したビルド成果物が見つからないとうログが出力されていたためおかしいと思い、artifactsの内容について試行錯誤してたんですね。

(そもそもビルド成果物見つからないのであればエラーにしてほしいところなんですが)

今回の場合、typoしたpre_buildだけじゃなく、phasesを全て素通りしてartifactsの処理だけ走っていたようです。

(それが規定動作なのかは現状不明ですが)

終わりに

typoには気をつけましょう。

あと、codebuildはビルド対象のブランチを指定できないようですね。codebuild単体だとjenkins氏やCircleCI等のCIサービスにすぐにとってかわるということは無さそうです。(他のcode〜シリーズと組み合わせること前提ですかね?)

2016年10月24日 (月)

stoplight.ioを使ってみよう

こんにちは。エンジニアの島袋です。 先週末「君の名は」を見てきました。 おもしろかったです。(小学生並みの感想)

今回はstoplight.ioというwebサービスについて紹介します。

TL;DR

  • stoplight.ioを利用すると
  • swagger-specの作成がめちゃくちゃ捗る

本題に入る前に、swaggerについて

Swaggerとは

swaggerは大雑把にいうと、REST APIを仕様を記述するためのツール群です。 swaggerについては以下のスライドが詳しいです。

Swaggerで始めるモデルファーストなAPI開発

このswagger、何が嬉しいかといいますと、apiのレスポンスがswaggerで記述した仕様(swagger-spec)に準拠しているか検査できるんですね。

これを目視、手動でやるとか正直考えたくないです。

(筆者は前職で手動でやってました)

そもそもREST APIに仕様書いるのかっていう話なんですが、まあ必要ですよね。

API実装を外注に出す場合、あるいはこちらがAPIの実装を受託する場合など、特に必須になると思います。

その他、自社サービスのAPIをサードパーティ向けに公開する場合なども必要になってくるでしょう。

また、swagger-specはswagger-uiで人間が読むためのドキュメントとして表示できることも嬉しいポイントです。

(swagger-ui立ち上げるのが面倒くさい場合はbootprint-swaggerで静的htmlに落とします)

Swaggerのつらいところ

そんな便利なswaggerなんですが、swagger-specを記述するのはかなり大変です。

swagger-specはjsonかyamlで記述するのですが、レスポンス項目が5つしかない簡単なサンプルでも以下のような分量になります。

swagger: '2.0'
info:
  version: ''
  title: MySample
  description: sample for cloud blog
host: 'localhost:9944'
basePath: /api
schemes:
  - http
paths:
  '/student/{student_id}':
    parameters:
      - name: student_id
        in: path
        required: true
        type: string
    get:
      operationId: GET_student-student_id
      produces:
        - application/json
      responses:
        '200':
          description: ''
          schema:
            type: object
            properties:
              student_id:
                type: string
              grade:
                type: integer
                minimum: 1
                maximum: 6
              first_name:
                type: string
                minLength: 1
                maxLength: 20
              last_name:
                type: string
                minLength: 1
                maxLength: 20
              hobbies:
                type: array
                items:
                  type: string
                  minLength: 1
                  maxLength: 100
            required:
              - student_id
              - grade
              - first_name
              - last_name
              - hobbies
          examples:
            application/json:
              student_id: aliqua sint proident nostrud
              grade: 3
              first_name: id consect
              last_name: magna mol
              hobbies:
                - dolore Lorem ut elit dolor
                - ad nisi amet adipisicing
                - ipsum aliqua
                - consequat sunt
definitions: {}

はい。つらいですね。

それを解消できるサービスがstoplight.ioです。

stoplight.ioについて

stoplight.ioは簡単にいうと、REST APIをブラウザ上でモデリングし、swagger-specとしてエクスポートすることが可能なサービスです。

(ブラウザ上といいましたが、デスクトップアプリも提供されています。(恐らくElectron製))

個人で利用する分には無料のようなので、とりあえず使ってみましょう。

githubアカウントでもサインアップ可能です。

stoplightで簡単なAPIエンドポイントを作成してみる

上記で貼り付けたyamlの内容と同じものをstoplightで作成してみます。

/student/{student_id}にGETリクエストを投げると、該当する学生の情報をjsonで返却するものとします。

返却項目は以下とします。

  • student_id : 学生番号(必須, 文字列)
  • grade : 学年(必須, 数値1〜6の間)
  • first_name : 名(必須, 文字列, 1文字以上、20文字以内)
  • last_name : 姓(必須, 文字列, 1文字以上、20文字以内)
  • hobbies : 趣味(必須, 文字列配列, 無い場合は空の配列, 要素は1文字以上、100文字以内)

Stoplight_dashboard


まず、ダッシュボード画面から、New Endpointを選択します。

http methodや、endpointのパスなどの入力欄が表示されますので、下図のように入力します。

Endpoint_basic


次にレスポンス項目を設定していきます。

画面を下にスクロールしていくと、Responsesという項目があるので、そちらでレスポンス項目の設定を行っていきます。

「object」の横の+をクリックすると、レスポンス項目を追加していくことができます。

各レスポンス項目左側がプロパティ名、右側がプロパティの型となります。

プロパティ名は当然自分で入力する必要がありますが、型についてはjson-schemaで定義されている型が下図のように選択肢としてポップアップするので、そこから選択することができます。

Type_select


response項目にはバリデーション設定を加えることができます。

右端にあるvalidationsとrequiredというチェックボックスを見てください。

requiredにチェックを入れると、その項目は必須返却項目となります。

validationsをクリックすると、詳細なバリデーション設定項目が表示されます。

ここでは返却値の文字数制限(最小、最大)や、返却値の文字種の制限(正規表現)などの設定が可能です。

Validations_2


一通り入力し終えたら、JSON Schemaタブを開いてみます。

JSON Schemaタブにはeditorタブで入力した内容がJSON Schemaに変換されたjsonが表示されます。 どうでしょうか?

swagger-editorではここで表示されている内容を自分で入力する必要がありましたが、stoplightのeditorでは非常に簡単に入力することができたと思います。

Response_json_schema


次にexampleタブを開いてみます 。 最初は何も表示されていないと思います。

GENERATE EXAMPLE FROM SCHEMAをクリックしてみてください。

レスポンスのサンプルが表示されるはずです。

ここでGENERATE SCHEMA FROM EXAMPLEを押下すると、レスポンスサンプルからjson schemaが出力されます。

(なのでクリックしないでください。)

逆にいうと、「こんな感じの出力が欲しい」というサンプルをベースに仕様を起こすことが可能ということです。

Responses_example


ここまで完了したら、画面上部にあるsaveをクリックし、エンドポイントの作成を保存します。

最後にyamlでswagger-specとしてエクスポートしてみます。

画面右上のexportをクリックしてください。

エクスポート形式が復数表示されるので、ここでは

OAS(Swagger 2).yamlを選択します。

Export_select


するとブラウザの別タブが開き、yaml形式のswagger-specが表示されますので、右クリックから別名で保存でファイルに保存します。

stoplight.io便利なので使いましょう

如何でしたでしょうか?(大分雑な説明でしたが)

エクスポートしたyamlファイルの長さを見ると、swagger-editorで記述するのと比較し、stoplightでモデリングを行うのがとても楽だというのが分かると思います。

今回はstoplightのモデリング機能に絞って紹介しましたので、次回はstoplightのmock機能について紹介したいと思います。

(いつになるかわかりませんが)

2016年7月10日 (日)

Go言語のクエリビルダ−をタイプセーフに扱いたい

こんにちは。エンジニアの島袋です。

今回のテーマは表題の通りです。

TL;DR

  • クエリビルダにおけるテーブル名、カラム名の指定がタイプセーフではないため
  • 正確なテーブル名、カラム名を出力する関数を自動生成するコマンドを作り
  • クエリビルダをタイプセーフに扱えるようにした

Go言語におけるクエリビルダー

Go言語にはORMとしてメソッドチェインでSQLを組み立てるクエリビルダーと呼ばれるタイプのライブラリがいくつか存在します。

クエリビルダーでSQLを構築する場合、以下のようなコードになります。(dbr利用の場合)

クエリビルダは直感的にSQLが組み立てられるため、学習コストが低く扱い易いのですが、一つ問題があります。

上記の通り、カラム名やテーブル名を文字列で指定するため、タイプセーフではありません。

テーブル名やカラム名をtypoしてしまうと、クエリビルダによって生成されたSQLは実行時にエラーとなります。

これを解消するためには、正確なテーブル名、カラム名を出力してくれる関数があれば良さそうです。

そこで、データベースのメタデータからGo言語のソースコードを生成するCLIツール tsg を作ってみました。

(javaだとquerydslとかjooqとかコード生成機能付きのクエリビルダーがあるんですけどね)

tsg

tsgは以下のように使います。

(hostはlocalhost, portは5432がデフォルト)

  • u : データベースのユーザ名
  • pass : データベースのパスワード
  • d : コード生成対象のデータベース

コマンド実行時、以下のテーブルがあったとすると、

  • テーブル名 : users
    • カラム名 : id
    • カラム名 : name

以下のソースコードが出力されます。

このコードを利用すると、冒頭のクエリビルダ−のサンプルは以下のように修正できます。

上記のように、関数から正確なテーブル名、カラム名が取得できることが保証されていれば、Goのビルドが通った時点でテーブル名、カラム名について正確なSQLが発行されることが保証されます。

(SQLが意図通りかどうかは別)

さらに、エディタの自動補完機能を利用すると関数のtypo自体も防げます。

(文字列を直接指定する場合、エディタの自動補完機能に頼ることができません)

生成される構造体、関数の機能

下記のコードのコメントアウト部分のように、N()でテーブル名、カラム名が出力されます。 テーブルに別名を与える場合はA()を実行し、そのままメソッドチェインでつなげてN()を実行します。

未実装の機能

サポートしてるRDBが現状postgresqlのみです。

また、出力するソースコードにコメントがないため、golintを使っていると大量に警告が出ます。

加えて出力後のソースコードが整形されていないため、手動でgofmtをかける必要があります。

終わりに

今回のツールによりクエリビルダーをタイプセーフに扱うことができるようになりました。

(週末に作ったのでまだ実務で使っていませんが)

実はまだクエリビルダ−を便利に扱う上で課題がありまして、現状select文の結果をマッピングするための構造体は自分で手書きで作成する必要があります。

単一のテーブルに対応するだけでよければ、今回のように自動生成することも難しくはないのですが、問題はテーブルをjoinするケースです。

クエリビルダーにより出力されたSQLを見ないと、どのテーブルのどのカラムが必要なのか分からないため、事前にselect結果をマッピングする構造体を自動生成するということができません。

このあたり、何かうまいソリューションはないものでしょうか。。。。

2016年7月 3日 (日)

lambdaからlambdaを起動するリレー方式SQSコンシューマ

こんにちは。エンジニアの島袋です。 最近AWS LambdaによるSQSコンシューマを実装していて、困ったことがあったのでそれについて書きます。

TL;DR

  • SQSのコンシューマをLambdaで実装している
  • scheduled eventでlambdaを起動して、キューに存在するメッセージを全て処理したい
  • キューが空になるまでlambdaからlambdaを起動するリレー方式を実装した

作ってるもの

以下のようなシステムを作っています。

  • S3にファイルがputされる
  • S3 Event NotificationでSQSにメッセージをキューイング
  • Lambdaで実装したコンシューマがメッセージを取り出し、S3にputされたファイルについて何らかの処理を行う

割とあるあるな感じのユースケースだと思います。

S3 Event Notificationで直接Lambdaを起動すれば良いような気もしますが、先日こんなことがありましたね。

サーバレスアーキテクチャの「AWS Lambda」、米国東リージョンの一部のユーザーで障害。約3時間エラーや遅延など

Lamdaの障害が記憶に新しい〜新しすぎる〜(;´Д`)

S3 Event Notificationで直接Lambdaを起動した際にエラーが発生すると、リトライ処理が面倒そうなので、間にSQSを挟むことにしました。

(SQSの場合、メッセージコンシューマが明示的にメッセージを削除しないと一定時間経過後(visibilityTimeout)にメッセージは再びキューに入るので、自動リトライ可能)

Lambda自体はscheduled eventで一定時間毎に起動する想定です。

ファイルの処理に即時性が要求されないのであれば、特に問題の無い構成に見えます。

しかし、LambdaでSQSコンシューマを実装する場合、次の問題があります。

Lambdaは1回の起動時間に制限があるため、1度の起動で全メッセージを処理することが難しい

即時性が要求されないとはいえ、まだキューにメッセージが残ってるのに一定時間経過後にしか処理されないというのは少し微妙です。

できれば1回の起動で現在キューに存在しているメッセージについては全て処理して欲しいところです。

しかし、上記の通りLambdaには起動時間の制限があります。

Lambdaの起動時間上限目一杯までループで処理を繰り返す方法もありますが、起動時間管理の処理を書くのは結構面倒そうです。

Lambdaを複数並列に起動すればいけそうですが、仮にファイルの処理結果をなんらかのデータストアに書き込むと想定すると、データストアにかかる負荷が気になります。

データストア等にに過負荷をかけることなく、キューに残存するメッセージを全て処理する方法はないものでしょうか??

後続処理をまかせるLambdaを起動すればよいのでは?

一度メッセージを処理し終わって、sqsにキューにメッセージが残っているか確認し、残っていれば自身と同じLambdaファンクションを指定して起動するようにすれば良さそうです。

リレーでバトンタッチするイメージですね。

Undoukai_baton_relay


この方式の場合、厳密には「一回の起動」ではありませんが、次回のスケジューリングを待つことなくキュー内のメッセージが全て処理されます。

以下のような感じ

import λ from 'apex.js';
import AWS from 'aws-sdk';
import 'babel-polyfill';

const main = async () => {
  const lambda = new AWS.Lambda();
  const sqs = new AWS.SQS();
  // メッセージを取得
  const data = await sqs.receiveMessage({
    QueueUrl: 'xxx',
    MaxNumberOfMessages: 0,
    WaitTimeSeconds: 1,
  }).promise();

 if (data.Messages.length === 0) return;

  // メッセージに対してなんらかの処理をすると同時にdeleteMessageBatch用のEntryを作成
  const entries = data.Messages.map((message, idx) => {
    /* do somethng */
    return {
      id: idx.toString(),
      ReceiptHandle: message.ReceiptHandle,
    };
  });

  // 処理済みのメッセージを削除
  await sqs.deleteMessageBatch({
    QueueUrl: 'xxx',
    Entries: entries,
  }).promise();

  // キューに残っているメッセージ数を問い合わせ
  const attr = await sqs.getQueueAttribute({
    QueueUrl: 'xxx',
    AttributeNames: ['ApproximateNumberOfMessages'],
  }).promise();

  // String型なので、数値変換
  const remains = parseInt(attr.Attributes.ApproximateNumberOfMessages, 10);

  // メッセージが残っていればlambda起動
  if (remains > 0) {
    await lambda.invoke({
      FunctionName: 'xxx',
      InvocationType: 'event',
      LogType: 'none',
    }).promise();
  }
};

export default λ((e, ctx) => {
  main();
});

全体的にエラー処理ははしょってるので注意してください

(async/awaitつかってるところはtry-catchで囲む必要があります)

export defaultのところが見慣れない感じになっていますが、これはapexでlambdaを管理するときに利用できるラッパーです。

(弊社、Salesforceメインの会社なので紛らわしいプロダクト名ですね。。。)

重要なところはlambda.invokeのパラメータInvocationTypeにeventを指定しているところです。

ここでeventではなくRequestResponseを指定すると、起動したLambdaの処理が完了するまで処理をブロックしてしまうため、キューが空になるか、タイムアウトが発生するまで処理が返ってこないことになるため、無駄にコンピューティング時間を消費します。

eventを指定した場合、Lambda起動後に即座に処理が返ってくるので、あとは後続のLambdaにおまかせということになります。

まだ本番投入はしていませんが、動かしてみたところまずまず問題なさそうです。

Lambdaにgo言語サポートを加えて欲しい

実は今回のSQSコンシューマはいったんgo言語で実装していたんですが、serverlessの風にあてられて、もしくは社内でserverless風吹かせたくてサーバの運用コスト削減のためにLambdaで実装し直したという経緯があります。

前述のapexを利用すると裏ワザ的な方法でgo言語をLambdaで利用することが可能なんですが、残念ながらSQSをサポートしていないため諦めました。

Javascriptで書くと非同期処理の管理が煩雑になるんですよね。

今回はasync/awaitを使っているので大分マシですが、Promiseでやるとすごい縦長になって、あとからソースを見て読解するときになかなかつらいことになりそうです。

(愚直にcallback使うのは無論論外)

じゃあPython使えよって話になるんですが(実際ググったらPython利用しているケースが多い)、

  • サーバサイド : go言語
  • フロントエンド : javascript
  • Lambda : Python

と3言語同時に扱うのはなかなか厳しい(´・ω・`)

というわけでAWSさんは一刻も早くgo言語のサポートをお願いします(人∀・)タノム

おまけ : ショートポーリングに対する誤解

lambdaを5分毎に起動して、receiveMessageでmaxの10件づつメッセージを取り出して処理すればいいと考えていたんですが、実際に書いて動かしてみたところ、キュー内には10件以上メッセージが存在するにも関わらず、メッセージが1,2件しか返ってきません。

APIドキュメントを確認すると、以下の記述が見つかりました。

Short poll is the default behavior where a weighted random set of machines is sampled on a ReceiveMessage call. This means only the messages on the sampled machines are returned. If the number of messages in the queue is small (less than 1000), it is likely you will get fewer messages than you requested per ReceiveMessage call. If the number of messages in the queue is extremely small, you might not receive any messages in a particular ReceiveMessage response; in which case you should repeat the request.

aws-sdk APIリファレンスより引用

ショートポーリングの場合、キュー内のメッセージ数が少ない(1000以下など)場合、キュー内のメッセージ数で重み付けされたサンプル数のサーバにしか問い合わせにいかないため、MaxNumberOfMessagesに指定した数より少ないメッセージしか返ってこず、キュー内のメッセージ数が極めて少ない場合、メッセージが返却されないことがあるため、繰り返しreceiveMessageを発行する必要がある。

という理解であってるんですかね?

ロングポーリングの場合はどうなんでしょうか? 次の記述が見つかりました。

もう 1 つのメリットは、キューに使用可能なメッセージがあるがレスポンスに含まれていない場合に、偽の空のレスポンスを減らすことができる点です。この状況は、Amazon SQS がショート(標準)ポーリングを使用する場合(デフォルトの動作)に発生します。このとき、レスポンスに含めることができるメッセージがあるかどうかを調べるために、(重み付けされたランダム分散に基づいて)サーバーのサブセットだけに対してクエリが実行されます。一方、ロングポーリングを有効にすると、Amazon SQS はすべてのサーバーに対してクエリを実行します。

Amazon SQS ロングポーリング - Amazon Simple Queue Service より引用

どうやらロングポーリングの場合、上記の問題は発生しないようです。

(receiveMessageのWaitTimeSecondsを1以上にすると、ロングポーリングになる)

Lambdaはコンピューティング時間で課金されるため、なるべく「待ち」の時間は減らしたいところですが、ショートポーリングで1,2件づつメッセージを処理するよりは効率が良さそうです。

2016年6月25日 (土)

Go言語でトランザクション制御のラッパー関数を作った話

こんにちは。エンジニアの島袋です。ブログ初投稿になります。

今回はGo言語でトランザクション制御のためのラッパー関数を作った話をします。

なぜラッパー関数が必要になったのか

最近はginを使ってAPIを実装しているのですが、ginにはトランザクション制御のためのミドルウェアがなく、自作する必要がありました。

(手動でbegin, commit( or rollback)を書くのはさすがにつらいので。。。)

ググったところ、違うWAFですがechoのサンプルリポジトリのeurie-inc / echo-sampleが参考になりそうでした。

以下はecho-sample/middleware/transaction.go からの抜粋です。

func TransactionHandler(db *dbr.Session) echo.MiddlewareFunc {

    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return echo.HandlerFunc(func(c echo.Context) error {

            tx, _ := db.Begin()

            c.Set(TxKey, tx)

            if err := next(c); err != nil {
                tx.Rollback()
                logrus.Debug("Transction Rollback: ", err)
                return err
            }
            logrus.Debug("Transaction Commit")
            tx.Commit()

            return nil
        })
    }
}

上記はミドルウェアの中でトランザクションを開始し、httpハンドラ本体からerrorが帰ってきたらrollbackを実行、errorが帰ってこなければcommitするという実装になっているようです。

これをパクって真似してginのミドルウェアに移植すればよさそうです。

(ところで、筆者はつい最近までSAStruts(のカスタム)で仕事をしていたのですが、GoのWAFでいうMiddlewareってSAStrutsでいうところのInterceptorなんですよね。やらせる仕事は同じなのに言語やフレームワークで用語が異なるのってちょっとめんどくさ混乱しますね。)

2,3行書き換えれば終わりじゃん、そう考えていた時期が私にもありました

ほぼコピペでいけるじゃないですかと、楽勝ムード漂っていたときにあることに気づきました。

type HandlerFunc func(*Context)

上記はginのHandlerFunc型の関数シグネチャです。

本体処理のハンドラも、middlewareも上記のシグネチャを満たす必要があります。

ええ、返り値がないんです。よって、errorをreturnしてmiddlewareになんらかの処理をさせるということができません。

/(^o^)\

error発生時にpanicにすればmiddlewareの中でrecoverで拾うことは可能ですが、panicを多用するのってgo言語の作法的にどうなんですかという問題があります。

ラッパー関数という手段

さらにググったところ、stackoverflowのdatabase/sql Tx - detecting Commit or Rollbackという投稿が見つかりました。

以下は回答者のコードの抜粋です。

func Transact(db *sql.DB, txFunc func(*sql.Tx) error) (err error) {
    tx, err := db.Begin()
    if err != nil {
        return
    }
    defer func() {
        if p := recover(); p != nil {
            switch p := p.(type) {
            case error:
                err = p
            default:
                err = fmt.Errorf("%s", p)
            }
        }
        if err != nil {
            tx.Rollback()
            return
        }
        err = tx.Commit()
    }()
    return txFunc(tx)
}

実行したい関数を引数txFuncとして受け取り、トランザクションを開始してからtxFuncを実行。

txFuncの中でerror、もしくはpanicが起こったらdeferのところでrecoverを使って拾い、rollbackを実行、問題なければcommitという流れのようです。

これはいいですね。middlewareでトランザクション制御を行う場合、トランザクション境界がリクエストの開始〜終了となりますが、ラッパー関数方式の場合、トランザクション境界を柔軟に変更することが可能です。

gin用に書き換えてみた

func Transact(sess *dbr.Session, c *gin.Context, txFunc func(*dbr.Tx, *gin.Context) error) (err error) {
    tx, err := sess.Begin()
    if err != nil {
        return
    }
    defer func() {
        if p := recover(); p != nil {
            switch p := p.(type) {
            case error:
                err = p
            default:
                err = fmt.Errorf("%s", p)
            }
        }
        if err != nil {
            tx.Rollback()
            return
        }
        err = tx.Commit()
    }()
    return txFunc(tx, c)
}

txFuncの中でginの*Contextが参照できるように書き換えてみました。

また、筆者はORマッパーとしてdbrを利用しているので、database/sqlを使っている部分をdbr用に書き換えてあります。

(dbrの説明はdbr – Go 言語 O/R Mapper の紹介が詳しいです。)

このラッパー関数は以下のように使うことができます。

func hoge(c *gin.Context) {
   sess := conn.NewSession(nil)
   err := Transact(sess, c, func(tx *dbr.Tx, c *gin.Context) error {
              // なんらかの処理
              })

  if err != nil {
    //エラー処理
  }
}

とりあえずトランザクション制御できるようになった

上記のラッパーを使ってとりあえずはトランザクション制御ができるようになりました。

go言語、書いてて楽しいのですが、WAFは薄いものが多いため、結構足回りの機能を自作する必要がある感じです。

(まあ今回はほぼコピペだったわけですが。。。)

このあたりの知見がもっと共有されるようになると嬉しいですねー

2016年3月24日 (木)

Seleniumを使ってIoT案件のSalesforce画面を画像に落とす

エンジニアの佐藤です。こんにちは。今回はSeleniumのお話をさせていただきたいと思います。

きっかけ

先週のこと、筆者はエビデンス(システム動作の証拠情報)画像を保存する作業を担当することになりました。 対象システムは、あるIoT案件のシステムです。データは多量にありますが、問題は作業時間です。 画面をひとつひとつ手作業でキャプチャして保存していたのでは長い時間を要します。 何かうまい方法はないものかと考えていた時、数年前に弊社のランチ勉強会 でSeleniumが取り上げられたのを思い出しました。今回はこれが効果的だろうと思って試してみました。

Seleniumとは

Seleniumとは、 Webブラウザをプログラムから操作する仕掛けです。 普段キー入力やクリックで実行しているブラウザ操作をコードを記述することで実行し、 またブラウザからHTML要素の値などを取得して評価することができます。 Webサイトのテストツールとして幅広く使われています。

実装方法

今回のエビデンス対象画面はSalesforceのオブジェクト詳細情報画面で Canvas を表示するページで、以下のようなレイアウトになっています。

Cloudblog_img05


この画面のURLは以下のような構造になっています。

https://apN.salesfoce.com/{Salesforce ID}

Salesforce IDはSalesforceオブジェクトそれぞれに発番されていますので、エビデンス画面の名称とSalesforce IDの対応表があれば、以下の処理フローが成立しそうです。

Salesforce IDからURLを生成
↓
ブラウザでページを開く
↓
画面名称からファイル名を生成して、ページ画像を保存

このフローをSeleniumで自動化できれば、あとは寝ているだけで網羅的な画像ファイル集ができあがるという算段です。

ではさっそく「エビデンス画面とSalesforce IDの対応表」を作成してみましょう。 いろいろな方法がありますが、筆者のおすすめはForce.com CLI です。以下のようにコマンドを打つと、目的のCSVが取得できます。

$ force login -u=user@example.com -p=PASSWORD (ID、パスワードは仮)
$ force query "SELECT Id, Name FROM XXX__c ORDER BY Name DESC" --format:csv > out.csv
$ cat out.csv
"Id","Name"
"a099900000XXXXXXXX","20160321-4378"
"a099900000XXXXXXXX","20160321-4377"
"a099900000XXXXXXXX","20160321-4376"
"a099900000XXXXXXXX","20160321-4375"
"a099900000XXXXXXXX","20160321-4374"
"a099900000XXXXXXXX","20160321-4373"
"a099900000XXXXXXXX","20160320-4372"
"a099900000XXXXXXXX","20160320-4371"
"a099900000XXXXXXXX","20160320-4370"
...

次はこのCSVを読み込んでSeleniumを実行するプログラムの作成です。Seleniumはさまざまな言語で実装できますが、 今回は最もメジャーなJavaで作成しました。 今回もソースコード全体は弊社GitHub公開レポジトリ で公開しています。
最初にCSVファイルを読み込んで、キーバリューペアの配列を作成します。(getIdNamesメソッド 。この部分はJavaの一般的な話なので、省略させていただきたいと思います。)
次にSeleniumを使ってSalesforceにログインします。以下のコードはSeleniumにFirefoxを起動させ、IDとパスワード文字列を打ち込んでログインボタンをクリックさせます。

WebDriver driver = new FirefoxDriver();
driver.manage().window().setSize(new Dimension(1200, 1600));

driver.get("http://login.salesforce.com/");
Thread.sleep(4000);

driver.findElement(By.id("username")).sendKeys("user@example.com");
driver.findElement(By.id("password")).sendKeys("PASSWORD");
driver.findElement(By.id("Login")).click();

Thread.sleep(4000);

ログインが完了したら、いよいよCSVデータから作成したURLにナビゲートして画面を保存していきます。

private static void takeScreenOfId(
    WebDriver driver,
    String sfid,
    String name
) throws Exception {
    driver.navigate().to("https://apX.salesforce.com/" + sfid);
    Thread.sleep(12000);
    File scrFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
    FileUtils.copyFile(
        scrFile,
        new File("/home/user01/Downloads/" + name + ".png"))
    ;
} // end of takeScrenOfId

プログラムを実行すると、Firefoxが自動で動いて画面が移り変わっていきます。

Cloudblog_img01


一晩たってから確認してみると…目論見通りキャプチャ画像が保存されていました!

Cloudblog_img02


サーバー上で実行するには

Seleniumはブラウザを操作するプログラムであり、ブラウザは動作のためにデスクトップ画面を必要とします。ですので今回の話を手元のノートパソコンで実行すると、実行している間は画面を占拠されてしまい、仕事になりません。
この問題を解決する方法としては、サーバーに仮想デスクトップを設定する方法があります。仮想デスクトップ上でSeleniumプログラムを実行し、正常動作が確認されたらリモートデスクトップ接続を切ります。頃合いを見計らって再接続して、結果を確かめます。
仮想デスクトップの設定方法につきましては、過去ブログ「DockerでLinux仮想デスクトップサーバをつくる話」 などをご参照ください。

Cloudblog_img03


Salesforceのガバナ制限に注意

実は筆者はここで、思わぬ制限要因に引っかかってしまいました。対象ページはCanvasを使用しているため、 1ユーザにつき24時間で5,000回というSalesforceのガバナ制限 を超えてしまったのです。結果ある時点から先はすべてエラー画像ファイルとなり、やり直しでした。プログラム的なバッチ処理は、人が画面を使用する場合を大きく超えた思わぬリソース消費を伴うことがあります。注意したいポイントです。

2016年2月27日 (土)

ソラコムBeamでUDP通信機器の暗号化を実装

要約

  • ソラコムBeamを使用すると、UDP通信をHTTPリクエストに変換してくれる。
  • 端末側の対応不要で通信セキュリティを確保できる。
  • コストについてはケースに応じて比較検討を要する。

エンジニアの佐藤です。今回はソラコムBeamのお話をさせていただきたいと思います。

「モノのインターネット(IoT)」なる案件を手掛けたときに筆者が発見したのは、「デバイスよ、お前はなんて貧弱なんだ。」という事実です。「なんとかドラゴン」だの「スカイレーク」だの言われているスマホやパソコンと異なり、ほんのわずかなリソースしかない。
こんな時に光ってくるのがUDPプロトコルです。コネクションなしで、IPパケットにポート番号を追加しただけの非常に単純な仕様。即時性重視のデバイスにはとても向いています。
一方でUDPの弱点は、暗号化非対応、64KB以上のデータを一度に送信できない、不達エラーを検出できない、などがあります。筆者もこの「暗号化非対応」に困っていました。

そんな時に聞いたのが「ソラコムBeam」(以下、Beam)の話です。簡単に言うと「閉域網でUDP(他もある)を受けて、HTTPSでリクエストしてくれる」サービスで、デバイスに負担をかけずにUDPをセキュアに使えるのです。

設定

詳しくはソラコムのホームページに書かれていますが、大まかに以下の手順です。

  1. HTTPSサーバーを用意する。
  2. ソラコムのSIMを購入して有効化する。
  3. グループを作ってSIMを登録する。
  4. このグループに対してBeam機能を有効化する。 (ア) 払い出されるUDPエンドポイント(ホスト名・ポート番号)をデバイスに設定する。 (イ) 用意したHTTPSサーバーのホスト名・ポート番号・リクエストパスを登録する。カスタムヘッダも登録できる。

278car394_11


どんなリクエストが来るのか?

今、デバイスがUDPで送信するのは、あるバイナリデータです。テキストではありません。どうやってHTTPリクエストになっているのでしょうか? 観察してみると、POSTリクエストのbodyに以下のJSON文字列(ASCIIエンコード)が入っていました。

{payload:"YWJjZGVmZ2hpam…"}

キーは「payload」、値はUDPバイナリをbase64エンコードした文字列です。 従ってUDPで送信されたバイナリを復元する手順は以下になります。

  1. JSONデコード
  2. 「payload」キー値の評価
  3. 値をbase64デコード

Node.jsでの受信処理は以下のようになりました。

// SORACOM Beam受信処理
var https = require('https');

httpsOpts = {};
httpsOpts['key']  = fs.readFileSync(PRIVATE_KEY_FILE);
httpsOpts['cert'] = fs.readFileSync(CERTIFICATE_FILE);
httpsOpts['ca']   = fs.readFileSync(CERTIFICATE_CHAIN_FILE);

https.createServer(httpsOpts, function(req, res) {
    var rawData = null;

    req.on('data', function(d) {
        var base64 = JSON.parse(d.toString())['payload'];
        rawData = new Buffer(base64, 'base64');
    }); // end of on('data')
    req.on('end', function() { 

        // ここにrawData評価処理を記述

        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end();
    }); // end of on('end')
).listen(LISTEN_PORT); // end of createServer

UDPの受信処理と比べてみると、だいぶ複雑にはなりましたが、流れは同じです。

// 元々のUDP受信処理
var dgram = require('dgram');

var udpServer = dgram.createSocket('udp4');
udpServer.bind(LISTEN_PORT);
udpServer.on('message', function(buf, rinfo) {
    // ここにbufの評価処理を記述
}); // end of on('message')

UDP直使用と比べて、どうか

筆者が少し試した範囲では、Beamは性能については特に問題はないと思いました。即時性も良好です。HTTPになれば通常のWebトラフィックと同様にスケールアウトできますから、何かと楽です。

費用はどうでしょうか。Beamの利用料金は本稿執筆時点で1リクエストあたり0.0009円で、UDPパケット1個の転送で2リクエストカウントされます。
3秒ごとにデータを送信すると24時間では
24 * 3600 / 3 = 28,800回の送信になり、
1日あたりの料金は
28800 * 2 * 0.0009 = 51.84円(税別)になります。
元々の通信料金と比べてみましょう。
UDPパケットの長さを、仮にプロトコルヘッダ込みで100バイトとすると、
24時間では
100 * 28800 = 2.88メガバイトで、約3メガバイトです。
本稿執筆時点の通信料金は最も安い料金クラスの場合、1メガバイトあたり0.2円ですから、3メガバイトでは0.6円となります。これに1日あたりの基本料金10円が加算されますので、1日あたりの通信料金は10.6円(税別)です。
つまり、UDP直使用と比較してBeamの通信コストは
(51.84 + 10.6) / 10.6 ≒ 5.9倍になります。
また、HTTPSを受けるCPUコストもありますので、筆者の感覚では、同一サーバーでさばけるデバイス数は、少なくとも数分の一になるでしょう。

「セキュリティ」のコストはそれなりにかかります。 ソリューション設計にあたっては、諸事情勘案の上で比較する必要があると思います。

続きを読む "ソラコムBeamでUDP通信機器の暗号化を実装" »

採用情報

株式会社フレクトでは、事業拡大のため、
・Salesforce/Force.comのアプリケーション開発
・HerokuやAWSなどのクラウドプラットフォーム上での
Webアプリケーション開発
エンジニア、マネージャーを募集中です。

未経験でも、これからクラウドをやってみたい方、
是非ご応募下さい。

フレクト採用ページへ

会社紹介

株式会社フレクトは、
認定コンサルタント
認定上級デベロッパー
認定デベロッパー
が在籍している、セールスフォースパートナーです。
また、heroku partnersにも登録されています。
herokuパートナー
株式会社フレクトのSalesforce/Force.com
導入支援サービス
弊社の認定プロフェッショナルが支援致します。
・Visualforce/Apexによるアプリ開発
・Salesforceと連携するWebアプリ開発
も承っております。
セールスフォースご検討の際は、
お気軽にお問合せください。

2024年3月

          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            
ブログ powered by TypePad