FolkBears で利用している aware-micro サーバーをビルドして Docker コンテナで動かせるようになるまで

接触確認アプリ FolkBears では、接触データを Aware というサーバーに送信しています。Aware framework https://awareframework.com/ は、モバイルから加速度データとか WiFi 取得のデータとかをうまく収集できる研究用アプリ+サーバーで、結構便利…なんですが、FolkBears の BLE アドバタイズと利用した接触データの収集とはちょっと相性が悪くてですね(これは数年後に気付いたわけですが)、FolkBears 内部では切り離しを進行中です。

が、データ収集部分は Aware を使わないとしても、データ収集をするためのサーバーのほうは aware サーバーでもよいか、ということでそのままになっています。このサーバーコードが aware-micro https://github.com/denzilferreira/aware-micro な訳ですが。さて、これ以前は PHP で書かれていたような気がするのですが、現在は Kotlin で書かれています。Kotlin はまあいいんですが、内部的に Eclipse Vert.x https://vertx.io/ を使っていてですね、ちょっと再構築にコツが入りそうなのです。
というか、1年程前に自前でサーバーを構築しようして失敗したものだから、面倒くさくなって Azure Functions で仮実装して実験的に動かしていました。ごく一部分だけ使うわけだし、FolkBears からのデータアップロードだけ部分なので、C# で書いても大して手間ではなかったのですが。が、内部で SQL Server を使って、暫くほっといていたら月2万円の請求になってしまい(無料枠が1.2万円ほどだたので、実際の支払は8000円ほどです)、これは放置するとアカンなと思った次第です。

本格的にデータ収集するとなるとギガバイト単位の DB になってしまうので考え直さないといけないのですが、実験データ程度ならば aws の EC2 + t3.micro でもいいはずです。まあ、安く済ませるならば、さくらの VPS を借りて PHP で実装したほうがいいのですが、それば別の機会にでも。

以下、試行錯誤も含めて作業ログ的に書き進めていきます。

docker-compose.yml の作成

aware-micro https://github.com/denzilferreira/aware-micro に Dockerfile の例があるので Ubuntu + Docker で構築することを考えます。

FROM openjdk:11

# Set the location of the verticles
ENV VERTICLE_HOME /usr/verticles

# Set the name of the verticle to deploy
ENV VERTICLE_AWARE_JAR micro-1.0.0-SNAPSHOT-fat.jar
ENV VERTICLE_AWARE_CONFIG aware-config.json

EXPOSE 8080

# Set vertx option
ENV VERTX_OPTIONS ""

# Copy your verticle and configuration to the container
COPY $VERTICLE_AWARE_JAR $VERTICLE_HOME/
COPY $VERTICLE_AWARE_CONFIG $VERTICLE_HOME/

WORKDIR $VERTICLE_HOME
ENTRYPOINT ["sh", "-c"]
CMD ["exec java -jar $VERTICLE_AWARE_JAR"]

git から aware-micro のコードを持ってきてビルドするのではなくて、あらかじめ micro-1.0.0-SNAPSHOT-fat.jar を作成しておく方式です。設定ファイルとして aware-config.json も必要そうです。以前は、これをどうやってつくるのか?が分からなかったのですが、aware-micro https://github.com/denzilferreira/aware-micro の readme に書いてありました。

こっちのほうは、実際にサーバーを動かす方

cd aware-micro
./gradlew clean build run

こっちのほうが、*.jar ファイルを作る方

cd aware-micro
./gradlew clean build shadowJar

build/lib/micro-1.0.0.SNAPSHOT-fat.jar というファイルができあがります。build.gradle の kotlinOptions.jvmTarget を最新の 17 にしていますが通ります。dependencies にあるライブラリのバージョンが古いかもしれませんが、ひとまずこのままにしておきます。

Copilot を使って docker-compose.yml を作って貰います。

version: "3.9"

services:
  mysql:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: aware
      MYSQL_USER: aware
      MYSQL_PASSWORD: awarepass
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-proot"]
      interval: 10s
      timeout: 5s
      retries: 5

  aware-micro:
    build:
      context: dev-local
      dockerfile: Dockerfile
    image: aware-micro:dev
    depends_on:
      mysql:
        condition: service_healthy
    environment:
      VERTX_OPTIONS: ""
    ports:
      - "8080:8080"
    volumes:
      - ./dev-local/aware-config.json:/usr/verticles/aware-config.json:ro
    restart: unless-stopped

