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

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

タグ:rest

ちょっとした目的で実験的に調査&開発していたウェブアプリの話がポシャりそうなので、アプリだけ公開することにしました。

やりたかったのはウェブアプリで
 1 日本語音声を認識して日本語テキスト化して、
 2 そのテキストを形態素解析して単語分解した上で、
 3 単語の出現頻度をタグクラウドで視覚化し、
 4 (ある程度以上の語彙が集まったら)テキスト内容から感情分析を行ってグラフ化する
 5 1~4をリアルタイムに行う(音声認識が途切れるタイミングで全情報を更新する)
というものでした。

技術的には色々な実現手段があるのですが、今回は実験的に作っていた経緯もあるので、
 1 Web Speech API
 2 TinySegmenter
 3 jQCloud
 4 色々使って独自実装
で作っています。

本当は4で IBM Personality Insight API とか使えると簡単でよかったのですが、今回は使っていないことを白状しておきます。また1の実装の都合上、PC 版 FireFox では動きません。後述のサンプルを利用する際には PC の Chrome か  Edge で試していただきたいです(互換性という意味では Android 版 Chrome でも動くことになっていますが、自分のテストでは認識精度がかなり悪かったので PC での利用をお勧めします)。また4の実装の中で利用回数に制約のある無料 API を使っているため、使いすぎて(使われすぎて)限度回数を超えてしまうとその月の間は動かなくなる、という点をご了承ください。

で、作って公開してみたのがこちらです。繰り返しますが PC 版 Chrome でアクセスしてください:
https://dotnsf.github.io/webspeechpi/


アクセスするとこのような画面が表示されます。右上の青いマイクボタンをクリックすると音声認識モードに切り替わります(初回のみマイクアクセスの許可を求められるので「許可」してください):
2022052901


音声認識モードに切り替わると青だったボタンは赤く変わります。またウェブページのタブに音声収集中であることを示す赤いマークが表示されます(ちなみにもう一度赤マイクボタンをクリックすると青マイクボタンに戻り、音声認識モードからも抜けます):
2022052902


この音声認識モードの状態でマイクに日本語で話しかけると、その文章が認識されて表示されます。ある程度の長さの無音状態が続くまでは1つの文章とみなして、たまに内容を変更・調整しながら認識を続けます:
2022052903


ある程度の長さの無音状態を認識すると、それまでに認識した文章を形態素解析し、名詞や動詞、形容詞といった、文章内容の肝となりうる単語がタグクラウドで表示されます:
2022052904


この認識文章の量が少ないとタグクラウドだけが表示されますが、ある程度以上の文章が認識されるとタグクラウドに加えて、そのテキスト内容から話している人の感情を5つの要素で分析して、その結果がレーダーチャートで表示されます。このタグクラウドとレーダーチャートは文章が入力されるごとに更新されます:
2022052905


と、まあこんな感じのものです。もともとはある業務目的のために作っていたものですが、不要になりそうだったので、せっかくなのでアプリ部分だけ公開することにしました。感情分析の精度は恥ずかしくなるようなものなので、ネタ程度に使ってみてください(笑)。本当は形態素解析ももう少し高度にできるのですが、無料公開できるものを作ろうとするとこんな感じになりました。

ウェブアプリそのものの(フロントエンド部分の)ソースコードはこちらで公開しています:
https://github.com/dotnsf/webspeechpi


感情分析 API はフロントエンドには含まれていません(公開していない理由はあまりに雑な実装で恥ずかしいため)。こちらの実装の中身に興味ある方がいたら教えてください。




ちまたで(僕の 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/
https://dotnsf-fx.herokuapp.com/

(2022/04/23 追記 上記 URL を変更しました)


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 テクニカルガイドブック

このページのトップヘ