読者です 読者をやめる 読者になる 読者になる

Rails on Dockerでbinding.pryする

RailsをDocker上で動かして開発する
DatabaseはMySQLを使ってそれは別コンテナで立ち上げる
これらをうまく扱うためにdocker-compose使ってdocker-compose upで一気に立ち上げられるようにする

みたいなのはよく見る

docker-compose.yml

version: "3"
services:
  database:
    image: mysql:5.7.8
    environment:
      - MYSQL_ROOT_PASSWORD=password
    ports:
      - '3306:3306'
  rails:
    image: my-rails-image
    command: ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]
    depends_on:
      - database
    volumes:
      - .:/my-rails-project
    ports:
      - "3000:3000"

こんな感じの設定ファイルを用意してdocker-compose upすればブラウザ上でport3000でアクセスできたとする

問題

github.com

を使ってコード内でbinding.pryをしてデバッグをしているが Docker上でたてたrails serverではそれがスルーされてしまった
これをまずとめるにはdocker-compose.ymlを修正する必要があり

docker-compose.yml

version: "3"
services:
  database:
    image: mysql:5.7.8
    environment:
      - MYSQL_ROOT_PASSWORD=password
    ports:
      - '3306:3306'
  rails:
    tty: true # この行と
    stdin_open: true # この行を追加
    image: my-rails-image
    command: ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]
    depends_on:
      - database
    volumes:
      - .:/my-rails-project
    ports:
      - "3000:3000"

これを追加してコンテナを立ち上げなおす
するとdocker-compose logsを見てみるととまったことがわかる
ただそこはlogsの中身なのでコマンドはうてない

どうするかでいうと docker attach を使う

docs.docker.com

これにより今回の例だとrails serverのプロセスにattachすることが出来るのでbinding.pryでとまっているところでデバッグすることが出来るようになる
終了するときはCtrl-cで抜けることが出来るのだが、これはプロセス自体もとまってしまうのでコンテナがとまってしまうので docker-compose start railsのようにしてstartしないといけない

最後に

docker attachした後Ctrl-cでぬけるとプロセスが終了してしまうのはattachしつづければ良い話でもある
一旦はこれでやっていくがもっとスマートな方法があったら知りたい

ヘルスケアデータを半自動でGCSに投入しGCFを通してRailsにPOSTする

※ ここに書かれているものはGCFが beta の時にかかれているもので正式リリースされた時にはかわっているかもしれません

hatappi.hateblo.jp

以前ヘルスケアデータをGoogleCloudStorage(GCS)にアップロードするプログラムを作成した
そして

hatappi.hateblo.jp

ここでGoogleCloudFunctions(GCF)を使ってサーバーレスにふれた

今回やること

今回はそれらを使ってヘルスケアデータがGCSにアップロードされた際にそのイベントを元にGCFでRailsへのアップロード処理を行う

Rails

Railsに関しては今回POSTで/health_care/uploadにファイルをアップロードをうけつけるcontrollerだけを定義する

# config/routes.rb
Rails.application.routes.draw do
  post 'health_care/upload' => 'health_care#upload'
end

# app/controllers/health_care_controller.rb
class HealthCareController < ApplicationController
  protect_from_forgery with: :null_session

  def upload
    file = upload_params[:upload_file]
    # something
  end

  private

  def upload_params
    params.permit(:upload_file)
  end
end

GoogleCloudFunctions

前回はHTTP Triggerを使用したが今回はGoogle Cloud Storage Triggersを使用する

package.json

{
  "name": "post_health_care",
  "version": "1.0.0",
  "description": "post health care data",
  "main": "index.js",
  "dependencies": {
    "@google-cloud/storage": "^1.1.0",
    "request": "^2.81.0"
  },
  "author": "hatappi",
  "license": "MIT"
}

index.js

const storage = require('@google-cloud/storage')();
const request = require('request');
const fs = require('fs');

exports.uploadHealthCare = function uploadHealthCare (event, callback) {
  const eventData = event.data;
  console.log('bucket is ', eventData.bucket, 'name is ', eventData.name);
  const file = storage.bucket(eventData.bucket).file(eventData.name);
  file.download().then(function(data) {
    const contents = data[0];
    # データを一度tmp領域に保存してそのファイルをアップロードする
    const tmpPath = '/tmp/' + eventData.name.match(/[^\/]+$/)[0];
    fs.writeFileSync(tmpPath, contents);
    const formData = {
      upload_file: fs.createReadStream(tmpPath)
    };
    request.post({ url:'Railsのアップロード先', formData: formData }, function optionalCallback(err, response, body) {
      if (err) {
        console.log('upload failed:', err);
      } else {
        console.log('Upload successful!  Server responded with:', body);
      }
      callback();
    });
  })
  .catch(function(err){
    console.log(err);
    callback();
  });
};

前回のHTTP Triggerの時のように処理を終了したことを明示するためにcallback();を記載する必要がある
これを指定しないと処理が終了せずに後にタイムアウトをむかえて終了してしまう

また今回はファイルをアップロードするために一度tmp領域にファイルを保存した後にPOSTリクエストを行っている
ここに記載されているが、GCFではtmpfsと呼ばれるメモリに保存するマウントポイントが/tmpとして用意されていて使用することが出来る

ここまでくると後はデプロイするだけ

$ gcloud beta functions deploy uploadHealthCare --stage-bucket [codeを保存するGCSのバケット名] --trigger-bucket [トリガーとして監視するGCSのバケット名]

最後に

ここまでくればGCSにヘルスケアデータがおかれるとRailsへのアップロード処理が行われる
ただ今回行った処理はGCSにアップロードする処理をRailsへと向ければよくGCFをはさむ必要はなかったが使ってみたさで今回は行った

