Ponz Dev Log

ゆるくてマイペースな開発日記

AWS Batchに渡したCommandがプログラムの引数に渡らない時はDockerfileを見よう

お仕事でも使ってるAWS BatchでCommandに渡した値がプログラム(Javaだとmain関数のargs部分)に渡ってこなくてハマった時の対象方法メモ。 AWSの公式ドキュメントが分かりずらいので、Dockerのドキュメントも参照しながら解決しました。

Commandとコマンドライン引数の関係

そもそもの話。AWS Batchのコンテナプロパティ"Command"はDockerfileに記述されたどの部分にマッピングされるのだろうか? AWS Batchのジョブ定義のパラメータのセクションを見ると以下のように書いてあります。

このパラメータは、Docker Remote API の コンテナを作成する セクションの Cmd にマッピングし、 COMMAND パラメータを docker run にマッピングします。Docker CMD パラメーターの詳細については、https://docs.docker.com/engine/reference/builder/#cmd を参照してください。

🤔???

分かりづらいので噛み砕いて説明します。。。AWS Batchに渡されたジョブ定義のCommandはdocker runの後に続くパラメータとして渡されます。Dockerfile内での記述でどうも引数の渡され方の挙動が変わる らしく、以下のような感じになります。

  • CMDでコマンドを指定した --> ジョブ定義で指定したCommandの1番目が実行可能バイナリ、2番目以降が引数となる
  • ENTRYPOINTでコマンドを指定した --> ジョブ定義で指定したCommandが全てENTRYPOINTのコマンドの引数となる
  • CMDとENTRYPOINTでコマンドを指定した --> ジョブ定義で指定したCommandが全てENTRYPOINTのコマンドの引数となる

3番目のパターンに関してはDockerのドキュメントの方に以下のようなコメントがありました。

If the image also specifies an ENTRYPOINT then the CMD or COMMAND get appended as arguments to the ENTRYPOINT

例えば下のようなDockerfileに対してジョブ定義のコマンドで "hoge fuga"と渡した場合は java -jar /app/application.jar hoge fuga となります。 混乱しないようにするためには、Dockerfile内はENTRYPOINTのみ指定して、ジョブ定義は引数扱いにした方が良いですね。

FROM openjdk:8-alpine
COPY target/scala-2.12/args-logger-1.0.0.jar /app/application.jar
ENTRYPOINT ["java", "-jar", "/app/application.jar"]

今回引っかかったところはDockerfileでCMDを指定したつもりでコマンドを書いていたら実はENTRYPOINTだったから引数が欠けてしまったというオチでした。。。

CommandとParameterの関係

ついでに、ジョブ定義にはCommandとParameterというパラメータがあります。関係性としては、Command内に変数を埋め込んだ場合に実際の値としてParameterの値を入れることになります。値を外から(CloudWatch Eventsで指定した値とか)Parameterとして注入することでアプリ内で動きを変えるといった使い方ができるようです。

DockerfileとCommandとParameterを設定して動かす

実際に動かして確かめます。渡された引数をログ出力するだけのScalaのアプリを使います。 commandパラメータはジョブ定義で以下のように ["Ref::jobType", "Ref::language"] として変数化しました。

f:id:accelerk:20190218002513p:plain

実際に渡したパラメータは下図の通り。

f:id:accelerk:20190218002550p:plain

実際に動かしてみると...

f:id:accelerk:20190218002637p:plain

確かにcommandパラメータにParameterを埋め込んだ値で引数が全て渡っていますね。

GCP Professional Data Engineer認定取得しました

GCP Professional Data Engineer認定試験に合格してきました! 認定取得にあたり、取得するまでのポイントや試験のことを書いておきます。

cloud.google.com

事前に準備したこと

  • Coursera   - 各サービスのコンセプトやらユースケースを細かく詳しくレクチャーしてくれる。
    • 分量が多いので、概要を掴むくらいを期待するなら重すぎるかも。   - 間に手を動かすパートと小テストがあったから少しは自信がつく

