まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:rest

ちまたで(僕の SNS のタイムラインで)マスターマインド系ミニゲームが流行っていたせいか、自分も少しインスパイアされたようで、REST API のみでマスターマインドゲームを作ってみました。

REST API なので curl のような HTTP クライアントがあれば利用できます。今後変更するかもしれませんが、現状は GET リクエストのみで使えるので、より正確には「GET リクエストのみで使えるマスタマーマインド」を作ってみました。GET だけで使えるということはウェブブラウザからも使えるということを意味しています。


【マスターマインドとは】
昔からある「数字当て」ゲームです。細かなルールの違いはあると思いますが、一般的には4桁くらいの数字を誰か一人が適当に思い浮かべて、以下で紹介するルールに沿って正解の数字を絞り込み、なるべく早く当てる、というゲームです。

以下のルール説明では数字を思い浮かべた人を「マスター」とします。それ以外のゲーム参加者(数字を当てる人)は「プレイヤー」と呼びます。1回のゲームにおいてマスターは(交代しても構いませんが)一人、プレイヤーは1人以上です。なおプレイヤーが複数の場合、他のプレイヤーとの情報交換はできないものとします。

プレイヤーはマスターが思い浮かべた数字を当てることが目的です。といっても4桁の数字をそう簡単に当てることはできません。そこで以下のようなルールを設定して、答を絞り込めるようにしています:

(ルール1)プレイヤーは4桁の数字をマスターに伝える

(ルール2)マスターは自分が思い浮かべた数字とプレイヤーから聞いた数字を1文字ずつ比べて、以下の計算をします:
 (1)プレイヤー数字1文字がマスターの数字の中で使われていて、かつ使っている位置も一致していた場合、「1ヒット」となる
 (2)プレイヤー数字1文字がマスターの数字の中で使われているが、使っている位置が一致していない場合、「1エラー」となる

※この「ヒット」や「エラー」の部分にもいろんな呼び方の亜流があるように感じています。

(ルール3)マスターはヒットの数とエラーの数をプレイヤーに伝える


例えばマスターの思い浮かべた数字が "2639" で、プレイヤーから伝えられた数字が "1369" だった場合、位置まで含めて一致しているのは4桁目の "9" 1つで、位置は間違えているが使われている数字は "3" と "6" の2つです。したがってマスターからプレイヤーには「1ヒット2エラー」と答を返します。

プレイヤーは自分の数字と、その数字に対するマスターからの答をヒントに、再度数字を推測してマスターに伝え、マスターはその推測内容に対してまた答を返します。。。 これを数字が一致するまで(つまり「4ヒット」という答が返ってくるまで)続ける、というものです。

数字の桁数や、同じ数字を使っていいか/ダメか、などのマイナールールの違いはありますが、これがマスターマインドというゲームです。二人以上であればマスター役を交代しながら遊べるアナログゲームで、ペンとメモがあれば、或いは記憶力があれば道具不要で遊べます。最初の1回は当てずっぽうになりますが、2回目以降は1回目の結果を元に当てに行くのか/範囲を狭めにいくのか、という戦略も必要で、結構頭の付かれるゲームです。もちろんコンピュータにマスター役を任せて一人で遊べるゲームも多く存在しているはずです。


【マスターマインド REST API とは?】
ここからは今回作成した Web サービスの紹介です。一言で言うと、上述のマスターマインドの「マスター役」に必要な機能を REST API の形で公開した、というものです。

まだちょっとわかりにくいと思うので、もう少し具体的に説明します。まず今回公開したマスターマインド Web サービスは以下のルールを前提としています:
・推測に使われる文字は '0' から '9' までの10種類、重複無し
・推測に文字列の長さは初期化時に指定する(2以上9以下、無指定時は4)
・推測する文字列の長さは初期化時に指定した長さでないといけない、またその中に重複した文字は使えない(いずれもエラー扱いとなって返されます)
・推測した時の返答は「〇ヒット△エラー(〇と△は数字)」で、これとは別に「ハイorロー(推測値が正解値と比べて大きいか小さいか)」を返すようにすることも可能(返す場合は初期化時に指定)


そしてマスターマインドのマスター役に最低限必要な機能は以下4つだと思っています:
(1)ゲーム開始(初期化)
(2)プレイヤーが推測した数値を受け取り、判定して、結果を返す
(3)これまでの推測内容とその判定結果の履歴を返す
(4)プレイヤーがギブアップした場合の対処


今回公開した REST API は上記4つの機能に相当する4つの関数です:
(1)GET /api/init ・・・ ゲーム開始(初期化)、細かなルールはパラメータで指定。ゲームIDを返す
(2)GET /api/guess ・・・ ゲームIDとプレイヤーが推測した数値をパラメータで受け取り、判定して、結果を返す
(3)GET /api/status ・・・ ゲームIDを受け取り、現在までの推測状況を履歴として返す
(4)GET /api/giveup ・・・ ゲームIDを受け取り、ギブアップしたことにする

※今回は処理内容的には POST で扱うべきものも含まれていますが、ウェブブラウザだけでも利用できるよう、全て GET 関数として用意しました。


これら4つの関数を使うことで1人用マスターマインドが作れます。以下で具体的なアプリケーションロジックを意識した形で紹介します:

1 (ゲーム初期化)

推測する文字数(length)と、ハイorロー機能を使うかどうか(highlow)をパラメータに指定してゲームを初期化します。実行結果にゲームIDが含まれていて、以降のロジック処理の中でこのゲームIDを指定します。

(例)
GET /api/init?length=4&highlow=0 (文字数=4、ハイorロー機能は使わないルールでゲームを初期化)

返り値例: { status: true, length: 4, highlow: 0, id: "xxxx" } ("xxxx" がゲームID)


2 (値の推測)

ゲームIDと推測する文字列を指定して、その推測結果を取得します。正解するかギブアップするまで何回でも実行できます。

(例)
GET /api/guess?id=xxxx&value=0123 (ゲームID=xxxx、推測値=0123 で推測)

返り値例: { status: true, id: "xxxx", value: "0123", hit: 1, error: 0 } (1ヒット0エラーだった)


3 (推測履歴)

ゲームIDを指定して、過去の推測履歴を取得します。

(例)
GET /api/status?id=xxxx

返り値例: { status: true, id: "xxxx", value: "****", histories: [ value: "0123", hit: 1, error: 0 }, { value: "4567", hit: 0, error: 2 } ] } (最初に "0123" で推測して1ヒット0エラー、次に "4567" で推測して0ヒット2エラーだった)


4 (ギブアップ)

プレイヤーがギブアップしたことにする。

(例)
GET /api/giveup?id=xxxx

返り値例: { status: true, value: "0459" } (ギブアップ処理済み、正解は "0459"。以降 ID=xxxx のゲームは履歴は見れるが推測できない)


この4つの機能を使うことでマスターマインドのマスター役として機能します。以下で実際にウェブブラウザ(または curl コマンド)を使ってマスターマインドを遊んでみましょう。


【マスターマインド REST API を使って、ブラウザ(または curl )でマスターマインドを遊んでみる】
以下ではウェブブラウザを使って実際に REST API だけでマスターマインドを遊んでみます。curl コマンドでも同様の遊び方が楽しめるので、慣れている方は curl を使ってみてください。

まずはゲームを初期化します。REST API は https://mastermind-restapi.herokuapp.com/ でホスティングしているので、このサーバー名を使って URL を指定して実行します。なお Heroku の無料枠を使っている関係で、初回のアクセス時は返答までに時間がかかる可能性がある点をご注意ください(30 分間使われていないとサーバーが停止状態になるよう設定されているためです)。

では実際に初期化コマンドを実行します。今回は数字は4桁でハイorローのヒント機能を使うことにします。ウェブブラウザのアドレス欄に以下を指定して実行します:

https://mastermind-restapi.herokuapp.com/api/init?length=4&highlow=1

2022012101


※ curl コマンドを使う場合は以下のコマンドを実行します(以下、同様)
curl "https://mastermind-restapi.herokuapp.com/api/init?length=4&highlow=1"

