Chatwork Creator's Note

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

ビジネスチャット「Chatwork」のエンジニアとデザイナーのブログです。

読者になる

ChatWorkの過去アドベントカレンダーまとめ

こんにちは。10月に入社したあらいです。よろしくお願いします。

この記事はChatWork Advent Calendar 2017の6日目です。

さて、予告なく始まったChatWorkアドベントカレンダー2017ですが、実は2年ぶりのカレンダーです! お楽しみ頂けているでしょうか? この記事では過去のアドベントカレンダーと人気記事を振り返っていきたいと思います。

2013年

adventar.org

記念すべき最初のアドベントカレンダーは2013年でした。 CTO山本による黒い画面不要のサイトジェネレータPhestの紹介や、 当時公開されたばかりのChatWork API の活用記事が話題になりました!

2014年

adventar.org

翌2014年のカレンダーです。 2014年は4月にPHPからScalaへの移行を発表、 また7月にはロゴのリニューアルを行い、 フロント・バックエンドともにプロダクトの改善に向けた努力をする中で得た知見を共有しました。

2015年〜2016年

さて2015年〜2016年ですがカレンダーがありません。 おかしいですね。何かあったんでしょうか(お察し下さい)。 ヒントはこちらの記事の中に隠されています。

creators-note.chatwork.com

2017年

adventar.org

そして今年2017年のカレンダーです。 最近の技術への取り組みのほか、オフィス移転の裏話などを予定しています。 どうぞお楽しみに!

PHPでJSを書く話

ChatWork Advent Calendar 2017の5日目の記事です。

こんにちは。インフラマネジメント部の @cw-ozakii です。

先日のphpcon 2017にてbabel-preset-phpのLTをさせていただきました。

このときはLTの枠内に収めるためにbabel-preset-phpで何ができるのかをデモを中心に進めました。が、LTの内容から落とした技術的な話の中にはbabel-preset-phpをいかに導入するかという知見が詰まっていたので、今回はそのあたりの話をご紹介したいと思います。

babel-preset-phpの使い方

$ npm install --save-dev babel-cli babel-preset-php
$ $(npm bin)/babel source.php -o output.js --presets=php

基本的には上記のようにNodeプロジェクトでbabel-cliとbabel-preset-phpをインストールし、babelを実行するだけでPHPのコードをNodeのコードに変換することができます。もし、複数のファイルを変換する場合は

$ $(npm bin)/babel src --out-dir dist --presets=php

というようにすれば複数のファイルをNodeのコードに変換をすることはできます。しかし、ここでrequireの問題が出てきます。

requireの問題

LTでも少し話しましたがbabel-preset-phpはPSR-4のautoloaderに対応していません。そこで別のファイルを読み出すにはPHPのrequireやincludeを使う必要が出てきます。

Foo.php

<?
class Foo {
}

index.php

<?
require(__DIR__ . '/Foo.php');

$foo = new Foo();

autoloaderに対応する前の懐かしいPHPの書き方ですね。しかし、このコードをNodeに変換しようとするといくつか注意点が出てきます。

1. PHPのrequire ≠ Nodeのrequire

一つ目はPHPのrequireとNodeのrequireで微妙に挙動が違うということです。

相対パスを書いたときPHPのrequireはPHPのマニュアルにある通りカレントディレクトリからの相対パスになります。しかし、Nodeのrequireはそのファイルからの相対パスとなり、基準となるところが違うためPHPとNodeで両方に対応した相対パスを書くことはできません。 そのため、もしrequireでパスを指定する場合はDIRを使って絶対パスになるように指定する必要があります。

余談ですが、

<?
require(dirname(__FILE__)  . '/Foo.php');

のようにdirname(FILE)の場合はdirnameがbabel-preset-phpが対応していないためエラーになります。

2. クラス定義の読み込み

二つ目はクラス定義の読み込みの仕方がPHPとNodeで違うということです。

PHPの場合はクラスを定義した.phpファイルをrequireすれば、そのクラスを利用できるようになります。 対してNodeはというと

foo.js

class Foo {
}

module.exports = Foo;

上記のようにクラスをexportして

index.js

var Foo = require(__dirname + '/foo');

のようにexportされたクラスを受け取る必要があります。

これを解決するためにはPHPかNodeのどちらかに寄せる必要があります。

PHPに寄せる場合は

Foo.php

<?
class Foo {
}

if (global) { // on Node
    global.Foo = Foo;
}

というようにNodeのグローバル空間にクラス定義を書き込むことで、requireするだけでクラスを利用できるようになります。

逆にNodeに寄せる場合は

Foo.php

<?
class Foo {
}

return Foo;

index.php

<?
$Foo = require(__DIR__ . '/Foo.php');

$foo = new $Foo();