www.coursera.org

  • qwiklabs   - 手を動かす用. アカウントは制限時間ありで用意してくれる

押さえるべきサービス

データに関するサービスだけでなく、セキュリティやログ出力設定まで聞かれます(システムを作る/使う上では基本だもんね)。

ざっと列挙するだけでも以下のサービスを一通り抑える必要がありました。 もちろん、現時点で使っているサービスがあるならそれを掘り下げると良い。自分みたいにほとんど知らないなら、チュートリアルの一つ目だけやって触るだけでもしたほうがいいです。

特にBigQuery, IAM, ストレージ/データベースサービスの使い分け, 機械学習の方法(特徴量エンジニアリングとか)は押さえておく必要がありました。

  • IAM (Identity and Access Management)
    • 権限, 職掌分離を行うサービス。他のクラウドベンダーのIAMとほぼ同じ機能。
  • BigQuery
    • いわゆるデータウェアハウス, 引くレベルで高速にデータをSQLで探索できる。
    • テーブルやビューの作成を作成してデータを保存できるけど、データの検索に重きを置いている印象。
  • Google Cloud Storage
    • ストレージサービス。アクセス頻度やオブジェクトを置くリージョンによって最適なストレージタイプが異なる。(ここはAmazon S3と同じかも)
    • ストレージタイプはリージョンマルチリージョン/ 単一リージョン / 低頻度アクセス用 / アーカイブ用の4タイプ。
  • Cloud Pub/Sub
    • Publish-Subscribe型のメッセージングサービス。
    • プッシュ通知的なpush型, キュー的なpull型の2タイプあり。
  • Cloud Dataflow
    • データ処理のパイプライン(複数のデータ処理を順序づけて/ 並列にして実行する1セット)を構築するサービス。
    • ストリーミングもできるし、バッチ処理もできる。
    • GCPのサービス間(Pub/Sub -> BQ, GCS(Avroファイル) -> BigTable)の転送するだけならば事前に用意されたテンプレートを使えばOK!
    • カスタムの処理を組み込む場合は Apache Beam SDK(Java or Python)で書いてデプロイする。
  • Cloud Datalab
    • Jupyter Notebookのホスティングサービス。
    • Git連携(ungit)だけでなくBigQuery, ML EngineといったGCPのサービスとの連携もできる。
  • Cloud Dataproc
    • Spark / Hadoopホスティングサービス。
    • 一度クラスターを立てたらずっと起動したままではなく、一定時間未使用のまま経過したらクラスターをシャットダウンできるのでお財布に優しい!
    • 既存のSpark / Hadoopで組んだサービスをクラウドに移行するならDataprocに移すのが良いとされる。
  • Cloud Dataprep
    • GUIベースのデータ変換、データクリーニングサービス。
    • BigQueryのテーブルやGCSにおいたCSV等のデータの中身の欠損値を除去したり、テーブルJOINしたりできる。
    • 操作はレシピという形で実行されるけど、実態はDataflowのジョブのようです。
  • Cloud ML Engine
  • Vision API
    • 学習済みの画像認識機械学習モデルをAPI経由で呼び出せるサービス。
    • Amazon RekognitionやWatson Visual Recognitionに相当
    • 認定試験も問題を見る限り、自分でTensorflowでモデル作るよりも既に学習済みのモデルあるならそっち使った方が早いよという意図で扱っているようです。
  • Stackdriver Logging / Monitoring
    • 名前の通り、ログやモニタリングのサービス。
    • ほとんどのサービスでは組み込みでStackdriver Loggingでログを見れるようにはなってる。
  • Cloud Bigtable
    • ワイドカラム型のスキーマを持ったNoSQLデータベース。
    • 読み込み、書き込みのレイテンシが早い。
    • IoT機器からのデータの保存先で使ったり、時系列データをストリーミングして書き込んだ後に分析用にBQへデータを流し込むといったユースケースが考えられる。
  • Cloud SQL / Cloud Spanner
  • Cloud Composer
    • Apache Airflowベースの構成管理, ワークロード管理ツール
    • AWSのCloudFormation + Step Functionを合わせたような印象だけど、複数のサービスを組み合わせたジョブスケジューラーとして最適かも。
  • Cloud Storage Transfer Service
    • 特定のデータソースに格納されたデータをGCSに転送するサービス。
    • データソースには、HTTP/HTTPSのエンドポイントをもつソース / Amazon S3(!?) / GCSが使える!