2022012102


実行すると以下のような結果が表示されます:
2022012101
{
  status: true,
  id: "d4429600-7a62-11ec-9034-592f2f001736",
  highlow: true,
  length: 4
}


この JSON オブジェクト内の id の値(この画面であれば "d4429600-7a62-11ec-9034-592f2f001736")がゲーム ID です。以降の操作で常に使うことになるため、すぐに使えるようコピーしておきましょう。

初期化が終わったら最初の推測です。1回目は何のヒントもない状態なので適当な文字列を指定し、ゲームID(xxxx ここは初期化時に得た値を指定)と一緒に入力します。

https://mastermind-restapi.herokuapp.com/api/guess?id=xxxx&value=0123


以下のような結果が表示されます:
2022012103
{
  status: true,
  id: "d4429600-7a62-11ec-9034-592f2f001736",
  length: 4,
  value: "0123",
  hit: 1,
  error: 0,
  highlow: "low"
}


"0123" という推測値は(このゲームIDに対して)「1ヒット0エラー」でした。また(初期化時に highlow オプションを有効にしていたので)"0123" は正解と比べて "low"(小さすぎる)、という結果も含まれています。


この結果をもとに次の推測をします。この結果を採用して(つまり "0123" のどれかは位置も含めて当たっていた前提で)次の数値を指定するか、全く別の数値を指定して使われている数字を絞り込む方針にするか、戦略の分かれるところです。例えば後者の戦略をとって次に "4567" という数値を指定してみました:

https://mastermind-restapi.herokuapp.com/api/guess?id=xxxx&value=4567

2022012104
{
  status: true,
  id: "d4429600-7a62-11ec-9034-592f2f001736",
  length: 4,
  value: "4567",
  hit: 0,
  error: 2,
  highlow: "high"
}


"4567" は「0ヒット2エラー」で、「(正解と比べて)大きすぎる」という結果でした。最初の結果と合わせると「8か9のどちらかは使われている」こともわかります。


これを繰り返して正解を当てるのが目的です。途中で完全に諦めた場合はギブアップすることもできて、その場合は

https://mastermind-restapi.herokuapp.com/api/giveup?id=xxxx

で正解を知ることもできます(今回はしません)。


最後まで推測を続けることにします。途中で過去の推測状況を確認したくなったら /api/status で確認できます:

https://mastermind-restapi.herokuapp.com/api/status?id=xxxx

2022012105
{
  status: true,
  id: "d4429600-7a62-11ec-9034-592f2f001736",
  value: "****",
  histories: [
    {
      value: "0123",
      timestamp: 1642732702754,
      hit: 1,
      error: 0,
      highlow: "low"
    },
    {
      value: "4567",
      timestamp: 1642733001191,
      hit: 1,
      error: 0,
      highlow: "low"
    },
      :
      :
  ]
}


正解になるとこんな感じの結果となります(正解は "0784" でした):

https://mastermind-restapi.herokuapp.com/api/guess?id=xxxx&value=0784

2022012106
{
  status: true,
  id: "d4429600-7a62-11ec-9034-592f2f001736",
  length: 4,
  value: "0784",
  hit: 4,
  error: 0,
  highlow: "equal",
  message: "Congrats!"
}


なお正解後に再度 /api/status で状況を確認すると履歴から7回目の推測で解けたこと、また正解する前には表示されていなかった正解値が表示されてます:

https://mastermind-restapi.herokuapp.com/api/status?id=xxxx

2022012107
{
  status: true,
  id: "d4429600-7a62-11ec-9034-592f2f001736",
  value: "0784",
  histories: [
    {
      value: "0123",
      timestamp: 1642732702754,
      hit: 1,
      error: 0,
      highlow: "low"
    },
    {
      value: "4567",
      timestamp: 1642733001191,
      hit: 1,
      error: 0,
      highlow: "low"
    },
      :
      :
  ]
}


もう一度遊ぶ場合は初期化からやり直します。新しいゲーム ID が取得できるので、新しい ID で同様にして続けることができます。

といった感じで、GET の HTTP リクエストだけで遊べるマスターマインドです。CLI が大好きな方に使っていただけると嬉しいです(笑)。

※上でも触れていますが、ゲームIDの情報はサーバーが 30 分間使われていない状態が続くとサーバーが停止状態になって消えてしまいますのでご注意を。


この REST API のソースコードはこちらで公開しています:
https://github.com/dotnsf/mastermind


また、このままサーバー上で動くわけではありませんが、オンライン API ドキュメントはこちらです(ソースコードを git clone して、localhost で動かせば Swagger API として動きます)。実際には上で紹介した以外の管理系 API (全ゲーム状況参照とか、リセットとか)も実装していますが、公開機能ではありません:
https://github.com/dotnsf/mastermind/doc


また CLI や JSON があまり得意でない人向けというか、この REST API を使ったフロントエンドアプリケーションサンプルも作って Github ページで公開しておきました(API は heroku 上にあるため、クロスサイトで AJAX を実行しているので CORS を有効にしています。このあたり詳しくは上述のソースコードを参照ください)。公開した REST API のフロントエンド・サンプルという位置付けでこちらもソースコードに含まれています。 ただ遊びたいだけの人はこちらをどうぞ:
https://dotnsf.github.io/mastermind/

2022012100



【作った上での所感】
で、作ってみた上で気になったことがあります。マスターマインド自体が偶然の要素の強いゲームではあるし、使う数値の種類数や重複を認めるかどうか、ハイorローを使うかどうかなどのルールの違いによっても戦略は変わってくると思うのですが、「マスターマインドを解くための最適戦略ってどうなるんだろう?」が気になりつつあります。

(特にハイorロー機能を考えた場合に)最初の1回目の推測は本当に適当な数字でいいのか?
その結果を受けて、2回目はどうすべきなのか?
効率的な絞り込みをするにはどうするのか?
絞り込みを優先するべきか、当てに行く優先度を上げるべきか?

・・・などなど。 気になってくると調べたくもなりますが、今回のようにランダムで問題を生成する API があればいくらでも調べたり、戦略を比較するといったことができるので、そういう目的でもこの REST API は使えそうだな、、と感じています。ただこちらの方が API を作るよりも遥かに難しそうなので、もう少しまとまった時間の取れそうなときにでも。


まず最初に、自分がもともと今回勝利する技術に興味を持ったきっかけは GIGAZINE に掲載されたこのネタでした:


これに近いことを自分でもやろうとするとどうすればよいか、、と考えました。Kubernetes (以下 "k8s")には API サーバーが存在していて、kubectl はこの API サーバー経由でリクエスト/レスポンスを使って動く、ということは知っていたので、うまくやれば REST API でなんとかなるんじゃないか、、、という発想から色々調べてみたのでした。

調べてみるとわかるのですが、k8s の API サーバーを直接利用するには kubectl コマンドの裏で行われている認証などの面倒な部分も意識する必要がありました:
k8s_api



本ブログエントリでは、認証などの面倒な指定を回避して利用できるよう k8s の proxy 機能を使って k8s クラスタの外部から k8s API を実行してクラスタの状態を確認できる様子を紹介します。 なおここで紹介する k8s の環境としては IBM Cloud の 30 日無料 k8s クラスタ環境を使って紹介します。


【k8s クラスタの準備】
まずは k8s クラスタを用意します。手元で用意できる環境があればそれを使ってもいいのですが、ここでは IBM Cloud から提供されている 30 日間無料のシングルワーカーノードクラスタ環境を使うことにします。この環境を入手するまでの具体的な手順については以前に記載したこちらの別エントリを参照してください:


次にこのシングルワーカーノードクラスタ環境をカレントコンテキストにして kubectl から利用できる状態にします。

まず、今回紹介する機能は CLI コマンドを使います。最低限必要なのは kubectl コマンドで、こちらはまだインストールできていない場合はこちらなどを参考にインストールしてください。また IBM Cloud の k8s を使う場合は kubectl に加えて ibmcloud コマンドも必要です。ibmcloud コマンドのインストールについてはこちらのページ等を参照して、自分の環境に合わせた ibmcloud コマンドをインストールしてください:


