Scala.jsでReactアプリケーションを作って公開したはなし
モチベーション
最近、流星のロックマン3というゲームに再燃しています。
Wi-Fiを使ってネット対戦を行うことができるのですが、任天堂公式のWi-Fiサービス終了により長らく対戦を行えない状況でした。
しかし、近年、有志の方がWi-Fiサーバをエミュレートするサーバを立てたことにより、再びネット対戦をすることが可能になりました。
さて、流星3には「シークレットサテライトサーバ」という、対戦中に使える特別なカードがあり、決められたカードテーブルからある程度狙ったカードを引くことができます。
しかし、カードテーブルの内容を全て記憶するのは困難であり、いちいち調べるのも面倒でした。
最近、Scala.jsに興味があることから、Webアプリケーションとしてこのシークレットサテライトサーバの早見表を作ってみようというのがきっかけです。
作ったもの
こちらで公開しています。
SSSのデータを読み込む部分の実装ができた。最低限使えるレベルにはなったのではなかろうか。 pic.twitter.com/ec7uX5ZkBx
— nomadblacky (@nomadblacky) July 23, 2020
アーキテクチャの話
ソースコードも公開しています。
Scala.js
いわゆるTypeScriptのようなAltJSの一種で、ScalaコードをJavaScriptにトランスパイルすることで、Scalaの強力な言語機能や静的型付けなど、その書き心地をそのままWebアプリケーションなどに持ち込むことができます。
2020年2月には待望のメジャーバージョンである v1.x 系がリリースされたりと、開発もそこそこ活発なのではないかと思います。
詳しい説明は色々な記事があると思うのでここでは語りません。
ちなみに、Scala.jsの概要を知るのにこのページが(面白くて)おすすめです。
Slinky
Scala.jsでReact.jsを扱うためのライブラリです。
似たライブラリに scalajs-react があります。
Slinkyを採用した理由としては、Webpackを用いた開発からパッケージングまでサポートされたgiter8テンプレートがありとっつきやすかったことです。
書き味としてはReact.js色が強いので予めReact.js公式のチュートリアルを触っておくと実装の感覚をつかみやすいと思います。
以下はソースコードの一部です。雰囲気を感じていただければと。
@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
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
TypeScriptのコードをScala.jsで使えるよう変換をしてくれるとても意欲的なsbtプラグインです。
Scala.jsでJavaScriptのライブラリを型安全に扱うにはFacadeというScalaとJavaScriptの世界を繋ぐものが必要になるのですが、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で作れたりしました。
ScalaとJavaScriptの世界を縮めてくれる、なかなか面白いツールだと思うので今後の動向に注目したいところです。
コードがお察しだしバグってるけどScala.jsでSlackのBoltアプリがなんか動いてしまった😂 pic.twitter.com/NvhGhCdM4o
— nomadblacky (@nomadblacky) July 1, 2020
actions-gh-pages
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コードを共有できる点があると思いますが、これはまだ試したことがないのでやってみたいところです。
気になるところとしてはやはり、ScalaとJavaScriptを繋ぐ点がどうしても黒魔術感が出てしまうところでしょうか。
ScalaでもJavaScriptでもない、「Scala.jsの知識」というものはどうしても必要になってきます。(まぁ当たり前といえばそうかも)
Scala.jsの世界に触れてみたい、という方はぜひ shadaj/create-react-scala-app.g8 から触れてみることをおすすめします。
$ sbt new shadaj/create-react-scala-app.g8
今回、アプリケーションに使うデータを作成するにあたり、攻略サテライト様のデータを使わせていただきました。この場を借りてお礼申し上げます。
DigdagのExtensionを実装する
モチベーション
個人のリポジトリでdigdag-plugin-datadogを開発していますが、これを使うユースケースは主に _error
のタスクでワークフローのエラーをDatadogに通知することであり、これは複数のワークフローが存在する場合、各ワークフローに書いて回る必要があります。
エラー処理を書いて回るのはとても面倒なので、Digdagサーバで共通のエラー通知を実現したいです。
これを実現するためにExtension機能が使えないかと思ったのがきっかけです。
Extensionとは
Digdagを設計した @frsyuki さんのツイートを引用させていただきます🙇
Digdag Extensionは、GuiceのModuleを仕込める物で、Guiceの起動前にロードされ、システム全体のGuiceに対して自由に影響できる:https://t.co/UKCAwjyifz
— Sadayuki Furuhashi (@frsyuki) May 19, 2017
このExtensionを使ってエラー通知の振る舞いを変更したいです。
結論
今回の結論を話してしまうと、掲題の「ワークフロー共通のエラー通知の実現」という面だけであれば、Digdagのnotification機能を使ってやるのが一番早いと思います。
少なくとも自分がやろうとした手段ではエラー時の取れる情報も変わりません。後述しますが、Extensionの導入は少し複雑な面もあり、notification.shell
を使うほうが比較的シンプルです。
ただ、ExtensionにはDigdag全体の振る舞いに対して影響を及ぼすことが可能なので、カスタマイズの可能性はとても高く、このユースケース以外でも色々作れそうな気がします。
成果物
サンプルのリポジトリはこちら
以下、実装方法に関する詳細です。
ビルド設定
今回、ExtensionはScalaで実装します。
build.sbt
に digdag-spi
を Provided
で追加しておきます。
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機能のデフォルト実装である DefaultNotifier
は notification.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などでは痒くて届かなかった部分に手を出すことができるようになるかもしれません。
参考にしたもの
- https://github.com/yoyama/digdag-wait-op
- https://twitter.com/frsyuki/status/865462007163637762
- https://github.com/treasure-data/digdag
- https://github.com/treasure-data/digdag/blob/6547542a274ac1a30c88c82e509ac0742d6d872e/digdag-core/src/main/java/io/digdag/core/ExtensionServiceLoaderModule.java#L32
- https://github.com/treasure-data/digdag/blob/d02fb5e6d757979a979fde548b7ccf3d1fc5716e/digdag-standards/src/main/java/io/digdag/standards/StandardsExtension.java
- https://github.com/treasure-data/digdag/blob/a7039bc76c3de08022411b8897517757bec42172/digdag-core/src/main/java/io/digdag/core/DigdagEmbed.java#L212
- https://github.com/treasure-data/digdag/blob/6547542a274ac1a30c88c82e509ac0742d6d872e/digdag-storage-s3/src/main/java/io/digdag/storage/s3/S3StorageExtension.java
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/
追記前
Scala.jsでReactのチュートリアルを完遂したのでどこかにデプロイしたい
なんとなくAWS上で動かしたいのでAmplifyが使えないか調べてみる
ログ
- 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
- ビルドに使うイメージには制限があるっぽい。自前でビルド用のイメージをつくる。
- curl, git, openssh, (npm) が動作するイメージでないとだめ
- 自前のイメージを使った
FROM adoptopenjdk/openjdk8:alpine-slim RUN apk add curl git openssh npm
- 今度は何故かGitHubとの認証に失敗したとか言われる。???
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
コマンドを使うとJDKとScala開発に必要なツールをまとめてインストールできるよ。
最近追加された install コマンド
Coursier といえば sbt v1.3.0 で取り込まれた高速でライブラリ依存を取得するためのOSSとして有名ですが、このCoursierにはCLIが提供されており、ライブラリ依存のグラフを出力したり、アプリケーションの起動スクリプトを用意するコマンドが提供されています。
最近の Coursier v2.0.0-RC のアップデートで install
コマンドが追加されました。
Announcement: The install command of coursier is not experimental anymore, and is ready for prime time.
— Alex Archambault (@alxarchambault) February 19, 2020
Install / update #Scala CLI tools just using their names, and install your own applications this way too!https://t.co/65XKIj0WjW
これはいわばパッケージマネージャのような機能で、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以下にプルリクエストを送れば自身で作成したアプリケーションを追加することもできそうです。
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_HOME
や PATH
の設定が追加されていますので、 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 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)
Scalaの拡張REPL、およびScalaスクリプトの実行環境です。
Magic Importsを使った動的なライブラリの追加ができるので動作確認をしたり、Scalaでスクリプトを書いて簡単な自動化を行ったりと、活用の幅は結構広く筆者オススメのツールです。
宣伝になりますが、Ammoniteについて同人誌を書いております。
興味がある方はぜひお手にとっていただければ幸いです🙇
# 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本体の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!
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
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スクリプト入門』を書きました
中止となってしまった技術書典8で頒布予定だったこの本ですが、
で販売する運びとなりました。
ScalaのREPLおよびスクリプトの実行環境である、Ammoniteの使い方を解説する本となっております。
Ammoniteを使ったScalaスクリプトの開発を始められるようになるためのHowが詰まった一冊になったと思います。
Scalaの活用するための方法のひとつとして皆様のお役に立てれば幸いです。
余談
Ammoniteの開発者であるLi Haoyiさんのスポンサーになりました。
この本での売上の一部はこちらに還元されます。
これだけ便利なOSSを使わせてもらっているうえに、これをテーマにした本まで売り出すとなるとこういった形で支援していかないとなという気持ちに駆られました。
今後は本体にも貢献できるようになりたいですね。
2019年振り返り
去年
今年のテーマは「自己表現」で
— blac_k_ey (@blac_k_ey) December 31, 2018
できごと
- インフラ/アプリケーション監視周りに色々興味を持ち始めて勉強を始めた
- 『入門監視』を読んだり
- Datadogを使い始めてみたり
- OSS活動に対する抵抗がなくなってきた
- 去年初めてOSSにコントリビュートしてからだいぶIssueやPRを投げるのに慣れてきた
- すぐ直せそうなところならスッとPR出せるようになった
- 拙作のscaladogもそこそこにStarもらえたり、公式のニュースレターで紹介してもらったりと微力ながらも役に立てているのかな(本当か?)と嬉しい気持ちになれた
Datadog Newsletter 12月でdigdag-plugin-datadogとscaladogを紹介していただきました!
— blac_k_ey (@blac_k_ey) December 23, 2019
ありがとうございます!🙏 pic.twitter.com/CDx75wqiJD
- Scala Matsuri 2019
- 技術読本書いたり
弊社ノベリティの技術読本、面白い話が詰まっているので是非読んでみてください!💪
— blac_k_ey (@blac_k_ey) June 28, 2019
自分はServerlessFramewok + Scalaのサーバレスアプリケーションについて書きました😉#ScalaMatsuri pic.twitter.com/JFRI1HKDaN
- Scala本体にプルリク投げたり
これでいいのかわからん…って感じだけどScalaにプルリク投げたぞ…!https://t.co/nLRf0X9cM4#ScalaMatsuri
— blac_k_ey (@blac_k_ey) June 27, 2019
- アンカンファレンスで相変わらずAmmoniteについて発表したり
- と、大変充実したイベントだった
- 技術読本書いたり
- 転職して1年と9ヶ月ぐらい
- 周りから刺激を受けるいい環境に居るなぁと常々
- SREっぽい仕事をしたかったので、ちょっと無理言ってチーム異動させてもらった
- 異動からまだ2ヶ月ほどだが、興味のある仕事に取り組むことができるようになってモチベーションは高い
- ブログをそれなりに書いた
- 半分ぐらいアドベントカレンダーなので習慣づいているとはいえない
- それとなくインフラを触れるようになる
- AWS CDKに出会ってコードからAWSのインフラを理解できるようになったのはとても大きい経験だった
AWS CDKと!Scalaで!!インフラコードが!!!書けるぞ!!!!(あげなおし) pic.twitter.com/Pj2NqxMQtG
— blac_k_ey (@blac_k_ey) July 13, 2019
- AWS CDKに出会ってコードからAWSのインフラを理解できるようになったのはとても大きい経験だった
- TGM TI の SHIRASE モードをクリアした
- 8年越し?の悲願達成
動揺して見切れてるけど…
— blac_k_ey (@blac_k_ey) October 5, 2019
はじめてのプレイから8年ぐらい?
SHIRASE初めてカンストした!!!
やばいめっちゃ嬉しい😂😂😂#tgm_series pic.twitter.com/WM3q4XoZ2Z
よかったこと
- OSS活動がそこそこにできた
- インフラ構築の知見を得られたので、自らの力でバックエンドアプリケーションをゼロから構築して動かせるようになった
- 監視周りの知見を得られたので、Observabilityを意識したインフラ・アプリケーション開発ができるようになった
わるかったこと
- 当初の目標であった「自己表現」に関しては進歩があまりなかったと思う
- 英語も同様…
- 僻みっぽい性格がTwitterで時々現れてしまう
- 昔からだけど。まぁこれは半分あきらめている…
総括
まぁまぁがんばった
相変わらずなんとなく興味をもってやってみて、運良くなんとかなっているという気持ちが強いので、自分の選択に時々不安に襲われてしまうのは悪いところなのかなと。
2020年は仕事問わず、人生における何かしらの指針を持つというところを目標にしたいですね(難しくない? 1年後も悩んでそう…)
今後ともどうぞよろしくお願い致します🙇
過去ツイートを漁って気になったやつ
最近は「エンジニアのためのエンジニアリング」をやりたいって思いが強くなってきたけど、これに通づる話だと思う。
— blac_k_ey (@blac_k_ey) January 17, 2019
自分の課題 ≒ エンジニアの課題
だから。
なので、自分の顧客はエンジニアであるべきかもしれない。
これは苦手意識からの逃げであり、妥協である。https://t.co/eA7faluKu7
自分は0を1にするのも、1を10にするのも得意じゃなくて、-1を1にするのが得意な気がしてきた
— blac_k_ey (@blac_k_ey) January 14, 2019
— blac_k_ey (@blac_k_ey) January 3, 2019
今日の目標達成!💪😂👍#tgm_series pic.twitter.com/pOVaIxMgwz
— blac_k_ey (@blac_k_ey) March 21, 2019
愚かにも程があった。良いツールを使えば良いモニタリングが出来ると思っていたけどそれ以前の問題だった。
— blac_k_ey (@blac_k_ey) May 31, 2019
良いコードを書いたら終わりじゃない。
SLIを定め、リリース後も常にテストして品質を良くしていこうという文化が一番必要なものだったんだ。
scaladogでScalaからDatadogにメトリクス・イベントを送信する
この記事は Scala Advent Calendar 2019 および Datadog Advent Calendar 2019 の21日目(遅刻)です。
scaladogとは?
scaladog は拙作のScala製 Datadog 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の画面からメトリクスを送信できているか確認しましょう。
メトリクスを確認できました!
イベントを送信する
続けてイベントを送信してみましょう。
以下のコードを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の画面からイベントを確認してみましょう。
イベントが無事送信できていますね!
アラートの種類やタグの付与にも対応していますので、必要があれば以下のように追加できます。
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 を鋭意開発中です。
そのほか、sbtに組み込んでコンパイル時間やカバレッジを記録するのも面白いんじゃないかなぁと考えています。
あとがき
拙作のscaladogの紹介をしていきました。
まだまだサポートしているAPIは少ないのでこれから頑張って増やしていきたいです。
…とはいえ「このAPI使いたいな…じゃあ実装しよう!」というモチベーションから開発が始まるので、もし「このAPIをサポートしてほしい!」等あればIssueを投げてくれるときっと実装する気持ちになれると思います! 4
ぜひ、scaladogを使ってDatadogをカジュアルに使ってくれると嬉しいです!