試験受験したとき

大きく分けて以下2種類。出題数的には前者8割、後者2割でした。ケーススタディは試験の公式サイトに載っているやつそのままです。

  • 特定のユースケースに沿って、どのサービスをどのように使うか
  • ケーススタディに沿って各課題に対するソリューションとしてどのサービスを使うか

時間は2時間ありましたが、1.5時間で一通り解き終わりました。全部見直しするには少し時間が足りなかったです。。。 認定試験のサイトだとケーススタディを猛烈プッシュしていましたが、実際には各サービスについての一問一答形式が多くて焦ります。AWSの試験を受けたことがある方には同じ形式の出題だったと言えば分かりますかね笑

また、ケーススタディはビジネス要件(履歴データを集約して予測分析したい、本番環境を柔軟にスケールさせたい)、技術要件(出来るだけマネージドサービスを使う、Hadoopワークロードはそのまま移行したい etc...)が出題として問われたり、時にはヒントになっていました。 ただ、受験した会場特有なのか分かりませんが、左に問題、右にケーススタディの構成や要件が表示という2窓構成でした。めっちゃ見づらかった。。。

認定試験を通しての学び

Kubernatesを触るために最初に使い始めたGCPでしたが、今回の認定試験の学習を通してデータ分析や分析基盤の構築について学べました。 特に以下のことについては今まで触ったことのあるクラウド(IBM Cloud, AWS)以上に理解できたと自信を持てます。

  • 分析用途のデータ処理パイプライン構築にはどのようなサービスを組み合わせれば良いか。
  • 機械学習サービスは他のクラウドベンダーのAIとは何が違うのか。どのように使うのか。事前に使えるモデルは何か。
  • 分析に使うデータウェアハウスのBigQueryの特徴。
  • ストレージの使い分け。

個人的にはデータをSQLで分析するBigQueryやDataflow, Dataprepと他のクラウドで処理したデータを連携させたりできないかとアイデアを膨らませるいいきかっけになったかと。 例えばAWSGCPの2つを使ってアプリのログのデータ分析をマルチクラウドできるんじゃないか? (冗長かもしれないけど)

  • AWS ECSで動くアプリケーションのログをCloudWatch Logsに集約
  • CloudWatch Logsのログ生成をAmazon SNSのトピックに流し込む
  • SNSをサブスクライブするLambda経由Kinesis Firehoseにデータを入れる
  • Kinesis FirehoseからAWS S3にログをストリーミングで直接保存
  • Cloud Storage Transfer ServiceでS3からGCSにログデータを転送
  • DataflowでGCSからBigQueryに流し込む
  • BigQueryで分析

後日談

認定通ったら社内のPrimary Job Role Specialtyが Application Developer: Google Cloud Microservicesになってました⸜(* ॑꒳ ॑*  )⸝笑 今後はGCP Associate Engineer取ってみたいですね。あとはAWS SAAも!

BuildKitを使って docker build のビルド時間を半分以下にする

元ネタはAWS CodeBuildでBuildKitがサポートされたという話。

dev.classmethod.jp

BuildKitとは、Docker v18.06から搭載された "次世代 docker build" のようです。BuildKitを使わないよりも圧倒的にビルドパフォーマンスが高くなる&セキュアにビルドできるようなので試してみました。

BuildKit自体の説明は上の記事でも紹介されていたSlideShareが一番簡潔で分かりやすいので是非見ていただければと。

www.slideshare.net

実践

