astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

Java17に対応するための下調べをしたら思いのほかやることが多くて泣いた話

こんにちは!ICPの主にバックエンドの面倒をみているまるやまです。

今秋いよいよ2年ぶりにJavaのLTS版であるJava17がリリースされます。ICPのバックエンドではJavaを実行環境として利用しているので、次期LTSのリリースに合わせて実行環境の最新化を検討し始めました。が、軽い気持ちで調べ始めたところ、APIサーバだけでも思いの外やるべきことが多くて泣けました。今回はその辺の話でもしようと思います。

まずは結論から

現在のICPで実装しているAPIサーバの主要な技術スタックは以下のような構成になっています。

  • Play Framework 2.6
  • ScalikeJDBC3.4.x
  • Scala 2.12.10
  • Java11 (旧AdoptOpenJDK)

上記を出発点として、今後APIサーバがJava17に対応した結果、以下のような構成にアップデートする必要がありそうです。

  • Play Framework 2.9
  • ScalikeJDBC3.4.x
  • Scala 2.13.6
  • Java17 (Adoptium)

また、最終的な構成に至るまでの工程は、おおよそ以下のようになるでしょう。

  • Java17対応までの道
    1. まずはPlayのバージョンを2.8に上げる
    2. Scalaのバージョンを2.13.6に上げる
    3. Play2.9にバージョンを上げる(Playリリース後なる早)
    4. Java17にバージョンアップ ← 我々が本当にほしかったもの

以下では、上記のロードマップを引くに至るまでに得た情報を整理したいと思います。

各種ライブラリを取り巻く状況の整理

Java17に対応するにあたっての事前調査では、以下のような考えを持って調査を進めていきました。

  • [前提] 万が一フレームワーク/ライブラリ起因の問題が発生したときに開発元のサポートを期待できる推奨環境は満たしておきたい
    • 基本的には弊社内ですべての面倒を見る前提ですが、それでも解決できない問題が発生したときに開発元の助言が得られる可能性が高まるのでチョット安心
  • [仮説] Java17へのバージョンアップはリリースされたらなるべく早く対応したい
    • Java11のLTSについては、リリース当初のアナウンスではリリースから約2年(次期LTSのリリースとともに終了)と設定されている実装が多かったと記憶している
    • そのため、Java17のリリースに伴い一時的に推奨環境を満たさない期間が発生しそうな見込み
    • よって、サポートの前提条件を再び満たすためにJava17がリリースされたらなるべく早いうちにアップデートしておく必要があるのでは?
  • [仮説/要望] Scala自体のバージョンアップは可能であれば後回しにしたい
    • 影響範囲を極小化したいため、Java17対応とは別の時間軸で対応できるのがベター

Java

AdoptOpenJDK11のLTSが少なくとも2024年の10月までは提供されることが明記されていました。よって、Java17リリースからしばらくの間はJava11もLTSは継続されるため、リリースから約2年(次期LTSのリリースとともに終了)という当初の仮説は杞憂であることがわかりました。ありがたやありがたや。

https://adoptium.net/support.html

Scala

Scala2.13.6、及び2.12.5がJava17との互換性を保証していることが明記されています。

https://docs.scala-lang.org/overviews/jdk-compatibility/overview.html

パッチレベルでのバージョンアップはしておいた方がよいものの、2.12系で全く動かないわけではないということがわかりました。よって、2.12系のままJava17対応イケるかも、そう思っていた時期が私にもありました。

Play Framework

そもそもの話として、Play FrameworkがJava11を公式にサポートしているのはPlay2.8からであることを見落としていることが判明しました😇

https://www.playframework.com/documentation/2.8.x/Highlights28#Java-11-support

昨年APIサーバにおけるPlayのバージョンアップを行った際、当初は2.8までのアップデートを計画していた(?)ものの、2.6に上げた時点で力尽きてそのまま忘れていたようです。というわけで、何かあったときに開発元の助力を得られそうなPlay2.8へのアップデートは取り急ぎ対応する必要があります。死にたい

