ぶらっ記ぃ

日本語の練習をしています

Scala.jsでReactアプリケーションを作って公開したはなし

モチベーション

最近、流星のロックマン3というゲームに再燃しています。
Wi-Fiを使ってネット対戦を行うことができるのですが、任天堂公式のWi-Fiサービス終了により長らく対戦を行えない状況でした。
しかし、近年、有志の方がWi-Fiサーバをエミュレートするサーバを立てたことにより、再びネット対戦をすることが可能になりました。

さて、流星3には「シークレットサテライトサーバ」という、対戦中に使える特別なカードがあり、決められたカードテーブルからある程度狙ったカードを引くことができます。
しかし、カードテーブルの内容を全て記憶するのは困難であり、いちいち調べるのも面倒でした。

最近、Scala.jsに興味があることから、Webアプリケーションとしてこのシークレットサテライトサーバの早見表を作ってみようというのがきっかけです。

作ったもの

こちらで公開しています。

mmsf-hub.nomadblacky.dev

アーキテクチャの話

ソースコードも公開しています。

github.com

Scala.js

www.scala-js.org

いわゆるTypeScriptのようなAltJSの一種で、ScalaコードをJavaScriptにトランスパイルすることで、Scalaの強力な言語機能や静的型付けなど、その書き心地をそのままWebアプリケーションなどに持ち込むことができます。
2020年2月には待望のメジャーバージョンである v1.x 系がリリースされたりと、開発もそこそこ活発なのではないかと思います。
詳しい説明は色々な記事があると思うのでここでは語りません。

ちなみに、Scala.jsの概要を知るのにこのページが(面白くて)おすすめです。

tototoshi.github.io

Slinky

slinky.dev

Scala.jsでReact.jsを扱うためのライブラリです。
似たライブラリに scalajs-react があります。

Slinkyを採用した理由としては、Webpackを用いた開発からパッケージングまでサポートされたgiter8テンプレートがありとっつきやすかったことです。

github.com

書き味としてはReact.js色が強いので予めReact.js公式のチュートリアルを触っておくと実装の感覚をつかみやすいと思います。

reactjs.org

以下はソースコードの一部です。雰囲気を感じていただければと。

@react class CardTableComponent extends StatelessComponent {
  case class Props(cardTable: CardTable, selectedCardIndexes: Set[Int])

  private type IndexInCardTable = Int
  private type CardWithIndex    = (BattleCard, IndexInCardTable)

  def render(): ReactElement =
    div(className := "table")(
      props.cardTable.cards.zipWithIndex
        .grouped(5)
        .zipWithIndex
        .map { case (row, i) => renderRows(row, i) }
        .toSeq
    )

  private def renderRows(row: Iterable[CardWithIndex], rowIndex: Int): ReactElement =
    div(key := rowIndex.toString, className := "row")(
      row.map { case (card, cId) => renderCard(card, cId) }
    )

  private def renderCard(card: BattleCard, index: IndexInCardTable): ReactElement = {
    val bg = if (props.selectedCardIndexes.contains(index)) "lightgreen" else "inherit"
    div(
      key := index.toString,
      id := index.toString,
      className := "card",
      style := js.Dynamic.literal(background = bg)
    )(card.name)
  }
}

scala-js-bundler

scalacenter.github.io

Scala.js向けにWebpackを扱うためのsbtプラグインです。
Webpackについてはここでは語りません(語れません)

npmのライブラリ管理や、Webpackを使ったパッケージングなどを行ってくれます。

以下はScala.jsアプリケーションをパッケージングする例です。
./build ディレクトリに吐き出されるので、これをWebサーバなどに配置すればアプリケーションを公開できます。

sbt:mmsf-hub> fullOptJS::webpack
[info] Full optimizing /home/blacky/projects/scala/mmsf-hub/target/scala-2.13/scalajs-bundler/main/mmsf-hub-opt.js
[info] Closure: 0 error(s), 0 warning(s)
[info] Writing scalajs.webpack.config.js
[info] Bundling the application with its NPM dependencies
[info] ℹ 「wdm」: Compiling...
[info] ℹ 「wdm」: Hash: 0b2fc06d929fe96b0158
[info] Version: webpack 4.43.0
[info] Time: 100ms
[info] Built at: 2020-07-24 0:36:07
[info]      Asset      Size  Chunks             Chunk Names
[info] index.html  1.49 KiB          [emitted]
[info]  + 1 hidden asset
[info] Entrypoint mmsf-hub-fastopt = mmsf-hub-fastopt.js
[info] [./scalajs-entry.js] 435 bytes {mmsf-hub-fastopt} [built]
[info]     + 231 hidden modules
[info] ℹ 「wdm」: Compiled successfully.
[warn] asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
[warn] This can impact web performance.
[warn] Assets:
[warn]   mmsf-hub-opt.js (439 KiB)
[warn] entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
[warn] Entrypoints:
[warn]   mmsf-hub-opt (439 KiB)
[warn]       mmsf-hub-opt.js
[warn] webpack performance recommendations:
[warn] You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
[warn] For more info visit https://webpack.js.org/guides/code-splitting/
[info] Version: 4.43.0
[info] Hash: 90553944aab77f6c1bf7
[info] Time: 6835ms
[info] Path: /home/blacky/projects/scala/mmsf-hub/build
[info] Built at 2020-07-24T00:36:15.132342
[info]               Asset      Size                       Chunks
[info]    data/servers.csv     51562   [emitted]               []
[info]         favicon.ico    143480   [emitted]               []
[info]           index.css       725   [emitted]               []
[info]          index.html       502   [emitted]               []
[info]       manifest.json       315   [emitted]               []
[info]     mmsf-hub-opt.js    449853   [emitted]   [mmsf-hub-opt]
[info] mmsf-hub-opt.js.map   1299361   [emitted]   [mmsf-hub-opt]
[success] Total time: 15 s, completed 2020/07/24 0:36:15