GoogleCloudFunctionsにふれる

※ ここに書かれているものは beta の時にかかれているもので正式リリースされた時にはかわっているかもしれません

最近個人的にGoogleCloudPlatformを使用している
今回はpublic betaになったCloudFunctionsにふれる

Google Cloud Functions?

https://cloud.google.com/functions/docs/

Google Cloud Functions is a lightweight compute solution for developers to create single-purpose, stand-alone functions that respond to Cloud events without the need to manage a server or runtime environment.

  • Node.jsで動く
  • サーバーレス
  • イベント駆動
    • 現状は Cloud Pub/Sub, Cloud Storage, HTTP トリガー
  • ローカルエミュレータで開発することも出来る
  • 現状は動作するリージョンはus-central1

AWSでいうところのLambdaのようなもの

ふれてみる

今回はhttpリクエストするとQiitaから指定されたユーザーの投稿のタイトルを返すものを作成する

コード自体はブラウザでも書ける
が折角ならローカルな好きなエディタで開発したいし毎回CloudFunctionsにデプロイするのは時間が勿体ない

まずはローカルで開発できる環境を整える

CloudFunctionsにはlocalで検証できるエミュレータが用意されている(※ 現在はアルファ版)

github.com

インストールしてとりあえず動かすところまでは記載されている
ただzshユーザーは一点だけ注意が必要

エミュレータを使用する際に使用する際のコマンドが functions なのだが zshにはbuild in commandとして functions が用意されておりそちらが使用されてしまう( zshのdoc にて functions [grepすると出て来る)

aliasとして functions-emulator が用意されているのでこちらを使用すれば動く

コードをかいていく

今回はAPIにリクエストを送る必要があるのでrequestにはrequestを使う

github.com

package.json

{
  "name": "sample",
  "version": "1.0.0",
  "description": "google cloud functions sample",
  "main": "index.js",
  "author": "hatappi",
  "license": "MIT",
  "dependencies": {
    "request": "^2.81.0"
  }
}

index.js

/**
 * Responds to any HTTP request that can provide a "message" field in the body.
 *
 * @param {!Object} req Cloud Function request context.
 * @param {!Object} res Cloud Function response context.
 */
const request = require('request');

exports.qiita = function helloWorld(req, res) {
  let parameters = {};

  if (req.body.qiita_user_id) { parameters['query'] = `user:${req.body.qiita_user_id}`; }

  request.get('https://qiita.com/api/v2/items', {
    qs: parameters,
    json: true
  }, function (err, response, body) {
    if(err) {
      console.log(err);
      res.status(response.statusCode).send('QiitaAPI Request ERROR');
    } else {
      const titles = body.map(function(row) { return row['title']; });
      res.status(response.statusCode).send(titles);
    }
  });
};

リクエストとしては

$ curl --request POST \
  --url [trigger url] \
  --header 'content-type: application/json' \
  --data '{"qiita_user_id":"hatappi"}'

のようなものを想定しておりcontent-typeに application/json を指定している
CloudFunctions側ではcontent-typeを見てparseしてくれているので、下記のようなリクエストでも同じプログラムを使うことが出来る

$ curl --request POST \
    --url [trigger url] \
    --header 'content-type: application/x-www-form-urlencoded' \
    --data 'qiita_user_id=hatappi'

あとはプログラムのポイントとしては最終的に send(), end(), json()などの終了メッソドを呼び出して終わりを明示してあげる必要があること
これを指定しないといつまで終わりを待ち続けてしまいCloudFunctions側で設定されるタイムアウトによって事切れてしまう

デプロイ

ブラウザ上でコピペしても良いが今回はgcloud コマンドでデプロイする

$ gcloud beta functions deploy qiita --stage-bucket [コードを保存するGCSのバケット名] --trigger-http

確認

無事デプロイが終わりCloudFunctionsのページにいくとFunctionsが生成されている

f:id:hatappi1225:20170422163015p:plain

詳細ページ
f:id:hatappi1225:20170422163158p:plain

詳細ページのトリガータブでトリガーを確認出来る
今回はhttpを指定したのでurlが表示される

またテストタブからは任意のイベントを渡すことも出来る

f:id:hatappi1225:20170422163347p:plain

最後に

スケジュール機能ほしい

source-map-explorerでファイルサイズ削減

普段Angularを使っている
やっぱり気になるのはバンドルサイズ
とはいえただ小さくしていこうと闇雲にやっても仕方がないのでまず可視化をする

そんな時に役に立つのが

github.com

このsource-map-explorerを使用することでバンドルされたファイルのsourcemapをもとに
どのモジュールがどくらいの割合をしめているのかをhtmlに出力してブラウザで確認が出来る

とりあえずビルドした vendor.js(Angular本体とか使用するライブラリが入る) を可視化してみる

$ source-map-explorer vendor.bundle.js vendor.bundle.map

するとブラウザが開き下記のような画面が表示される

f:id:hatappi1225:20170421230752p:plain

おぉー可視化された
全体としては990kbあり例えば@angularはその中の39.2%の389kbを占めているなどを確認できる!!!

とりあえず今回は左下のrxjs(191kb)を削減していく
プロジェクトの中を見るとほとんどの場所で

import { Observable } from 'rxjs/Rx';

これを書いてしまうとrxjs内をすべてとりこんでしまうのでObservableだけ使用するように

import { Observable } from 'rxjs/Observable';

のようにリプレイスしていく
リプレイスしたのち再度ビルドして可視化する

f:id:hatappi1225:20170421233354p:plain

結果191kbあったものが54kbになった

とりあえず今回はrxjsを見ていったけどmomentjsのlocaleもja以外は使わないので削減できそう
可視化すると見えてくるものがあって良い