Embulk v0.11.0, v1.0に向けたMavenプラグインのCI環境構築とMavenプラグインの導入方法 (2021/5/28版)

現在、Embulkは次の安定版であるv0.11.0に向けた開発版としてv0.10がリリースされています。

メンテナであるdmikurubeさんのアナウンスに依ると、0.11.0以降はJRubyがデフォルトでembulkに組込まれなくなるため、プラグインは基本的にJavaで作ることが推奨される様になります。 また、JRubyがデフォルトで入らなくなるため、基本となるプラグインの配布プラットフォームはMavenリポジトリになる予定です。

JavaプラグインAPIもいくつか変更されており、新しいバージョンに対応するためには多少の修正が必要になります。

基本的な開発ガイドについては、以下の記事を参考にすると良いでしょう。

zenn.dev

zenn.dev

ある程度embulkのプラグイン開発に慣れていれば、上記の記事で実装とビルドまでは何とかなるんですが、当分の間0.9系が生き続けることは間違いないため、プラグイン開発者としては出来るなら0.9系と0.11系で両対応できる様にしたいと考えるのは自然なことでしょう。

そのため、複数のembulkバージョンを利用してCIを実行できる様にしたいと思い、実際に構築したのですが、その過程で結構ハマる箇所があったので、その辺りについて解説します。

先に、実際に作業したリポジトリを紹介しておきます。

github.com

CI環境での実行確認は各バージョンのembulkをインストールした後、シンプルなコンフィグを実際に実行してみてエラーが無ければOKというシンプルなものです。(別途JUnitで書いたテストはある)

また、ここで紹介する方法はバリバリのbeta版という感じの内容なので、今後のembulkの開発次第で大きく変更になる可能性があります。ご注意ください。

Embulk System Propertiesについて

v0.9系であれば普通にビルドした時の生成物のパスをCLIオプションでLOAD PATHに追加すれば、rubygemsの仕組みを経由してembulkに読み込むことができ、それで実行できました。 circleciの設定を抜粋するとこんな感じ。

# 省略
      - run: ./gradlew gem

      - run:
          name: run-embulk
          command: ~/repo/embulk run -I ../../../build/gemContents/lib config_acceptance.yml
          working_directory: src/test/resources

しかし、v0.10以降だとJRubyがデフォルトで存在しないため、jrubyを追加するかmavenプラグインとしてインストールするかのどちらかをやる必要があります。

この時に利用するのがEmbulk System Propertiesです。

詳しくは、以下の記事に書かれているのですが、.embulk/embulk.propertiesというファイルを作っておくと、embulkの基本設定をjavaのpropertyファイル形式で記述できます。

zenn.dev

このembulk.propertiesにjrubyのjarファイルの所在を指定することで、embulkでJRubyが利用可能になります。そうすれば、以前と同じ様にrubygems経由でプラグインの読み込みが出来る様になります。

一方で、gemプラグインは今後プラグイン開発の本流では無くなりそうなので、このタイミングでmavenプラグインの活用方法について学んでおきたいと思い、embulk-output-kafkaではmavenプラグインとしてプラグインを導入する方法を調査しました。

で、めちゃくちゃハマったのが、そもそもドキュメントが現時点では全く無いということですw まだ開発版なのでプラグインインストールのための機構そのものが未整備であり、正にこれから作られている最中です。 一般ユーザーならここで諦めても別に良いのですが、プラグインをちょくちょく開発していて、出来れば新しいバージョンにも速やかに対応したいと考えている開発者としては、ここでキャッチアップしておきたいので、頑張って探し当てた結果や質問して得られた回答を共有していきます。

CI環境におけるMavenプラグインの導入方法

1. プラグイン情報の記述

まず、Embulk System Propertiesかyamlのconfigファイルに利用したいプラグインの情報を記述します。

embulk-output-kafkaの例だと以下の様になります。

embulk.propertiesの例

plugins.output.kafka=maven:io.github.joker1007:kafka:0.3.0

重要なのがgroupIdの後のブロックです。ここはembulk-output-kafkaにはなりません。plugins.outputで始まっているキーの場合、kafkaと入れておくと自動でartifactIdがembulk-output-kafkaマッピングされてMavenリポジトリの探索が行われます。ここでembulk-output-kafkaと書いてしまうとembulk-output-embulk-output-kafkaが探索されることになるので注意しましょう。

config fileの例

out:
  type: { source: maven, group: io.github.joker1007, type: kafka, version: 0.3.0 }
  # other config parameters

ちなみにこのフォーマットについてですが、現在ガイドが全然無くて、embulkのソースコードを漁ってたらMavenPluginTypeというクラス内でそれを見つけることができました。

2. プラグインの配置

現在、プラグインを実際にMaven Centralからダウンロードしてくる仕組みがそもそも存在しません。ローカルでビルドしたものも、どうやってそこに読み込ませるかはパッと分かる仕組みやドキュメントはありません。

実際のところ、現時点ではどうやれば良いかというと、以下の様になります。

開発中のプラグインであれば、gradleのmaven-publishプラグインを利用してpublishToMavenLocalを実行すると、ローカルのMavenリポジトリキャッシュにビルド済みのプラグインがpublishされます。 デフォルトでは~/.m2/repositoryになるはずです。

そして、上記のユーザー向けv0.11ガイドに依ると、embulk.propertiesにはm2_repoというものを指定できます。 m2_repoにそのローカルMavenリポジトリキャッシュを指定すれば、読み込みのためのパスが通ります。

しかし、これだけでは十分ではありません。