また、今後のリリースでいつからJava17を公式サポートするかは気になるところです。そこで、次期バージョンとなるPlay2.9のマイルストーンを確認する限りでは、Play2.9がJava17をサポートする最初のバージョンになりそう、ということがわかりました。ええやん〜。

https://github.com/playframework/playframework/pull/10819

一方で、Play2.9でScala2.12のサポートを打ち切りそうな動きが見えており、Play2.9に上げる際には、サポートが受けられる前提条件としてScalaのバージョンを2.13に上げる必要がありそうということが判明します。まじかー。

https://github.com/playframework/playframework/pull/10956

Scalikejdbc

Play Frameworkをサポートしており、且つバージョン2.6以上であればサポートすることは明言しているので、ひとまずは今後も安心して使えそうです。

http://scalikejdbc.org/documentation/playframework-support.html

まとめ

ICPのAPIサーバをJava17に対応させようとすると、思いの外やることがたくさんあることがわかってきました。

f:id:astamuse:20210825093402p:plain
ライブラリ間のサポート関係とICPとしての打ち手

  • ① まずはPlayのバージョンを2.8に上げる
    • そもそもJava11を公式サポートするPlayのバージョンは2.8以上なので、ここを解消しにいくことが急務
  • ② Play2.9の開発状況を見守る
    • Play2.9がJava17をサポートする最初のバージョンになるかもしれない(未確定)
    • Play2.9がScala2.12を打ち切るかもしれない(未確定)
  • ③ (おそらく)Scalaのバージョンを2.13.xに上げる
    • まだ未確定ながら、Play2.9でScala2.12のサポートが打ち切られる可能性は高め。よって開発元の方針に因ることなく、率先してScala自体のバージョンを2.13に上げた方が長い目で見たときに手間が少なそう
    • 幸いJava11とScala2.13.xは互換性が担保されているので、Scala自体のバージョンを2.13に上げる上での障壁はなさそう
  • ④ Play2.9がリリースされたらできるだけ早いうちにPlay2.9にバージョンを上げる
  • ⑤ Java17にバージョンアップ

今回はあくまでもロードマップを提示することに話題を絞ったので内容としては少なめです。が、Java17対応に至るまでの各作業について言えば、地味ながらそれなりの規模があることは予見されます。大変めんどくさいですね。

とはいえ、AdoptOpenJDKのJava11のLTSは2024年8月までは最低限担保されているので、全ての工程を終えるまでに最大約3年の猶予があるのはせめてもの救いでしょう。もちろんそんなに時間をかけて対応するつもりはありませんが。

というわけで、今回調べたことが皆さまの一助になれば幸いです。

セキュアなトークンの作り方

開発部のにゃんです。主にバックエンドを弄っております。

Webアプリケーションではセキュリティ対策のためにランダムな文字列を使用する場面が多々あります。例えば

  • CSRF対策のトークン
  • OAuthやOpenID Connectで使用するnonce, state, code_verifier
  • メールの到達確認用URLのトークン
  • パスワードをhashする際に使用するsalt
  • セッションID

これらの値は単に衝突しなければOKというものではありません。十分なセキュリティ強度を確保するためには推測不可能なランダム値を使う必要があります。

以下は推測不可能なランダム値ではありません。セキュリティが求められる場面では使ってはいけません。

  • Math.randomなどの疑似乱数
  • 日付やユーザ情報のハッシュ

ではどのような値が適切なのでしょうか?

/dev/random/dev/urandom

Linuxには

  • /dev/random
  • /dev/urandom

という特別なファイルがあり、セキュリティの求められる場面でも利用可能な乱数を取得することができます。

/dev/randomはシステムのノイズを利用した真の乱数を返します。ノイズを貯めたエントロピープールが枯渇するとノイズが溜まるまでブロックされるというデメリットがあります。SSH鍵やTLSサーバ鍵など長期間利用され、高度な保護が求められる場合に使用します。

/dev/urandomは、/dev/randomに品質で劣りますがトークンなどの使い捨ての値には十分な強度があります。またエントロピープールが枯渇してもブロッキングされません。

