0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

売れない俳優 海老原雄一郎です。

これまでに ScalarDB 体験記 と ScalarDB Cluster 体験記 を書いてきました。

相変わらず売れていないので、今度は同じ Scalar 社の別プロダクト ScalarDL を触ってみました。
今回も公式のチュートリアルをやっていきます。

ScalarDLとは

マニュアルはこちら。

説明を意訳してみる。

株式会社Scalar が開発する、データの真正性、特にデータが許可なく変更・改ざんされていないことを保証してくれるデータベースミドルウェア。分散データベース環境におけるビザンチン故障を検知してくれる。

ビザンチン故障とな・・・?

ビザンチン故障

ビザンチン故障に関する詳細は Wikipedia の「ビザンチン将軍問題」の項を参照。

自分の理解で大雑把に説明すると、分散コンピューティング環境においてノード間でデータが食い違ったりデータの整合性を確認できなくなる状態のことを広くビザンチン故障と呼ぶ(たぶん)。ビザンチン故障は悪意のデータ改ざん、ソフトウェアバグ、ハードウェア故障、ネットワーク障害など、様々な要因により発生しうる。

ビザンチン故障に対するアプローチは大きく2種類ある。

  • ビザンチンフォールトトレランス(Byzantine Fault Tolerance, BFT
    • システムはビザンチン故障が発生しても正常な動作を継続できる
  • ビザンチンフォールトディテクション(Byzantine Fault Detection, BFD
    • システムはビザンチン故障を検知できる

BFT の方が理想的だし、ブロックチェーンのように BFT を実現するソリューションも存在する。しかしブロックチェーンは管理の容易性やパフォーマンス、スケーラビリティに欠ける。

一方、ScalarDL が提供するのは(同種・異種)分散データベース環境に特化した BFD だが、これらの欠点を解決しているのが特徴である。

なるほど。
でもそれがどういう場面で役立つのさ?

ScalarDL のユースケース

マニュアルでは代表的なユースケースとして データの真正性の保証データのトレーサビリティの管理 が挙げられている。

データの真正性は、以下のような場面で求められる。

  • GDPR (EU一般データ保護規則) や CCPA/CPRA (カリフォルニア州プライバシー権法) への準拠
  • 財務・税務文書の電子保存
  • 知的財産の先使用権の主張のための証拠
  • WP.29 (国連欧州経済委員会の自動車基準調和世界フォーラム) の無線を用いた自動車のソフトウェア更新に関する規制への準拠

データのトレーサビリティというのは、データの生成、変更、移動、使用などのライフサイクル全体を追跡・記録し、その履歴を管理できるようにすること。いろんな業種でトレーサビリティ確保を求める法規制があったりする。

ScalarDL は改ざん検知可能な追記型台帳データベースによってトレーサビリティを担保してくれる。

つまり ScalarDL とは

データの更新履歴を厳密に管理し、正規の手続きに基づかないデータ更新が発生するとバレちゃうようにするデータベースミドルウェア。

自社で管理しているデータについて、外部に対して「改ざんされていませんよ」と証明するのは思いのほか難しい。なぜなら管理権限さえあれば、改ざんの証拠となる過去のバックアップやアクセスログまで含めて改変するのは技術的には可能だから。ScalarDL を使うとそれが事実上不可能になる。

アセットとコントラクトとファンクション

ScalarDL を触り始める前に理解しておくと良い概念が3つあるので、まずはそれらを紹介しておきたい。
(自分はこれを知らずにチュートリアルをやってだいぶ苦労した)

アセット

ScalarDL がビザンチン故障検知の対象とするデータはアセットと呼ばれ、各アセットは一意な ID を持つ。

アセットは age と呼ばれるバージョン番号を持つ。アセットが最初に作成されると age は 0 となり、その後はアセットが更新されるたびに 1 ずつインクリメントされる。

アセットは論理的な追記専用の台帳型データベースに記録され、過去バージョンの全てが保持される。この特徴により ScalarDL はデータのトレーサビリティを担保する。

アセットの実体はJSON形式の半構造化データと改変検知のためのメタデータのセットである。半構造なので、リレーショナルDBのようにデータ種別によって異なるテーブルに分けて保存したりする必要はなく、ScalarDL は様々な種別のデータを一緒くたに管理する点には注意が必要だ。どんなデータでも放り込める asset という巨大なテーブルが1つだけあり、各レコードの解釈方法は利用側に委ねられているようなものである。

コントラクト

アセットの生成(作成)と更新はコントラクトという Java プログラムによってのみ行うことができる。アプリケーション開発者はアセットを操作する任意のビジネスロジックをコントラクトとして実装できる。

実装したコントラクトは事前に ScalarDL に登録しておき、データ更新時には ScalarDL に実行をリクエストする。

コントラクトもアセットと同様に追記型であり、一度登録すると更新することができない。各アセットの中には、それを作成・更新したコントラクトの情報も記録されているので、アセットがどのようなビジネスロジックによって作成・更新されたのかが検証可能となる。

コントラクトを通さないデータの改変や、コントラクト自体の改変は ScalarDL によって検知される。この特徴により ScalarDL はデータの真正性を担保する。

ファンクション

アセットのデータ構造はアプリケーションからは利用しづらいので、全てのデータをアセットで管理するのはあまり現実的とは言えそうにない。
そこでアプリケーションから ScalarDB API でアクセスできるデータベース(OLTP用DBとでも呼ぶことにする)を別で用意し、通常はそれを利用することにした上で、改ざん検知の必要性が特に高い一部のデータのみアセットとOLTP用DBの両方に記録、必要に応じて両者を照合することでOLTP用DB側の改ざんを検知する、という利用方法が考えられる。

その際にはアセットとOLTP用DBの整合性を確実に保つ必要がある。

そのようなケースで利用できるのがファンクションである。

ファンクションの実体はコントラクト同様、ScalarDL に事前登録される Java プログラムで、ScalarDB の API を利用してOLTP用DBのデータを操作することができる。

ファンクションは単独で実行することはできず、ScalarDL にコントラクト実行をリクエストする際に、同時に実行するファンクションを指定することができる。これによりアセットとOLTP用DBの両方が同期的に更新されるが、この2つの異なるDBに対する更新は ScalarDB の分散トランザクション機能により同一トランザクションで実行されるので、障害等によりどちらか片方しか更新されないといった不整合の発生を防ぐことができる。

ただしコントラクトとファンクションそれぞれの更新内容が互いに整合的であることは、アプリケーション開発者の責任において保証する必要がある。

以上の概念を図で表すと以下のようになる。

ScalarDL の物理構成

ScalarDL の主な構成要素は LedgerAuditorClient SDK の3つ。

Ledger と Auditor は ScalarDL のサーバーコンポーネントで、クライアントアプリケーションは Client SDK を利用して ScalarDL にアクセスする。

Ledger

Ledger はコントラクトの登録・実行、アセットの検証(改ざん検知)等を行うコンポーネント。

アセットやコントラクト等を管理するためのデータベースを1つ持つのに加え、ファンクションが更新する参照用データベースを任意の個数持つことができる。

これらのデータベースはどちらも「バックエンドDB」と呼ばれるので、混乱しないよう注意されたし。単なる想像だが、ScalarDL は内部で ScalarDB を利用しているが、おそらく ScalarDB のレイヤーでは両者を区別していないためであろう。

なおバックエンドDBとして利用できるDBの種類は ScalarDB に準ずる。

Auditor

Auditor は Ledger の監査を行うためのコンポーネント。

Auditor も Ledger と同じく、アセットやコントラクト等を管理するためのバックエンドDBを1つ持つ。両者が同一の情報を管理することで、Auditor と Ledger のどちらかが正しく動作していれば、データベースシステムにおけるビザンチン故障の検知が保証されるということのようだ。

Auditor の利用はオプショナルであり、Auditor なしで ScalarDL を構成することもできるが、完全なビザンチン故障検出はできなくなる。

本来 Ledger と Auditor の管理組織は分けないと監査の意味がなくなり、ビザンチン故障の検出レベルが下がるが、システム運用の都合で分けることが難しい場合は Auditor の実質的な意味がなくなるので、それならいっそ Auditor なしの簡易構成にしても一緒、ということなのだと思う(想像)。

Client SDK

ScalarDL のクライアントアプリケーションが ScalarDL の Ledger と Auditor にアクセスし、コントラクトの実行などを行うためのライブラリ。

以下の言語向けの SDK が提供されている。

  • Java
  • Node.js
  • ブラウザ内 JavaScript
  • Go

チュートリアルに挑戦

ドキュメントでは Auditor なしバージョンと Auditor ありバージョンの2種類のチュートリアルが用意されている。
自分は両方チャレンジしてみたが、Auditor ありバージョンの方は Auditor 向けの設定や、Auditor の起動手順が追加になっている程度だったので、ここでは主に Auditor なしバージョンのみ触れることにする。

1. Docker で ScalarDL をインストール

Docker と Docker Compose がインストールされているのが前提。

1-1. scalardl-samples リポジトリをクローン

ScalarDL のサンプルアプリケーションである scalar-labs/scalardl-samples リポジトリをクローンする。

$ git clone https://github.com/scalar-labs/scalardl-samples.git
$ cd scalardl-samples

1-2. Docker ログイン

ScalarDL のコンテナイメージはプライベートな GitHub Container Registry にホストされているので、アクセスするには正規ライセンスに基づく適切な権限が付与された GitHub アカウントと(詳細は割愛)、read:packages スコープが付与された Personal Access Token (PAT) が必要である。
自分の場合は ScalarDB Cluster のチュートリアルをやったときと同じ PAT を再利用することができた。

$ export CR_PAT=YOUR_PERSONAL_ACCESS_TOKEN
$ echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin

1-3. Ledger とバックエンドDBを起動

次のコマンドを実行すると、Ledger とバックエンドDBとして利用する Cassandra (とその他にもいくつか)のコンテナが起動する。

$ docker-compose up -d

2. JDK のインストール

これは ScalarDB のときにやった し、利用可能な JDK バージョンも同じなので、今回はスキップ。

Client SDK のダウンロード

Java 用の SDK である scalardl-java-client-sdk をダウンロードする。

まずはこのページで、最新のバージョン番号を確認する。2024年7月25日現在では v3.9.1 だったので、バージョン番号は v を除いた 3.9.1 だ。

次にターミナルを開き、シェル変数に確認したバージョン番号をセットする。

$ VERSION=3.9.1

ではリポジトリをクローンしたり、最新バージョンをチェックアウトしたり、関連ツール類をダウンロードしたり。

$ git clone https://github.com/scalar-labs/scalardl-java-client-sdk.git
$ cd scalardl-java-client-sdk
$ git checkout v$VERSION
$ curl -OL https://github.com/scalar-labs/scalardl-java-client-sdk/releases/download/v$VERSION/scalardl-java-client-sdk-$VERSION.zip
$ unzip scalardl-java-client-sdk-$VERSION.zip
$ mv scalardl-java-client-sdk-$VERSION client

次の2つのステップは、オリジナルのチュートリアルとは順番を入れ替えて紹介する。たぶんその方が分かりやすいから。

3. 証明書の作成

まずは証明書の作成。

チュートリアルからリンクされている以下のページのガイドに従う。

3-1. 前提条件

Go を asdf でインストールする。
プラグインを入れてから、存在する Go バージョンを確認。

$ asdf plugin add golang
$ asdf list all golang

いちばん新しい安定版っぽいバージョンをインストール。今回は 1.22.4

$ asdf install golang 1.22.4

OpenSSL も必要。まだ入っていなければ Homebrew でインストール。

$ brew install openssl

cfssl と cfssljson というコマンドも必要らしい。Go バージョンが 1.18 以上なら次のコマンド一発で両方ともインストールできる

$ go install github.com/cloudflare/cfssl/cmd/...@latest

3-2. 秘密鍵と CSR(証明書署名要求)の作成

この作業は、Client SDK の scalardl-java-client-sdk ディレクトリをカレントとして行う。

まずは秘密鍵を作成する。

$ openssl ecparam -name prime256v1 -out prime256v1.pem

prime256v1.pem という秘密鍵ファイルがカレントディレクトリに作成されるので、それを使って CSR(証明書署名要求)ファイルを作成する。

$ openssl req -new -newkey ec:prime256v1.pem -nodes -keyout client-key.pem.pkcs8 -out client.csr

Country Name とか State or Province Name とかいろいろ質問されるが、検証用なので真面目に入力する必要はなく、すべて空のまま Enter キーを連打して問題ない。
全ての質問への回答が終わると、カレントディレクトリに client-key.pem.pkcs8client.csr という2つのファイルが作成される。

最後に PKCS#8 形式の client-key.pem.pkcs8 を PEM 形式に変換する。

$ openssl ec -in client-key.pem.pkcs8 -out client-key.pem

client-key.pem ファイルが作成される。

3-3. 認証局から証明書ファイルを受け取る

実は先ほど docker-compose を実行したときに、CFSSLサーバーというコンテナも起動されていて、これに今作成した CSR を送信すると、署名された証明書を受け取ることができる。
これの細かい意味については割愛。

$ cfssl sign -remote "localhost:8888" -profile "client" client.csr | cfssljson -bare client -

署名された証明書ファイル client.pem が作成される。

4. Client SDK のプロパティ設定

conf/client.properties というサンプルファイルをベースに、必要箇所だけ書き換えて使う。

まずはサンプルファイルをコピー。

$ cp conf/client.properties .

そのファイルを適当なテキストエディターで開く。
まずは scalar.dl.client.cert_path の行を探す。

# Required. The path of the certificate file in PEM format.
scalar.dl.client.cert_path=/path/to/foo.pem

この /path/to/foo.pem の部分を、前のステップで作成した client.pem ファイルの絶対パスに置換する。

次は scalar.dl.client.private_key_path の行。

# Required. The path of a corresponding private key file in PEM format to the certificate.
scalar.dl.client.private_key_path=/path/to/foo-key.pem

この /path/to/foo-key.pemclient-key.pem ファイルの絶対パスで置換する。

書き換え箇所は以上。保存してエディターを閉じる。

5. 証明書の登録

ScalarDL の Ledger に証明書を登録する。
SDK に含まれている client/bin/scalardl というツールが利用できる。

$ client/bin/scalardl register-cert --properties client.properties
{
  "status_code" : "OK",
  "output" : null
}

このように OK が返ってくれば成功。

6. コントラクトの作成

作成、というか、Java SDK の中に StateUpdater というサンプルコントラクトが src/main/java/com/org1/contract/StateUpdater.java として用意されているのでそれを使う。

コード全体についてはチュートリアルを参照してもらうとして、日本語で簡単に解説しておく。

public class StateUpdater extends JacksonBasedContract {

  @Nullable
  @Override
  public JsonNode invoke(Ledger<JsonNode> ledger, JsonNode argument, @Nullable JsonNode properties) {
    ...

StateUpdater は Java で書かれたクラスで、ScalarDL が提供する JacksonBasedContract というクラスを継承し、抽象メソッド invoke() をオーバーライドしている。

コントラクト実行時には任意のパラメータを指定することが可能で(コントラクト引数)、それらは invoke()argument 引数にマップ形式のデータとして割り当てられる。

このサンプルではアセットID asset_id と状態 state という2つのコントラクト引数を受け取る想定となっており、argument 引数からこんな感じで抽出できる。

    String assetId = argument.get("asset_id").asText();
    int state = argument.get("state").asInt();

次に指定されたアセットIDを持つ既存アセットがないか確認する。

    Optional<Asset<JsonNode>> asset = ledger.get(assetId);

なければ新規アセットを作成、あれば既存アセットを更新する(ただし state の値に変更がなければ何もしない)。

    if (!asset.isPresent() || asset.get().data().get("state").asInt() != state) {
      ledger.put(assetId, getObjectMapper().createObjectNode().put("state", state));
    }

この作成 or 更新は ledger.put() 一文で記述できる。いわゆる UPSERT みたいな感じだ。

コントラクトの実行内容を確認したらコンパイルしよう。

$ ./gradlew assemble
Starting a Gradle Daemon (subsequent builds will be faster)
...
BUILD SUCCESSFUL in 2s
3 actionable tasks: 3 up-to-date

これで build/classes/java/main/com/org1/contract/StateUpdater.class という class ファイルが作成される。

7. コントラクトの登録

ScalarDL にはコントラクトの class ファイルを事前登録しておく必要がある。
コントラクトの登録は証明書の登録と同じ client/bin/scalardl を利用して行う。

$ client/bin/scalardl register-contract --properties client.properties --contract-id StateUpdater --contract-binary-name com.org1.contract.StateUpdater --contract-class-file build/classes/java/main/com/org1/contract/StateUpdater.class
{
  "status_code" : "OK",
  "output" : null
}

各パラメータについて簡単に説明。

  • --properties
    • Client SDK のプロパティ設定が記述されたファイルのパス
  • --contract-id
    • コントラクトID = コントラクトを一意に識別可能な名前
    • ユニークでありさえすれば何でもいい
  • --contract-binary-name
    • コントラクトの Java クラス名
  • --contract-class-file
    • コントラクトの Java クラスファイルのパス

この記事の上の方で、

コントラクトもアセットと同様に追記型であり、一度登録すると更新することができない

と説明したが、コントラクトの中身を書き換えて再登録したい場合は注意が必要だ。
まず同じクラス名で上書き登録できないので、クラス名を変更する必要がある。ということは、Javaソースファイル名も合わせて変更することになり、出力されるクラスファイル名も変更になる。
そしてコントラクトIDも新しくしないといけない。

なので StateUpdaterV1StateUpdaterV2 のように、クラス名やコントラクトIDのサフィックスにバージョン番号を付加する運用ルールが必要になりそう。

8. コントラクトの実行

いよいよコントラクトを実行してみる。
これも client/bin/scalardl で行う。

some_asset というアセットIDで新しいアセットを作成し、その中に state = 3 という値を記録する。

$ client/bin/scalardl execute-contract --properties client.properties --contract-id StateUpdater --contract-argument '{"asset_id":"some_asset", "state":3}'

何のメッセージもなくコマンド終了。

$ echo $?
0

成功はしている。

ちなみにコントラクトの実行は client/bin/scalardl コマンドを使わなくてもできる。
ClientService というクラスを利用すると、任意の Java プログラム内からコントラクトを実行可能だ。

9. アセットの状態検証

Ledger に ID が some_asset であるアセットの検証をリクエストする。

$ client/bin/scalardl validate-ledger --properties client.properties --asset-id="some_asset"
{
  "status_code" : "OK",
  "Ledger" : {
    "id" : "some_asset",
    "age" : 0,
    "nonce" : "2d6a91e0-60c2-402c-b4b4-f8dd85fc401e",
    "hash" : "Kk2bLbMN5mR4/pnTfJEI13aahPzpOFMnVBdGCHbtyCY=",
    "signature" : "MEUCIB7NFc4M0Hgze+Umtk9+JNnWTaYjybKan95CVP1Pdwn8AiEAhI+RAr8+ApiJUiHfePvPOhihhM7m6lrbFlWziE49oII="
  },
  "Auditor" : null
}

やはりちゃんと登録されていたようだ。
ただこのコマンドで分かるのはアセットの状態が正しいかどうか(改ざんされていないかどうか)だけで、アセットの中身(この場合は state 値)までは分からないようだ。

気になるから Cassandra の中を直接覗いてみちゃおうかな。
まずは Cassandra のコマンドラインツールが欲しいので、ローカルに Cassandra をインストール。

$ brew install cassandra

Cassandra クライアントは cqlsh というらしい。

$ cqlsh
WARNING: cqlsh was built against 4.1.5, but this server is 3.11.17.  All features may not work!
Connected to Test Cluster at 127.0.0.1:9042
[cqlsh 6.1.0 | Cassandra 3.11.17 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
cqlsh>

お、つながった。

cqlsh> select * from
coordinator.        scalar.             system.             system_auth.        system_distributed. system_schema.      system_traces.

select * from までタイプしてからタブキーを打ったらコマンド補完が出てくれたぞ。
scalar. ってのがあやしい。

cqlsh> select * from scalar.
asset          asset_metadata certificate    contract       contract_class function       secret         tx_state

select * from scalar. までタイプしてからタブキーでテーブル名の候補が出た。
ここは asset だろ。

cqlsh> select * from scalar.asset;

 id         | age | argument                                                                              | before_argument | before_contract_id | before_hash | before_input | before_output | before_prev_hash | before_signature | before_tx_committed_at | before_tx_id | before_tx_prepared_at | before_tx_state | before_tx_version | contract_id        | hash                                                               | input | output      | prev_hash | signature                                                                                                                                        | tx_committed_at | tx_id                                | tx_prepared_at | tx_state | tx_version
------------+-----+---------------------------------------------------------------------------------------+-----------------+--------------------+-------------+--------------+---------------+------------------+------------------+------------------------+--------------+-----------------------+-----------------+-------------------+--------------------+--------------------------------------------------------------------+-------+-------------+-----------+--------------------------------------------------------------------------------------------------------------------------------------------------+-----------------+--------------------------------------+----------------+----------+------------
 some_asset |   0 | V2\x012d6a91e0-60c2-402c-b4b4-f8dd85fc401e\x03\x03{"asset_id":"some_asset","state":3} |            null |               null |        null |         null |          null |             null |             null |                   null |         null |                  null |            null |              null | foo/1/StateUpdater | 0x2a4d9b2db30de66478fe99d37c9108d7769a84fce93853275417460876edc826 |    {} | {"state":3} |      null | 0x304502200fee8470db9d741f8096abce3b439d0ef96321dd214c2268fec40c0bc7585aaa022100cc1baa5c0cea89a8ec0abd13c9ef7c94555cd123388767a23b160cc39ef02e48 |   1721966918603 | 2d6a91e0-60c2-402c-b4b4-f8dd85fc401e |  1721966918560 |        3 |          1

(1 rows)

カラムがたくさんあるけど、output 列の値に {"state":3} というのが見えるね!

10. オリジナルのアセットを作成してみよう!

ここまでの情報を参考に、次はオリジナルのアセットを作ってみよう!
次のページが参考になるよ!

というわけで、チュートリアル本編はここまで。

11. お片付け

Ledger と Cassandra のコンテナを停止しておく。

$ docker-compose down

ちなみに Auditor ありバージョンのチュートリアル

こっちが Auditor ありバージョン。

Auditor なしと同様に Docker を利用するのが最初は現実的なので、そのための注意点をいくつか。

まず Auditor なしチュートリアルで作られた Ledger のバックエンドDBはクリアしておくこと。
Cassandra が使っていた Docker ボリュームを削除するだけで、次に Ledger を起動するときにはまっさらな Cassandra データベースが再作成される。

"Configure properties", "Start Ledger and Auditor", "Register each certificate of Ledger and Auditor" のステップは不要で、Docker 環境構築ガイドに記載されている以下のコマンドを実行するだけで Ledger と Auditor が起動し、証明書の登録も完了する。

$ docker-compose -f docker-compose.yml -f docker-compose-auditor.yml up -d

Auditor ありで ScalarDL を起動できてしまえば、後のコントラクト登録や実行は Auditor のあるなしで特に変わることはない。

最後、Ledger と Auditor を停止するときのコマンドはこう。

$ docker-compose -f docker-compose.yml -f docker-compose-auditor.yml down

まとめ

チュートリアルをひと通りやってみた結果、それだけだとなかなか ScalarDL の仕組みや価値を理解するのは難しいなと思ったので、コンセプトや機能の説明の方にずいぶんと力を入れてしまいました。
特に、ScalarDL を利用する際の勘所になりそうだと感じたアセット・コントラクト・ファンクションまわりを丁寧に。

セキュリティ系のプロダクトって、ただ使っているだけではその良さが分からないことも多いですが、ScalarDL もその例に漏れず、といった感じですね。

この記事が今後 ScalarDL に触れようとする人の参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?