ScalablyTyped

scalablytyped.org

TypeScriptのコードをScala.jsで使えるよう変換をしてくれるとても意欲的なsbtプラグインです。
Scala.jsでJavaScriptのライブラリを型安全に扱うにはFacadeというScalaJavaScriptの世界を繋ぐものが必要になるのですが、ScalabyTypeはそのFacedeの作成を自動で行います。

以下は使用例です。(この csv-parser の例だとあまり型安全感は出ていませんが…)

build.sbt

enablePlugins(ScalaJSBundlerPlugin, ScalablyTypedConverterPlugin)

npmDependencies in Compile ++= Seq(
  // ...
  "csv-parse"   -> "4.11.1"
)

Main.scala

import typings.csvParse.mod.Options
import typings.csvParse.{libSyncMod => parseCsv}

// ...

object Main {
  // ...
  private def loadApp(xhr: XMLHttpRequest, container: Element): Unit = {
    val rawServers =
      parseCsv(
        xhr.responseText,
        js.Dynamic.literal(columns = true).asInstanceOf[Options]
      ).asInstanceOf[js.Array[js.Dictionary[String]]]

    val servers = rawServers.map { dict =>
      Server(
        id = dict("id").toInt,
        serverType = ServerType.values(dict("type").toInt),
        level = dict("level").toInt,
        name = dict("name"),
        cardTable = CardTable(parseCards(dict("cards")))
      )
    }.toSeq

    ReactDOM.render(SSSViewer(servers), container)
  }
}

ちなみに、このScalablyTypedを使ってSlackのBoltアプリケーションをScala.jsで作れたりしました。
ScalaJavaScriptの世界を縮めてくれる、なかなか面白いツールだと思うので今後の動向に注目したいところです。

actions-gh-pages

github.com

GitHub Actions を使ってアプリケーションをGitHub Pagesに公開するのに使いました。 使い方は簡単で、アプリケーションのルートになるディレクトリを指定するだけです。gh-pagesブランチへのコミットはなどを自動で行ってくれます。

今回は、scala-js-bundler のパッケージング結果である ./build を指定しました。

name: GitHub pages

on:
  push:
    branches:
      - master

jobs:
  deploy_to_gh-pages:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-java@v1
        with:
          java-version: '11'

      - uses: actions/setup-node@v2-beta
        with:
          node-version: '12'

      - name: Cache Coursier and node_modules
        uses: actions/cache@v2
        with:
          path: |
            ~/.cache/coursier
            .target/scala-2.13/scalajs-bundler/main/node_modules
          key: ${{ runner.os }}-cache-${{ github.run_id }}
          restore-keys: |
            ${{ runner.os }}-cache-
      - name: Build App
        run: ./sbt fullOptJS::webpack

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./build

最後に

最近Scala.jsを触れた身ではありますが、簡単なWebアプリケーションを作って公開するところまでできました。
エコシステム周りもだいぶ充実してきている印象で、Scalaに書き慣れた方はWebアプリケーションを実装するひとつの選択肢として考えても良いのではないでしょうか。
Scala.jsで実装する利点のひとつとして、サーバとクライアントでScalaコードを共有できる点があると思いますが、これはまだ試したことがないのでやってみたいところです。

気になるところとしてはやはり、ScalaJavaScriptを繋ぐ点がどうしても黒魔術感が出てしまうところでしょうか。
ScalaでもJavaScriptでもない、「Scala.jsの知識」というものはどうしても必要になってきます。(まぁ当たり前といえばそうかも)

Scala.jsの世界に触れてみたい、という方はぜひ shadaj/create-react-scala-app.g8 から触れてみることをおすすめします。

$ sbt new shadaj/create-react-scala-app.g8

今回、アプリケーションに使うデータを作成するにあたり、攻略サテライト様のデータを使わせていただきました。この場を借りてお礼申し上げます。

wily.xrea.jp

DigdagのExtensionを実装する

モチベーション

個人のリポジトリdigdag-plugin-datadogを開発していますが、これを使うユースケースは主に _error のタスクでワークフローのエラーをDatadogに通知することであり、これは複数のワークフローが存在する場合、各ワークフローに書いて回る必要があります。
エラー処理を書いて回るのはとても面倒なので、Digdagサーバで共通のエラー通知を実現したいです。
これを実現するためにExtension機能が使えないかと思ったのがきっかけです。

Extensionとは

Digdagを設計した @frsyuki さんのツイートを引用させていただきます🙇

このExtensionを使ってエラー通知の振る舞いを変更したいです。

結論

今回の結論を話してしまうと、掲題の「ワークフロー共通のエラー通知の実現」という面だけであれば、Digdagのnotification機能を使ってやるのが一番早いと思います。
少なくとも自分がやろうとした手段ではエラー時の取れる情報も変わりません。後述しますが、Extensionの導入は少し複雑な面もあり、notification.shell を使うほうが比較的シンプルです。
ただ、ExtensionにはDigdag全体の振る舞いに対して影響を及ぼすことが可能なので、カスタマイズの可能性はとても高く、このユースケース以外でも色々作れそうな気がします。

成果物

サンプルのリポジトリはこちら