今回は以下のようなJavaのコードをGradleでJarに固めることを試します。 やっていることはSlackのWebhook URLにUUIDをメッセージとして投げつけているだけです。

package net.ponzmild;

import java.io.IOException;
import java.util.UUID;
import net.ponzmild.client.SlackClient;

/**
 * 処理のエントリーポイント
 * @author ponzmild
 */
public class Handler {

  public static void main(String[] args) {

    String uuid = UUID.randomUUID().toString();
    System.out.println("Message UUID is " + uuid);

    String webhookUrl = System.getenv("SLACK_URL");
    if (webhookUrl == null || webhookUrl.isEmpty()) {
      System.out.println("Environment variable 'SLACK_URL' doesn't set or is empty.");
      System.exit(1);
    }

    SlackClient slackClient = new SlackClient(webhookUrl);
    try {
      String responseStr = slackClient.postMsg(uuid);
      System.out.println("Response is " + responseStr);
    } catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
    }
  }

}

また、Dockerfileは以下のようにマルチステージビルドで何もしなくても比較的高速でビルドされるようにします。

FROM openjdk:8-jdk-alpine as build
COPY . /usr
WORKDIR /usr
RUN ./gradlew build

FROM openjdk:8-jre-alpine
COPY --from=build /usr/build/libs/dck-slack-messenger-1.0.jar /app/
ENV SLACK_URL https://hooks.slack.com/services/xxxxxx/yyyyyyyyy/zzzzzzzzzzzz
CMD java -jar /app/dck-slack-messenger-1.0.jar

BuildKitを使わない場合

まずは BuildKitを使わないdocker buildから。以下のように見たことあるようなログが出ますね。(ログが長かったのでGradleのビルド部分しか見えませんが...) ビルド所要時間は 約90秒 でした。

f:id:accelerk:20190109015058p:plain
BuildKitを使わない場合の docker build

BuildKitを使った場合

お次は BuildKitを使った場合です。 BuildKitの使い方は簡単で、ターミナル上で export DOCKER_BUILDKIT=1 をセットするだけ!ビルドしてみましょう。

f:id:accelerk:20190109015322p:plain
BuildKitを使った場合の docker build

おお!全然ログが違いますね。。。ビルド時間は 41秒 !! BuildKitを使わない場合の半分以下じゃん! それと自分の設定によるものかもしれませんが、ログ出力の観点では以下のような違いもありました。

  • 各ステップの所要時間が出力されるようになった。上の画像の右端に1/10秒単位で吐かれていますね。
  • Dockerfileで指定した EVN がログに出力されなくなった。
  • Gradleのビルド途中経過のログが一切出力されなくなった。
  • マルチステージビルドの場合は、ステージ名も出力されるようになった。

環境変数がログに出なくなったのは個人的には嬉しいです。デバッグしづらいかもしれませんが、秘密鍵とかトークンがそのままログに出るとセキュアではないですしね。

AWS以外でもDockerのバージョンさえ満たしていれば、他のクラウドやCIサービスで大いに活用できそうです。CI時間が長いとお困りの方は使ってみてはいかかでしょうか?

GCP BigQueryチュートリアルお触りメモ

GCPのData Engineer認定試験に向けて、データウェアハウスに分類されるBigQueryのチュートリアルを一周したのとドキュメントを読み漁った時のメモです。最近メモ続きで今回はコードは一行も出てきませんが、試験直前に見直すために書き残します。そろそろパイプラインとか自前で作ってみないと。。。

データロード方法

データを流し込むのは以下の方法がある。

データロードのTips

  • 直接ファイルをBQに入れると Upload -> Importの二段階が走って途中でこけると悲しい。GCS経由だと10倍程度早いようだ。
  • 圧縮データでロードする場合は、Avroが最適、次点がPurquet。圧縮率が高いのに、並列読み込みできるから。
  • CSVJSONファイルは、gzip圧縮よりも圧縮なしの方が早い。
  • 基本は文字コード UTF-8で入れること。ISO-8859-1でも読み込み可能だが、UTF-8に変換しながら読み込むため遅い。