CLI の用意ができたら IBM Cloud のダッシュボード画面から作成したクラスタを選択し、画面上部の「ヘルプ」と書かれた箇所をクリックします:
2022011401


すると画面右側にヘルプメニューが表示されます。ここで「クラスターへのログイン」と書かれた箇所を展開して、その中に書かれている2つのコマンド(ログインコマンドとカレントコンテキストを指定するコマンド)をターミナルなどから続けて実行することで、CLI からも IBM Cloud にログインし、k8s クラスタをカレントコンテキストに切り替えることができます:
2022011402


$ ibmcloud login

$ ibmcloud ks cluster config -c XXXX(一意のクラスタID)XXXX

ここまで実行することで CLI 環境のカレントコンテキストが IBM Cloud 上の k8s になり、kubectl コマンドも IBM Cloud の k8s に対して実行できるようになります(以下は実行例です):
$ kubectl get all

NAME                            READY   STATUS      RESTARTS   AGE
pod/hostname-7b4f76fd59-c8b2l   1/1     Running     0          4d22h

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service/hostname     NodePort    172.21.105.49           8080:30080/TCP   4d22h
service/kubernetes   ClusterIP   172.21.0.1              443/TCP          4d23h

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/hostname   1/1     1            1           4d22h

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/hostname-7b4f76fd59   1         1         1       4d22h

k8s 環境の準備段階としてここまでが必要です。IBM Cloud 以外の k8s クラスタ環境を利用する場合でもここまでと同様の準備ができていれば、以下の Proxy 実行から続けて行うことができるはずです。


【k8s Proxy の起動】
この状態で k8s Proxy を起動します:
$ kubectl proxy
Starting to serve on 127.0.0.1:8001

↑8001 番ポートで Proxy が起動した状態になります。Proxy を終了するにはこの画面で Ctrl+C を押します。

ここまでできていれば localhost:8001 を経由して k8s REST API を実行し、その結果を参照することができるようになっています。


【k8s REST API の実行】
いくつか k8s REST API を実行してみることにします。今回は(別のターミナルから)curl コマンドを使って REST API を実行してみます。

例えばクラスタ上で稼働している Pods の一覧を取得してみます。kubectl であれば
$ kubectl get pods

NAME                        READY   STATUS    RESTARTS   AGE
hostname-7b4f76fd59-c8b2l   1/1     Running   0          4d22h
というコマンドを実行するところです。今回は上記のように hostname という名前の Pod が1つだけ存在している状態であるとします。

一方、REST API ではバージョンや namespace を指定して以下のように実行します。また結果もこのように取得できます:
$ curl -X GET http://localhost:8001/api/v1/namespaces/default/pods

{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "92526"
  },
  "items": [
    {
      "metadata": {
        "name": "hostname-7b4f76fd59-c8b2l",
        "generateName": "hostname-7b4f76fd59-",
        "namespace": "default",
        "uid": "3753ac40-6d2c-428a-86ac-4181dfc0cce9",
        "resourceVersion": "2162",
        "creationTimestamp": "2022-01-09T07:27:25Z",
        "labels": {
          "app": "hostname",
          "pod-template-hash": "7b4f76fd59"
        },
        "annotations": {
          "cni.projectcalico.org/containerID": "1e1592caee1c1a76426c1fca88a70ddb681fae650301cd0cbe3985f0b0975d45",
          "cni.projectcalico.org/podIP": "172.30.153.142/32",
          "cni.projectcalico.org/podIPs": "172.30.153.142/32",
          "kubernetes.io/psp": "ibm-privileged-psp"
        },
        "ownerReferences": [
          {
            "apiVersion": "apps/v1",
            "kind": "ReplicaSet",
            "name": "hostname-7b4f76fd59",
            "uid": "9160ac38-3e4e-4a01-80a1-affba620fa9c",
            "controller": true,
            "blockOwnerDeletion": true
          }
        ],
        "managedFields": [
          {
            "manager": "kube-controller-manager",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-01-09T07:27:25Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {"f:metadata":{"f:generateName":{},"f:labels":{".":{},"f:app":{},"f:pod-template-hash":{}},"f:ownerReferences":{".":{},"k:{\"uid\":\"9160ac38-3e4e-4a01-80a1-affba620fa9c\"}":{".":{},"f:apiVersion":{},"f:blockOwnerDeletion":{},"f:controller":{},"f:kind":{},"f:name":{},"f:uid":{}}}},"f:spec":{"f:containers":{"k:{\"name\":\"hostname\"}":{".":{},"f:image":{},"f:imagePullPolicy":{},"f:name":{},"f:ports":{".":{},"k:{\"containerPort\":8080,\"protocol\":\"TCP\"}":{".":{},"f:containerPort":{},"f:protocol":{}}},"f:resources":{},"f:terminationMessagePath":{},"f:terminationMessagePolicy":{}}},"f:dnsPolicy":{},"f:enableServiceLinks":{},"f:restartPolicy":{},"f:schedulerName":{},"f:securityContext":{},"f:terminationGracePeriodSeconds":{}}}
          },
          {
            "manager": "calico",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-01-09T07:27:26Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {"f:metadata":{"f:annotations":{"f:cni.projectcalico.org/containerID":{},"f:cni.projectcalico.org/podIP":{},"f:cni.projectcalico.org/podIPs":{}}}}
          },
          {
            "manager": "kubelet",
            "operation": "Update",
            "apiVersion": "v1",
            "time": "2022-01-09T07:27:36Z",
            "fieldsType": "FieldsV1",
            "fieldsV1": {"f:status":{"f:conditions":{"k:{\"type\":\"ContainersReady\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"Initialized\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}},"k:{\"type\":\"Ready\"}":{".":{},"f:lastProbeTime":{},"f:lastTransitionTime":{},"f:status":{},"f:type":{}}},"f:containerStatuses":{},"f:hostIP":{},"f:phase":{},"f:podIP":{},"f:podIPs":{".":{},"k:{\"ip\":\"172.30.153.142\"}":{".":{},"f:ip":{}}},"f:startTime":{}}}
          }
        ]
      },
      "spec": {
        "volumes": [
          {
            "name": "kube-api-access-4qr4h",
            "projected": {
              "sources": [
                {
                  "serviceAccountToken": {
                    "expirationSeconds": 3607,
                    "path": "token"
                  }
                },
                {
                  "configMap": {
                    "name": "kube-root-ca.crt",
                    "items": [
                      {
                        "key": "ca.crt",
                        "path": "ca.crt"
                      }
                    ]
                  }
                },
                {
                  "downwardAPI": {
                    "items": [
                      {
                        "path": "namespace",
                        "fieldRef": {
                          "apiVersion": "v1",
                          "fieldPath": "metadata.namespace"
                        }
                      }
                    ]
                  }
                }
              ],
              "defaultMode": 420
            }
          }
        ],
        "containers": [
          {
            "name": "hostname",
            "image": "dotnsf/hostname",
            "ports": [
              {
                "containerPort": 8080,
                "protocol": "TCP"
              }
            ],
            "resources": {

            },
            "volumeMounts": [
              {
                "name": "kube-api-access-4qr4h",
                "readOnly": true,
                "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
              }
            ],
            "terminationMessagePath": "/dev/termination-log",
            "terminationMessagePolicy": "File",
            "imagePullPolicy": "Always"
          }
        ],
        "restartPolicy": "Always",
        "terminationGracePeriodSeconds": 30,
        "dnsPolicy": "ClusterFirst",
        "serviceAccountName": "default",
        "serviceAccount": "default",
        "nodeName": "10.144.214.171",
        "securityContext": {

        },
        "imagePullSecrets": [
          {
            "name": "all-icr-io"
          }
        ],
        "schedulerName": "default-scheduler",
        "tolerations": [
          {
            "key": "node.kubernetes.io/not-ready",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 600
          },
          {
            "key": "node.kubernetes.io/unreachable",
            "operator": "Exists",
            "effect": "NoExecute",
            "tolerationSeconds": 600
          }
        ],
        "priority": 0,
        "enableServiceLinks": true,
        "preemptionPolicy": "PreemptLowerPriority"
      },
      "status": {
        "phase": "Running",
        "conditions": [
          {
            "type": "Initialized",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-01-09T07:27:25Z"
          },
          {
            "type": "Ready",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-01-09T07:27:36Z"
          },
          {
            "type": "ContainersReady",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-01-09T07:27:36Z"
          },
          {
            "type": "PodScheduled",
            "status": "True",
            "lastProbeTime": null,
            "lastTransitionTime": "2022-01-09T07:27:25Z"
          }
        ],
        "hostIP": "10.144.214.171",
        "podIP": "172.30.153.142",
        "podIPs": [
          {
            "ip": "172.30.153.142"
          }
        ],
        "startTime": "2022-01-09T07:27:25Z",
        "containerStatuses": [
          {
            "name": "hostname",
            "state": {
              "running": {
                "startedAt": "2022-01-09T07:27:36Z"
              }
            },
            "lastState": {

            },
            "ready": true,
            "restartCount": 0,
            "image": "docker.io/dotnsf/hostname:latest",
            "imageID": "docker.io/dotnsf/hostname@sha256:e96808f33e747004d895d8079bc05d0e98010114b054aea825a6c0b1573c759e",
            "containerID": "containerd://7cc6b4eafd0723f46f78fc933a43ddefc6d4ddc75796608548b34a9aaae77f17",
            "started": true
          }
        ],
        "qosClass": "BestEffort"
      }
    }
  ]
}