Webアプリケーションなどのリアルタイム用途ではブロッキングを避けたいのと、大抵の場合、暗号鍵ほどの強度は求められないので /dev/urandomを使うのが良いでしょう。

java.security.SecureRandom

プログラミングでは/dev/random/dev/urandomを直接利用せずライブラリやAPIを通してアクセスします。

Javaではjava.security.SecureRandomというクラスが用意されています。実装はJDKによって異なっていたり、設定によって切り替わるのですが、概ね

  • SecureRandom.getStrongInstance() - /dev/randomに相当する真の乱数生成器を取得、エントロピー枯渇時にブロッキングする。
  • new SecureRandom() - /dev/urandomに相当する暗号論的疑似乱数生成器を取得。ブロッキングしない。

となっているようです。なお、他の言語では

  • PHP: random_bytes
  • Ruby: SecureRandom
  • Python: secrets.SystemRandom
  • Node.JS: crypto.randomBytes
  • Web API: crypto.getRandomValues

といった安全なランダム値を取得するAPIが用意されています。

サンプルコード

アカウント登録時やメールアドレス変更時に以下のような文面のメールを送ることがあると思います。

以下のURLにアクセスしてメールアドレス変更を行ってください

http://exmaple.com/mail/confirm?token=laLg4Whuf0NA27AYLDyigk2i6_X16g6tNBbBSR_eLlugTHMQDptfqbePrPP4lT62

このメール文面にあるtokenを安全に作成するコードを例示します。

import org.apache.commons.codec.binary.Base64;  
import org.apache.commons.codec.binary.Hex;  
import org.apache.commons.codec.digest.DigestUtils;  
  
import java.nio.ByteBuffer;  
import java.security.SecureRandom;  
  
public class Token {  
    public static void main(String[] args) {  
        System.out.println(generateMailVerificationToken("test@example.com"));
    }  
  
    public static String generateMailVerificationToken(String email) {  
        ByteBuffer buf = ByteBuffer.allocate(48);  
  
        // 16byteのランダム文字列
        byte[] randomBytes = new byte[16];  
        SecureRandom secureRandom = new SecureRandom();  
        secureRandom.nextBytes(randomBytes);  
        buf.put(randomBytes);  
 
        // 32byteのemailハッシュ値
        byte[] emailHashBytes = DigestUtils.sha256(
            email + Hex.encodeHexString(randomBytes));
        buf.put(emailHashBytes);  
  
        return Base64.encodeBase64URLSafeString(buf.array());  
    }  
}

ポイントはMath.randomやjava.util.Randomではなく、java.security.SecureRandomを使うことです。トークンとしてはランダム部分の16バイトで十分な長さがあり衝突の心配はありません。今回はメールアドレスのsha256ハッシュ値32バイトを追加していますが、この部分は本質的には不要です

というわけで、今回は地味目な話題でしたがトークンってどう作ればいいのかな?と思った時には参考にしていただけると幸いです

pythonのArgumentParserような使い心地!picocliのご紹介

f:id:astamuse:20200415113243p:plain

こんにちは、開発部のnishikawaです。本日はpicocliというライブラリを使ってScalaでコマンドを実装する機会があったのでご紹介します。

コマンド作りあるある

コマンドを作る時によく実装されるのが引数のパースです。これはどんな言語で実装されていても多くの人が実装すると思います。よく使われるのがgetoptsですが、これを使っても毎回引数のパースを作り込むのは面倒です。

もちろんjavaも例外ではないのですが、この実装が面倒で時間がない時はバリデーションを省略するような実装をすることも多いと思います。

コマンド作るならやっぱりpythonだよねー

その点pythonだと、デフォルトで高機能な引数のパーサーがあります。それがArgumentParserです。これが存在することや便利なライブラリがあるのでコマンド作りはpythonでという人は多いと思います。

しかし、プロダクトの多くをJVM言語で実装しているようなチームでは資産が流用できないのでやっぱりpythonは・・・というようなところも多いかもしれません。じゃあJVM言語でもArgumentParserのような機能が欲しいということで出てくるのがpicocliです。

picocli とは