テーブル構造

  • 構造化データを入れる
    • ただし、JSONのようにネストした形式のデータでもOK。この場合は、カラム名comitter.name,comitter.timestamp のようにドットつなぎ
    • スキーマは手動で定義するか、サポートされたファイルから読み込む場合は自動検出で定義可能。

ストリーム処理

  • ストリーミングの上限10.000行 / sec
  • ストリーミングインサートするときは、上記の上限に引っかからないようにDataflowを噛ませた方が良さそう。
  • DataflowならPub/Sub, GCS, JDBC(アプリ経由)のテンプレートが用意されているから使うハードルは低い。
  • バッチは強力な整合性、ストリーミングは結果整合性と整合性の性質が異なる。(チュートリアルでFluentd -> BQの場合は1分くらいラグがあった)

課金の話

  • 課金対象は、読み込んだ行数 で決まるので出力結果だけ絞っても金額は絞らない場合と変わらない。(よって、LIMIT句を加えても課金額は変わらない!!)
    • 全ての列を読み込む SELECT * は最低の所業。必要な列だけ取りなさい。
    • テーブルの中身を見るだけなら、プレビューを使おう。単に中身をチラ見するだけなのにクエリを投げるのは課金されるので無駄。
    • 特定の期間のデータのみクエリしたい場合は、時系列のパーティション属性(隠しフィールド扱い?)を使って、時系列パーティションでクエリを投げよう。
    • JOINが多い/共通のクエリがあるならば、事前に中間テーブルを作ろう。

権限設定周り

模擬試験でも出てきて焦ったところだけれども、この人にはこのデータセットは見せたくないとか権限設定は大事。

  • アクセス制御の基本はここを参照
  • 権限設定はプロジェクトレベルが適用されて、あとはデータセット単位(?)
  • 役割ごとに見せたいビューやデータセットを制限したい場合は、データセットを分けること。
  • データセットの権限付与と承認は新しいコンソール画面からじゃできない。"従来のUI”からのみ実行可能。

JavaScriptの2次元配列の展開・重複削除・集約をLodashを簡潔に書く

2018年末に書いたSlack Botを少しリファクタリングした時の話。2次元配列と配列要素の重複削除ってよく使いそうで意外とベストなやり方ってよく分からないですよね。自分の場合は2次元配列は mapforeach、重複削除は Set オブジェクトで実装していましたが、後からぱっと見て処理が訳わからんコードになりがちです。JSって flatMap ってないので気合いで書いちゃう。

以前書いたTrello/Slack/OpenWhiskで作ったBotでも同じような処理を実装しなければいけなくて、やっぱり読みづらいコードが爆誕してしまいました。正直コード書いてから1週間経った今でさえ分からんw

ponzmild.hatenablog.com

さて、こんなダメコードを書いていてはいけないと解決策を模索したところ辿り着いたのがLodash

lodash.com

ライブラリではよく中の実装で使われている印象があり今更かよ感が出ていますが、Lodashで関数を組み合わせて実装してみると思った以上に読みやすくなったのでBefore/Afterで見比べながら書き残します。

解決策

入力データ(Trello APIを呼び出した時のレスポンス)は以下のようなJSONです。

[
  {
    id: "dfhajsdfhalsdfhasrber",
    name: "task1",
    labels: [
      { id: "aaa", name: "hoge" },
      { id: "bbb", name: "fuga" }
    ]
  },
  {
    id: "ruweqioryqweryqweyr",
    name: "task2",
    labels: [
      { id: "aaa", name: "hoge" },
      { id: "ccc", name: "piyo" }
    ]
  }
]

この関数のアウトプットとしては、{name: "hoge", count: n} というオブジェクトの配列にしたい。

改善前

まずは年末の働かない頭で書いた以下のコードを見て欲しい。