次に API で Pod を作成してみます。Pod そのものはシンプルな Hello World 出力コンテナとして、この Pod を API で作成するには以下のように指定します(YAML 部分をファイルで指定する方法がよくわからないので、ご存じの人がいたら教えてください):
$ curl -X POST -H 'Content-Type: application/yaml' http://localhost:8001/api/v1/namespaces/default/pods -d '
apiVersion: v1
kind: Pod
metadata:
  name: pod-example
spec:
  containers:
  - name: ubuntu
    image: ubuntu:trusty
    command: ["echo"]
    args: ["Hello World"]
  restartPolicy: Never
'

{
  "kind": "Pod",
  "apiVersion": "v1",
  "metadata": {
    "name": "pod-example",
    "namespace": "default",
    "uid": "79d0f086-6c6f-445a-9819-69ae8630710a",
    "resourceVersion": "92782",
    "creationTimestamp": "2022-01-14T06:25:25Z",
    "annotations": {
      "kubernetes.io/psp": "ibm-privileged-psp"
    },
    "managedFields": [
      {
        "manager": "curl",
        "operation": "Update",
        "apiVersion": "v1",
        "time": "2022-01-14T06:25:25Z",
        "fieldsType": "FieldsV1",
        "fieldsV1": {"f:spec":{"f:containers":{"k:{\"name\":\"ubuntu\"}":{".":{},"f:args":{},"f:command":{},"f:image":{},"f:imagePullPolicy":{},"f:name":{},"f:resources":{},"f:terminationMessagePath":{},"f:terminationMessagePolicy":{}}},"f:dnsPolicy":{},"f:enableServiceLinks":{},"f:restartPolicy":{},"f:schedulerName":{},"f:securityContext":{},"f:terminationGracePeriodSeconds":{}}}
      }
    ]
  },
  "spec": {
    "volumes": [
      {
        "name": "kube-api-access-mc7tz",
        "projected": {
          "sources": [
            {
              "serviceAccountToken": {
                "expirationSeconds": 3607,
                "path": "token"
              }
            },
            {
              "configMap": {
                "name": "kube-root-ca.crt",
                "items": [
                  {
                    "key": "ca.crt",
                    "path": "ca.crt"
                  }
                ]
              }
            },
            {
              "downwardAPI": {
                "items": [
                  {
                    "path": "namespace",
                    "fieldRef": {
                      "apiVersion": "v1",
                      "fieldPath": "metadata.namespace"
                    }
                  }
                ]
              }
            }
          ],
          "defaultMode": 420
        }
      }
    ],
    "containers": [
      {
        "name": "ubuntu",
        "image": "ubuntu:trusty",
        "command": [
          "echo"
        ],
        "args": [
          "Hello World"
        ],
        "resources": {

        },
        "volumeMounts": [
          {
            "name": "kube-api-access-mc7tz",
            "readOnly": true,
            "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
          }
        ],
        "terminationMessagePath": "/dev/termination-log",
        "terminationMessagePolicy": "File",
        "imagePullPolicy": "IfNotPresent"
      }
    ],
    "restartPolicy": "Never",
    "terminationGracePeriodSeconds": 30,
    "dnsPolicy": "ClusterFirst",
    "serviceAccountName": "default",
    "serviceAccount": "default",
    "securityContext": {

    },
    "imagePullSecrets": [
      {
        "name": "all-icr-io"
      }
    ],
    "schedulerName": "default-scheduler",
    "tolerations": [
      {
        "key": "node.kubernetes.io/not-ready",
        "operator": "Exists",
        "effect": "NoExecute",
        "tolerationSeconds": 600
      },
      {
        "key": "node.kubernetes.io/unreachable",
        "operator": "Exists",
        "effect": "NoExecute",
        "tolerationSeconds": 600
      }
    ],
    "priority": 0,
    "enableServiceLinks": true,
    "preemptionPolicy": "PreemptLowerPriority"
  },
  "status": {
    "phase": "Pending",
    "qosClass": "BestEffort"
  }
}

kubectl コマンドで Pods 一覧を見ると、Pods が追加/実行され、終了していることが確認できます:
$ kubectl get pods

NAME                        READY   STATUS      RESTARTS   AGE
hostname-7b4f76fd59-c8b2l   1/1     Running     0          4d23h
pod-example                 0/1     Completed   0          3m


といった感じで、k8s Proxy を使うことで簡単に k8s API を利用できる REST API サーバーを作ることができそうでした。もともとこの技術に興味を持った GIGAZINE に掲載されたこのネタも、REST API の Proxy を経由して API を実行して結果を GUI で視覚化して・・・ というのも、そこまで難しくない気がしています。

なお k8s の REST API についてはここから一覧を参照できます:
https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/


(参照)
https://qiita.com/iaoiui/items/36e86d173e451a7b18be

こんなのを作ってみました。これ自体がそのまま直接役立つとは思ってませんが、cron などのスケジュールジョブと合わせて使うことで時系列データを簡単に入手できるので、「生の解析用サンプルデータを用意する」のが比較的容易にできちゃうと思っています。

文字通りの「為替取得 REST API」です。エンドポイント URL はこちらです:
http://fx.mybluemix.net/


REST API なので、何らかの(機械的な)HTTP クライアントからアクセスされることを想定しています。とりあえずデータ・フォーマットを確認する目的で、ウェブブラウザでアクセスしてみると、このような JSON テキストが得られるはずです:
2021061501


JSON テキストを整形するとこんな感じの内容です:
{
 "status":true,
 "result":{
  "datetime":"2021-06-16 14:48:27+0",
  "rate":{
   "USDJPY":109.961,
   "EURJPY":133.339,
   "EURUSD":1.21255,
   "AUDJPY":84.707,
   "GBPJPY":155.129,
   "NZDJPY":78.49,
   "CADJPY":90.334,
   "CHFJPY":122.216,
   "HKDJPY":14.158,
   "GBPUSD":1.4107,
   "USDCHF":0.89958,
   "ZARJPY":7.99,
   "AUDUSD":0.77031,
   "NZDUSD":0.71376,
   "EURAUD":1.57405,
   "TRYJPY":12.872,
   "CNHJPY":17.186,
   "NOKJPY":13.116,
   "SEKJPY":13.137,
   "MXNJPY":5.465
  }
 }
}

