2013年9月26日 (木)

Play1のcommons-codecは1.4

commons-codec-1.4以前のバージョンではBase64#encodeBase64Stringはchunked(結果文字列が長い場合改行される)で返されていた。

1.5以降のバージョンでは改行なし。

なかなか気がつかなくて時間無駄にした。。。(--

ていうかPlay1で使うことが想定されているライブラリを書く場合はBase64エンコード時にreplaceAll("\r\n", "")した方が良いのかもとか考え始めるとかなり悩ましい。

http://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/binary/Base64.html#encodeBase64String(byte[])

2013年8月15日 (木)

HerokuでOAuth

こんにちは

成り行きでHerokuのOAuthを使った単純なアプリを作ったので手順をメモしておきます。

 

★クライアントアプリケーションを登録する

まずはダッシュボードのアカウントページでOAuthを使用するアプリケーションを登録します。

ページの真ん中あたり「Register API Client」のボタンをクリックすると、アプリ名と認証後のリダイレクトURLを入力するフォームが表示されるので適当に設定します。

登録するとclient_idとclient_secretが発行されるので、これらをコピーして環境変数等に設定しておきます。

ちなみにリダイレクトURLは「http://localhost:9000/login」のようにローカルホストをhttpプロトコルで指定しても問題ありません。実運用の場合はインターネット上に公開されたURL(もちろんherokuapp.comでOK)をhttpsで指定するわけですが、ローカルホスト用のアプリとHeroku上のアプリの二つを登録しておけばローカルでテストしたものをgit pushしてすぐに確認できるのでとても楽です。

 

★PlatformAPIのラッパー

今回サーバーサイドはPlay1.2.6を使用して作成しました。Play2(Scala)も考えたんですが将来的にPlatformAPIのラッパーは汎用ライブラリ化するかも?と思っていて、それを考えたら最初からJavaで書いた方が楽なので。

で、そのプロトタイプコードをGistに公開しています。

HerokuApiラッパーのプロトタイプ

Playを使用しているので通信部分にはWSクラスを使用していますが、汎用化する場合はもちろんこの部分は書き換えます。AccountクラスやHerokuApiExceptionなどの本来は単体クラスとするべきものがpublic staticクラスになっているのもプロトタイプとしての割り切りです。

実際に実装したのはgetAccountメソッドとgetRateLimitメソッドの二つだけですが、他のメソッドもほぼ同じパターンで実装可能なはずです。

ちなみにHerokuAPIのJavaラッパーとしてはHeroku社謹製のheroku.jarがありますが、これはAPIKeyを引数にインスタンスを作成するライブラリなのでOAuthでは使用できません。

ざっとドキュメントを見た感じでは、HerokuのWebAPIは

  • HerokuAPI
    herokuコマンドが内部的に呼び出しているWebAPI。これを利用すればherokuコマンドでやっていることを何でもプログラムから実行できる(はず)
  • PlatformAPI
    OAuth経由でherokuを操作するためのAPI。

の2種類がきっちり区別されていてそれぞれできることが異なります。

待ってたらそのうちPlatformAPIのJavaラッパーも作られると思いますが、そっちが早いか自分が必要に迫られるのが早いか。。。(^^;;;

 

★コードサンプル

今回作成したアプリのコードは公開されていないので、上記クラスを使用した簡単なコードサンプルを載せておきます。

 

ログイン用のURLの生成

public static void index() {
    String clientId = System.getenv().get("HEROKU_OAUTH_ID");
    String url = HerokuApi.getOAuthUrl(clientId, HerokuApi.Scope.Identity);
    render(url);
}

 

クライアントIDとScope(認可を要求する権限の範囲)を引数にHerokuApi#getOAuthUrlメソッドを呼び出すと認証用のURLが返されるのでそれをHTML内にリンクとして配置します。

Scope.Identityはユーザー情報のみにアクセスできるもっとも狭い権限のScopeです。

 

リダイレクトされたコードからHerokuApiを生成してメソッドを実行

リンクをクリックするとHerokuの認証画面が表示され、そこで「Allow Access」をクリックするとアカウント画面で登録したリダイレクトURLに「code」というURLパラメータをつけてリダイレクトされます。

Herokuoauth

リダイレクトされた側のコードは以下のような感じです。

 

public static void login(String code) throws Exception{
    String secret = System.getenv().get("HEROKU_OAUTH_SECRET");
    HerokuApi api = HerokuApi.authenticate(secret, code);
    String email = api.getAccount().getEmail();
    renderText(email);
}

 

これでログインしたユーザーのメールアドレスが画面に表示されます。

OAuth初期はGoogleやFacebookでまったく思った通りに動かず苦労した記憶があるんですが、仕様がこなれてきたのかもの凄くあっさりと動きました。(^^v

ちなみにもう一個の実装メソッドであるgetRateLimitメソッドはAPIの実行可能回数(PlatformAPIは1時間に1200回までと制限されている)を返すメソッドですが、多分Heroku側がバグっている気がします。(^^;;;(サポート問い合わせ中です。)

2013年7月24日 (水)

herokuにSQLを実行するプラグインを作成する

こんにちは

今朝herokuからwebinarの案内が来ていました。日本時間だと午前2時からのWebinarなんで多分スルーなんですが(^^;;;、アジェンダの中にたまたま目にとまった1行がありました。

 

•Using heroku-pg-extras to gain visibility into database performance

 

「heroku-pg-extras」って何じゃい?と思って検索したところ予想通りGitHubでソースが見つかりました。(^^v

 

https://github.com/heroku/heroku-pg-extras

 

ふむふむ。postgresqlの統計情報ビューからSELECTした結果をコマンド一発で表示するプラグインなのね。

ここで実行されているSQLがどういう時に有用なのかはよくわかりませんが。。。(^^;

ソースの方も見ましたが中身は単純にSQLを発行しているだけなんで、自分用にカスタマイズするのも簡単そうです。

□□□□

だいたい内容はわかったので、興味を失いかけたところでふと気がつきました。

あれ?これちょっとカスタマイズすれば。。。

 

heroku myapp:user_count
heroku myapp:sales

 

みたいな感じで個別案件で欲しい情報がコマンド一発でとれるプラグインが作れるんじゃね?

改めてソースを眺めてみたところ多分簡単にできるんじゃないかと思ったので、ちょっと試してみました。

 

★gitリポジトリをコピる

herokuコマンドのプラグイン拡張は直接gitから必要なモジュール一式を引っこ抜くという大胆な構成をしています。つまり、pluginを作成するにはgitリポジトリが必要です。

通常ならGitHubを使えば良いんですが、今回は個別案件専用のプラグインなのでBacklogのgitを使いました。

 

git clone git@github.com:heroku/heroku-pg-extras.git
git remote rm origin
git remote add origin xxx@xxx.git.backlog.jp:/xxx-project/myapp-sqlplugin.git
git push origin master
heroku plugins:install xxx@xxx.git.backlog.jp:/xxx-project/myapp-sqlplugin.git

heroku pg:cache_hit

 

pluginのインストールがGitHub以外からでも行えるかどうかイマイチ確信がなかったんですが、特に問題ないようです。

ちなみにプラグイン名はGitのリポジトリ名(上の例の場合は「myapp-sqlplugin」)になるようです。

 

★案件用にカスタマイズする

とりあえずpg-extrasを別の名前でプラグインとしてインストールすることに成功したのでここから案件用にカスタマイズしていきます。

pg-extrasは「heroku pg」にコマンドを追加するという形で実装されているんですが、個別案件用の名前として「pg」はふさわしくないのでまずはコマンド名を変更してみます。

 

class Heroku::Command::Pg < Heroku::Command::Base
↓
class Heroku::Command::Myapp < Heroku::Command::Pg

 

クラス名がコマンド名だろうというのは単なる勘です。消去法でここしかそれらしいところが無いので多分そうだろうという。。。(^^;;;

最初は継承元のクラスをBaseのままにして試しましたが、それだと動きませんでした。まぁpostgresqlを使うためのするためのprivateメソッドが何かあるんでしょう。継承元をPgに変更したところ見事「heroku myapp:cache_hit」というコマンドで動作するようになりました。(^^v

 

★pluginsディレクトリで直接修正してみる

ところで先の修正は最初にcloneしたgitリポジトリで行っていました。これはherokuコマンドの参照するプラグイン本体ではないので修正内容をプラグインに反映させるためには以下のコマンドを叩く必要があります。

 

git add .
git commit -m "Modify class name"
git push origin master
heroku pugins:update myapp-sqlplugin

 

一度修正内容をgitにpushして「heroku plugins」コマンドでそれを取り直す必要があるわけです。まぁめんどくさいです。

トライ&エラーで修正している時にはこんな手順踏んでられないので、Herokuのプラグインディレクトリで直接修正することにしました。

Windowsの場合はHerokuプラグインは

 

<ユーザーホーム>/.heroku/plugins

 

にあります。「heroku plugins:update」コマンドは要するにここで「git pull」を実行しているだけなので、ここを修正のワークにしても特に問題ないはずです。

これでソースを修正したらすぐに動作確認ができるようになりました。(^^v

 

★案件用のコマンドを追加してみる

ソース眺めると一目瞭然でコマンド追加方法は見当がつくんですが、例えばユーザー数をカウントするコマンドの追加は以下のようになります。

 

  def user
    sql = %q(
      SELECT count(*) AS user_count FROM user_table
    )
    puts exec_sql(sql)
  end

 

要するにpublicメソッドがそのままコマンドになって、putsで結果を出力しているだけなんですね。

コマンド追加で修正するのはほとんどSQLだけなので、僕のようにRubyにあんまり詳しくなくても特につまることなく修正、動作確認までできました。(^^v

元々あるcache_hitなどのコマンドは使わないのであれば削除してしまってOKです。

 

★オプション引数を追加してみる

例えば売上取得のSELECT文なんかでは年、月をパラメータで指定したいわけですよ。

ここまですこぶる快調に実装できてきたので、あとはパラメータを使用するサンプルだけ書いて、残りは案件担当者に自分で必要なSQLを追加してもらえば良いかと思っていたんですが、最後の最後ではめられました。(--

というか、やり方が分かった時には「こんなんアリかよ。。。」とかなりの衝撃を受けたんですが、これがRuby界隈ではわりと普通のことなのかそれともHerokuの独自仕様なのかを是非とも知りたいです。

□□□□

以下、その内容です。ソース見てると引数は「option[:xxxx]」または「shift_argument」で取得するらしいということはすぐにわかりました。

前者はオプション引数「--xxxx」で指定した内容に相当し、後者はオプションなしの引数に相当します。(「shift_」の名前が示すように実行するとポインタがずれるので2回目の実行では2つ目の引数が取れ最後はnilになります。)

今回の場合はオプションで年、月を指定したかったのでとりあえずこんなコードを書いてみました。

 

  def test
    month = options[:month]
    puts month
  end

 

引数「month」を出力しているだけ。

これで「heroku myapp:test --month 7」と叩いた時に「7」が出力されるかなぁと期待したんですが。。。

残念ながら何も出力されません。どうやらどこかで有効なオプションを登録する必要があるようです。

まぁlogsコマンドの「--tail」みたいに引数を取らないオプションもあるわけだし、それは予想の範疇です。

。。。が!

どこのソースを見てもGREPかけてもどうにもそれらしいところが見つかりません。

また、調べているうちにさらなる疑問もわいてきました。

例えば「--tail」はエイリアスとして「-t」としてもOKですが、この情報もどこにも書かれていない気がするんですよね。。。

確かに各種コマンドのコメントには引数として何が使えるかということが全部書かれて入るんですが。。。

。。。コメント。。。

コメントはコメントですよ。。。

しかし他にそれらしいところが無い。。。

 

  #   -m, --month MONTH  # month
  def test
    month = options[:month]
    puts month
  end

 

heroku myapp:test --month 7

りたーん

7

マジでかーーーー!!!

heroku myapp:test -m 7

りたーん

7

本気かーーーー!!!!

気付くか!?こんなもーーーん!!!(--

□□□□

。。。Rubyな人達はこれ見当つくもんなんでしょうか。。。(--

それともDSLとかそっち系の文化???

いずれにしても結構な衝撃でした。コメントはコメントではないかもしれない。ということをこれからは頭に入れておくべき???(いや、ないだろ。。。(--)

さらにざっと調べたところ「heroku help」コマンドが出力しているヘルプがメソッドのコメントをそのまま出力しているだけということがわかりました。多分その辺りのコードでオプション引数を解析して登録しているんでしょうね。

これで引数つきのSQL実行コマンドもめでたく作成できたわけですが、そこはもはやどうでもいい気がするので割愛します。

今回SELECTしか試してませんが多分INSERTやUPDATEも問題ないでしょう。

ちなみにこれに関するドキュメント(というかプラグイン開発全般)はDevCenterのどこにもない気がします。

2013年7月 2日 (火)

Google Spreadsheetで特定のシートに直リンクする

こんにちは

昨日からGoogle Spreadsheet APIで作成したシートに直リンクするURLを生成したくて色々調べてました。

「API使うならそれくらい簡単にできるんじゃないの?」って思うでしょ?えぇ、僕もそう思いました。

しかし、これが。。。(--

一応最終的にはできたんですが、その方法が「マジですか?!」とかなり愕然とするものだったのでここにメモとして残しておきます。

 

★Google SpreadsheetのURL

実際にシートを作ってみながらブラウザのアドレスバーを確認するとすぐにわかりますが、GoogleSpreadsheetのURLは以下のような形式となっています。

 

https://docs.google.com/spreadsheet/ccc?key=xxxxxxxx#gid=1

 

アカウントが組織に属している場合は若干URLのパスが変わりますが、ここで重要なのは

  • URLパラメータの「key」がスプレッドシートを特定するキーとなっている
  • URLパラメータの「gid」がワークシートを特定するキーとなっている

という点です。ちなみにgidはおそらく単純にシートの作成順による連番です。途中でワークシートの削除や移動があった場合でもgidが振り直されることはなく単純なインクリメントで生成されます。

要するにAPIでこのkeyとgidが取得できれば、URLを組み立てることができるわけです。楽勝です。

 

★Google Spreadsheet APIを使う

今回アプリはPlay2で作成しているのでJavaのAPIを使用しました。Google Spreadsheet APIの使い方については良記事がたくさんあるので割愛しますが、スプレッドシート、ワークシートの情報はそれぞれ、

  • SpreadsheetEntry
  • WorksheetEntry

というクラスから取得できます。

これらのAPIリファレンスを眺めると目的とする情報はそれぞれ

  • SpreadsheetEntry#getSpreadsheetLink
  • WorksheetEntry#getId

というメソッドから取得できそうです。楽勝です。

 

★gidが取れないことに気がつく

さて、ここまで来たら後は試すだけですがまず、SpreadsheetEntry#getSpreadsheetLinkは完全に目的にマッチするものでした。keyだけでなくスキームから始まる完全なURLとして取得できます。楽勝楽勝(^^v

が、WorksheetEntry#getIdの方はgidではなく、こちらも「https://...」で始まるURIでした。そもそもgetIdメソッドはWorksheetEntryクラスのメソッドではなく、その基底クラスであるBaseEntryクラスのメソッドなのでSpreadsheetだけでなく他のGoogle Appsオブジェクトまで全部含めたIdentifierになっているわけです。同一のスプレッドシート内でのみユニークとなるgidとは意味合いが異なります。

それではgidはどうやって取得するのかと言うと。。。これはいろいろと試行錯誤したんですが、どうもそのためのメソッドは無いようでした。(--

最終的にはRESTのレスポンスXMLの中にそれっぽい値が入っていないことから、どうやら本当に無いらしいと結論しました。

 

★シート名でのリンクを試みる

APIでgidが取れないとなると、どうやって直リンクすれば良いのでしょうか?シートの削除や移動があるのでgidは単純な連番とはなりません。世の中にGoogleSpreadsheetの特定シートに直リンクしたい人は山ほどいるはずです。

。。。と、あおってはみたものの実際のところはここまでにさんざん検索をかけているのでシート名でリンクする( = URLパラメータに「sheet=xxxxx」を指定する)方法があるらしいことはわかってはいたんです。

が、これ途中でも試してたんですが何故かうまく動きません。検索結果にも「動かねー」という怨嗟の声が山ほどあるし、公式ドキュメントにもそれができるという記述が見つけられないので結局この方向もあきらめました。。。(--

 

★できないはずがないとあがく

手詰まりです。

絶対に必要と言う機能でもないし、もうあきらめようかとも思ったんですがこんな簡単なことができないということがどうしても信じられず検索を続けました。

そして。。。ついに、見つけた解答がこれです。

http://stackoverflow.com/questions/11290337/how-to-convert-google-spreadsheets-worksheet-string-id-to-integer-index-gid

一番最後にコード例が載っていますが、要約するとこうです。

 

  1. WorksheetEntry#getIdから最後の「/」以降の3桁を取得する
  2. その文字列を36進数として数値に変換する
  3. その数値と31578のXORを取るとgidが取得できる

 

。。。(--

。。。。。(--;;;

。。。できた。。。できたけどっ!

36進数って何だよ?!31578は一体どこから出てきた???

長くこの業界にいるけど、こんなの初めて見たよ!!!

動きはしたけどなんだか全然釈然としない。誰かこのアルゴリズムの根拠の分かる人がいたら教えてください。m(_ _)m

2013年7月 1日 (月)

FLECT OSS Libraryが公開になりました。

こんにちは

なんと、今日から7月です。つまり2013年ももう半分終わってしまいました。。。(--

そして、小西がFLECTに入社してからちょうど1年半たったということでもあります。
はやっ!年々時が経つのが速くなっているようでかないませんな。(--

ちなみにこのブログを書き始めたのは2012年の5月末です。1年1カ月で記事数31件。
さぼっている時期もありますがだいたい月に2回位書いている計算ですね。もうちょっと頑張ります。

 

★FLECT OSS Libraryとは

これです。

http://oss.flect.co.jp/

この1年半で僕が作ったものがあらかた入っている感じですね。内容はかなり雑多な感じです。なんでこういうものを作ろうと思ったかは以前にも書いたので割愛します。

個人的に汎用性の高いモノ、開発者にとって有益なモノを作りたいという志向性が強いのでこういう形での開発はテンションがあがります。(^^;

 

★Salesforce関連

思い起こせば1年半前、Salesforceを外部からAPI連携するのをやりやすくするライブラリを整備してほしいというお題に対して、いきなりSOAPClientから作り始めたのがFLECTでのキャリアの始まりでした。

それをベースにして作成したSalesforce APIのラッパーがSalesforceClientで、これでどれ位のことができるかを示すために作成したアプリが、Salesforce Explorerです。

完全に上司の想像のナナメ上をいっていたと思います。(^^;

Salesforceを使う開発者ならそれなりに便利に使えると思うので興味ある人は触ってみると良いかもしれません。(多分)

 

★Excel関連

excel2canvasはHTML5Canvasの調査をしている時に「絵心無いし、Canvasにテスト描画する適当なものが思いつかねー(--」と困ったあげくなんとなくExcelをそのまま描画してみようと思い立ったのが最初です。凝り性なのでやり始めると最初のお題からまったく関係ないシロモノになっていきましたがそこはそれ。(^^;R&Dとはそういうものです。(キッパリ)

これ、何気にSlideも結構なViewを稼いでいるし少なくとも日本では絶対うける思うんですけどね。どの位Excelを再現できるかはサービス化を目論んで開発したExcelReportで確認できるので、なんか適当な帳票フォーマットをExcelで持っている人はアップロードしてみてください。

 うまくブラウザで描画されないという場合は連絡くれれば可能な限り直します。(多分)

 

★その他

あとはHeroku上で動かすことを前提として作成したアプリをいくつか公開しています。

ちなみに最近作っているアプリは全部Play2で開発しています。社内のHerokuチームが使用している言語はPlay1なんですが、最近はメンテもされてないようなので次期主力言語として使えるかどうかを評価するという目的も兼ねています。

ちなみにPlay2の評価は。。。うーん、どうなんだろう?(--
まだまだベストプラクティスを模索している状態なので、書いていてももっと良い書き方があるんじゃないの?とか、本当にこんなめんどくさいことを毎回書かないといけないのか?とか思うこともしばしばです。

(↑もっとも、Scalaのおかげで圧倒的に楽になっている部分もあるので、このように不満点だけをあげるのは正当な評価ではありません。Play2については近日資料にまとめるつもりです。)

とにかく最近はHerokuが面白くてしょうがないので、Herokuを便利に使うための小物ツールをもうちょっと作っていこうかなと思っています。まだOSS Libraryにはリストされてませんが現在つくっているものもHerokuユーザーにとってはかなり便利です。(多分)

 

会社としても絶賛Herokuエンジニア募集中なので興味ある人は覗いてみてください。

2013年6月 4日 (火)

Herokuにバイナリを組み込むbuildpackを作成する

こんにちは

FFmpegというツールをご存知でしょうか?

私は映像関係の技術にはまったく明るくないのですが、動画のフォーマット変換や合成などが行えるライブラリらしいです。

今回このライブラリをHeroku上で動作するようにし、さらにはそれをbuildpack化するという作業をやったので、その過程をご紹介します。

題材としてはffmpegを扱っていますが、自分でbuildしたバイナリをHerokuに組み込む場合は同じ手順でできるはずです。

ちなみに参考にしたURLは以下です。

最初は言わずと知れたDevCenter。次のはBinaryのビルドで使用するBuildServer「Vulcan」のGitHub。最後は過去にffmpegをHerokuに組み込んだ先達の残してくれたコマンドログです。

特に最後のGistは秀逸でHeroku上でffmpegを動かすだけならここに載っているコマンドを上から叩いていけばそれだけで動きます。

心から感謝します。

 

★Vulcanとは何か?

さて、上のGistを見ると最初にgemコマンドでvulcanをインストールしています。

Vulcanとは何かと言うとHeroku上で動作するビルドサーバーです。

Node.jsで作られたサーバーアプリケーションとRubyで作られたコマンドラインクライアントからなります。

この二つが連携して、クライアント側にあるソースコードをサーバーに送って、サーバー側でビルドした結果のバイナリをクライアントに送り返す、ということをやってくれます。

ここでVulcanサーバーがHerokuアプリであることは非常に重要です。というのは今時のLinuxではそれぞれのDistributionでライブラリのバージョンがどうなっているとかの事情がまったくわかりませんし、ましてやHerokuのDynoでベースのOSに対してどのようなカスタマイズが行われているかなど知りようが無いからです。(例えばDynoに「heroku run bash」で入ってもviは使えません。。。)

なので、異なる環境でビルドしたバイナリをそのままHerokuに持って行っても必ずしもそのまま動くとは限らず、Heroku上で動かすバイナリであるならばHeroku上でビルドしてしまうのが一番確実な訳です。

 

★ffmpegをビルドしてHerokuで使ってみる

といっても、Gistにあるコマンドを上から実行していけば良いだけなのですが。(^^;

注意が必要なのはこれらのコマンドは必ずLinuxまたはMacから実行しなければならないという点です。

これには二つの理由があります。

  • vulcanクライアントが内部的にシェルのコマンドを実行しているため、そもそもWindowsでは動かない
  • Executableなバイナリをgit pushする場合はpermissionを維持しなければならないがWindowsでは無視される

ということで、普段はほとんどの作業をWindowsで行っている私も以降の作業はUbuntuで実行しました。(後にbuildpackを作るためにMacでもやり直しました。)

手順としは本当にGistを上からなぞるだけなんですが、一応列記しておくと

  1. gemでvulcanをインストール
  2. vulcanのコマンドでHerokuアプリの作成
    • クライアント側にサーバーのURL等の設定情報が保存される
    • Addon(Cloundant無償版)も同時に追加される
    • このためコマンドを実行するHerokuユーザーにはクレジットカード情報が必須
  3. ffmpegのソースをgitで取得
  4. vulcanでビルド
    • 実行完了時にDownload URLが表示される
    • curlかwgetで完成したバイナリのtgzをダウンロード
  5. 解凍したバイナリを自分のHerokuアプリのフォルダにコピー
  6. heroku configでffmpegを実行するためのPATHとLD_LIBRARY_PATHを設定
  7. heroku にgit push

以上、終わりです。「heroku run "ffmpeg -version"」を実行することでffmpegが正しく実行できることが確認できます。

 

★buildpack化を検討してみる

ここまでの結果からHerokuでバイナリを実行するためには要するに

  • 実行可能なバイナリをコピー
  • PATHとLD_LIBRARY_PATHを適切に設定

すれば良いことがわかります。普通にLinuxを使う場合とまるっきり同じです。

実際に使うことを考えた場合、毎回プロジェクトにバイナリを含めてgit push、というやり方は面倒ですし、先にも述べたとおりpermissionの問題があるのでWindowsからは実行できません。

なのでbuildpackにすることを検討してみます。

buildpack化するにしても例えばJavaのbuildpackにちょっと手を加えて目的のバイナリをインストールするコードを付け加えるという方法はあまり嬉しくありません。

それだとRubyやPythonなどの他の言語を使う際には使えないので。

なのでheroku-buildpack-multiを使って常に他のbuildpackと組み合わせて使うbuildpackとして作成することにしました。

 

★作ってみた

で、できあがったbuildpackがこれです。(いきなり)

heroku-buildpack-ffmpeg

実のところ最初からブログのネタにすることを念頭に作っていたんですが、これが思った以上に簡単でもうソースを読んでもらえばそれで良いんじゃないかと言う気がするんですよね。。。(^^;;;

それでもいくつかポイントになるところはあるので以降それらについて解説します。

 

buildpackの構成

buildpackの実体はbinディレクトリにある3つのスクリプトファイルです。それぞれ以下の役割があります。

detect このbuildpackが実行可能かどうかを判断し、実行可能であれば0を返す
例えばJavaのbuildpackの場合はpom.xmlがあれば0を返すようになっている
compile build処理本体。実行に必要なファイルを収集してBUILD_DIRにぶちこむ
release リリース時に設定する環境変数や追加するアドオンをYAML形式で標準出力に出力する
(環境変数の設定とアドオン追加は初回のgit push時にのみ実行される)

今回の場合detectとreleaseの二つのファイルについては

  • detect -> 常に実行
  • release -> 特に設定項目なし

なのでほぼ空っぽです。
(最初はPATHやLD_LIBRARY_PATHはreleaseで設定すれば良いかと思ってましたが、buildpack-multiがreleaseについては最後に実行したbuildpackの出力を採用するという仕様のため後述する「.profile.d」を使用する方法に変更しました。)

つまり書いたのはほぼcompileスクリプトのみなわけですが、今回の場合ここでやらなければいけないことは以下の2つです。

  • インターネット上のどこかからffmpegのバイナリを取ってきて展開する
  • PATHとLD_LIBRARY_PATHを適切に設定する

あとは何をやっているかがわかるようにちょいちょいechoをだしてやればOKです。(ちなみに「----->」や先頭スペース6個のインデントはHeroku推奨のecho書式です。)

 

GitHubページにバイナリを置く

さてダウンロードするバイナリの置き場所について、Herokuの推奨はS3なんですが、今回はGitHub上に置くことにしました。

その方がバージョン管理も同時にできて楽かなぁと思ったもので。(^^;;;;

当初はmasterにそのまま置いてrawURLでダウンロードしてくれば良いかと思いましたが、実はGitHubのrawURLにはサイズ制限がありました。(何MBだったかは忘れましたが。。。)

なのでGitHubページを作ってそこに置くことにしました。

GitHubページとはGitHub上のプロジェクトに対して(静的ファイルから構成される)専用のホームページを持つことができるというあれです。

本来の用途とは違うのでこちらもそのうち制限がかかる可能性はありますが、mavenリポジトリとして使用している人が何人もいる位なので多分大丈夫でしょう。(^^;

 

「.profile.d」にログインスクリプトを置く

次は環境変数の設定です。

これは最初に仕組みを知った時にはかなり「へぇ~」と思ったことなんですが、Herokuのdynoって本当に普通にログインして普通にコマンド叩いてるだけなんですよね。

なので「.profile.d」にスクリプトを置いておけばDyno起動時にそれが実行されます。

ということで、compileの中で環境変数を設定するログインスクリプトをechoで生成しちゃえばそれでOK。

う~ん、よくできてる。(^^v

 

★Hackingとか

以上完成です。

READMEにも書いてある通り、.buildpacksを用意してbuildpack-multiを使って試してみてください。(注意が必要なのはメインのbuildpackは最後に書かなければならないという点です。先にも少し触れましたが、buildpack-multiは最後に実行したbuildpackのreleaseスクリプトから環境変数等を設定します。)

既存のアプリに後からBUILDPACK_URLを設定することで変更することもできますし、もしも他のバイナリを組み込むbuildpackを作成したならばそれらを複数組み合わせることも可能です。(やりすぎるとSlugサイズはひどいことになるかもしれませんが。。。)

他のバイナリを組み込むbuildpackの作成も数行の変更で可能なのでぜひ試してみてください。

2013年5月27日 (月)

Heroku Postgresでdblinkを使用する。

こんにちは

Heroku Postgresでdblinkを設定したのでその際に行ったことを備忘として残しておきます。

 

★下準備

「create extension」コマンドでdblinkのextensionをインストールします。

 

create extension dblink;

 

一般的にはcreate extensionの前に設定ファイルの変更等が必要なようですが、Herokuの場合はいきなりcreate extensionを実行すればOKです。

大仰に書き始めましたが、実は設定はこれだけで終わりです。(^^;;;

 

★dblink経由でSELECTしてみる。

dblink経由でSELECTを実行する場合のSQLは以下のようになります。

 

select * from dblink(
  'host=xxxx.compute-1.amazonaws.com ' ||
  'port=xxxx ' ||
  'dbname=xxxx ' ||
  'user=xxxx ' ||
  'password=xxxx', 
  
  'select id, email from accounts'
  ) as T1(id int, email varchar(255)) 
  where email like '%flect%';

 

構文よりdblink関数はTABLE句相当になることがわかります。

第1引数が接続情報で第2引数が実行するSQLです。上の例では必要な接続情報を明確にするために「||」でのコンカチを行っていますが、通常はもちろん1ラインで記述すれば良いです。

接続情報は「heroku config」で表示されるDATABASE_URLから取得できます。

dblink経由でSELECTした結果に対してWHERE句も機能しますし、例にはありませんがローカル側のテーブルとのJOINも問題なく行えます。

ちなみにWHERE句は第2引数の中に含めることもできます。

 

  ...
  'select id, email from accounts where email like ''%flect%'''

 

この場合、おそらく条件の絞り込みはリモート側のサーバで行われて条件にマッチしたデータのみが転送されてくると思われます。

対して外側にWHERE句を書く方法の場合はすべてのデータが転送されてきた後にローカル側で条件が評価されると想像します。つまり両者では結果が同じでもデータ転送量が異なるためパフォーマンス的には大きな差があることが予想されます。

今回試したテストデータでは顕著なパフォーマンス差は見られませんでしたが、可能な場合はリモートで実行するSQL内にWHERE句を書いた方が良いと思います。(この程度の単純なSQLの場合はオプティマイザの最適化によってどちらでも同じになる可能性もありますが、基本的な考え方としてはこれであっているはずです。)

 

★dblink接続をセッション内で永続化する。

先の記法はSELECTの実行毎に毎回接続情報を書かなければならないという点でかなり冗長です。

これを回避する方法としてdblinkに名前をつけて接続を開きっぱなしにすることができます。

 

select dblink_connect('mycon', 'host=xxxx.amazonaws.com port=xxxx ....');

 

一度接続が確立するとそれ以降は接続情報の代わりに名前を使用してdblink経由のSQLを実行できます。

 

select * from dblink('mycon',
  'select id, email from accounts'
  ) as T1(id int, email varchar(255)) 
  where email like '%flect%'

 

ただし定義したdblinkの有効期間はセッションなので接続ごとに再度dblink_connectを実行する必要があります。

同一セッション内で全く同じdblink_connect文を複数回実行した場合には2回目以降は「duplicate connection name」というエラーになるのでコネクションプールのある環境では扱いが面倒な気がします。。。(--

リモートユーザーの権限設定で参照/更新範囲を制限できるのでDBインスタンス単位で有効なdblinkだって作れても良さそうなものですけどね。

まぁ、どの道Heroku Postgresではcreate userできないので権限の制限はできません。。。(--

今回要件にないので更新系のSQL実行は試していませんがドキュメント見る限りHerokuでも特に問題なく実行できると思います。「dblink_exec」などの更新用の関数の実行をSELECT文内で実行するという仕様には最初びっくりしましたけど、SQLに新たな構文を追加することなく実装するために実行結果がSELECT結果として返ってくるようにしたんですかね。(^^;;;

2013年5月24日 (金)

HerokuにPlay2アプリをpushする際にキャッシュをクリアする

こんにちは

最近GitHub上に自前のMaven-Repositoryを作成しました。

僕は普段の仕事では汎用のライブラリ(Java)を書く時間とWebアプリ(Play1 or Play2)を書く時間が半々くらいなんですが、これまではアプリから自前のライブラリを使用する場合は作成したjarファイルを直接アプリのlibフォルダにコピーしていました。

それがMaven-Repositoryをインターネット上に置くことによってアプリのライブラリ管理フレームワーク(Play1ではivy、Play2ではSBT)から直接取得することができるようになるわけです。

作成したアプリはたいていHerokuで動かしているんですが、Play2の場合はBuild.scalaにリポジトリの情報を設定することで自前のリポジトリからもライブラリを取得することができます。

残念ながらPlay1の場合はHerokuから自前のリポジトリを使うことはできません。Play1の枠組みではリポジトリの設定は「ivysetting.xml」で行うのですが、Herokuのbuildpackが内部でこのファイルを上書きしてしまうためです。(自分でカスタムbuildpackを作れば対応は可能なはずですが。)

 

★Play2で設定してみる

せっかくリポジトリを作成したので現在作成中のアプリでjarをリポジトリから取得するように修正してみます。

修正するファイルはBuild.scalaの1ファイルのみです。

 

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName         = "sqlsync"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    jdbc,
    anorm,
    "postgresql" % "postgresql" % "9.1-901.jdbc4",
    "jp.co.flect" % "flectSalesforce" % "1.0-SNAPSHOT"
  )


  val main = play.Project(appName, appVersion, appDependencies).settings(
    // Add your own project settings here      
    resolvers += "FLECT Repository" at "http://shunjikonishi.github.io/maven-repo/"
  )
}

 

こんな感じ。

元々はlibフォルダにflectSalesforceとそれの依存するflectCommon, flectSoapの各jarを置いていたんですけど、それらをまるっと削除して起動してみます。

すると起動時にSBTによる依存性解決のログがずらずらと流れて無事に起動。ちゃんと動きます。

次いでHerokuにpushしてみると、Slugコンパイル時に依存性が解決されてこちらも無事に動作しました。

素晴らしい。

 

★ライブラリ開発中はこんな設定するもんじゃない

と、喜んではみたものの実は現在の開発はライブラリの開発が主でアプリは単純にそれにUIをかぶせているだけのものだったのでした。。。

つまり、これまでは日常的に以下のような操作を行っていました。

  1. エディタでライブラリのコードを修正
  2. Mavenでビルド
  3. Antで生成したjarファイルをアプリのlibフォルダにコピー
  4. アプリ再起動して動作確認

めんどくさい。(--
(ちなみにIDEの類は普段まったく使いません。)

これがリポジトリを使用するようになると以下のように手順が変わります。

  1. エディタでライブラリのコードを修正
  2. Mavenでビルド
  3. MavenでGitHubのリポジトリにデプロイ
  4. play cleanコマンド実行
  5. アプリ再起動して動作確認

「play clean」が必要なのはそうしないとSBTのキャッシュが効いて更新されたjarファイルがリポジトリから取得されないためです。

そもそも手順が増えてるし、GitHubへのデプロイはかなり時間がかかるし、キャッシュがなくなっているせいで起動時に毎回依存性の全解決が走るし、とまったく良いことがありません。。。(--

ていうか、コード1行修正しただけでもデプロイが必要とか明らかに間違ってますよね。

ライブラリの安定ビルドを使用するだけのアプリならリポジトリから持ってくれば良いですが、今回のようにライブラリ開発とアプリ開発がセットになっているケースでは、これまで通りjarを直接libにコピーする方式を取った方が良いように思います。

ここに至って初めてMavenがプロジェクト新規作成時に付加するバージョン番号「1.0-SNAPSHOT」の意図がちゃんとわかった気がします。(^^;;;

 

★HerokuのSBTキャッシュをクリアする

ところでこの話、まだ続きがあります。

ライブラリを何回か更新したところで、再びHerokuにpushしたんですがこれはコンパイルエラーになりました。

理由は追加したはずのメソッドが見つからないため。そう、Herokuのスラグコンパイル時にもSBTのキャッシュは有効なため新しいjarを持ってきてくれないんですよね。。。

今回の場合リポジトリを使う設定を外してlibコピー方式に変更すれば良いのですが、なんか方法があるだろうと思って探してみたところ見つけました。

http://stackoverflow.com/questions/15945263/heroku-play-framework-2-sbt-dependencies-cache

要約するとheroku configに「BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-scala.git#cleancache」を指定するだけ。そうすると次回ビルド時にはキャッシュクリアバージョンのbuildpackがスラグコンパイルしてくれるという寸法です。

実のところこの解決方法自体は予想通りのモノでした。探し始めた時点でbuildpackにキャッシュクリアする1行を足せばできるよなぁ、と思っていたので。

そんな中で今回の発見はこれ。

BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-scala.git#cleancache

buildpackは元々GitHubで公開されたものを直接持ってくるという大胆な仕組みで動いてますが、URLの末尾に「#xxxx」をつけるとmasterではなく指定のブランチを持ってくることができるのでした。

そう思ってみると、実はbuildpackのブランチには若干の機能違いと思われるブランチがいくつか並んでいたりもします。

いや~、知らなんだ。。。

残念ながらブランチの説明というのは(git的にも)ないので、どこが違うのかを見るためにはdiffを見るしかないですがGitHubのブランチはこういう使い方もアリなんですね。(^^;

2013年4月25日 (木)

Heroku Europeリージョン来ました。

2日続けてこんにちは

今朝Heroku ヨーロッパリージョンリリースのニュースが飛び込んできました。
めでたい。v(^^)v

ヨーロッパができるなら時間の問題でTokyoにも来ると思うので、それに備えて素振りしておきたいと思います。

 

★リージョンの指定

単純にheroku createコマンドに「--region eu」をつけるだけのようです。

heroku create my-app --region eu

作成したアプリのリージョンを途中で変更することはおそらくできません。つまり現在USリージョンで動いているアプリをEU(あるいは今後登場が予想されるTokyo)で動かそうと思ったら新しくアプリを作り直す必要があります。

git pushはともかく、環境変数やAddonの追加まで考えると結構な手間ですが、これを簡略化するために「heroku fork」という新しいコマンドが用意されました。

forkは要するに既存のアプリをコピーして新しいアプリを作成するコマンドで、これを使用すると

  • gitのコードベース
  • 環境変数
  • すべてのAddOn(!)
  • PostgreSQLのデータ(!!)

がコピーされるらしいです。

課金AddOnは多分同じプランでコピーされるんでしょうね。実際に移行するとなるとある程度並行稼働(というよりもテスト)の期間が必要でしょうからそこは注意が必要です。

PostgreSQLのデータコピーも嬉しいんですが、並行稼働を考えるとどの道pgbackups経由のデータ移行が必要になると思われます。

逆にコピーされない情報としてはドメインの設定全般(おそらくSSLの証明書も)と、あとはPapertrail等のサードベンダー製Addonの設定情報も無理でしょうね。Papertrailでログ監視設定をたくさん設定している場合などはちょっとめんどくさいかもしれません。

DNSの設定はどうなるんだろ???
僕はほとんどDNSの設定を自分でやることがないのであまり知見がありませんが、Route53とかだと設定変更はすぐに反映される印象があります。
単純に新しいHerokuアプリを指すようにすれば良いだけのはずですが、既存のHerokuアプリをリネームして新しいアプリを同じ名前で作るという方法でも可能なはずです。
(直観的には素直にやる方のがリスク少ないだろうと思いますけど。)

 

★AddOnとか

EUリージョンにともないいくつかのAddOnもEUで動くようになったようです。

これ、すごく重要なことで例えばPostgreSQLなどはアプリがEU、DBはUSという状態で動かすとかなり悲惨なことになります。

PostgreSQLに限らず通信の発生するAddOnでは常にネットワーク遅延の問題がつきまとうので、アプリをEUで動かすなら使用するAddOnも同じリージョンで動くことはほぼ必須です。

幸いなことに僕が日常的に使用しているAddOnはすべてEUに来ています。ので、こいつらもいずれ日本に来ると信じてます。

ちなみにAddOnのリージョンはアプリのリージョンによって自動的に選択されるようですね。デフォルトはそれでまったく問題ないんですが、PostgreSQLのFollowDBなんかは違うリージョンに作りたいことがあるんじゃないの?ともちょっと思います。

 

★ひとつのアプリを複数のリージョンで動かす場合

単純にアメリカの人はUSにあるホストを、ヨーロッパの人はEUのホストを見るようにしてもらえと言うポリシーのようです。

それはそれで良い気がしますが、DBが別になるのでワールドワイドで単一のDBを参照させたい場合とかはちょっと難しいかもしれません。

参照専用であればFollowDBで。。。
。。。
あ、今リージョン違いのFollowは作れないんじゃ疑惑をさっき書いたばかりだった。。。(--

まぁ、いいや。多分FollowDBは時間の問題でリージョン指定して作れるようになると思うので、参照専用の場合はそれで良いとして、次に欲しくなるのはPostgreSQLの双方向レプリケーションのような気がします。

 

□□□□

日本にも来るのが待ち遠しいですな。(^^;

2013年4月24日 (水)

Salesforce BulkAPIの動作検証

こんにちは

最近珍しくSalesforceを触っています。(FLECTではSalesforce素人の僕の方が少数派なんですが。。。)

案件需要として「RDBMSのデータを定期的にSalesforceに同期する」という要件があるのでそれが汎用的にできるツールを作成しているのです。

で、そのバックロジックとしてはBulk APIを使うのでざっと検証したその動作を書きとめておきたいと思います。

 

★Bulk APIとは?

大雑把に言ってしまえばCSVで作成したデータを一気にSalesforceに取り込むためのAPIです。
他にもXML形式で取り込めたりクエリでSELECTした大量のCSVデータをダウンロードしたりもできますがここでは触れません。

Salesforce識者の方にはDataLoaderが裏で使用しているAPIというのがわかりやすいでしょうか。 (といいつつ、DataLoaderはデフォルトではSOAP APIを使用する設定になっており設定変更しないとBulkAPIは使われませんが。)

 

★とりあえず使ってみる

BulkAPIの使い方自体はドキュメントを読めばなんとなくわかります。

簡単にまとめると

  1. WebAPIのOpenJOBメソッドでジョブを開始
  2. WebAPIのAddBatchメソッドでCSVファイルをPOST(複数回可)
  3. WebAPIのCloseJOBメソッドでジョブを完了

ジョブの実行自体は非同期なのでAddBatchがリターンした時にはまだデータの更新は完了していません。ただしAddBatch自体がかなり重たいメソッドのようで7MB程度のファイルのアップロードで2分前後の時間がかかっています。

AddBatchからリターンした後にはまだデータ更新の途中であってもCloseJobメソッドを実行してしまって構いません。それでデータ更新が中断されるようなことはなく、データ更新終了時に自動的にクローズされます。

CloseJobを実行しなかった場合、ジョブは一週間オープンしたまま残ります。(とドキュメントに書いてあります。)

クローズした後でもJobのステータスは確認できるのでAddBatchが終わったらさっさとクローズしてしまった方が良いです。

 

★ジョブの実行結果の確認

ジョブのステータスの確認はWebAPIでもできますが、Salesforceの画面上で行う方が簡単です。

 

    設定 > 管理者設定 > 監視 > 一括データ読み込みジョブ

 

の項にジョブの状況確認画面があります。

この画面では処理に成功したレコードが何件あるか、失敗したレコードは何件あるかと言ったサマリ的な情報を確認できる他にバッチの「結果を表示」を実行することで処理を行った全レコードについて

  • レコードのID
  • 処理に成功したかどうか
  • 処理内容はInsertかUpdateか
  • エラーがあった場合そのエラーメッセージ

が、CSV形式で確認できます。

 

★エラー時の動作検証

さて、このBulkAPI。正常ケースは特に迷うところはありません。
API実行しました。データ入りました。以上終わり。です。

ここではガバナ制限やレコードデータの不正などエラーケースについてどのような動作をするかを検証していきたいと思います。

ちなみにテストはDeveloperEditionを使用して1万5000件くらいのデータを使用して行いましたが、その環境は現在こんなことになっています。

Salesforcestorage_2

ストレージ使用量600%です。(^^;;;
BulkAPIでもディスク容量のチェックはもちろん行われますが、ジョブの実行中はしばらく容量超過に気が付かないようで、100%を越えてもしばらくはデータがInsertできます。

バッチの途中からでもエラー(ストレージ超過)になることはあるので、どういうタイミングでチェックが行われているのかは謎ですが、これはそういうもののようです。

まぁそうでなければ今回のテストはほとんど実行できなかったので良いのですが。(^^;;
ちなみにこれだけ容量を超過していてもBulkでのUPDATEは問題なく実行できます。(INSERTはもちろんエラーになりますが。)

以下、今回チェックしたエラー時の動作です。検証はBulk APIのドキュメントにあるLimitsを超過したケースといくつかのレコードエラーについて実施しました。

 

AddBatchのファイルサイズが10MBを越えていた場合

この場合、AddBatchのレスポンスとしてHttp status「400 BadRequest」が返ってきます。当然バッチはジョブに追加されませんし実行もされません。

 

CSVに1万1行以上のレコードがあった場合

AddBatchには成功し、バッチジョブも実行されます。ただし、1万行を越えたレコードについては無視されて処理が実行されません。
実行結果を確認すると、「失敗したバッチ数」はカウントアップされていますが、「レコードの失敗」には無視されたレコードはカウントされません。

 

CSVに1000万文字以上の文字があった場合

未テストです。日本語データを含む場合1000万文字は確実に10MBを超えると思われるのであまり意味がありません。

 

1フィールドに32000文字以上の文字があった場合

AddBatchには成功しますが、バッチ処理全体が実行されません。例えば1行だけにエラー行があって他のレコードには問題がない場合であっても1行も処理は実行されません。

エラーのない行については処理してくれても良さそうなものですが。。。LongTextの上限が32768文字であるにも関わらずここでの制限が32000文字であることも謎です。

 

CSVに5000フィールド以上あった場合

未テストです。。。(--

ネタとしてMetadataAPIで5000フィールド作ろうかとも思いましたがやっぱり面倒なので。(^^;;;
BulkうんぬんよりもSalesforceの設定画面の方が先に限界を迎えそうな気がします。

 

1レコードに40万文字以上あった場合

これも未テストです。。。(--

一般的なスキーマでテストしようとすると先に1フィールド32000文字の制限に引っ掛かると思うので。
おそらく結果は1フィールドの超過の場合と同じだろうと予想しています。

 

テキスト項目サイズ超過(256文字)のレコードがある場合

そのレコードだけがエラーになります。

エラー内容はジョブの確認画面からダウンロードできる結果CSVで行単位に確認できます。

 

選択項目に未定義の選択値がある場合

エラーとはならずそのまま更新されます。

予想としてはエラーになると思ったんですが、識者に確認するとこれはそういうものらしいです。。(--

 

メール項目に不正なメールアドレスがある場合

そのレコードだけがエラーになります。

エラー内容はテキスト項目サイズ超過の場合と同じように確認できます。

基本的にはレコードのエラーはその行だけがエラーになると考えて良さそうです。
(選択項目の挙動がイマイチ納得いきませんが。。。)

 

★Bulk APIの運用方法

ここまでの結果から、なんとなくCSVを作成してとりあえず投げてみて結果はジョブ確認画面で確認。で十分な気がします。

レコード数超過だけは半端な結果になるのでCSV作成時に10000行を越えないことだけは注意した方が良いですが、それ以外のケースはエラー確認後にCSVを修正してリトライで十分な気がします。

レコードの項目エラーについてもSalesforce側でちゃんとチェックしてくれるので、事前にチェックする必要性はあまり感じません。(メールアドレスなんかは事前にチェックしたとしてもチェック内容が同じかどうかなんてわかりませんし。。。)

□□□□

そんな感じでRDBからのSELECT結果をなんとなくBulkで突っ込むツールがもうじき完成します。(^^;

採用情報

株式会社フレクトでは、事業拡大のため、
Salesforce/Force.comのアプリケーション
開発
HerokuやAWSなどのクラウドプラッ
トフォーム上でのWebアプリケーション開発

エンジニア、マネージャーを募集中です。

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

フレクト採用ページへ

会社紹介

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