以下、実装方法に関する詳細です。

ビルド設定

今回、ExtensionはScalaで実装します。 build.sbtdigdag-spiProvided で追加しておきます。

lazy val root = (project in file("."))
  .settings(
    name := "digdag-extension-example",
    libraryDependencies ++= Seq(
        "io.digdag"     % "digdag-spi" % "0.9.42" % Provided,
        "org.scalatest" %% "scalatest" % "3.0.8"  % Test
      )
  )

Extensionの実装

まずはExtension本体の実装です。
io.digdag.spi.Extension を実装したクラス、GuiceのModule、実際にDIしたいクラスを追加します。
今回はワークフローがエラーで終了した際に通知を送る際に実行される NotificationSender を追加し、Names.named で名前をつけてやります。(サンプルはただログを出すだけ)
そうすることで、notification機能のデフォルト実装である DefaultNotifiernotification.type プロパティを参照し、その名前のアノテーションのついた NotifierSender の実装を使ってくれます。

package dev.nomadblacky.digdag_extension_example
import java.util
    
import com.google.common.collect.ImmutableList
import com.google.inject.name.Names
import com.google.inject.{Binder, Module}
import io.digdag.spi.{Extension, Notification, NotificationSender}
import org.slf4j.LoggerFactory
    
class ExampleExtension extends Extension {
  override def getModules: util.List[Module] = ImmutableList.of(ExampleModule)
}
    
object ExampleModule extends Module {
  override def configure(binder: Binder): Unit =
    binder
      .bind(classOf[NotificationSender])
      .annotatedWith(Names.named("example"))
      .to(classOf[ExampleNotificationSender])
}
    
class ExampleNotificationSender extends NotificationSender {
  private val logger = LoggerFactory.getLogger(classOf[ExampleNotificationSender])

  override def sendNotification(notification: Notification): Unit = {
    logger.info("========== ExampleNotificationSender ==========")
    logger.info(notification.toString)
    logger.info("===============================================")
  }
}

ServiceLoader の設定

DigdagのExtensionおよびPluginの機構はJava標準ライブラリに含まれるServiceLoaderという仕組みで実装が追加されるようになっています。
Digdagに対して「このプラグインを読み込んでね」と教えるために以下のようにリソースファイルを追加します。

$ cat src/main/resources/META-INF/services/io.digdag.spi.Extension
dev.nomadblacky.digdag_extension_example.ExampleExtension

Extensionのパブリッシュ

基本的なExtensionの実装は以上です。これを実際にDigdagに読み込めるようにするべく、Jarに固めておきます。

$ sbt publishLocal
$ tree -L 1 target/scala-2.13/
target/scala-2.13/
├── api
├── classes
├── digdag-extension-example_2.13-0.1.0-SNAPSHOT-javadoc.jar
├── digdag-extension-example_2.13-0.1.0-SNAPSHOT-sources.jar
├── digdag-extension-example_2.13-0.1.0-SNAPSHOT.jar
├── digdag-extension-example_2.13-0.1.0-SNAPSHOT.pom
├── ivy-0.1.0-SNAPSHOT.xml
├── resolution-cache
└── update

4 directories, 5 files

今回は、digdag-spi 以外に依存がなかったのでこれで良いですが、これ以外の依存が含まれる場合は assembly などでライブラリを含めた fatjar などを作ってやる必要がありそうです。

ワークフローを実行

さて、Extensionを使ってワークフローを実行しましょう。
今回は fail> するだけの簡単なワークフローを用意します。

timezone: UTC

+oops:
  fail>: Oops!!!

Extensionを読み込む方法ですが、現状は java コマンドの -cp オプションで直接クラスパスに含める必要があります。
以下のように digdag と今回作成したExtensionのJarをクラスパスに含めてDigdagのCLIを実行します。

$ java -cp $(which digdag):target/scala-2.13/digdag-extension-example_2.13-0.1.0-SNAPSHOT.jar \
  io.digdag.cli.Main run --no-save example.dig --config digdag.properties

以下のようにログが流れます。

2020-07-20 20:26:24 +0900: Digdag v0.9.41
2020-07-20 20:26:26 +0900 [WARN] (main): Using a new session time 2020-07-20T00:00:00+00:00.
2020-07-20 20:26:26 +0900 [INFO] (main): Starting a new session project id=1 workflow name=example session_time=2020-07-20T00:00:00+00:00
2020-07-20 20:26:27 +0900 [INFO] (0017@[0:default]+example+oops): fail>: Oops!!!
2020-07-20 20:26:27 +0900 [ERROR] (0017@[0:default]+example+oops): Task +example+oops failed.
Oops!!!
2020-07-20 20:26:28 +0900 [INFO] (0017@[0:default]+example^failure-alert): type: notify
2020-07-20 20:26:28 +0900 [INFO] (0017@[0:default]+example^failure-alert): ========== ExampleNotificationSender ==========
2020-07-20 20:26:28 +0900 [INFO] (0017@[0:default]+example^failure-alert): Notification{timestamp=2020-07-20T11:26:28.154Z, message=Workflow session attempt failed, siteId=0, projectId=1, projectName=default, workflowName=example, revision=2020-07-20T11:26:26.466Z, attemptId=1, sessionId=1, taskName=+example^failure-alert, timeZone=UTC, sessionUuid=b3ceed18-aeed-4b75-8e1c-f9124b13d34b, sessionTime=2020-07-20T00:00Z}
2020-07-20 20:26:28 +0900 [INFO] (0017@[0:default]+example^failure-alert): ===============================================
error:
  * +example+oops:
    Oops!!!