status が API の実行結果(true/false)で、成功した場合は result がその結果です。result.datetime が取得したタイミングの GMT 日時です( API を実行した瞬間の日時になっているはずです)。そして result.rate 内にそのタイミングでの(リアルタイムの)各通貨ペアの為替情報が格納されています。例えば上の例では result.rate.USDJPY = 109.961 となっていますが、これはこのタイミングで「1米ドル(USD)=109.961日本円(JPY)」だったことを表しています。同様にして result.rate.EURJPY = 133.339 なので「1ユーロ(EUR)=133.339日本円(JPY)」です。他の通貨表記についてはこのあたりを参照してください:
https://www.gaitame.com/gaitame/gaika/gaika_index.html


このように上記 URL に GET リクエストを発行するだけでリアルタイムな為替情報 20 ペア分が取得できるものです。後はこれを1分おきとか1時間おきに取得して、その結果を RDB なり、JSON DB なりに格納する、という処理を1~10日くらい動かしっぱなしにしておけば、それなりにまとまったデータが取得できるはずです。一応数値は本物の為替情報で、深く考えなくてもただ GET リクエストを実行すれば結果が取得できるので、解析元となるデータを集める上では比較的便利かな、と思っています。

これはあくまで一例ですが、Node-RED のインジェクションノードを定期実行する設定にした上でこんな感じのフローを作るだけで定期的な為替情報を集めて DB に格納するまでが(簡単に)できちゃいます:
2021061502


後はここで集めたデータを使って解析学習時のデータにするもよし、予測機能を作るもよし、ご自由にお使いください。

「REST API の標準化」を考える機会がありました。実際に格納したり取得したりするデータ自体の構造は当然ケース・バイ・ケースになるわけですが、その呼び出し方とか、パラメータの指定方法とか、結果のフォーマットとかを社内や団体内で共通化すると、単に便利なだけでなく、一度使った後に新しい別の API を利用する際にも理解を早めることができます。

で、具体的にはどのように共通化すべきで、そこにはどういった要素が考慮されるべきか、といった内容を自分なりに考えてみました。

ここには色んな流派というか、考え方の基本となるパターンがあるのですが、今回は自分の経験を元に、自分はこういう API にしている、こういう API だとわかりやすい/覚えやすい、という基準で、自分なりに標準化したものを紹介します。


【考慮すべき要素】
詳しくは後述しますが、以下の6つについては標準化時に抑えておく必要がある要素であると思っています:

0. id と日付時刻の扱い
1. API をリクエストする際のメソッドと URI
2. API をリクエストする際のパラメータ
3. API からのレスポンス
4. セキュリティ
5. テスト
6. 公開方法



また、以下で説明する内容については、下図のような商品マスターデータ(items)を対象とした REST API を作ることを想定した例として紹介します:
idcategory_idnamemaker_idpriceimage_urlcreatedupdated
x100x051○○シャンプーx10001700http://xxxxx/100.jpg16054028361605402836
x101x051××シャンプーx10002800http://xxxxx/101.png16054038361605403836
x102x052□□コンディショナーx100101000http://xxxxx/102.jpg16054048361605404836
x103x053▲▲ヘアオイルx10001500http://xxxxx/103.jpg16054058361605405836
x104x061◎◎ボディソープx100211000http://xxxxx/104.png16054068361605406836
x105x071◆◆天然化粧水x100102000http://xxxxx/105.png16054078361605408836

※数字は全て integer 。category_id や maker_id は別途 category や maker のマスターデータが存在していて、外部参照キーだけが格納されているイメージです。


【0. id と日付時刻の扱い】
まずは API の考慮点というよりも、その API で対象とするデータにおける考慮点です。既に存在しているデータベース等を対象とする場合は今からの変更は難しいかもしれませんが、新たにデータベースから作成できるのであれば API 利用を想定した設計にしておくべき、という考慮点です。

まずいわゆる id 値について。基本的にはテーブル内でユニークな値であればよいのですが、ここを integer 型とすべきか string 型とすべきか、という考慮点があります。メリット・デメリットを考慮した上で選ぶべきであると思っています:
 メリットデメリット
integer 型・データ量が少ない
・データベース側に自動生成機能があることが多い
・推測しやすい、推測される可能性がある
string 型・存在する id の推測が困難
・値そのものに意味をもたせることも可能(例:メールアドレス)
・1件あたりのデータ量が増える(といっても現代では大したことはないかも)
・生成の仕組みが必要


どちらのケースも存在しているし、どちらか一方に不利な要素があるわけではないのですが、個人的には「今から新規に作るなら string 型」だと思っています。理由は「今では string 型のデメリットが大したことない」のと「 ID が自動生成されるミドルウェアでは string 型になることが多い」ので、結果的に「ID は string 型にした方が統一しやすい」と思っています。

ID に加えて、データレコードの作成日時(created)と最終更新日時(updated)は全てのレコードに加えるべきだと思っています。ソフトデリートを有効にする場合は削除日時(deleted、初期値は 0 か null)も加えます。


なお日付時刻については後述する特別な理由がない限りは UNIX タイムスタンプ値を使うべきです。YYYY-MM-DD のような特定の文字列フォーマットで格納してしまうとタイムゾーンを考慮することができなくなってしまうため、データとしてはタイムスタンプ値で格納し、表示する際に変換する、という方法が理想的です。

※ただし「日本からしか使わない」ことに加えて「特定日だけのデータを取り出す」といった用途がある場合などはあらかじめ日付フォーマット変換しておいた方が便利になることもあります。日付フォーマットを利用する場合は、そのフォーマットもあらかじめ標準化しておくべきです(YYYY-MM-DD とか YYYY-MM-DD hh:mm:ss など)。なお、このあたりは個別事情を考慮して対応する必要があるため、標準化ルールの対象外とする場合もあります。


【1. API をリクエストする際のメソッドと URI】
リクエスト時の考慮ポイントが1番多くあると思っています。順に説明します。

「メソッド」はいわゆる HTTP メソッドのことで、一般的には GET(取得・検索)、POST(作成)、PUT(更新)、DELETE(削除)が用いられます。API の用途や目的に合わせてメソッドを選びます。

より多くの考慮が必要なのは URI 部分となります。まず前提として以下の条件を逸脱しないよう注意してください:
  • 文字コードは UTF-8
  • リクエストデータに外字は使わない
  • URI ではキャメルケース(userData)ではなくスネークケース(user_data)を使う※
  • %(パーセント)でエンコーディングする必要があるデータは URI に含めない
※ホスト名部分が大文字・小文字の区別をしないためです

次にホスト名ですが、理想的にはホスト名の一部に "api" という文字が含まれていることが挙げられます。これが難しい場合はパス名のどこかに "api" を含めて、この URI が API のための URI であることを明示します。以下では api.mycompany.com というホスト名を使う想定で説明を続けます。

パス名の中には今後の仕様変更に対応できるよう、 API のバージョン("v1" など)を含めるようにします。

データの種類(この場合は items)も URI の一部に含まれている必要があります。このあたりから流派によって扱いに違いが出てくる部分ですが、自分は「単数を扱う API なら単数形(item)」、「複数を扱う API なら複数形(items)」としています。そして単数を対象とする場合はその id をパス内で指定するようにします。

例えば以下のようにする、ということです:
  • GET https://api.mycompany.com/v1/items (複数の items を取り出す
  • GET https://api.mycompany.com/v1/item/x100 (id = "x100" の item を取り出す
  • POST https://api.mycompany.com/v1/item (item を1件新規登録する)
  • PUT https://api.mycompany.com/v1/item/x101 (id = "x101" の item を更新する)
  • DELETE https://api.mycompany.com/v1/item/x102 (id = "x102" の item を削除する)
  • POST https://api.mycompany.com/v1/items (items をまとめてバルクインサートする)
  • DELETE https://api.mycompany.com/v1/items (items をまとめてバルクデリートする)
  •   :