volumes:
  mysql-data:

volumes 部分を変更して、aware-config.json と micro-1.0.0-SNAPSHOT-fat.jar をコピーできるようにします。このファイルは aware-micro プロジェクトの方からコピーします。

    volumes:
      - ./aware-config.json:/usr/verticles/aware-config.json:ro
      - ./micro-1.0.0-SNAPSHOT-fat.jar:/usr/verticles/micro-1.0.0-SNAPSHOT-fat.jar:ro

aware-config.json の server の設定を変えておきます。データベース名は docker-compose.yml のほうに合わせました。database_host は docker-composer にある “mysql” を使う筈。

{
  "server" : {
    "database_engine" : "mysql",
    "database_host" : "mysql",
    "database_name" : "aware",
    "database_user" : "aware",
    "database_pwd" : "awarepass",
    "database_port" : 3306,
    "server_host" : "http://localhost",
    "server_port" : 8080,
    "websocket_port" : 8081,
    "path_fullchain_pem" : "",
    "path_key_pem" : ""
  },
  "study" : {
    "study_key" : "4lph4num3ric",
    "study_number" : 1,
    "study_name" : "AWARE Micro demo study",
    "study_active" : true,
    "study_start" : 1770433225185,
    "study_description" : "This is a demo study to test AWARE Micro",
    "researcher_first" : "First Name",
    "researcher_last" : "Last Name",
    "researcher_contact" : "your@email.com"
  },
  "sensors" : [ {
    "sensor" : "accelerometer",
    "title" : "Accelerometer",

micro-1.0.0-SNAPSHOT-fat.jar の作成

./gradlew clean build run のほうはサーバー立ち上げなので 8080 番でポート待ちです。

*.jar ファイルを作るのは ./gradlew clean build shadowJar のほう

aware-config.json ファイルは ./gradlew clean build run あるいは ./gradlew run で自動生成されます。

Docker コンテナを作成&実行

ひとまず Windows 上で docker compose up -d を試します。

Javaのランタイムバージョンでこけるので、Dockerfile の先頭を変えておきます。

FROM eclipse-temurin:17-jre

micro-1.0.0-SNAPSHOT-fat.jar をビルドしたときのバージョンとあわせておきます。

無事 Docker コンテナが立ち上がれば ok です。私の環境の場合、ホストPCに MySQL が入っているので、”3307:3306″ にしておきます。

MySQL Workbench で接続できるところまで確認します。

ブラウザから http://localhost:8080/ のように接続すると、こんな形に出れば aware サーバーが動いています。

テーブルを作成する

データベースにテーブルを作成しておきます。もともと aware-micro にはテーブルを作成する機能があるのですが、これがコメントアウトされています。まあ、URL を通してばかすかテーブルを作られてしまっても困るので、ガードしてあります。

その中で createTable メソッドがあるので、次のようにテーブル作ります。

CREATE TABLE IF NOT EXISTS `$table`
(
  `_id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 
  `timestamp` DOUBLE NOT NULL, 
  `device_id` VARCHAR(128) NOT NULL, 
  `data` JSON NOT NULL, 
  INDEX `timestamp_device` (`timestamp`, `device_id`)
)

`$table` のところは実際に作成するテーブル名をいれます。クライアントから aware-micro に送られたデータは、data カラムに入ります。data カラムは JSON 形式なのでとにかく JSON にしておくと何でも突っ込めます。逆に言えば JSON 形式しか突っ込めません。

クライアント(FolkBears)からデータを送信する

aware-micro サーバーが出来上がったところで、データを送信してみます。aware-micro に送信するときにフォーマットがちょっとややこしくて、

  • POST メソッドを使う
  • content-type を x-www-form-urlencoded 形式で送る
  • device_id パラメータが必須
  • JSON 形式は必ず配列にして data パラメータで送る(複数データが前提になっている!)

という形式になっています。
curl コマンドで送信するときは以下の形式です。

curl -X POST "http://localhost:8080/index.php/1/4lph4num3ric/sample/insert" `
  -H "Content-Type: application/x-www-form-urlencoded" `
  -d "device_id=xxx" `
  -d 'data=[{"timestamp": 1, "name": "masdua"}]' 

URL がまたややこしくて、aware-config.json に記述した、study_number と study_key を使います。index.php は昔 PHP で作成したときの名残りっぽいです。たぶん、もともとは加速度センサーなどの情報を一気に入れるようになったものを、micro にして汎用化したんじゃないかなぁと。

呼び出す URL の形式はこんな感じ

/index.php/:studyNumber/:studyKey/:table/:operation

  • index.php: 固定
  • :studyNumber: aware-config.json の study_number
  • :studyKey: aware-config.json の study_key
  • :table: 操作するテーブル名
  • :operation: 操作方法(insert, update など)

で、無事データが insert されると、こんな風になります。

ひとまず、aware-micro の Docker コンテナに送信できたので、一旦終了。

FolkBears Android 版からの送信

この方式だと微妙に面倒なので、retrofit2 ライブラリを使っています。

package jp.mamori_i.app.webapi

import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST

interface AwareService {
    @FormUrlEncoded
    @POST("trace_data/insert")
    fun uploadData(
        @Field("device_id") device_id: String,
        @Field("data") data: String
    ): Call<Void>

    @FormUrlEncoded
    @POST("trace_data/query")
    fun queryData(
        @Field("device_id") device_id: String,
        @Field("start") start: Long,
        @Field("end") end: Long
    ): Call<String>


    @FormUrlEncoded
    @POST("temp_user_id/insert")
    fun uploadTempUserId(
        @Field("device_id") device_id: String,
        @Field("data") data: String
    ): Call<Void>

    @FormUrlEncoded
    @POST("temp_user_id/query")
    fun queryTempUserId(
        @Field("device_id") device_id: String,
        @Field("start") start: Long,
        @Field("end") end: Long
    ): Call<String>

    @FormUrlEncoded
    @POST("deep_contact_user/insert")
    fun uploadDeepContactUser(
        @Field("device_id") device_id: String,
        @Field("data") data: String
    ): Call<Void>

    @FormUrlEncoded
    @POST("deep_contact_user/query")
    fun queryDeepContactUser(
        @Field("device_id") device_id: String,
        @Field("start") start: Long,
        @Field("end") end: Long
    ): Call<String>

}

object RetrofitClient {
    private const val BASE_URL = "https://aware....jp/index.php/1/folkbears/"

    val awareService: AwareService by lazy {
        retrofit2.Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(retrofit2.converter.scalars.ScalarsConverterFactory.create())
            .build()
            .create(AwareService::class.java)
    }
}

実際の呼び出しは TraceDataUpload.upload で。

package jp.mamori_i.app.webapi

import com.google.gson.Gson
import jp.mamori_i.app.data.TraceDataEntity
import jp.mamori_i.app.webapi.RetrofitClient


class TraceDataUpload
{
    /**
     * Aware に POST 送信する body を作成する
     */
    fun fromTraceDataList(items: List<TraceDataEntity> ) : String {
        if ( items.isEmpty() ) {
            // データがない場合は空配列を返す
            return "[]"
        }

        /** 
        * 送信用の TraceData のデータクラス
        */
        data class TraceData(
            val temp_id: String,
            val timezone: Int,
            val timestamp: Long,
            val tx_power: Int,
            val jsonVersion: Int,
            val os: String,
            val rssi: Int,
            val label: String
        ){}

        var lst = mutableListOf<TraceData>()
        items.map {
            lst.add( TraceData(
                temp_id = it.tempId,
                timezone = 9,
                timestamp = it.timestamp,
                tx_power = 0,
                jsonVersion = 0,
                os = "android",
                rssi =  it.rssi,
                label = ""
            ))
        }
        return Gson().toJson(lst)
    }

    fun upload( deviceId: String, data: String) : Boolean {
        val response = RetrofitClient.awareService
            .uploadData(device_id = deviceId, data = data )
            .execute()
        return response.isSuccessful
    }

カテゴリー: 開発, FolkBears パーマリンク