しっかりとログが流れていますね。
Notification の内容は notification.type=shell などと同じ内容で、残念ながら現状は原因となったエラーメッセージとスタックトレースは取得できません。
どうやらここばかりはDigdagの実装自体を変更する必要がありそうです。
ただ、どのワークフローのどのセッションでエラーが発生したかなど、最低限必要な情報は揃っているので一旦はこれで通知を実装してやっても十分かなと思います。

最後に

今回はExtensionの実装にScalaを用いましたが、JVMのクラスパスに直接含める必要があるぶん、これ以外のExtensionやPluginにバイナリ互換製のないScalaバージョンが使われていたときに何か悪さを起こしてしまうかもしれません。
(このあたり、ClassLoader周りに詳しくないので間違っていることを言ってるかもしれません…)

なにより、Extensionを使うことでDigdagの色々な振る舞いをいじれることがわかったのは大きいです。
Pluginなどでは痒くて届かなかった部分に手を出すことができるようになるかもしれません。

参考にしたもの

AWS Amplifyを使ってScala.jsアプリをデプロイしようとしたけどうまくいかなかったログ → できました

追記(2020/04/28 23:55)

ビルドに使うDockerのイメージを alpine → ubuntu にしたらうまくいきました

FROM adoptopenjdk/openjdk8:latest

RUN apt-get update && apt-get install --yes curl git openssh-client npm

https://master.d24dpd4k731ni4.amplifyapp.com/

f:id:Nomad_Blacky:20200428235652p:plain

追記前

Scala.jsでReactのチュートリアルを完遂したのでどこかにデプロイしたい
なんとなくAWS上で動かしたいのでAmplifyが使えないか調べてみる

リポジトリ

github.com

ログ

  • Amplifyやるぞ、AWS界のFirebaseっぽいやつ?
  • GitHubリポジトリをAmplifyのアプリとして直接追加できるっぽい
  • amplify.yml というファイルにビルド設定を追加する。 GitLab CI っぽい。
version: 0.1
frontend:
  phases:
    # IMPORTANT - Please verify your build commands
    build:
      commands:
        - ./sbt build
  artifacts:
    # IMPORTANT - Please verify your build output directory
    baseDirectory: build
    files:
      - '**/*'
  cache:
    paths: []
  • ビルドイメージは任意のパブリックイメージを使えるっぽい
  • sbt のイメージを使ったけどなぜか java コマンドがないとか言われる

hseeberger/scala-sbt:11.0.7_1.3.10_2.13.2

2020-04-28T12:11:28.854Z [WARNING]: /usr/bin/sbt: line 336: java: command not found
2020-04-28T12:11:28.861Z [INFO]: copying runtime jar...
2020-04-28T12:11:28.862Z [WARNING]: mkdir: cannot create directory ‘’: No such file or directory
2020-04-28T12:11:28.863Z [WARNING]: /usr/bin/sbt: line 343: java: command not found
2020-04-28T12:11:28.863Z [WARNING]: /usr/bin/sbt: line 127: exec: java: not found
2020-04-28T12:11:28.863Z [ERROR]: !!! Build failed
2020-04-28T12:11:28.864Z [ERROR]: !!! Non-Zero Exit Code detected
FROM adoptopenjdk/openjdk8:alpine-slim

RUN apk add curl git openssh npm
  • 今度は何故かGitHubとの認証に失敗したとか言われる。???

f:id:Nomad_Blacky:20200428231128p:plain

There was an issue connecting to your repo provider, click "Re-authenticate app" in General Settings, and then try your build again.

  • GitHubのIssueがみつかる。再認証してくれとのこと。
  • 再認証を試みるも状況変わらず
  • \(^o^)/オワタ

まとめ

AmplifyのDXは最高! …なはずだったのにどうしてこうなった

Coursier を使って最速でScalaの開発環境を整える

ざっくりまとめ

Coursierの setup コマンドを使うとJDKScala開発に必要なツールをまとめてインストールできるよ。

最近追加された install コマンド

get-coursier.io

Coursier といえば sbt v1.3.0 で取り込まれた高速でライブラリ依存を取得するためのOSSとして有名ですが、このCoursierにはCLIが提供されており、ライブラリ依存のグラフを出力したりアプリケーションの起動スクリプトを用意するコマンドが提供されています。

最近の Coursier v2.0.0-RC のアップデートで install コマンドが追加されました。

これはいわばパッケージマネージャのような機能で、JVM向けのCLIアプリケーションをインストールできるコマンドです。
以下の例はAmmoniteをインストールする例です。
以下のコマンドを実行するだけで、Ammoniteの最新版がインストールされます。

$ cs install ammonite
https://repo1.maven.org/maven2/org/scala-lang/scala-library/maven-metadata.xml
  No new update since 2020-03-16 10:26:09
https://repo1.maven.org/maven2/com/lihaoyi/ammonite_2.13.1/maven-metadata.xml
  No new update since 2020-01-14 04:26:58
Warning: /home/blacky/.local/share/coursier/bin is not in your PATH
To fix that, add the following line to your shell configuration file

export PATH="$PATH:/home/blacky/.local/share/coursier/bin"

$  ~/.local/share/coursier/bin/amm
Loading...
Compiling (synthetic)/ammonite/predef/interpBridge.sc
Compiling (synthetic)/ammonite/predef/replBridge.sc
Compiling (synthetic)/ammonite/predef/sourceBridge.sc  
Compiling (synthetic)/ammonite/predef/frontEndBridge.sc
Compiling (synthetic)/ammonite/predef/DefaultPredef.sc
Welcome to the Ammonite Repl 2.0.4 (Scala 2.13.1 Java 1.8.0_232)
@ println("Foo!")
Foo!