リクエストした結果をどのようなフォーマットで取得するか、というフォーマットの指定も(特定フォーマットで固定、ということでなければ)リクエストに含めることができるべきです。一般的には URI の最後に拡張子の形で指定できるようにすることが多いようです:
  • GET https://api.mycompany.com/v1/items.json (複数の items を JSON で取り出す)
  • GET https://api.mycompany.com/v1/items.xml (複数の items を XML で取り出す)
  • GET https://api.mycompany.com/v1/item/x100.json (id = "x100" の item を JSON で取り出す)
  • GET https://api.mycompany.com/v1/item/x100.csv (id = "x100" の item を CSV で取り出す)
  •   :
拡張子が指定されていなかった場合は、エラー扱いとするか、またはあらかじめ決められたデフォルトフォーマットのリクエストに転送されて処理されるようにするか、を決めておきます。

なお、そもそも CSV で取り出すことができないようなデータ構造のオブジェクトを対象とする場合は無理に CSV に対応する必要はないと思っています。

また「検索」や「アップロード」といった処理については別の URI を用意することになります。



【2. API をリクエストする際のパラメータ】
一部 3. で考慮する内容も含まれるのですが、リクエスト時の URL パラメータをどのように標準化するべきかを記載します。

複数のデータが返ってくる可能性のある API を実行する場合、全データ量が膨大にならないようパラメータで制御することがあります。この例を使って以下を紹介します。

一般的には limit パラメータと offset パラメータを用いて取得範囲を指定します。limit は取得件数、offset は「何件目から」を指定するパラメータです。一般的に offset のデフォルト値は 0(1件目から)で、limit のデフォルト値は 100 以下が推奨されています:
  • GET https://api.mycompany.com/v1/items.json?limit=20&offset=100 (items を 100 件目から 20 件、JSON で取り出す)
  •   :

これら以外によく使われるパラメータとしては以下があります。どのようなパラメータに対応すべきかを事前に決めておきます:
パラメータ用途
sortソートキーを指定
since指定タイムスタンプ以降のデータを取り出す
until指定タイムスタンプ以前のデータを取り出す
fields実行結果に含まれるフィールド値を指定したものだけにする(カンマ区切り)


【3. API からのレスポンス】
API の(実行できなかったケースも含めた)実行結果にもある程度の共通化・標準化がされていると、実行後の処理ハンドリングに手間取ることが少なくなります。このレスポンスは (1) 成功した時と、(2) 失敗した時 の両方を考慮した上で標準化する必要があります。ただし KML や iCal, GeoJSON など、フォーマットが規定済みである場合は、そのフォーマットに従うものとします。

考慮点を以下に挙げますが、多くの場合で開発者はまず API を実行し、その結果を見て開発していく、という流れになります。そのため返却された内容で実行結果を判断できるようになっていることが望ましいと考えられます。

まず (1) 成功時のレスポンスデータには以下のような内容が含まれているべきと考えられます:
パラメータ用途
statusHTTP ステータス
resultset全データ件数、offset 値、limit 値
result実行結果(配列)


{
  status: 200,
  resultset: {
    count: 3,
    offset: 10,
    limit: 20
  },
  result: []
}


また (2) エラー時のレスポンスデータには以下の情報を返して原因追求できるようにするべきです:
パラメータ用途
statusHTTP ステータス
typeエラー種別
errorエラー内容


{
  status: 401,
  type: "not authorized",
  error: {
    message: "Not authorized to perform this operation."
  }
}

【4. セキュリティ】
フォーマットの取り決め、という意味での標準化は上述のような感じですが、フォーマット以外にも標準化の対象はあります。その1つがセキュリティ項目です。代表的なものを挙げてみました:
観点対策説明
通信の改ざん、盗聴通信暗号化SSL(https)通信に対応することで送受信データの信頼性担保および情報漏えいの防止を行う。
情報漏えいAPI キーなどによる認証API 利用者に認証をかけることで API を利用できる人を制限する。また利用者毎の利用頻度を確認できるようにすることで API キーが漏洩している可能性を早めに特定できるようになる。アクセストークンを利用する場合はアクセストークンの有効期間を適切に設定して、トークン漏洩時の2次被害を最小にする。
負荷対策利用制限、キャッシュAPI に DDoS 攻撃が行われることを想定した管理が必要。コール数に対する利用制限をかけたり、同一の GET リクエストに対してキャッシュレスポンスで対応する、など
クロスドメイン間の通信CORS 対応ウェブブラウザではクロスサイトスクリプティング防止の観点からクロスドメイン間通信は行わない仕様となっている。場合によってはクロスドメイン間通信を許可する必要が生じ、そのための対応が必要となる




【5. テスト】
API を実際に動かしてテストする上での、テスト環境やテスト方法を標準化することで、テスター担当者の負担を軽減することができます。

一般的には動作を確認できるようなフォームやサンプルプログラムを用意して実際に実行し、そのレスポンスを確認することになります。

なお、6. で後述する Swagger ドキュメントを利用することでテスト用フォームを自動生成することができます。


【6. 公開方法】
API をドキュメント化してテスターや開発者に公開するまでの流れを標準化する、という項目です。このための各種ツールも存在していますが、以下では Swagger ドキュメントを紹介します。

Swagger ドキュメントは Open API Initiative が提供するオープン企画 "OAS" で採用されている REST API のドキュメント化規格です。

Swagger Editor や、YAML による特定フォーマットで API を記述することで、インタラクティブなドキュメント UI を生成することができます。単に URI やパラメータの説明をするだけでなく、実際にAPI を動かすことができる、という点が最大の特徴となっています。開発者視点では実際に API の動作を確認しながら仕様を確認できるのでとても有用な API ドキュメントといえます:
2020111801



【参考】
 内閣官房情報通信技術(IT)総合戦略室  API テクニカルガイドブック

この記事の続きです:
開発者視点で「理想的なブロックチェーン」とは?


開発開始から4週間、ブロックチェーン対応 RESTful データベース HATOYA がある程度動くようになった記念の Docker イメージを公開します。なお現時点で公開しているのは amd64 版の(x86 Linux 版の)Docker イメージです。

以下ではとりあえず開発用途として使うためのシングルノードで動かす場合の使い方を含めて紹介します(マルチノードで動かすこともできるのですが、、、その説明はまた別の機会に。興味ある方は連絡いただければ何か対応しますw)。


(追記)
マルチノード運用の記事を追加しました:
HATOYA をマルチノード運用して、ブロックチェーン情報を分散管理する


【HATOYA とは?】
HATOYA の特徴は大きくは以下の3点です:
(1) REST API でデータベースやその中のアイテムデータを CRUD(Create/Read/Update/Delete) するデータベースです。
(2) 変更を伴う API(読み取り以外の API)を実行すると、その内容は内蔵ブロックチェーンに自動記録されます。
(3) マルチノードで動かす場合、ブロックチェーン部分はマルチノード間で同期します(データベース部分は同期しません)。
 (3-1)データの破損時や環境引越し時などはマルチノード内の他ノードのブロックチェーンを使って、データベースやブロックチェーンのリストアが可能です。
 (3-2)ブロックチェーンネットワークへはプライベートネットワークからでも参加可能です(この場合、push 型の同期になります)


【事前準備】
x86 版の Docker 環境をインストールしておいてください。後述の公開イメージが amd64 アーキテクチャ用のため、Windows コンテナ版やラズパイコンテナ版ではなく、x86 Linux 用のものが必要です。

※ソースコード的にはラズパイ上でも動くことを確認しています。どなたかラズパイコンテナ用のイメージと併せてマルチアーキテクチャイメージにする具体的な方法をご存知の方がいたら教えていただけると助かります。


【Docker イメージ】
DockerHub 上に公開しました:
https://hub.docker.com/repository/docker/dotnsf/hatoya


2020080300


【起動方法】
イメージを DockerHub から pull して起動します。HATOYA はデフォルトで 4126 番ポートでリクエストを待ち受けるので、同じ 4126 番ポートで待ち受ける形で外部からもリクエストを受け付ける形にするのであれば、以下のように起動します:
$ docker pull dotnsf/hatoya