picocliはjavaで実装されたコマンドラインインターフェースを提供するライブラリです。とても高機能でJVM言語で利用可能です。

picocli: https://github.com/remkop/picocli

pythonのArgumentParserと比較しながら使ってみる

picocliはArgumentParserのような使い心地です。実際にpythonのArgumentParserと比較してみたいと思います。

以下は引数に数字を列挙すると最大値を返し、オプションで「--sum」をつけると列挙した引数の数字を合計するコマンドをArgumentParserとpicocliを利用して実装した例です。

ArgumentParser

まずはpythonのArgumentParserを使った例です。

test.py

import sys
from argparse import ArgumentParser, _SubParsersAction


import argparse

def main():
    parser = argparse.ArgumentParser(description='Process some integers.')
    parser.add_argument('integers', metavar='N', type=int, nargs='+',
                        help='an integer for the accumulator')
    parser.add_argument('--sum', dest='accumulate', action='store_const',
                        const=sum, default=max,
                        help='sum the integers (default: find the max)')

    args = parser.parse_args()
    print(args.accumulate(args.integers))

    return 0


if __name__ == '__main__':
    sys.exit(main())

サンプルを実装したら、実行してみます。

ヘルプ実行例

$ python3 test.py -h
usage: test.py [-h] [--sum] N [N ...]

Process some integers.

positional arguments:
  N           an integer for the accumulator

optional arguments:
  -h, --help  show this help message and exit
  --sum       sum the integers (default: find the max)
$

通常実行(--sumオプションなし)

$ python3 test.py 1 2 3
3
$

通常実行(--sumオプションあり)

$ python3 test.py 1 2 3 --sum
6
$

picocli

TestCommand.scala

package com.example

import java.util.concurrent.Callable

import picocli.CommandLine
import picocli.CommandLine.{Command, Option, Parameters}

import scala.util.{Failure, Success, Try};



object TestCommand {

  def main(args: Array[String]) = {
    System.exit(new CommandLine(new TestCommand).execute(args: _*))
  }

}

@Command(name = "test", mixinStandardHelpOptions = true, version = Array("test sample command 1.0.0"))
class TestCommand extends Callable[Int] {

  @Parameters(arity = "1..*", paramLabel = "N", description = Array("an integer for the accumulator"))
  var integers: Array[Int] = Array()

  @Option(names = Array("-s", "--sum"), description = Array("sum the integers (default: find the max)"))
  var isSum: Boolean = false

  override def call(): Int = {
    Try { if (isSum) integers.sum else integers.max } match {
      case Success(result) =>
        println(result)
        0
      case Failure(exception) =>
        exception.printStackTrace()
        1
    }
  }

}

サンプルを実装したら、実行してみます。(scalaの例ではassemblyで実行可能なjarにあらかじめパッケージングしております)

ヘルプ実行例

$ java -jar test.jar -h
Usage: test [-hsV] N...
      N...        an integer for the accumulator
  -h, --help      Show this help message and exit.
  -s, --sum       sum the integers (default: find the max)
  -V, --version   Print version information and exit.
$

通常実行(--sumオプションなし)

$ java -jar test.jar 1 2 3
3
$

通常実行(--sumオプションあり)

$ java -jar test.jar 1 2 3 --sum
6
$

まとめ

  • コマンド実装をJVM言語で行う時にはpicocliを利用すると面倒な引数のパースを簡略化することができる
  • 使い勝手はpythonのArgumentParserに近くとても使いやすい

ArgumentParserとpicocliを使ってみての感想

以上、pythonとscalaで二つのライブラリを使用して同じコマンドを実装してみましたが、言語差異によるコンパイルなどの手間はありますが、それ以外はpythonでの実装に近い使い勝手だったかなと思いました。

また、今回は簡単な例を実装しましたが、picocliは機能が豊富でドキュメント量が多いのでもっと色んなことができそうです。

まだまだ盛んに開発が行われているので、コマンドやバッチの実装などがある方は導入を検討してみるのもいいと思います。

それでは。

Copyright © astamuse company, ltd. all rights reserved.