窓を作っては壊していた人のブログ

この謎のブログタイトルの由来を知るものはもういないだろう

Scala で Azure Functions やってみる(ローカル動作編)

仕事では 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 を使うには

  1. Library for Azure Functionsを Function アプリに追加する
  2. 入力バインディングや出力バインディングを考えて Function アプリを作る
  3. function.jsonhost.json をドキュメントとにらめっこして書く
  4. Functions アプリを Fat Jar に固める
  5. 実行する

の 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.jsonGitHub の 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 ファイルが用意できました。

実行する

動作させるには

  1. Azure Functions Core Tools を使う
  2. 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.jsonfunction.json を元にアプリケーションを実行します。 そのため前述したディレクトリを Docker の /home/site/wwwroot 以下にマウントすることで Docker で動作させることが可能となります。

実行するためのコマンドとしては docker run -p 8080:80 --rm -v $(pwd)/app:/home/site/wwwroot ${ビルドしたImage} こんな感じです。 app 以下が前述したディレクトリ構成になっている想定です。

これで最低限手元で Azure Functions を試してみることが出来ました。 実際作っていうえでリモートデバッグがしたくなったりなど色々ありますが、それはまた今度単発で出す予定です。

↓こんな感じ

あとはこれを本番環境にデプロイするわけですが、方法が様々あるのでそれは体力が回復次第…

一番楽なのが func azure functionapp publish みたいなコマンドを使うことでしょうか。 ローカルで作ってそのままデプロイ、という手段を取るのであればこれが一番ベターだと思います。 しかし CI サービスからそのままデプロイとなると話が変わってくるので、なかなか難しいところです…

C95でAzure Machine Learning Serviceを使った声質変換に関する記事を出します

女の子の声になりたいよな!!!!!!!!!!!!!!!!!!!

ということではい

ということで、今年の12月にGAしたAzure Machine Learning Service を使って、強いマシンがなくても機械学習が出来るよ、ということを書きました。 また題材として声質変換を採用し、r9y9/gantts を使って声質変換を試してみようみたいなことを軽く書いています。

VTuber Tech #2 Advent Calendar 2018 - Qiita で投稿予定だった内容に近いのですが、C95で出すし、まずはC95に全力版を出して、ゆるく内容については公開していこうと思います。

C95で出す内容のほぼ下書きとなったコードはこちらで公開しています。

github.com

一度出した本の中身は変更できないので、今後のアップデートはこちらで行う予定です。 GAに追いつけて無い部分が少しあったりがあるので、冬休みとかの間に修正したいですね……

何はともあれ

C95 2日目 東6 ト16b まどやをよろしくお願いいたします。