$ docker run -d -p 4126:4126 dotnsf/hatoya


(注 この方法で起動した場合、システム情報やデータ情報、ブロックチェーン情報は永続化されないため、コンテナを再起動するとデータを失ってしまいます。これを避けるためには以下のような(ボリュームを使った)起動パラメータを指定して、システムフォルダを永続化する必要があります)
$ docker run -d -v /tmp/.system:/tmp/.system -e SYSTEM_FOLDER=/tmp/.system -p 4126:4126 dotnsf/hatoya



これで 4126 番ポートで REST API の待受状態になります。同一ホストからのみ http://localhost:4126/doc を開くことで Swagger API ドキュメントを参照/実行することができます(Swagger API ドキュメントは他ホストからでも参照は可能ですが、実際に API を実行することは CORS 制約によりできません):
2020080101



【いくつかの API を実行してみる】
では Swagger API ドキュメントを使って、実際にいくつかの REST API を実行してみます。

※この節で書かれた内容が難しくてわからん、という方は下の「ダッシュボードを使う」を参照してください。同じ操作を GUI で行えます。

例えば、「現在のデータベース一覧」は GET /api/dbs という REST API で取得できます(Swagger API ドキュメント上では GET /dbs と表示されていますが、API は全て /api パス以下にあるため省略表記されています。以下同様):
2020080102


実際に実行するには Swagger API ドキュメントから "Try it out" を押した後に "Execute" を実行します。なおこの画面ではパラメータに token (トークン)を指定できるようになっていますが、今回はトークンを無効にして起動しているので指定する必要はありません(トークンがないと API が実行できないように設定することは可能です):
2020080103


