Tweet / Photo allowed.
This work by
Windymelt
is licensed under
CC BY-SA 4.0
class: center, middle # Scala Native on Cloudflare Containers
2025-07-18
[Cloudflare Workers Tech Talks in Kyoto #1 `id:Windymelt`](https://workers-tech.connpass.com/event/359726/) --- ## 話すこと - Cloudflare Containers とは - Scala Native とは - Scala Native コンテナを Cloudflare Containers にデプロイするぞ - 感想 --- ## 発表内容 - このスライドは https://www.3qe.us/2025/20250718-cloudflare.html で公開 - https://github.com/windymelt/cf-containers-exercise でコードは公開 --- ## Windymelt - [Windymelt](https://www.3qe.us/)です - 全宇宙でこの ID を使っています - GitHub, X/Twitter, mixi2, etc... - Signal: `windymelt.23` - PGP: `F2FC 63C2 42C0 4D9D` - 2016- [株式会社はてな](https://hatena.co.jp/) - [AIを利用した発話分析ソリューション toitta](https://toitta.com/)の開発 - プライベート - [最近MCPサーバをScalaでフルスクラッチした](https://blog.3qe.us/entry/2025/04/29/004251) - [ディレクトリローカルなエイリアスツールを作成した](https://blog.3qe.us/entry/2025/07/14/000748) --- ## Windymelt
- [PR]プログラミング言語コミュニティ『Scalaわいわいランド』主宰 - ↑ググって〜 --- class: center, middle ## 遠い昔・・・ --- ## 昔は Cloudflare Workers で直接 Java が走った - Worker runtime v1 では Java がサポートされていた - が、 v2 になって Java はサポート外に - → **Scala.js** --- ## Scala.js - Scala を ECMAScript にトランスパイルするツール - ほぼフルセットの Scala が動作し、非常に成熟している - Workers のコードを Scala.js で書くと動く --- ## Scala.js on Cloudflare 様子 - https://blog.3qe.us/entry/2023/12/21/223253 --- class: center, middle ## で、今 --- ## Cloudflare Containers - https://developers.cloudflare.com/containers/ - *Cloudflareのエッジ上でDockerコンテナが起動* し、HTTPをサーブできる仕組み - Earthリージョンにデプロイされ、どこからでも利用可能 --- ## やってみよう - コンテナならどの言語でもイケるはず --- ## CF Containers 必要なもの - CF アカウント - `wrangler.jsonc` - いつもの - 最初にリクエストを受け取る Worker のための js ファイル - `Dockerfile` --- ## テンプレを使う - `pnpm create cloudflare@latest --template=cloudflare/templates/containers-template` - `wrangler.jsonc` などを自動的に生成してくれる - `pnpm wrangler deploy` - これだけでいい --- ## どのような仕組み?(windymelt の理解) - クライアントからのリクエスト - 通常通り Worker が受ける - Container - Worker にアタッチされる - Container 情報 - 起動時に Durable Object 経由で渡る - Worker の中でコンテナメタデータを利用して接続する --- ## `wrangler.jsonc` ```jsonc { // ... "containers": [{ "class_name": "MyContainer", "image": "./Dockerfile", "max_instances": 10, "name": "hello-containers" }], "durable_objects": { "bindings": [{ "class_name": "MyContainer", "name": "MY_CONTAINER" }]}, "migrations": [{"new_sqlite_classes": ["MyContainer"], "tag": "v1"}] } ``` --- ## Worker からコンテナを呼び付ける - まずメタデータ定義 ```typescript import { Container } from "@cloudflare/containers"; export class MyContainer extends Container { defaultPort = 8080; sleepAfter = "2m"; // envvarを渡せる envVars = { MESSAGE: "I was passed in via the container class!", }; // Optional lifecycle hooks override onStart() {} override onStop() {} override onError(error: unknown) {} } ``` --- ## Worker からコンテナを呼び付ける (2) - バインディングとして Worker に渡ってくる ```typescript import { Hono } from "hono"; const app = new Hono<{ Bindings: { MY_CONTAINER: DurableObjectNamespace< MyContainer > }; }>(); ``` --- ## Worker からコンテナを呼び付ける (3) - `getContainer` するとコンテナのハンドラが得られる - 普通のサーバのように`fetch()` できる - `fetch()`結果をそのまま返せばコンテナに処理を丸投げ可 ```typescript import { getContainer } from "@cloudflare/containers"; app.get("/hello/:name", async (c) => { * const container = getContainer(c.env.MY_CONTAINER); return await container.fetch(c.req.raw); }); ``` --- ## `getContainer` 系ツール - `getContainer()` - キーをもとにコンテナを選択する - 複数回のリクエストで別々のコンテナインスタンスに泣き別れにならないための仕組みっぽい - 特にキーを指定しなければデフォルトで`cf-singleton-container`を指定したことになるようだ - `getRandom()` - Load balancing - N 個のコンテナから適当なものを選ぶ --- ## Dockerfile - サンプル実装では Go の `Dockerfile` が用意されていた - `Dockerfile` さえあればよいことがわかったので Scala でやる --- ## そもそも Scala 何 - Scalable Programming Language - JVMを土台に生まれた言語 - **非同期処理**や**関数型プログラミング**に強みあり - RustにGCひっついたと思ってもらえればよさそう --- ## Scala Native - Scalaをネイティブコンパイルするコンパイラ - Scala風の言語が動くのではなく、フルセットのScala - LLVMをバックに動く - JVMと比較して**高速起動・省メモリ・省サイズ**なのが売り - 50MiBくらいのメモリで動作する - 10MiBくらいのバイナリになる - Native C API を呼べる --- ## 今回のビルドツールは Scala CLI - コマンド1つでJVM・JS・Nativeにパッケージング可能 - Denoライクな使い勝手 - スクリプト自体に依存パッケージを直接書くと解決してくれる - スクリプト単体で処理系のバージョン固定が可能 - ポータブル --- ## サーバコード(1) - `import`などは略している - HTTP サーバライブラリ: `http4s` ```scala object Main extends EpollApp { def run(args: List[String]): IO[ExitCode] = EmberServerBuilder .default[IO] .withHost(ipv4"0.0.0.0") .withPort(port"8080") * .withHttpApp(helloWorldService.orNotFound) .withShutdownTimeout(Duration(1, "second")) .build .use(_ => IO.println("Server started") >> IO.never) .onCancel(IO.println("Cancelling...")) .as(ExitCode.Success) } ``` --- ## サーバコード (2) ```scala def getDeploymentId: IO[Option[String]] = Env[IO].get("CLOUDFLARE_DEPLOYMENT_ID") def getLocation: IO[Option[String]] = Env[IO].get("CLOUDFLARE_LOCATION") val helloWorldService = HttpRoutes.of[IO] { case GET -> Root / "hello" / name => for { depId <- getDeploymentId loc <- getLocation resp <- Ok( s"Hello, $name from Scala Native Container (${depId})! I'm running at ${loc}" ) } yield resp } ``` --- ## コンテナにする - よくあるタンデム構成のマルチステージビルド ```Dockerfile FROM virtuslab/scala-cli:1.8.4 AS builder RUN apt install libssl-dev && apt update RUN mkdir /app WORKDIR /app COPY server.scala server.scala RUN --mount=type=cache,target=~/.ivy2 --mount=type=cache,target=~/.cache/coursier \ scala-cli --power package -o /app/server server.scala FROM debian:bookworm-slim RUN apt update && apt install -y openssl RUN mkdir /app COPY --from=builder /app/server /app/server EXPOSE 8080 # Avoiding PID 1 CMD ["sh", "-c", "/app/server"] ``` --- ## 完成 - [https://cf-containers-experiment.windymelt.workers.dev/hello/windymelt](https://cf-containers-experiment.windymelt.workers.dev/hello/windymelt) --- ## 用途? - ファイルシステムが使える(エフェメラルだが) - QRコード生成 - 分散FSとかのエッジを動かしたりしたら面白いのでは - sidecar - ログとか - API Proxy (envoyみたいな) - バッチ処理のオフロード - 他にはSSG?レンダラとか --- ## おもしろいと思った点 - コンテナレジストリの設定が透過的になっている - `wrangler deploy` するだけで勝手にどこかのリポジトリに適当な名前で push される - コンテナも HTTP でやりとりすればよいし、丸投げもできる - Static build したらもっとコンテナが簡素になるかも? --- ## まとめ - Cloudflare Containers を利用して Scala Native コンテナを動作させられた - デプロイしやすいのでちょっとしたオモチャの置き場としても良いのでは ---