function summarizeLabels(cards) {

  // 二次元配列でラベルを取り出して展開し、シンプルな文字列のみの配列にする ... (1)
  let labelLists = [];
  cards.map(card => {
    card.labels.forEach(label => labelLists.push(label.name));
  });

  // 配列内のラベル一覧を重複削除で取り出す ... (2)
  const labelSet = new Set(labelLists);

  // ラベルごとの件数を配列とラベル一覧から導出 ... (3)
  let labelResults = [];
  for (let targetLabel of labelSet) {
    const labelCount =
      labelLists.filter(cardLabel => cardLabel == targetLabel).length || 0;
    labelResults.push({ name: targetLabel, count: labelCount });
  }

  return labelResults;
}

やっていることとしては、以下3つ。文字に起こすと長いですね。

  1. まず label プロパティを取り出すと配列の中に配列が入れ子になった2次元配列の形になる。これは扱いづらいから展開しておく(1)。
  2. 次にキーを取り出すために(1)の配列を重複削除してキーのリストを取り出す(2)。
  3. 最後に(2)のキーのそれぞれに対して(1)の中で合致するラベルの数をカウントしてオブジェクトに突っ込む(3)。

いやー、、、 よ゛み゛つ゛ら゛い゛よ゛お゛

正直後で直したくないコードが見事に爆誕です。理解しづらい理由としては、2次元配列を展開していることを中から外に向かって処理を追わないと理解できない、Setってなんだっけ、filter関数と突っ込む先の配列定義が離れすぎて何の配列か分からない。こんなところでしょうか。

改善後

Lodashで関数チェーンを作り上げて直感的に書くことを試みます。最終的なアウトプットに値を集計する関数のみ自分で作成し、これ以外はLodashで定義された関数を使います。

const _ = require("lodash");

/**
 * 名称ごとの出現数を集計する.
 * @param {Object} stat 集計値
 * @param {String} name 集計キー ... 配列内の個々の値が入る
 */
const gatherNames = (stat, name) => {
  if (_.isUndefined(stat[name])) {
    stat[name] = { name: name, count: 0 };
  }
  stat[name].count++;
  return stat;
};

function summarizeLabels(cards) {
  const labelResults = _.chain(cards)
    .flatMap(_.property("labels")) // ... (1)
    .map(label => label.name)
    .reduce(gatherNames, {}) // ... (2)/(3)
    .values()
    .value(); // ... これを呼び出して初めて上記の関数チェーンが実行される
  return labelResults;
}

以前よりもコードが美しくなった気がします! 改善前のコードで分かりづらかった2次元配列の展開は _.flatMap() で一発で書けます。Lodashのメソッドもmap, reduceといった比較的聞き覚えのある名称なので理解しやすい。for文回さずシンプルに書けるところもポイント高いですね。 一番読みやすくなっているポイントは、処理を関数を組み合わせて関数チェーンで表現できていることでしょうか。

Lodashくん、、、君はすごいライブラリだったのか。。。色んなライブラリで多用される理由も納得です。

JavaScriptのクロージャって結局なんだよ

JavaScriptでは初歩的なコードでは現れないけれどふとした拍子に出てくる単語っていくつかいるような気がします。 特にカリー化、高階関数クロージャ、ファンクターあたり。関数型プログラミングを進めていたら最初の方に出てきて戸惑ったのが クロージャ ってやつです。走り書きでn番煎じですが調べて噛み砕いた結果の備忘録です。

結論

要は 関数 です。("スコープ" と書いてあることが多いけど、実際に指しているのは関数の模様)

関数の中でも、以下の特徴を持つ関数のようですね。

  1. 未実行の関数を返す関数である。
  2. 子の関数からアクセスできる変数を保持する。

ただの関数とは何が違うわけ?

上の(1)だけ見てもJavaで言うところのファクトリメソッド, JavaScriptだと関数ファクトリ(?)っぽいです。 半分合っていそうですが、違いはソースコードを元に見た方が早い。

let outerVar = "outerVar";

