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

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

タグ:api

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

やりたかったのはウェブアプリで
 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

IBM Cloud から提供されているユーザーディレクトリ(ログイン機能用サービス)である App ID で、デモ用途などでまとめて一括でユーザーを作成したいことはないでしょうか? 1件や2件程度であればサービスダッシュボードから直接作成してもいいのですが、10件とか100件とかになると面倒ですよね。

というわけで「指定した CSV ファイルに記載された情報を使って App ID のユーザーを API 経由で作成するツール」を作って公開してみました。Node.js 環境があれば実行可能です:
https://github.com/dotnsf/appid_users


上記リポジトリを git clone するかダウンロード&展開すると test.csv という CSV ファイルが見つかります。このフォーマットに従う形で、
 表示名,メールアドレス,パスワード
という順に値が1行ずつ記録された CSV ファイルを用意します(メールアドレス=ログインIDになります)。

次に settings.js ファイルを編集して、利用する App ID のサービス接続情報に書き換えます。

準備の最後に依存ライブラリをインストールします:
$ npm install


そして create_user.js を Node.js で実行すると、この CSV ファイルに記述されたユーザーをまとめて作成します(最後に対象 CSV ファイルを指定します):
$ node create_user test.csv

この作業後に App ID のコンソールからクラウド・ディレクトリーのユーザー一覧を確認すると、CSV ファイルで指定したユーザーが作成されていることを確認できます:
2021071001


作成後にログイン確認用のウェブアプリケーションを実行して、ユーザーが正しく作成されているかどうかを確認することができます。その場合はリダイレクト URL に http://localhost:8080/appid/callback を追加しておいてください:
2021071003


改めてアプリケーション起動後($ node app)に http://localhost:8080/ にアクセスし、CSV ファイルに含まれていた ID とパスワードを指定して、正しくログインできるかどうかを確認してください。ログインできるようになっていれば無事に CSV ファイルからユーザーをインポートすることができた、ことになります:
$ node app


2021071002



こんなのを作ってみました。これ自体がそのまま直接役立つとは思ってませんが、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


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

このページのトップヘ