インストール可能なアプリケーションは以下のリポジトリで管理されており、このcontrib以下にプルリクエストを送れば自身で作成したアプリケーションを追加することもできそうです。

github.com

setup コマンドを使って一発でScala環境を整える

さて、本題になりますが、このCoursierにはさらに setup というコマンドが追加されています。
これを使うとJDKを含め、Scalaの開発環境に必要なアプリケーションをまとめてインストールしてくれます。

これを実験するために ubuntu:18.04 のコンテナからScalaの環境を整えてみます。
もちろん、JDKもインストールされていない状態です。

$ docker run -it --rm ubuntu:18.04
# java -version
bash: java: command not found

Coursierのインストールにcurlコマンドを使うのでインストールしておきます。

# apt update && apt install -y curl

Coursierコマンドをインストールします。ドキュメントに従ってコマンドを実行します。

# curl -Lo cs https://git.io/coursier-cli-linux && chmod +x cs && ./cs --help
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   144  100   144    0     0    126      0  0:00:01  0:00:01 --:--:--  140k
100 51.3M  100 51.3M    0     0  5889k      0  0:00:08  0:00:08 --:--:-- 8030k
Coursier 2.0.0-RC6-11
Usage: cs \[options\] [command] [command-options]

Available commands: bootstrap, complete, fetch, install, java, java-home, launch, publish, resolve, setup, uninstall, update

Type  cs command --help  for help on an individual command

インストールが成功し、 ./cs --help コマンドを実行することができました。
Coursier CLIはGraalVMのNative Imageを使ってLinuxほか各OS専用のバイナリを作っているため、JVMのインストールなしに起動できます。

ここまでできたら setup コマンドを実行してみましょう。

# ./cs setup -y
Checking if a JVM is installed
No JVM found, should we try to install one? [Y/n] Y
Extracting
  /root/.cache/coursier/v1/https/github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u242-b08/OpenJDK8U-jdk_x64_linux_hotspot_8u242b08.tar.gz
in
  /root/.cache/coursier/jvm/adopt@1.8.0-242
Done
Should we update ~/.profile? [Y/n] Y
Some shell configuration files were updated. It is recommended to close this terminal once the setup command is done, and open a new one for the changes to be taken into account.

Checking if ~/.local/share/coursier/bin is in PATH
Should we add ~/.local/share/coursier/bin to your PATH via ~/.profile? [Y/n] Y

Checking if the standard Scala applications are installed
  Installed ammonite
  Installed cs
  Installed coursier
  Installed scala
  Installed scalac
  Installed sbt-launcher
  Installed scalafmt

JDKおよびScala関連のアプリケーションがインストールされました。
ログに出ている通り、 ~/.profile が更新され、 JAVA_HOMEPATH の設定が追加されていますので、 source コマンドを叩いて設定を反映させます。

# cat ~/.profile
# ~/.profile: executed by Bourne-compatible login shells.

if [ "$BASH" ]; then
  if [ -f ~/.bashrc ]; then
    . ~/.bashrc
  fi
fi

mesg n || true

# >>> JVM installed by coursier >>>
export JAVA_HOME="/root/.cache/coursier/jvm/adopt@1.8.0-242"
export PATH="$PATH:/root/.cache/coursier/jvm/adopt@1.8.0-242/bin"
# <<< JVM installed by coursier <<<

# >>> coursier install directory >>>
export PATH="$PATH:/root/.local/share/coursier/bin"
# <<< coursier install directory <<<

# source ~/.profile

あとは好きなようにコマンドを叩きましょう!

# java -version
openjdk version "1.8.0_242"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_242-b08)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.242-b08, mixed mode)

# scala
Welcome to Scala 2.13.1 (OpenJDK 64-Bit Server VM, Java 1.8.0_242).
Type in expressions for evaluation. Or try :help.

scala> println("Foo!!")
Foo!!

setup コマンドでインストールされるアプリケーション

AdoptOpenJDK

デフォルトではAdoptOpenJDK 8、HotSpotVMがインストールされます。
インストールされるバージョンはコマンドのオプションで指定可能で--jvm 11 とするとJDK11がインストールされるようです。

# java -version
openjdk version "1.8.0_242"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_242-b08)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.242-b08, mixed mode)

ammonite

Scalaの拡張REPL、およびScalaスクリプトの実行環境です。
Magic Importsを使った動的なライブラリの追加ができるので動作確認をしたり、Scalaスクリプトを書いて簡単な自動化を行ったりと、活用の幅は結構広く筆者オススメのツールです。

宣伝になりますが、Ammoniteについて同人誌を書いております。
興味がある方はぜひお手にとっていただければ幸いです🙇

booth.pm