function makeInner(params) {
  const innerVar = "InnerVar";

  function inner() {
    console.log(`I can see: ${outerVar}, ${innerVar}, ${params}`);
  }

  return inner;  // 未実行の関数を返す
}

const inner = makeInner("params");
inner();  // --> I can see: outerVar, innerVar, params

上の例だと、makeInner 関数がクロージャになります。inner 関数を返していますね。この makeInner 実行時には inner() としていないから未実行です。(定義1)

また、クロージャ内のローカル変数は inner 宣言時には関数のスコープ外になっていますが消えません。(定義2)

こうやって親の関数から返却される子の関数から、子&親の関数内/関数の引数/グローバルスコープの変数にアクセスできるところが他の関数とは違うところ。 ただ、変数は残ってはいるもののあくまで 参照のみが残っているだけ であって値がそのまま保持されるわけではなさそう。

(let outerVar ~ makeInnerクロージャの宣言まで同じ)

// クロージャから内部の関数を引数を渡して生成する前でも後でも参照する値は変更可能
// outerVar = "Outer2"; // inner() --> outerVar=Outer2
const inner = makeInner("params");
// outerVar = "Outer3"; // inner() --> outerVar=Outer3
inner();  // --> I can see: outerVar, innerVar, params

2番目の例の通り、途中で書き変わると最終的にクロージャから返却される関数の実行結果は変わるようですね。

参考

developer.mozilla.org

JavaScript関数型プログラミング 複雑性を抑える発想と実践法を学ぶ (impress top gear)

JavaScript関数型プログラミング 複雑性を抑える発想と実践法を学ぶ (impress top gear)

Gradleを使ってIBM Cloud Functions with Javaの関数を実装する

あけましておめでとうございます。新年一発目はサーバーレスです。 昨年はNode.jsしかほとんど触らなかったIBM Cloud Functions(OpenWhisk)をJavaで実装します。

ただし、公式のチュートリアルだと自力でgsonにCLASSPATH通してデプロイしてねと雑にしか書いていないので、こちらのブログのチュートリアルを使います。

www.ibm.com

とはいっても、作り方は2ステップで簡単です。 ブログ記事そのままですが、書き初めとうことで基本を押さえに行きます。

Javaの関数を書く

ポイントは2つ押さえておけばOKです。

  1. 実行する関数(厳密にはエントリーポイントとなるメソッドを指します)の名前は main 固定です。(別名にしてもいいけど仕組みに乗っかった方が楽)
  2. どうやらリクエストとレスポンスのJSONパーサーはgsonを使うことが推奨されています。(これ以外のJSONパーサーは使えないのかは未検証です)

簡単なHello worldは以下のようになります。

package com.example;

import com.google.gson.JsonObject;

public class Hello {

  public static JsonObject main(JsonObject args) {
    // リクエストを受け取る
    String name = args.getAsJsonPrimitive("name").getAsString();
    // レスポンスを返す
    JsonObject response = new JsonObject();
    response.addProperty("greeting", "Hello " + name + "!");
    return response;
  }
}

JARに固めてデプロイする

gsonはIBM Cloud側で提供されているので依存関係にJARとして含めなくても良いのですが、これ以外にもライブラリを含めたいとかありますよね?DIするならgoogle-guiceとか、HTTPクライアントにOkHttp使いたいとか諸々。そんな時は公式のチュートリアルのようにいちいちクラスパスを通すのは時間の無駄なのでGradleで一気にまとめます。(Jarに固めればいいので、Mavenでもできるはず)

上のHello worldの例だと最小限の記述で以下のように書けます。単純に依存関係のJARの取得先と何を入れるのかを書くだけ。

apply plugin: 'java'
version = '1.0'
repositories {
    mavenCentral()
}
dependencies {
    compile 'com.google.code.gson:gson:2.8.5'
}

あとは ibmcloud fn action create hello-wsk-java build/libs/hello-wsk-java-1.0.jar --main com.example.Hello でデプロイするだけ! ちょっとした関数を書くにはJavaを使うのは億劫ですが、かなり簡単にできるんですね。