というようにrequireの結果を受け取り、それをクラスとして利用します。

これはどちらが良い、悪いということはありません。PHPのように書きたければ一つ目の方法を、Nodeのように書きたければ二つ目の方法をということで導入するプロジェクトのスタイルに合わせて決定すれば良いかなと思います。

3. requireの拡張子が.phpのままになる

三つ目はbabelで変換したファイルは.jsに書き換えられるのですが、requireで指定したファイル名は.phpのままになってしまうということです。

index.js

<?
require(__dirname . '/Foo.php');

var foo = new Foo();

ここでgruntやgulpなどのタスクランナーを用いて、ビルドフローで文字列置換を行うのも一つの手ではあります。が、それはあまりイケてる手法ではないので、ここはWebpackを利用して依存を解決し一つのファイルにまとめてしまおうと思います。

$ npm install --save-dev webpack babel-loader

webpack.config.js

module.exports = {
  context: __dirname + '/src',
  entry: __dirname + '/src/index.php',
  output: {
    path: __dirname + '/public',
    filename: 'index.js'
  },
  module: {
    loaders: [
      {
        test: /\.php$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['php']
          }
        }
      }
    ]
  }
};

ここまで準備ができたら、あとはWebpackを実行すると複数のPHPファイルを一まとめのJSにまとめることができます。

$ $(npm bin)/webpack

余談ですが、同様のことをできるツールとしてBrowserifyではエラーになってしまいまとめることはできませんでした。 特に深くは追っていないのでもしかしたらBrowserifyでも可能かもしれませんので、どなたか自信がある方はチャレンジしてみてください。

まとめ

いかがでしたでしょうか。このrequireの問題さえ解決してしまえば、あとは既存のNodeの開発フローに乗せて、ElectronでもReact Nativeでも好きなように開発を進め始めることはできるのかなと思います。おそらくその過程でまだ見ぬ地雷を踏みぬくことができるので、そのときはぜひともどんなものを踏み抜いたのかお教えください。

自分はもう十分踏み抜いたと満足したのであとは任せます。

ChatWork Advent Calendar 2017、明日は @cw-arai です。

チャットワークのWebhookの署名検証を各言語で実装してみた その2

こんにちは。@eielhです。 id:cw-hayashiが投稿した記事である 「チャットワークのWebhookの署名検証を各言語で実装してみた」があります。 しかし、自分が利用しているプログラミング言語が網羅されていなかったので、続きがないのか確認してみたところ「自分でやってください」*1と言われてしまいました。 というわけで、早速挑戦してみました。

  • 準備するもの
  • Elixir
    • 参考
  • Rust
    • 参考
  • Haskell
    • 参考
  • まとめ

*1:正確には遠回しにしか聞いていません

続きを読む

ScalaアプリをDockerイメージ化してHerokuにデプロイした話

@hayasshi_です。Scalaのプロダクトを開発しています。

Heroku、気軽にWebアプリケーションを公開できて便利ですよね。
先日公開した「チャットワークのWebhookの署名検証を各言語で実装してみた」の記事を書くにあたって、検証用のScalaアプリを Heroku Container Registry をつかってデプロイしました。

もともとDockerでHerokuにデプロイできれば、Scala以外のアプリのときでもデプロイの手順差異が少ないと思って試してみたのですが、公式ドキュメント通りに進めたところ少し嵌ってしまったので、備忘録を兼ねて記録しておきたいと思います。

なおHerokuのContainer Registryを利用したデプロイに関するドキュメントは下記で、基本的には記載通りの手順で問題ありませんでした。

ScalaにおけるDockerイメージ作成用のツールの兼ね合いもあり、Pushing an image(s)の部分はPushing an existing imageの手順で進めました。

devcenter.heroku.com

アプリケーションを用意する

まずはデプロイ対象のアプリケーションが必要です。
今回はサンプルとして、akka-httpをつかってping-pongサーバーを作ってみたいと思います。

通常のScalaプロジェクトを作成して、下記のファイルを用意しました。

object PingPongServer extends App {

  implicit val system: ActorSystem             = ActorSystem()
  implicit val materializer: ActorMaterializer = ActorMaterializer()

  val route =
    pathEndOrSingleSlash {
      post {
        extractRequest { req =>
          val result = req.entity.dataBytes.map(_.utf8String).runWith(Sink.head)
          onSuccess(result) { body =>
            println(s"Client input is '$body'")
            val res = body.toLowerCase match {
              case "ping" => "pong"
              case _      => body
            }
            complete(HttpEntity(ContentTypes.`text/plain(UTF-8)`, res))
          }
        }
      }
    }

  val host = "0.0.0.0"
  val port = sys.env.getOrElse("PORT", "8080").toInt // $PORT is set by Heroku
  Http().bindAndHandle(route, host, port)