"Execute" すると、実際に実行された内容を curl コマンドで実行した場合のコマンド内容(curl -X GET "http://localhost:4126/api/dbs" -H "accept: application/json")や、エンドポイント URL (http://localhost:4126/api/dbs)とともに実行結果が表示されます:
2020080104


ここでは実行結果は以下のようになっています。この実行結果の意味は実行は成功(status: true)し、データベースの一覧は空([])になっている、という内容でした:
{
  status: true,
  dbs: []
}

※注 HATOYA の REST API は(添付ファイル参照など)バイナリデータを返すもの以外は、実行結果は原則的に JSON データとなります。またその status 要素が true の時は成功、false の時は失敗を意味するよう統一されています。


なお、localhost 以外のホストから同じ API を実行する場合は curl コマンドを参照し、localhost 部分を IP アドレス(以下の例では xx.xx.xx.xx)に置き換えて実行することで実現できます:
$ curl -X GET "http://xx.xx.xx.xx:4126/api/dbs" -H "accept: application/json"

{
  status: true,
  dbs: []
}

この節の以下の説明では全て Swagger API ドキュメントを使って操作しますが、外部から実行する場合は同様にして操作する API と同じ curl コマンドを実行して試してみてください。


ではデータベースを1つ作成してみましょう。データベースの作成は POST /api/{_db} を使って行います:
2020080105


ここでも "Try it out" を押し、パラメータ _db に作成するデータベースの名前(以下の例では "mydb")を指定して "Execute" を実行します:
2020080106


すると実行結果(以下例では { status: true })が表示されます。成功したようです:
2020080107


改めてもう一度先程実行したデータベース一覧の API(GET /api/dbs)を実行します。すると先程空([])だった dbs の値が [ "mydb" ] となり、データベース作成が反映されたことが確認できます:
2020080108


では次に作成したデータベース mydb 内に1つアイテムを作成してみます。アイテムの新規作成 API は POST /api/{_db} で行います(下図では POST /api/{_db}/{_id} となっていますが、この {_id} 部分を指定しないと新規作成とみなすので、結局エンドポイントは POST /api/{_db} となります)。なおエンドポイントがデータベース作成時と同様ですが、ポストデータが存在する場合はアイテムの新規作成、存在しない場合はデータベースの新規作成とみなされます:
2020080101


ではこれまでと同様に一度 "Try it out" をクリックし、_db に "mydb" を指定後、送信 Body 内に保存したい内容の JSON オブジェクト(以下の例では { name: "K.Kimura", hobby: "Programming" })を記入して "Execute" を実行します:
2020080102


実行結果が以下のように( { status: true, id: "xxxxxx" } )表示されます。status: true となっているので処理は成功し、アイテムの id が割り振られて保存されました。その id 値も実行結果内に表示されています:
2020080103


実際にデータベース内にアイテムが格納されたことを確認してみます。データベース内のアイテム一覧を見るには GET /api/{_db} を実行します。この _db パラメータに "mydb" を指定して "Execute" します(なおこの API を実行する際に limit や offset を指定することも可能ですが、今回は使用せずに全データを表示します):
2020080104


こちらが実行結果({ status: true, items: [ "xxxxxx" ], limit: 0, offset: 0 })です。status: true が含まれているので処理は成功し、結果は items: [ "xxxxxx" ] という形で含められています。この items は指定したデータベースに含まれるアイテムの id の配列となっています。この時点では1つだけアイテムが含まれていることがわかります:
2020080105


ではこの id のアイテムが本当に先程入力したアイテムかどうかも確認してみます。特定のアイテムデータを取得するには GET /api/{_db}/{_id} を実行します。このパラメータのうち、_db は "mydb" 、_id は先程取得した items 配列の中に1つだけ含まれていた id の値を指定して "Execute" します:
2020080106


その実行結果がこちら({ status: true, item: { name: "K.Kimura", hobby: "Programming", id: "xxxxxx" } })です。先程の新規作成時に入力した内容が正しく記録されていました:
2020080107


今度はこのアイテムの内容を更新してみます。アイテムを更新する API は先程の新規作成時と同じ POST /api/{_db}/{_id} です({_id} を未指定時に新規作成とみなします)。この _db を "mydb" に、_id を先程指定した id 文字列値にして、body 部を適当に(下例では { name: "キムラ ケイ", hobby: "プログラミング" } と)して "Execute" します:
2020080108


再度アイテムの内容を確認する GET /api/{_db}/{_id} を、 _db に "mydb" 、_id に id 文字列値を指定して実行してみます。実行結果は { status: true, item: { name: "キムラ ケイ", hobby: "プログラミング", id: "xxxxxx" } } となり、正しくアイテムを更新することができました:
2020080109


今度はこのアイテムを削除してみます。アイテム削除の API は DELETE /api/{_db}/{_id} です。同様にして _db に "mydb" を、_id に id 文字列値を指定して "Execute" します:
2020080101


実行後に再度 GET /api/{_db}/{_id} を同じパラメータで実行しても、結果は { status: false } となり、実行に失敗することが確認できます。アイテムが消えてしまったので見つからない、という一覧の結果が確認できました:
2020080102


HATOYA にはこれらの基本的なアイテム CRUD 用 API 以外にも添付ファイルをアイテムとして登録する API があったり、複数アイテムをまとめて作成/更新/削除する、いわゆる「バルク処理」を行う API が存在してます。ここでは紹介しませんが、興味ある方は Swagger API ドキュメントを参照して試してみてください。


さて、ここまでが RESTful DB としての HATOYA の紹介でした。ここから先は HATOYA の特徴的な部分でもあるブロックチェーンプラットフォームとしての機能を紹介します。

ここまでの一連の処理の中で何度かシステムに変更を及ぼす API を実行しました。具体的には (1) mydb データベースの作成 (2) mydb データベースにアイテムを1件新規登録 (3) mydb データベースに登録したアイテムの内容を更新 (4) mydb データベースに登録したアイテムを削除 の計4回変更しています。これらの変更記録は全て HATOYA 内のブロックチェーンに自動登録されています。今度はブロックチェーンの中を参照する API を紹介します。

それが GET /api/ledgers です。早速実行してみよう・・と思うのですが、この API は実行時に serverid という HATOYA サーバーノードの id をパラメータ指定する必要があります。まずはこの serverid を取得しましょう:
2020080103


serverid 情報は GET /api/serverid で取得することができます。まずこの API を実行して、その実行結果に含まれる serverid 値を取り出します:
2020080104


GET /api/serverid 実行結果( { status: true, serverid: "zzzzzz" } )の "zzzzzz" 部分が、このサーバーノードの serverid 値です。今回の GET /api/ledgers 以外にも実行時にこの serverid 値をパラメータ指定する必要のある API がいくつかありますが、全てこの方法で確認可能です:
2020080105


では改めて、取得した serverid を指定して GET /api/ledgers を実行します:
2020080106


実行結果は以下のようになりました:
2020080107

{
  "status": true,
  "ledgers": [
    {
      "prev_id": null,
      "body": [
        {
          "serverid": 1594300581524,
          "method": "create_db",
          "db": "mydb"
        }
      ],
      "timestamps": [
        1596292186611
      ],
      "nonce": 34,
      "id": "0041cf982623372e59bafac8ee055f63b061fa702ea0364e6894fc5c453cd8ae0c0bf5e094892e8d34ac8d78229bcfb014f7641595bed4987545cf425b7e2d38"
    },
    {
      "prev_id": "0041cf982623372e59bafac8ee055f63b061fa702ea0364e6894fc5c453cd8ae0c0bf5e094892e8d34ac8d78229bcfb014f7641595bed4987545cf425b7e2d38",
      "body": [
        {
          "serverid": 1594300581524,
          "method": "create_item",
          "db": "mydb",
          "item": {
            "name": "K.Kimura",
            "hobby": "Programming",
            "id": "52ca8330-d405-11ea-a583-074fb3956e72"
          }
        }
      ],
      "timestamps": [
        1596292991723
      ],
      "nonce": 232,
      "id": "00ab2eddbec25b10560d8769c3d21a21efee0edb2f69e634da34fe1f326aae96a2d5346cff4ff794c88bc68fa3c45fcf73ba483b3c1ae1977f3b326efda3aeed"
    },
    {
      "prev_id": "00ab2eddbec25b10560d8769c3d21a21efee0edb2f69e634da34fe1f326aae96a2d5346cff4ff794c88bc68fa3c45fcf73ba483b3c1ae1977f3b326efda3aeed",
      "body": [
        {
          "serverid": 1594300581524,
          "method": "update_item",
          "db": "mydb",
          "item": {
            "name": "キムラ ケイ",
            "hobby": "プログラミング",
            "id": "52ca8330-d405-11ea-a583-074fb3956e72"
          }
        }
      ],
      "timestamps": [
        1596294186025
      ],
      "nonce": 427,
      "id": "0066ee6659b2ea71365ddc8b93a780e2d3684fc7f40d6c5f3ccec6835385f7863c1737f8eef239a0867440f2493d246166387082053f7cbb20aea468a8e74c7a"
    },
    {
      "prev_id": "0066ee6659b2ea71365ddc8b93a780e2d3684fc7f40d6c5f3ccec6835385f7863c1737f8eef239a0867440f2493d246166387082053f7cbb20aea468a8e74c7a",
      "body": [
        {
          "serverid": 1594300581524,
          "method": "delete_item",
          "db": "mydb",
          "id": "52ca8330-d405-11ea-a583-074fb3956e72"
        }
      ],
      "timestamps": [
        1596294559724
      ],
      "nonce": 88,
      "id": "006c91e3c08ded1791775aab6a0fa02b82cb842f5d2e6fba989d58b64444b5f0005ac115eb969b8491cad824ac6e5e924c4b8baa6567c04c7997b505a7891016"
    }
  ],
  "limit": 0,
  "offset": 0
}

実行結果の JSON オブジェクト内の ledgers が配列となっていて、その中にブロックチェーンの内容が含まれています。視認しやすいように上記では色をつけていますが、(1) mydb データベースの作成 (2) mydb データベースにアイテムを1件新規登録 (3) mydb データベースに登録したアイテムの内容を更新 (4) mydb データベースに登録したアイテムを削除 の4つが全て serverid 付きで登録されていることがわかります。しかも各オブジェクトには id と prev_id (と nonce)というキーがあり、これらがハッシュチェーンとなって繋がっていることで改ざんが困難なブロックチェーンとして機能しています。

つまり、この段階では HATOYA には mydb というデータベースが1つだけ存在していて、中身は何のアイテムデータが含まれていない状態になっていますが、これがデータベースが作られた直後ではなく、アイテムデータが作成され、変更され、削除された上での結果として何も含まれていない状態になっている、ということをブロックチェーンという技術が保証してくれていることになります。

これらのデータベースシステムへの変更作業が自動でブロックチェーンに登録される(そしてこのブロックチェーンは改ざんが困難なので変更履歴が保証される)ことが HATOYA の特徴の1つになっています。


【ダッシュボードを使う】
一つ上の節では Swagger API ドキュメントや REST API を使って HATOYA のデータベースを読み書きしたり、その結果生成されたブロックチェーンの様子を確認しました。これが理解しにくい、という人向けに簡易ダッシュボード機能も用意しています(ただしダッシュボードでは全ての REST API を実行できるわけではありません。例えば添付ファイルの読み書きやバルク API 実行は現在のダッシュボードからは行なえません)。

ダッシュボードへアクセスするにはブラウザでドキュメントルート(/)にアクセスします。localhost 上で実行している場合であれば http://localhost:4126/ へアクセスします。初回のみ Basic 認証を聞かれます。デフォルトではユーザー名 = username, パスワード = password です:
2020080202


※なお、この認証値は Docker コンテナ起動時の環境変数パラメータで変更可能です。例えばユーザー名 = user, パスワード = pass にするには以下のように起動します:
$ docker run -e BASIC_USERNAME=user -e BASIC_PASSWORD=pass -d -p 4126:4126 dotnsf/hatoya

正しい認証値を入力するとダッシュボードの画面が表示されます:
2020080201


画面左部分がメニューになっており、ブロックチェーン一覧や新規データベース作成はこちらから行えます。なおこの画面からブロックチェーン一覧を実行する場合、上述時に指定した serverid 値は自動的に指定されるので意識する必要はありません。なお serverid 値を確認したい場合は左上のハトマークにマウスカーソルを移動させると表示できます(シングルノード稼働の場合、(Config) と書かれた部分の環境設定は不要です、また config や ledgers という名前のデータベースを作成することはできません):
2020080203


データベースを作成すると (Config) の下に存在するデータベースが一覧で表示され、どれか1つを選択すると、そのデータベース内のアイテム一覧が表示したり、新規にアイテムを追加したり、編集/削除したりが行なえます。データベースの削除もこの画面から可能です:
2020080204


一通りの作業後に画面左の (Ledgers) を選択すると、その時点でのブロックチェーンの状態を確認することが可能です:
2020080205


↑UI 的にも全然イケてないダッシュボードですが、最小限の CRUD 操作を行ってブロックチェーンの確認までは行えるようになっています。Swagger API ドキュメントや curl の操作がよくわからない場合はこちらも併用してください。



【ブロックチェーンアプリケーション開発に】
HATOYA の全ての機能を紹介しているわけではないのですが、とりあえずこれらの情報を使うことでシングルノードの HATOYA を使ったブロックチェーン対応データストアが実現できると思っています。試験的にアプリケーションと組み合わせて使ってみていただいても構いませんし、HATOYA 対応アプリケーションを開発する用途であれば、その環境はシングルノードでも十分だと考えています。基本的には DB を REST で読み書き更新削除するだけでブロックチェーンに記録されるという特徴があるので、ブロックチェーンをほぼ意識することなくブロックチェーン対応アプリケーションが実装できると思っています(ちなみに拙作のマンホールマップ内でも使っています)。

今回紹介していませんが、本来は config 機能を使ってブロックチェーンネットワークを構築し、マルチノードでブロックチェーン部分を同期しながら運用することも想定しています。そこにもいくつかの特徴的な機能もあって我ながら面白い作品だと思っていますが、そのあたりについてはまた別の機会に。。


このページのトップヘ