Datomic Ions ワークショップに参加した

## Clojure/Conj 2019
昨年に引き続き、Cognitect社が本拠を置くノースカロライナ州ダーラムにて開催された[Clojure/Conj 2019](http://2019.clojure-conj.org/)に参加してきました。2回目ということで変化をつけるべく、カンファレンスの前日に開催されたDatomic Ionsワークショップに参加してきました。Cognitect社の社長であり、Datomicの主要開発者であるStuart Hallowayが講師としてハンズオンで教えてくれる形式でした。本稿では質疑応答で学んだことを混じえながらIonsを紹介します。

## Datomic Ionsとは
一文でまとめると、Datomic Cloud上で、Datomicのデータを必要とするビジネスロジックを、ネットワーク越しにクエリするオーバーヘッドなしに実行するための仕組みです。

本稿はDatomic Cloudをある程度理解していることが前提となります。初めての方は[Datomic on AWS](https://qiita.com/k2nakamura/items/28b48df33ebfdff2495e)をご参照ください。

Ionsが登場した経緯は、Datomic APIの変遷をたどることで理解することができます。
## Datomic APIの変遷
### Peer API
Datomicは当初、Peer APIのみがサポートされていました。Peer APIは、各々のアプリケーションのクラスパスに、DynamoDBなどのストレージにアクセスするためのライブラリと、Datalogのエンジンを含めることで、アプリのプロセス内でクエリを実行する仕組みになっていました。利点として、一つのDatalogクエリで複数のDBを対象に実行できること、取得したデータや新たにトランザクトされたデータがプロセス内のHeapにキャッシュとして残るため、同一のデータを対象にしたクエリ（SQLと異なり、前回実行したクエリに限りません）はストレージにアクセスする必要がないため、実質インメモリデータベースとしての速度を得られる点が挙げられます。

### Client API
しかし、Peer APIはDatomicとアプリケーションが深く結合しているモデルなので、Datomicとアプリケーションを分離して管理することができません。また、マイクロサービスアーキテクチャやサーバーレスでは、プロセスの寿命が短いため、そのたびにキャッシュを捨ててコールドスタートとなります。プロセスの数が増えると、ストレージサービスへのコネクション数も増えることになり、選択したストレージの種類によってはコネクション管理のオーバヘッドが問題になる場合もあるでしょう。
それらの短所を補うために、Peer Serverという、Peer API部分だけを独立したプロセスとして分離し、トランザクションとクエリをHTTPプロトコルで受け取り、結果をレスポンスとして返すサーバを提供するとともに、そのサーバと対話するためのHTTPクライアントベースのライブラリをClient APIとして提供するようになりました。


### Client APIの欠点
アプリケーションからPeer ServerへのHTTP問い合わせが増えた分のパフォーマンスオーバーヘッドが生じるようになりました。
また、クエリはPeer Server内で実行されるため、Peer APIでは可能だった、JavaとClojure Core以外の関数を使ったカスタムロジックをトランザクションやクエリに含めることができなくなりました。

PeerServerはDB名を引数に起動しているため、複数DBにまたがってのクエリができません。この点についてStuart Hallowayは、Client API設計時の考慮モレで、理論的には容易に対応できるが、既存コードのリファクタリングが必要となるため、技術的負債として認識しているとのことでした。

### Classpath Functions
カスタムロジックの問題に対処するため、Datomic TransactorやPeer ServerにカスタムJARファイルを含める手段が提供されるようになりました。 
Datomic OnPremではユーザーが実行環境を管理しているので、カスタムJARファイルを追加するのは自分の責任で行うことができますが、Datomic Cloudでは環境がCloudFormationで標準化しているため、Cognitect社としてはカスタムJARファイルを、マネージドサービスとして追加する手段を講じる必要がでてきました。

### Ionsの登場
Datomic Cloudのユーザーである開発者が、カスタムJARファイルをトランザクションやクエリのためにCloud環境にデプロイする仕組みをさらに発展させ、ビジネスロジックそのものも同じ仕組みの上で動かそうというアイデアがIonsになります。つまり、Client APIの短所であった、HTTPリクエストのホップを１段減らし、ビジネスロジックとPeer Serverを同一のサーバ上で動作させることでパフォーマンスの向上を狙うソリューションとなっています。

## Ions開発の流れ
実践する前に、開発の大まかな流れを押さえておきましょう。
![Ionsアーキテクチャ](https://docs.datomic.com/cloud/images/ions-who-do.png)

### 開発
JARレベルだけでなく、namespaceレベルでも依存関係を分析し、ソースコードを差分としてS3にアップロードするため、LeiningenやBootではなく、[tools.deps](https://clojure.org/guides/deps_and_cli)をプロジェクト管理ツールとして用いる必要があります。
tools.depsを使うにあたってのTipsをまとめてあるので、ご参照ください。
[Clojure CLI (tools.deps)を使いやすくするためのTips](https://qiita.com/k2nakamura/items/03eed17a06a4b963dac5)

`com.datomic/ion-dev`を`:ions-dev`エイリアスの依存関係として定義し、このエイリアスを使って`push`, `deploy`などの作業を行います。(例: `clojure -A:ions-dev '{:op :push}'`)

`datomic/ion-config.edn`をresourcesに作成します。

```clojure:datomic-ion-config.edn
{:app-name "myapp" ;; <- 必須。Lambdaの関数名の一部として使われる
 :lambdas          ;; <- CloudWatch Eventなどから直接呼び出す関数
   {:hello-world {:fn myapp.lambda/hello-world 
                  :description "Hello World"}}
 :http-direct      ;; <- API Gateway経由で呼び出すHTTPハンドラ
   {:handler-fn myapp.http/hello-world}
 :allow            ;; <- Transact/Query fnとして呼び出せるようにする
   [myapp.db-fn/valid-name?]}
```

### Push

開発が一段落ついた時点で、コードをS3に保存することができます。このPushはGit pushとは異なります。IonsはGitに依存していますが、GitHubなどのリモートリポジトリには依存していません。

まず、コードをローカルのGitリポにコミットします。Git Stageがクリーンな場合のみIons pushを実行できます。Ions pushは現時点でのソースコードと依存関係をS3 bucketにアップロードします。２回め以降は変更部分のみが差分としてアップロードされます。

`deps.edn`内で、依存関係のprocurerに`:local/root`を使うこともできます。それにより、開発途上のライブラリを含めてデプロイすることが可能です。ただしこの場合、ソースコードがimmutableではないので、ランダムなsuffixがバージョン末尾についてユニーク性を担保します。

`clojure -A:ions-dev '{:op :push}'` を実行すると下記のようなメッセージがレスポンスとして送られてきます。

```clojure
...
:deploy-command
 "clojure -A:ion-dev '{:op :deploy, :group myapp-Compute-RANDOM_ID, :rev \"d38bb30be124d878b37eae5b50ae32201fd459ef\"}'",
 :doc
 "To deploy to myapp-Compute-RANDOM_ID, issue the :deploy-command"}
```

このコマンドをコピーペーストしてDeployすることができます。

### Deploy
実行するとstatusを取得するためのコマンドが表示されるので、コピーペーストしてデプロイが完了したかどうかを確認することができます。

```clojure
{:execution-arn
 arn:aws:states:us-west-2:000000000000:execution:datomic-myapp-Compute-RANDOM_ID:myapp-Compute-RANDOM_ID-d38bb30be124d878b37eae5b50ae32201fd45-1574279299988,
 :status-command
 "clojure -A:ion-dev '{:op :deploy-status, :execution-arn arn:aws:states:us-west-2:000000000000:execution:datomic-myapp-Compute-RANDOM_ID:myapp-Compute-RANDOM_ID-d38bb30be124d878b37eae5b50ae32201fd45-1574279299988}'",
 :doc
 "To check the status of your deployment, issue the :status-command."}
```

ステータスを確認します。

```clojure
clojure -A:ion-dev '{:op :deploy-status, :execution-arn arn:aws:states:us-west-2:000000000000:execution:datomic-myapp-Compute-RANDOM_ID:myapp-Compute-RANDOM_ID-d38bb30be124d878b37eae5b50ae32201fd45-1574279299988}'

{:deploy-status "RUNNING", :code-deploy-status "RUNNING"}

clojure -A:ion-dev '{:op :deploy-status, :execution-arn arn:aws:states:us-west-2:000000000000:execution:datomic-myapp-Compute-RANDOM_ID:myapp-Compute-RANDOM_ID-d38bb30be124d878b37eae5b50ae32201fd45-1574279299988}'

{:deploy-status "SUCCEEDED", :code-deploy-status "SUCCEEDED"}
```

## Ionsの利点
### CD(Continuous Delivery)機能
Datomic IonsはAWS S3, CodeDeploy, Step Functionsを活用したCDパイプラインを提供しています。開発者はトランザクション・クエリファンクションやビジネスロジックをClojureの関数として開発することだけに集中し、`push`と`deploy`を行うCLIコマンドを実行するだけでコードをAWS環境にデプロイすることができます。

### データとビジネスロジックの距離の近さ
データアクセスを行うビジネスロジックとPeer Serverが同じサーバ群で動作するので、例えば業務種別ごとに専用のComputing Groupを割り当てることで、必要となるデータを集中的にキャッシュするローカリティを実現することができます。

### カスタムJARによるトランザクション・クエリ関数の拡張
上で述べたように、サードパーティライブラリを使ってDatomicの動作を拡張することができます。

### CloudWatch Eventとの連携
デプロイされたビジネスロジックは、Lambdaを経由して実行することができるので、様々な[CloudWatch Eventタイプ](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/EventTypes.html)をトリガーとして利用することが可能になります。

### API Gateway経由のHTTPエンドポイント
ビジネスロジックはLambda経由で実行できるので、API Gatewayを経由してHTTPエンドポイントとして公開することが可能です。

### ビジネスロジックとPeer Serverのオートスケール
ビジネスロジックとPeer Serverが同じサーバ群で動作するので、機能別にグループ分けしておけば、特定の機能の需要が高まった場合に、まとめてスケールさせることができます。

## やってみる

### Datomic Cloud環境の立ち上げ

[Datomic](https://www.datomic.com/)のサイトに行くと、AWS Marketplaceのバッジがあるので、ここからスタートします。
<img width="1038" alt="Datomic_-_Overview.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38240/ba59b279-e0a8-f348-3e77-22d4cf67d070.png">

Ionsを利用するためには、
[Setting up](https://docs.datomic.com/cloud/setting-up.html)の指示に従って設定してください。AWS Marketplaceの登録情報が間違っていたり、最新になっていない場合があるので、設定確認画面で、[最新のリリース](https://docs.datomic.com/cloud/releases.html#current)が選択されているか確認してください。

<img width="1255" alt="AWS_Marketplace__Datomic_Cloud.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38240/49303f59-ceff-a1e7-9774-f77c1b262db0.png">

[Ionsを動かすには新しいClojure CLIが必要](https://docs.datomic.com/cloud/troubleshooting.html#ion-dev-clojure-cli)ですので、バージョンを確認して、必要であれば最新版に更新してください。

## チュートリアルを動かしてみる
学習用のリポジトリがあるのでクローンします。
https://github.com/datomic/ion-starter

### deps.edn

`deps.edn`内で`ion-dev`を依存関係として定義します。このJarはデプロイ作業のみに使われるだけで、アプリケーション実行に必要なランタイムの依存関係としては必要ありません。つまり、Ionsとしてデプロイする関数はIons固有のnamespaceを依存関係として定義する必要がないということです。

```clojure
{...
 :mvn/repos {"datomic-cloud" {:url "s3://datomic-releases-1fc2183a/maven/releases"}
              "central"      {:url "https://repo1.maven.org/maven2/"}
              "clojars"      {:url "https://clojars.org/repo/"}}
...
 :aliases {:ion-dev {:deps {com.datomic/ion-dev {:mvn/version "0.9.247"}}
                     :main-opts ["-m" "datomic.ion.dev"]}}
...}
```

### resources/datomic/ion/starter/config.edn
この`config.edn`はDatomic Client APIの[client](https://docs.datomic.com/client-api/datomic.client.api.html#var-client)を生成するために使われます。

```clojure
{:server-type :ion ;; <== :cloudではなく:ionを指定
 :region "ap-northeast-1" ;; <== AWSリージョン
 :system "advent-2019" ;; <== Datomic cloud設定時に指定したシステム名
 :endpoint "http://entry.advent-2019.ap-northeast-1.datomic.net:8182" ;; <== システム名とリージョンを上記と一致させる
 :proxy-port 8182 ;; <== SOCKS proxyのポート
}
```

### resources/datomic/ion-config.edn
ionsの動作を定義するファイルです。Ionには下記の４タイプがあります。

|Ionタイプ | 引数 | 返り値 |
|:--------|:-----|:-----|
|トランザクション関数 | db + データ | トランザクションデータ | 
|クエリ関数 | データ | データ | 
|lambda関数 |:input JSON, :context map | String, InputStream, ByteBuffer, Fileのいずれか|
|web関数 | httpリクエスト | httpレスポンス | 

定義の例です。

```clojure
{:allow [datomic.ion.starter.attributes/valid-sku?]
 :lambdas {:get-schema
           {:fn datomic.ion.starter.lambdas/get-schema
            :description "returns the schema for the Datomic docs tutorial"}
           :get-items-by-type
           {:fn datomic.ion.starter.lambdas/get-items-by-type
            :description "return inventory items by type"}
           :get-items-by-type-lambda-proxy
           {:fn datomic.ion.starter.http/get-items-by-type-lambda-proxy
            :description "lambda proxy integration entry point for get-items-by-type"}
           }
 :http-direct {:handler-fn datomic.ion.starter.http/get-items-by-type}
 :app-name "advent-2019"}
```

トランザクション関数Ion、クエリ関数IonはDatomicの振る舞いを拡張するものです。（RDBMSにおけるストアド・プロシジャ的なもの）これらは `:allow`キーワードをキーとする配列で定義し、Datomic Client API経由で発行する`datomic.client.api/transact`や`datomic.client.api/q`からアクセス可能にします。

Lambda Ionは、関数をAWS Lambda経由でアクセス可能にするものです。`:lambdas`マップで定義された関数が、AWS Lambdaに登録され、アクセス可能になっています。

例えば、`datomic.ion.starter.lambdas/get-items-by-type`が登録されています。ソースはこのようになります。`starter`でDatomicのクエリのラッパー関数を呼んでいますが、Datomicを全く使わない関数でも構いません。

```clojure:lambdas.clj
(ns datomic.ion.starter.lambdas
  (:require
   [clojure.data.json :as json]
   [datomic.client.api :as d]
   [datomic.ion.starter :as starter]
   [datomic.ion.starter.edn :as edn]))
...
(defn get-items-by-type
  "Lambda ion that returns items matching type."
  [{:keys [input]}]
  (-> (starter/get-db)
      (starter/get-items-by-type (-> input json/read-str keyword)
                             [:inv/sku :inv/size :inv/color])
      edn/write-str))
```
これを`push`, `deploy`すると、下記のように実行できるようになります。

```
👉 aws lambda invoke --function-name advent-2019-Compute-RANDOM_ID-get-items-by-type --payload '"shirt"' /dev/stdout
[[#:inv{:sku "SKU-28", :size :xlarge, :color :green}]
 [#:inv{:sku "SKU-36", :size :medium, :color :blue}]
 [#:inv{:sku "SKU-48", :size :small, :color :yellow}]
```
`:lambdas`マップで定義された関数が、AWS Lambdaに登録され、アクセス可能になっています。

Web IonはHTTPリクエストを受け取り、レスポンスを返すclojure関数です。Soloトポロジー（開発・小規模本番環境用の高可用性がないプラン）の場合は、`datomic.ion.lambda.api-gateway/ionize`でそのWeb関数をLambda関数に変換し、API GatewayのProxy Serviceを利用することで公開することができます。Productionトポロジーの場合は`:http-direct`にWeb関数を登録し、VPC Linkを経由してDatomic Ionsスタックが起動しているNLBとAPI Gatewayを連携させます。

Web Ionを定義

```clojure:Web関数
(ns datomic.ion.starter.http
  (:require
   [clojure.java.io :as io]
   [datomic.ion.starter :as starter]
   [datomic.ion.starter.edn :as edn]
   [datomic.ion.lambda.api-gateway :as apigw]))
...
(defn get-items-by-type
  "Web handler that returns info about items matching type."
  [{:keys [headers body]}]
  (let [type (some-> body edn/read)]
    (if (keyword? type)
      (-> (starter/get-db)
          (starter/get-items-by-type type [:inv/sku :inv/size :inv/color])
          edn/write-str
          edn-response)
      {:status 400
       :headers {}
       :body "Expected a request body keyword naming a type"})))
```
Soloトポロジー用に、Web IonをLabmda Ionに変換する

```clojure:ionize
(ns datomic.ion.starter.http
  (:require
   [clojure.java.io :as io]
   [datomic.ion.starter :as starter]
   [datomic.ion.starter.edn :as edn]
   [datomic.ion.lambda.api-gateway :as apigw]))
...

(def get-items-by-type-lambda-proxy
  (apigw/ionize get-items-by-type))
```

`ion-config.edn`でLambda Ionとしてionizeした関数を登録

```clojure:lambdas
{:lambdas {:get-items-by-type-lambda-proxy
           {:fn datomic.ion.starter.http/get-items-by-type-lambda-proxy
            :description "lambda proxy integration entry point for get-items-by-type"}
           }
 :app-name "advent-2019"}
```
API GatewayでProxyエンドポイントを定義
![Cursor_and_API_Gateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38240/90aef815-41f8-fb3d-38c5-59850b7430f4.png)

`deploy`によって登録済みのLambda関数を指定
![Cursor_and_API_Gateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38240/36bac08c-00bf-6e75-8159-cea2d3963594.png)

Curlで関数の呼び出しが可能

```
👉 curl -s https://02dl854n3a.execute-api.ap-northeast-1.amazonaws.com/dev/datomic -d :hat |base64 --decode
[[#:inv{:sku "SKU-51", :size :small, :color :yellow}]
 [#:inv{:sku "SKU-19", :size :small, :color :green}]
 [#:inv{:sku "SKU-15", :size :xlarge, :color :red}]
 [#:inv{:sku "SKU-11", :size :large, :color :red}]
 [#:inv{:sku "SKU-55", :size :medium, :color :yellow}]
 [#:inv{:sku "SKU-7", :size :medium, :color :red}]
 [#:inv{:sku "SKU-63", :size :xlarge, :color :yellow}]
 [#:inv{:sku "SKU-31", :size :xlarge, :color :green}]
 [#:inv{:sku "SKU-47", :size :xlarge, :color :blue}]
 [#:inv{:sku "SKU-23", :size :medium, :color :green}]
 [#:inv{:sku "SKU-35", :size :small, :color :blue}]
 [#:inv{:sku "SKU-43", :size :large, :color :blue}]
 [#:inv{:sku "SKU-3", :size :small, :color :red}]
 [#:inv{:sku "SKU-27", :size :large, :color :green}]
 [#:inv{:sku "SKU-39", :size :medium, :color :blue}]
 [#:inv{:sku "SKU-59", :size :large, :color :yellow}]]
```

## まとめ
### Datomic Cloudの弱点を補う魅力
Datomic Cloudでできなかった、サードパーティライブラリも含む任意のロジックでトランザクション時に実行するロジックを拡張したり、クエリデータの変換やフィルタリングをIonsで行うことができるようになりました。Datomic Cloudの機能拡張が必要な場合は、Datomic CloudにIonsはすでに含まれているので、すぐにメリットを享受することができます。

### LambdaのContinuous Deliveryプラットフォームとしての魅力
Clojure関数をLambdaに登録するためには[mhjort/clj-lambda-utils](https://github.com/mhjort/clj-lambda-utils)や[juxt/pack.alpha](https://github.com/juxt/pack.alpha#lambda)とAWS CLIを使ってzipファイルをアップロードすることになりますが、IonsではAWS Code DeployとStep Functionsを用いて自動化しており、`clojure -A:dev-ions {:op ...}`でpushやdeployをすることができます。

また、ユーザが記述した関数が直接Lambdaとして登録されるわけではなく、`datomic.ion.lambda.handler.Thunk`がProxyとしてLambdaに登録されており、実際の関数はDatomic Cloudが起動したCompute GroupのEC2上で動作するため、コードサイズの上限などLambdaの制限にとらわれずにコードを記述できるのもメリットです。

### スケーラビリティ
Datomicを利用したWebアプリケーションを開発した場合、Httpリクエストの増大に伴い、Web関数やLambda関数のキャパシティを増やす必要があります。Query GroupのEC2サーバ上ではWeb関数やLambda関数とともにPeer Serverも作動しているので、Datomicのクエリ側のキャパシティがまとまってスケールするメリットがあります。

### コスト面のデメリット
IonsはDatomic CloudのQuery Groupで動作します。IonsはDatomicを利用しない場面でも有用であるにも関わらず、ライセンスコストが発生します。Datomic中心のアプリケーションであればよいのですが、それ以外の機能をIonsで記述し、スケールさせなければいけないときにDatomic Cloudのライセンスコストが増加するのは合理的ではないと思います。

### AWSとの密結合
Kubernetesが発展してきている現在、クラウド間のポータビリティが容易になってきているにも関わらず、Datomic CloudとIonsを選択することでAWS一択になってしまうリスクを考慮する必要があります。

### 最後に
Stuart Hallowayは、今後自分がアプリケーションを書くときには、Datomicを使うかどうかに関わらず、Ionsで開発するだろうと言っていたのが印象的でした。私はDatomicをDBとするWebアプリや、Kinesisなど、Lambdaをハンドラとして用いるアプリなどであれば、Ionsを使うメリットは大きいと思います。

一方で、JUXTの[Crux](https://github.com/juxt/crux)や、[KNative](https://knative.dev/)などのテクノロジーを組み合わせたオープンソースのアプローチもあるのではないかと思っています。機会があれば試してみたいと考えています。