# amm
Loading...
Welcome to the Ammonite Repl 2.0.4 (Scala 2.13.1 Java 1.8.0_242)
@ import $ivy.`com.lihaoyi::scalatags:0.8.2`
https://repo1.maven.org/maven2/com/lihaoyi/scalatags_2.13/0.8.2/scalatags_2.13-0.8.2.pom
  100.0% [##########] 1.6 KiB (1.3 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/scalatags_2.13/0.8.2/scalatags_2.13-0.8.2-sources.jar
  100.0% [##########] 69.8 KiB (70.5 KiB / s)
https://repo1.maven.org/maven2/com/lihaoyi/scalatags_2.13/0.8.2/scalatags_2.13-0.8.2.jar
  100.0% [##########] 475.8 KiB (256.2 KiB / s)
import $ivy.$

@ import scalatags.Text.all._
import scalatags.Text.all._

html(head(tag("title")("scalatags")), body(div(h1("Foo!!"), p("Yeah")))).toString
res6: String = "<html><head><title>scalatags</title></head><body><div><h1>Foo!!</h1><p>Yeah</p></div></body></html>"

cs / coursier

Coursier自身もインストールされます。
cs コマンドはネイティブで実行され、 coursier コマンドはJVMで実行される違いがあるようです。

# time cs --help
Coursier 2.0.0-RC6-11
Usage: cs \[options\] [command] [command-options]

Available commands: bootstrap, complete, fetch, install, java, java-home, launch, publish, resolve, setup, uninstall, update

Type  cs command --help  for help on an individual command

real 0m0.021s
user 0m0.005s
sys 0m0.016s

# time coursier --help
Coursier 2.0.0-RC6-11
Usage: coursier \[options\] [command] [command-options]

Available commands: bootstrap, complete, fetch, install, java, java-home, launch, publish, resolve, setup, uninstall, update

Type  coursier command --help  for help on an individual command

real 0m0.973s
user 0m1.583s
sys 0m0.105s

scala

Scala本体のREPLです。コンパイルしたScalaコードの実行、またスクリプトの実行も可能です。

# scala -version
Scala code runner version 2.13.1 -- Copyright 2002-2019, LAMP/EPFL and Lightbend, Inc.

# echo "println(123)" > script.scala

# scala script.scala
123

scalac

Scalaコンパイラです。 sbtプロジェクト上で開発する場合に直接触る機会は少ないですが、コンパイラオプションは普段の開発に役立つことも多いので使い方は覚えておいて損はないと思います。

# scalac -version
Scala compiler version 2.13.1 -- Copyright 2002-2019, LAMP/EPFL and Lightbend, Inc.

# echo 'object Main { def main(args: Array[String]): Unit = println("Hello, Scala!") }' > Main.scala

# scalac Main.scala

# scala -cp . Main
Hello, Scala!

sbt-launcher

Coursierが提供するsbtを起動するためのスクリプトです。

sbt本体の起動スクリプトと異なる点に注意してください。
詳しくは調べていませんが、 -mem オプションなど、使えないオプションもありそうです。

また、sbtプロジェクト外で起動したときに異常終了するIssueも報告されていますので、現状sbtプロジェクト外でsbtを起動したい際は以下のようにオプションでsbtのバージョンを指定して実行する必要があります。

# sbt -Dsbt.version=1.3.8 new scala/scala-seed.g8
Getting scala-compiler 2.12.10
 [##########]   Downloaded 3 POM files in 1 s
 [##########]   Downloaded 3 JAR files in 25 s
Getting sbt 1.3.8
 [##########]   Downloaded 87 POM files in 6 s
 [##########]   Downloaded 78 JAR files in 97 s
[info] Set current project to root (in build file:/)
[info] Set current project to root (in build file:/)
[info] downloading https://repo1.maven.org/maven2/org/scala-sbt/sbt-giter8-resolver/sbt-giter8-resolver_2.12/0.11.0/sbt-giter8-resolver_2.12-0.11.0.jar ...
[info] downloading https://repo1.maven.org/maven2/org/scala-lang/scala-library/2.12.10/scala-library-2.12.10.jar ...
[info] downloading https://repo1.maven.org/maven2/org/scala-sbt/template-resolver/0.1/template-resolver-0.1.jar ...
[info] downloading https://repo1.maven.org/maven2/org/foundweekends/giter8/giter8_2.12/0.11.0/giter8_2.12-0.11.0.jar ...
[info] downloading https://repo1.maven.org/maven2/org/foundweekends/giter8/giter8-lib_2.12/0.11.0/giter8-lib_2.12-0.11.0.jar ...
[info] downloading https://repo1.maven.org/maven2/com/github/scopt/scopt_2.12/3.7.0/scopt_2.12-3.7.0.jar ...
(略)

A minimal Scala project.

name [Scala Seed Project]:

Template applied in /./scala-seed-project

# ls -la scala-seed-project/
total 24
drwxr-xr-x 4 root root 4096 Mar 22 07:00 .
drwxr-xr-x 1 root root 4096 Mar 22 07:00 ..
-rw-r--r-- 1 root root    8 Mar 22 07:00 .gitignore
-rw-r--r-- 1 root root  447 Mar 22 07:00 build.sbt
drwxr-xr-x 2 root root 4096 Mar 22 07:00 project
drwxr-xr-x 4 root root 4096 Mar 22 07:00 src

scalafmt

Scalaコードのフォーマッタです。
ルールを定義しておくことで、プロジェクト内のコードインデント幅などを統一させることが可能です。

# cat src/main/scala/example/Hello.scala
package example

object Hello extends
Greeting
with App {
          println(greeting)
}

trait Greeting {
  lazy val greeting: String =
{
"hello"
  }
}

# scalafmt

# cat src/main/scala/example/Hello.scala
package example

object Hello extends Greeting with App {
  println(greeting)
}

trait Greeting {
  lazy val greeting: String = {
    "hello"
  }
}

あとがき

Coursierの setup コマンドでScalaの開発環境を整えるのはとても簡単でした。
Scalaを始めるにはまず sbt のインストールから」だったところが、「まずは Coursier をインストール」に変わっていく可能性も大いにあるんじゃないかなぁと思いました。

install コマンドも同様に便利で、自身でCLIアプリケーションを公開することも可能なので、また別の機会で試してみたいですね。

『軽量開発.scala AmmoniteではじめるScalaスクリプト入門』を書きました

f:id:Nomad_Blacky:20200315223521p:plain

中止となってしまった技術書典8で頒布予定だったこの本ですが、

で販売する運びとなりました。

ScalaのREPLおよびスクリプトの実行環境である、Ammoniteの使い方を解説する本となっております。

Ammoniteを使ったScalaスクリプトの開発を始められるようになるためのHowが詰まった一冊になったと思います。
Scalaの活用するための方法のひとつとして皆様のお役に立てれば幸いです。

余談

Ammoniteの開発者であるLi Haoyiさんのスポンサーになりました。
この本での売上の一部はこちらに還元されます。

f:id:Nomad_Blacky:20200315224310p:plain

これだけ便利なOSSを使わせてもらっているうえに、これをテーマにした本まで売り出すとなるとこういった形で支援していかないとなという気持ちに駆られました。

今後は本体にも貢献できるようになりたいですね。

2019年振り返り

去年

nomadblacky.hatenablog.com

できごと

  • インフラ/アプリケーション監視周りに色々興味を持ち始めて勉強を始めた
    • 『入門監視』を読んだり
    • Datadogを使い始めてみたり
  • OSS活動に対する抵抗がなくなってきた
    • 去年初めてOSSにコントリビュートしてからだいぶIssueやPRを投げるのに慣れてきた
    • すぐ直せそうなところならスッとPR出せるようになった
    • 拙作のscaladogもそこそこにStarもらえたり、公式のニュースレターで紹介してもらったりと微力ながらも役に立てているのかな(本当か?)と嬉しい気持ちになれた
  • Scala Matsuri 2019
    • 技術読本書いたり
    • Scala本体にプルリク投げたり
    • アンカンファレンスで相変わらずAmmoniteについて発表したり
    • と、大変充実したイベントだった
  • 転職して1年と9ヶ月ぐらい
    • 周りから刺激を受けるいい環境に居るなぁと常々
    • SREっぽい仕事をしたかったので、ちょっと無理言ってチーム異動させてもらった
      • 異動からまだ2ヶ月ほどだが、興味のある仕事に取り組むことができるようになってモチベーションは高い
  • ブログをそれなりに書いた
  • それとなくインフラを触れるようになる
    • AWS CDKに出会ってコードからAWSのインフラを理解できるようになったのはとても大きい経験だった
  • TGM TI の SHIRASE モードをクリアした

よかったこと

  • OSS活動がそこそこにできた
  • インフラ構築の知見を得られたので、自らの力でバックエンドアプリケーションをゼロから構築して動かせるようになった
  • 監視周りの知見を得られたので、Observabilityを意識したインフラ・アプリケーション開発ができるようになった

わるかったこと

  • 当初の目標であった「自己表現」に関しては進歩があまりなかったと思う
  • 英語も同様…
  • 僻みっぽい性格がTwitterで時々現れてしまう
    • 昔からだけど。まぁこれは半分あきらめている…

総括

まぁまぁがんばった

相変わらずなんとなく興味をもってやってみて、運良くなんとかなっているという気持ちが強いので、自分の選択に時々不安に襲われてしまうのは悪いところなのかなと。

2020年は仕事問わず、人生における何かしらの指針を持つというところを目標にしたいですね(難しくない? 1年後も悩んでそう…)

今後ともどうぞよろしくお願い致します🙇


過去ツイートを漁って気になったやつ

scaladogでScalaからDatadogにメトリクス・イベントを送信する

この記事は Scala Advent Calendar 2019 および Datadog Advent Calendar 2019 の21日目(遅刻)です。

scaladogとは?

github.com

scaladog は拙作のScalaDatadog API のクライアントライブラリです。
HTTPでAPIを直接叩いているため、実行環境にDatadog Agentをインストールする必要はありません。

仕事でもプライベートでもScalaとDatadogにお世話になっているので、いっそのことクライアントライブラリを作ってみようと思ったのが開発を始めたきっかけです。 1

今回はDatadogの主要なユースケースであるメトリクスとイベントの送信を、scaladogで行う方法をお伝えしていきます。

準備

Datadog API を叩くには API Key および、 Application Key が必要になります。
これらのキーを取得する方法はこちらを参照してください。

キーを取得したら、これらを環境変数に設定します。
scaladogは DATADOG_API_KEY , DATADOG_APP_KEY 環境変数に設定されたキーを読み込みます。

export DATADOG_API_KEY=<your-API-key>
export DATADOG_APP_KEY=<your-Application-key>

また、今回のお見せするScalaコードの実行にはAmmoniteを使います。
以下コマンドで v1.8.22 をインストールします。

sudo sh -c '(echo "#!/usr/bin/env sh" && curl -L https://github.com/lihaoyi/Ammonite/releases/download/1.8.2/2.13-1.8.2) > /usr/local/bin/amm && chmod +x /usr/local/bin/amm' && amm

実行すると、Ammoniteの実行バイナリがダウンロードされたのち、REPLが起動します。
import $ivy を使ってscaladogのライブラリをクラスパスに追加します。

import $ivy.`dev.nomadblacky::scaladog:0.4.2`

以下のようにライブラリ依存が追加されればOKです。

Loading...
Welcome to the Ammonite Repl 1.8.2
(Scala 2.13.1 Java 1.8.0_231)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ import $ivy.`dev.nomadblacky::scaladog:0.4.2`
https://repo1.maven.org/maven2/dev/nomadblacky/scaladog_2.13/0.4.2/scaladog_2.13-0.4.2.pom
  100.0% [##########] 2.2 KiB (1.3 KiB / s)
https://repo1.maven.org/maven2/dev/nomadblacky/scaladog_2.13/0.4.2/scaladog_2.13-0.4.2-sources.jar
  100.0% [##########] 13.0 KiB (16.9 KiB / s)
https://repo1.maven.org/maven2/dev/nomadblacky/scaladog_2.13/0.4.2/scaladog_2.13-0.4.2.jar
  100.0% [##########] 264.9 KiB (273.4 KiB / s)
import $ivy.$

続けてAPIクライアントとなるクラスのインスタンスを作成します。

@ val datadog = scaladog.Client()
datadog: scaladog.Client = scaladog.ClientImpl@5853495b

これで準備完了です!

カスタムメトリクスを送信する

まずはカスタムメトリクスを送信してみましょう。
以下のコードをREPLに入力します。

import scaladog.api.metrics._
import java.time.Instant
import scala.util.Random

while(true) {
  val series = Series(
    metric = "scaladog.example",
    points = Seq(Point(Instant.now(), Random.nextInt(1000))),
    tags = Seq("project:scaladog")
  )
  datadog.metrics.postMetrics(Seq(series))
  println(series)
  Thread.sleep(5000)
}

5秒感覚でてきとうなメトリクスを送信するコードになっています。3

@ { // ←ブロックで複数行のコードを入力できます
  import scaladog.api.metrics._
  import java.time.Instant
  import scala.util.Random

  while(true) {
    val series = Series(
      metric = "scaladog.example",
      points = Seq(Point(Instant.now(), Random.nextInt(1000))),
      tags = Seq("project:scaladog")
    )
    datadog.metrics.postMetrics(Seq(series))
    println(series)
    Thread.sleep(5000)
  }
  }
Series(scaladog.example,List(Point(2019-12-21T14:59:56.537Z,689)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:02.277Z,484)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:08.033Z,550)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:13.749Z,470)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:19.425Z,682)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:25.129Z,219)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:30.858Z,440)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:36.576Z,269)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:42.258Z,213)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:47.951Z,180)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:53.677Z,615)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:00:59.412Z,377)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:05.119Z,303)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:11.103Z,40)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:16.821Z,147)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:22.506Z,899)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:28.289Z,347)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:33.990Z,241)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:39.657Z,716)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:45.680Z,706)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:51.393Z,191)),,List(project:scaladog),Gauge)
Series(scaladog.example,List(Point(2019-12-21T15:01:57.118Z,696)),,List(project:scaladog),Gauge)
...
// 飽きたら Ctrl+C で抜ける