publishToMavenLocalだけではプラグインが依存しているライブラリのjarがローカルのMavenリポジトリに書き込まれないため、現時点では自力でそれらをダウンロードしてくる必要があります。

publishToMavenLocalを実施すると、build/publications/<task name>/pom-default.xmlというファイルが生成されます。そのpomファイルをコピーしてきて、mvnコマンドを使って依存関係を解決します。pom.xmlのあるディレクトリでmvn installを実行することで、依存ライブラリも全てローカルリポジトリにインストールされます。

その上でembulk runを実行すると、無事にmavenプラグイン形式でプラグインを読み込み、実行することができます。

開発中のプラグインで無い場合は、ダミーのpom.xmlを用意して、そこに利用したいプラグインmaven dependencyを記述しmvn installした上で、embulk.propertiesを設定すれば利用できる様になるはずです。

3. CI環境用のヘルパー

embulk.propertiesにgroupIdやバージョン指定が必要になるため、gradleに現在のプロジェクト定義情報からembulk.propertiesを生成するヘルパータスクを追加しました。

task generateEmbulkProperties {
    doLast {
        mkdir ".embulk"
        def f = file(".embulk/embulk.properties")
        f.write("m2_repo=${System.properties["user.home"]}/.m2/repository\nplugins.output.kafka=maven:${project.group}:kafka:${project.version}")
    }
}

4. circleciの設定例

kafkaの起動等も含むため、余計なものが入っていますがembulk-output-kafkaでは以下の様な設定でCircleCIを動かすことにしました。

  embulk-0.10:
    docker:
      - image: circleci/openjdk:8-jdk
      - image: wurstmeister/zookeeper
      - image: wurstmeister/kafka:2.13-2.7.0
        environment:
          KAFKA_ADVERTISED_HOST_NAME: localhost
          KAFKA_ADVERTISED_PORT: 9092
          KAFKA_PORT: 9092
          KAFKA_ZOOKEEPER_CONNECT: localhost:2181
          KAFKA_DELETE_TOPIC_ENABLE: true
          KAFKA_CREATE_TOPICS: "json-topic:1:1"

    working_directory: ~/repo
    environment:
      # Customize the JVM maximum heap limit
      JVM_OPTS: -Xmx3200m
      TERM: dumb
      SKIP_SIGNING: true

    steps:
      - checkout

      - run: curl -o ./embulk -L https://github.com/embulk/embulk/releases/download/v0.10.31/embulk-0.10.31.jar
      - run: chmod +x ./embulk

      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "build.gradle" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run: ./gradlew dependencies

      - save_cache:
          paths:
            - ~/.gradle
          key: v1-dependencies-{{ checksum "build.gradle" }}

      - run: ./gradlew publishToMavenLocal

      - run: cp build/publications/embulkPluginMaven/pom-default.xml pom.xml

      - restore_cache:
          keys:
            - m2-v1-{{ checksum "pom.xml" }}
            - m2-v1-

      - run: mvn install

      - save_cache:
          paths:
            - ~/.m2
          key: m2-v1-{{ checksum "pom.xml" }}

      - run: ./gradlew generateEmbulkProperties

      - run:
          name: run-embulk
          command: ~/repo/embulk run config_acceptance.yml
          working_directory: src/test/resources

5. その他の注意点

CI環境でpublishToMavenLocalを行う時に、生成されるpomファイルの定義にちゃんとgroupIdやartifactIdが設定される様に、以下の公式ドキュメントを参照して必要な情報を定義しておきましょう。

docs.gradle.org

必要な情報が足りないと、配置されるファイル名やリポジトリ内のディレクトリ名が環境依存で変化する場合があるので、embulkのプラグインローダーが正しくプラグインを見つけられなくなります。自分は、そもそもプラグインの読み込みの挙動が良く分かっていなかったこともあって、しょうもないことで1時間ぐらい無駄にしました。(artifactIdが抜けていて、jarが見つからなくなっていた)

CI環境で引っかかるもう一つのポイントが署名でした。

実際にMaven Centralにpublishするためにはgpg鍵による署名が必要になるんですが、それをgradleで実施するsigningというプラグインを使うと、publishToMavenLocal実行時にも自動的に署名処理が走る様になります。 しかし、別にCI環境で鍵署名とか必要無いし、そのためにgpgの秘密鍵をどうこうするのは危ないので、CI環境ではスキップさせる様にしました。

tasks.withType(Sign) {
    onlyIf { System.getenv().get("SKIP_SIGNING") == null }
}

CI環境ではSKIP_SIGNING環境変数を設定してスキップさせます。

Gradleに慣れてればサッと書けると思うんですが、Gradleにあんまり詳しくないので自分はちょっと悩みました。

最後に

これでv0.9系とv0.11系のMavenプラグインの両方を実際に動かして検証する準備が出来ました。

現時点ではMavenプラグインの導入方法は、ほとんどドキュメント化されておらず、またエコシステムやインストール支援機能の構築もまだまだこれからといった状況なので、ここで紹介した方法はどこかのタイミングで不要になることでしょう。(少なくとも一般ユーザーにとっては)

しかし、早い内から次期バージョンのAPIに対応したembulkプラグインを作っておきたい、利用しているプラグインを新しいembulkに対応させるパッチを書きたいといった開発者にとっては、しばらくは使えるノウハウになると思います。

自分は大体現状の動作については理解したので、自分がメンテしているプラグインは暇を見て対応していこうと思っています。

その他の参考資料: GitHub - embulk/embulk-input-s3: S3 file input plugin for Embulk