  sys.addShutdownHook({
    import scala.concurrent.duration._
    Await.ready(system.terminate(), 30.seconds)
  })
}

Dockerイメージを作成する

ChatWorkのScalaプロダクトではDockerイメージの作成は、sbt-native-packagerを利用しています。

今回も同様にsbt-native-packagerを利用しました。

plugins.sbt

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.1")

build.sbt

import com.typesafe.sbt.packager.docker.ExecCmd

lazy val root = (project in file("."))
  .settings(
    inThisBuild(List(
      organization := "com.chatwork.cwhayashi",
      scalaVersion := "2.12.3",
      version      := "0.1.0-SNAPSHOT"
    )),
    name := "ping-pong",
    libraryDependencies += "com.typesafe.akka" %% "akka-http"   % "10.0.10",
    libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.4.19",

    // Docker settings
    defaultLinuxInstallLocation in Docker := "/opt/application",
    executableScriptName := "app",
    dockerBaseImage := "openjdk:8u131-jdk-alpine",
    dockerUpdateLatest := true,
    mainClass in (Compile, bashScriptDefines) := Some("com.chatwork.cwhayashi.PingPongServer"),
    packageName in Docker := name.value,

    // Expose is NOT supported by Heroku
    // dockerExposedPorts := Seq(8080)

    // Run the app.  CMD is required to run on Heroku
    dockerCommands := dockerCommands.value.filter {
      case ExecCmd("CMD", _*) => false
      case _ => true
    }.map {
      case ExecCmd("ENTRYPOINT", args @ _*) => ExecCmd("CMD", args: _*)
      case other => other
    }
  )
  .enablePlugins(AshScriptPlugin)

いくつかポイントがあります。

  • Heroku Container Registry に Push するイメージのDockerfileではCMDを利用する
    • sbt-native-packagerではENTRYPOINTを使う形でDockerfileが出力されるので、変換するコードを入れています
  • DockerfileでEXPOSEは使えない
    • 記述があっても無視されます
  • 利用するコンテナポートはHerokuが環境変数PORTに設定して渡される
    • このサンプルアプリでは、プログラムコード内で環境変数PORTを取得して Listen するようにしています

特に一番目のポイントはドキュメントにも記載がなかったので、注意が必要です。

sbt-native-packagerは特別な設定をしなければENTRYPOINTでDockerfileを作成するのでうまく起動せず、ドキュメントに書いてあるHerokuサンプルのDockerfileをみて気付くことができました。

下記のコマンドを実行し、ScalaアプリのDockerイメージを作成します。

$ cd /path/to/sbt-project
$ sbt docker:publishLocal
...
$ docker images
REPOSITORY                                                                     TAG                 IMAGE ID            CREATED             SIZE
ping-pong        0.1.0-SNAPSHOT      6d789fb48461        42 seconds ago      121MB
ping-pong        latest              6d789fb48461        42 seconds ago      121MB

Herokuアプリを作成する

続いてHerokuのアプリ領域を作成します。
こちらはドキュメントに記載の通り、CLIツールをつかって簡単に作成できます。

$ heroku login
# Input Email and Password
Logged in as ...

$ heroku apps:create <app-name>
Creating ⬢ <app-name>... done
https://<app-name>.herokuapp.com/ | https://git.heroku.com/<app-name>.git

HerokuにイメージをPushする

すでに存在するDockerイメージをPushする場合は、ドキュメントのPushing an existing imageの手順を行う必要があります。
基本的にdockerコマンドをそのまま利用します。

# Heroku Container Registryへのログイン
$ heroku container:login

# Heroku Container Registry用にタグ付け
$ docker tag ping-pong registry.heroku.com/<app-name>/web

# Heroku Container RegistryへPush
$ docker push registry.heroku.com/<app-name>/web

Pushが完了した時点でアプリケーションはデプロイされ実行されます。

2018-06-27追記

確認したところ、明示的にreleaseのためのコマンドを実行する必要になった模様です。

Container Registry & Runtime (Docker Deploys) | Heroku Dev Center

heroku container:release web -a <app-name>

実行することでpushされたイメージからアプリが起動し、下記のように接続できるようになりました。

$ curl -X POST -d "test" https://<app-name>.herokuapp.com/
test

$ curl -X POST -d "ping" https://<app-name>.herokuapp.com/
pong

$ curl -X POST -d "🍣🍺" https://<app-name>.herokuapp.com/
🍣🍺

まとめ

無事にScalaアプリのDockerイメージをHerokuで実行することができました。
途中の嵌りポイントは、Heroku Container Registryを利用する上で共通と思われますのでご注意ください。

Herokuをつかって、チャットワークのWebhookやOAuthアプリケーションをぜひ作ってみてください!