Datadogの画面からメトリクスを送信できているか確認しましょう。

f:id:Nomad_Blacky:20191222004550p:plain

メトリクスを確認できました!

イベントを送信する

続けてイベントを送信してみましょう。
以下のコードをREPLに入力します。

scaladog.Client().events.postEvent(
  title = "TEST EVENT",
  text = "Hello, scaladog!"
)
@ scaladog.Client().events.postEvent(
    title = "TEST EVENT",
    text = "Hello, scaladog!"
  )
res7: scaladog.api.events.PostEventResponse = PostEventResponse(
  "ok",
  5245888207545475945L,
  "https://app.datadoghq.com/event/event?id=5245888207545475945"
)

イベントのIDとURLが評価結果として返ってきていることがわかります。
Datadogの画面からイベントを確認してみましょう。

f:id:Nomad_Blacky:20191222004703p:plain

イベントが無事送信できていますね!

アラートの種類やタグの付与にも対応していますので、必要があれば以下のように追加できます。

scaladog.Client().events.postEvent(
  title = "TEST EVENT",
  text = "This is a test event.",
  dateHappened = Instant.now(),
  priority = Priority.Low,
  tags = Seq("project:scaladog"),
  alertType = AlertType.Warning
)

活用例

これら以外のAPIも一部提供していますので、詳細はREADMEのコード例をご覧ください。

