仕事では Scala と TypeScript を使っていて C# を業務時間では一切触らなくなってしまった私です。
社内向けの便利 Slack コマンドなどを C# で実装して Azure Functions で Host して遊んでいるのですが、C# だとちょっとコントリビュートしづらいなぁみたいな話になっています。 ある程度社内共通言語である Scala でリライトすれば色んな人が手伝ってくれるかな、と思ったので Scala でいい感じにリライトしようというのを画策しています。
そこで今回は Scala で Azure Functions を使うまでに必要なことを書いていこうと思います。 既にやられている方がいたので、これを参考にして勧めてみようと思います。
以降は Scala はある程度書いたことがあって、プロジェクトを自分で最低限作れる人を前提に書いていきます。
開発環境
今回は macOSX 上で開発を進めました。
また手元で sbt
コマンドを叩きたかったので(こいつは Docker 上で環境を整えれば良かったんじゃないかみたいな感じはありますが)sbt と JDK 8 も入っている環境を用意します。
エスパーで書けるのであればビルドは通るか知らないがコード書いてしまって、 CI サービスに投げてみたいなやり方でもいいと思います(適当)。
今回のプロジェクトはyamachu/ScalaFunctionsのリポジトリで公開しているので、参考にしていただければと思います。
始めてみよう
Azure Functions Core Toolsを普段から使っている人は func
コマンドを叩いてプロジェクトのテンプレートを生成するのですが、現時点 Scala のプロジェクトテンプレートは存在しないので、自分で頑張る必要があります。
私は sbt
力が低いのでプロジェクトテンプレートを作るのが出来ないのでよしなにまず生成してみてください。
Scala(公式対応でい言えば Java)で Azure Functions を使うには
- Library for Azure Functionsを Function アプリに追加する
- 入力バインディングや出力バインディングを考えて Function アプリを作る
function.json
とhost.json
をドキュメントとにらめっこして書く- Functions アプリを Fat Jar に固める
- 実行する
の 5 ステップが必要です。
1 ステップずつ進めていきましょう。
Library for Azure Functions を追加
build.sbt
にライブラリを追加していきましょう。
バージョンは GitHub Release のページを見るか、maven リポジトリを見るといいと思います。
執筆時の最新バージョンは1.3.0です。
build.sbt
libraryDependencies += "com.microsoft.azure.functions" % "azure-functions-java-library" % "1.3.0"
こんな感じで追加していきます。
入出力バインディングを考えて Function アプリを作る
この辺りは Azure Functions を触ったことがないと難しいかもしれません。 ですが Function アプリを外部インタフェースに関係無い引数にしたり、プロパティを考えておけばあとは入出力バインディングを書くだけなのでちゃちゃっと行きましょう。
基本的には Java のコードを Scala に移植するのと同じ感覚なので、Java 向けの READMEやドキュメント を見て移植していきましょう。
例として Java のテンプレートコードを Scala に書き換えていきます。 Visual Studio Code で生成した Java の Function アプリのコードはこんな感じにいなっています。
package com.function; import java.util.*; import com.microsoft.azure.functions.annotation.*; import com.microsoft.azure.functions.*; /** * Azure Functions with HTTP Trigger. */ public class Function { /** * This function listens at endpoint "/api/HttpTrigger-Java". Two ways to invoke it using "curl" command in bash: * 1. curl -d "HTTP Body" {your host}/api/HttpTrigger-Java&code={your function key} * 2. curl "{your host}/api/HttpTrigger-Java?name=HTTP%20Query&code={your function key}" * Function Key is not needed when running locally, it is used to invoke function deployed to Azure. * More details: https://aka.ms/functions_authorization_keys */ @FunctionName("HttpTrigger-Java") public HttpResponseMessage run( @HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Optional<String>> request, final ExecutionContext context) { context.getLogger().info("Java HTTP trigger processed a request."); // Parse query parameter String query = request.getQueryParameters().get("name"); String name = request.getBody().orElse(query); if (name == null) { return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please pass a name on the query string or in the request body").build(); } else { return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build(); } } }
これを Scala で書き直すとこんな感じになります。
import java.util._ import com.microsoft.azure.functions._ import com.microsoft.azure.functions.annotation._ /** * Azure Functions with HTTP Trigger. */ class Function { /** * This function listens at endpoint "/api/HttpTrigger-Java". Two ways to invoke it using "curl" command in bash: * 1. curl -d "HTTP Body" {your host}/api/HttpTrigger-Java&code={your function key} * 2. curl "{your host}/api/HttpTrigger-Java?name=HTTP%20Query&code={your function key}" * Function Key is not needed when running locally, it is used to invoke function deployed to Azure. * More details: https://aka.ms/functions_authorization_keys */ @FunctionName("HttpTrigger-Java") def run( @HttpTrigger( name = "req", methods = Array(HttpMethod.GET, HttpMethod.POST), authLevel = AuthorizationLevel.FUNCTION) request: HttpRequestMessage[ Optional[String]], context: ExecutionContext): HttpResponseMessage = { context.getLogger.info("Java HTTP trigger processed a request.") // Parse query parameter val query: String = request.getQueryParameters.get("name") val name: String = request.getBody.orElse(query) if (name == null) { request .createResponseBuilder(HttpStatus.BAD_REQUEST) .body("Please pass a name on the query string or in the request body") .build() } else { request .createResponseBuilder(HttpStatus.OK) .body("Hello, " + name) .build() } } }
割とそのままだったりします。 Annotation 関係のものを Java からそのまま移植すれば動くので、ある程度どうにかなるかなぁという印象ではあります。 ある程度 Scala を書いている人であったらコンバート出来るかなぁみたいな印象があります。
Java は POST リクエストを POJO に落とし込めたりするので、case class にその Request の型を作って HttpRequestMessage
の TypeParameter に入れ込んだり、また GET のところで Query パラメータを使う場合は
case class Requests(name: String) object Requests { def fromMap(obj: collection.mutable.Map[String, String]): Either[Throwable, Requests] = obj.get("name") match { case Some(name) => Right(Requests(name = name)) case None => Left(new Exception("name parameter is required")) } }
みたいのを Query Parameter を閉じ込める case class を用意して、
import collection.JavaConverters._ val queryMap: collection.mutable.Map[String, String] = request.getQueryParameters.asScala Requests.fromMap(queryMap) ...
みたいのを外部からの入力が入ってくる interface で変換をかませるっていうので出来たりします。
Azure Functions 特有の config ファイルを頑張って書く
host.json
の書き方は公式ドキュメントかGitHub の host の wikiを見て書きます。
とりあえず試して見るぐらいであれば
{ "version": "2.0" }
を host.json
に書くことでひとまず動きます。
それ以上のことをやりたくなったらドキュメントを見ましょう(実際自分がこれ以上のことを書いたことが正直ないというところではあります…)。
function.json
はGitHub の Wiki に詳しく書かれています。
多く使われるであろう HTTP の GET や POST のハンドリングは
{ "bindings": [ { "type": "httpTrigger", "direction": "in", "route": "orders", "authLevel": "anonymous" }, { "type": "http", "direction": "out" } ] }
のような形で記述することが出来ます。
ステップ 2 で実装したアプリケーション用のfunction.json
を書くとするとこんな漢字でしょうか
{ "scriptFile": "ここにjarへのPATH", "entryPoint": "クラス名.メソッド名みたいな", "bindings": [ { "type": "httpTrigger", "name": "req", "direction": "in", "authLevel": "function", "methods": ["GET", "POST"] }, { "type": "http", "name": "$return", "direction": "out" } ] }
こんな感じで Azure Functions に必要なファイルを作っていきます。
Functions アプリを Fat Jar に固める
実行用の jar は Fat Jar なので、それを packaging 出来る環境を作っていきましょう。
私はsbt-assemblyを使っているので、sbt-assembly ベースの話をしていきます。
sbt のビルドタスクに加えていきたいので、plugins.sbt
に
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
を加えていって、sbt assembly
で jar を固めていきます。
これでデプロイ用の jar ファイルが用意できました。
実行する
動作させるには
- Azure Functions Core Tools を使う
- Azure/azure-functions-dockerで提供されている Docker Image を使う
と 2 パターンあります。
前提としてディレクトリ構成は以下のようなものを想定します。
. ├── HttpTrigger-Java │ └── function.json ├── MyAwesomeFunction.jar ├── host.json └── local.settings.json
まずは Azure Functions Core Tools の方法を試してみます。
host.json
があるディレクトリで func start
を叩いてみましょう。
すると色々とコンソールに流れて、
Now listening on: http://0.0.0.0:7071 Application started. Press Ctrl+C to shut down. Http Functions: HttpTrigger-Java: [GET, POST] http://localhost:7071/api/HttpTrigger-Java
こんな感じに最終的に出力されます。 あとは curl などでリクエストを送ると自分で作ったアプリケーションが実行できます。
もう 1 パターンである Docker は少し準備が必要です。 というのもDockerhubにはまだ Java 用の Docker Image が公開されていません。 そのため、リポジトリを clone して自分で Docker Image をビルドする必要があります。 対象の Dockerfile はローカルからなにかのファイルを引っ張ってくる依存がないので、ひとまず手元で build してみます。
Docker 内に存在する Azure Functions の Host は /home/site/wwwroot
以下に存在する host.json
や function.json
を元にアプリケーションを実行します。
そのため前述したディレクトリを Docker の /home/site/wwwroot
以下にマウントすることで Docker で動作させることが可能となります。
実行するためのコマンドとしては docker run -p 8080:80 --rm -v $(pwd)/app:/home/site/wwwroot ${ビルドしたImage}
こんな感じです。
app 以下が前述したディレクトリ構成になっている想定です。
これで最低限手元で Azure Functions を試してみることが出来ました。 実際作っていうえでリモートデバッグがしたくなったりなど色々ありますが、それはまた今度単発で出す予定です。
↓こんな感じ
よしよし、Docker上に立てたAzure FunctionsのJavaランタイムでリモートデバッグを有効にして、それをIntelliJからアタッチしてデバッグできたぞ pic.twitter.com/SrXS6W8zfb
— ちゅうこ@飽き性 (@y_chu5) January 29, 2019
あとはこれを本番環境にデプロイするわけですが、方法が様々あるのでそれは体力が回復次第…
一番楽なのが func azure functionapp publish
みたいなコマンドを使うことでしょうか。
ローカルで作ってそのままデプロイ、という手段を取るのであればこれが一番ベターだと思います。
しかし CI サービスからそのままデプロイとなると話が変わってくるので、なかなか難しいところです…