実際の利用例をご紹介しますと、社のブログ Scala Advent Calendar 6日目での、 esa.io の利用状況をDatadogに送信するScalaスクリプトにも scaladog が使われています。

また、お仕事でお世話になっているワークフローエンジンであるDigdagからDatadogを叩くための digdag-plugin-datadog を鋭意開発中です。

github.com

そのほか、sbtに組み込んでコンパイル時間やカバレッジを記録するのも面白いんじゃないかなぁと考えています。

あとがき

拙作のscaladogの紹介をしていきました。

まだまだサポートしているAPIは少ないのでこれから頑張って増やしていきたいです。
…とはいえ「このAPI使いたいな…じゃあ実装しよう!」というモチベーションから開発が始まるので、もし「このAPIをサポートしてほしい!」等あればIssueを投げてくれるときっと実装する気持ちになれると思います! 4

ぜひ、scaladogを使ってDatadogをカジュアルに使ってくれると嬉しいです!


  1. あと自分でOSSのメンテナンスを経験してみたかったのも理由のひとつです。

  2. 最新版のAmmonite v1.9.2 で動作しないバグをこの記事を書いているときに見つけました…。おそらくはAmmoniteとscaladog両方で依存しているupickleの変更によるものでしょうか。

  3. 今見返すと、メトリクスひとつ送るだけのコードが少し冗長に感じますね…単体のメトリクスを送るためのAPIがあっても良さそうな気がしました…

  4. プルリクもお待ちしてます…!