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

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

2022/01

ちまたで(僕の 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 を作るよりも遥かに難しそうなので、もう少しまとまった時間の取れそうなときにでも。


昨年、ブログを使ってこのような夏休み企画(休んでないけど)を実施しました:
「30 日後に死ぬ k8s」


IBM Cloud から提供されている k8s 環境は(ワーカーノード1つなどの条件の下で)30 日間無料で使えます。この環境を使って 30 日間で毎日1つずつ、計 30 種類のコンテナをデプロイして動かす、というものでした。後に追加で動作確認できたり、動作確認はできなかったがデプロイできてるっぽいものもあったのですが、その際も IBM Db2 は公式イメージを docker で動かすことはできたのですが、この k8s 環境ではうまくデプロイできずにいました。

が、最近になって改めて挑戦し、今度は同環境で IBM Db2 コンテナを動かすことができました。Db2 クライアントからの接続も確認できました。以前できなくて今回できた理由ははっきりとはわかっていませんが、自分の腕が上がったのか、イメージ側の更新と関係があるのか、あるいはその両方か・・・ ともあれ、遅ればせながら IBM Db2 コンテナイメージを IBM Cloud の 30 日間無料 k8s クラスタにデプロイして動かす方法を紹介します。


【30 日間無料の k8s クラスタを用意】
こちらの記事を参照して、IBM Cloud の 30 日間無料 k8s クラスタをセットアップしてください。また具体的な操作をする際に必要な ibmcloud CLI や kubectl CLI といったコマンドツール類のインストール手順も含まれています(リンク先最後の "$ kubectl get all" コマンドが正しく実行できる状態にしてください):
http://dotnsf.blog.jp/archives/1079287640.html


なおリンク先でも触れられていますが、この k8s クラスタ環境はシングルワーカーノードです。イメージをデプロイした後に "type = NodePort" を指定することでサービスを公開できます(Ingress などが提供されていないため、他の方法でサービス公開はできません)。


【IBM Db2 コンテナイメージ】
公式な IBM Db2 コミュニティ版のコンテナイメージはこちらから無料で提供されています:
https://hub.docker.com/r/ibmcom/db2


同ページ内の説明文によると、この無料コミュニティ版コンテナイメージを動かす前提条件として、「CPU: 4コア以下、メモリ: 16GB 以下」が挙げられています:
2022011801


一方で IBM Cloud の 30 日間無料 k8s クラスタのワーカーノードは「CPU: 2 コア、メモリ: 4GB」であることがわかっています:
2022011802


一応、条件を満たした環境で使う、ということになります。なおコンテナイメージのリンク先情報によると、2022/01/18 時点で特にバージョンを指定せずにこのコンテナを利用しようとすると、最新版である v11.5.7.0 が使われるようです(以下の内容はこのバージョンで動作確認しています):
2022011803



【IBM Db2 を k8s にデプロイするための yaml ファイル】
色々試行錯誤してできあがった IBM Cloud の 30 日間無料 k8s クラスタにデプロイできるよう調整した yaml ファイルはこちらです:
https://github.com/dotnsf/yamls_for_iks/blob/main/db2_deployment_nodeport.yaml

このファイルを指定して、
$ kubectl apply -f https://github.com/dotnsf/yamls_for_iks/blob/main/db2_deployment_nodeport.yaml

とすることでデプロイできます(その場合は db2inst1 ユーザーのパスワードは db2inst1 、サンプル(SAMPLE)データベースは作成、それとは別に db2inst1 ユーザーから使える mydb という空のデータベースも作成する、というオプションが初期設定されています)。

あるいは以下のように一度 wget コマンドでローカルにダウンロードしてから内容をカスタマイズして、その後に改めて kubectl コマンドでデプロイすることも可能です:
$ wget https://github.com/dotnsf/yamls_for_iks/blob/main/db2_deployment_nodeport.yaml

(ダウンロードした db2_deployment_nodeport.yaml ファイルをカスタマイズ)

$ kubectl apply db2_deployment_nodeport.yaml


例えば db2inst1 ユーザーのパスワードを変えたい場合は 38 行目を変更、サンプルデータベースを作る必要がなければ 39, 40 行目を削除、mydb データベースもこの時点で作る必要がなければ 41, 42 行目を削除、といった具合です。

なお後者のカスタマイズをする場合に指定可能なオプション内容については公式ページ内"Advanced Configuration Options" 欄を参考に編集してください。

"kubectl get all" コマンドでデプロイ後のクラスタの様子を確認してみます:
$ kubectl get all

NAME                            READY   STATUS    RESTARTS   AGE
pod/db2-54ff649944-w7d24        1/1     Running   0          2d14h
pod/hostname-7b4f76fd59-c8b2l   1/1     Running   0          8d

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)           AGE
service/db2          NodePort    172.21.54.211           50000:30500/TCP   2d14h
service/hostname     NodePort    172.21.105.49           8080:30080/TCP    8d
service/kubernetes   ClusterIP   172.21.0.1              443/TCP           8d

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/db2        1/1     1            1           2d14h
deployment.apps/hostname   1/1     1            1           8d

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/db2-54ff649944        1         1         1       2d14h
replicaset.apps/hostname-7b4f76fd59   1         1         1       8d

↑のように Db2 関連の pod, service, deployment, replicaset が1つずつ追加されていればコマンドは成功しています。 また service を見ると 50000 番ポート(Db2 の標準動作ポート)が 30500 番で公開されていることも確認できます。つまり Db2 は
 (ワーカーノードのIPアドレス※):30500
で公開されて動いていることがわかります(※ワーカーノードの IP アドレス確認方法はこちらを参照してください)。


【動作確認】
では実際に Db2 サーバーが正しく動作していることを動作確認してます。。。 と、実はここが大きな問題でした。Db2 サーバーは正しくデプロイされて動作しているのですが、Db2 サーバーに接続試験を行おうとすると、その環境準備がちょっと面倒だったりするのです。

Db2 サーバーに CLI でアクセスしようとすると Db2 クライアントライブラリのダウンロード&インストールが必要です。Node.js などのプログラムから接続するにもクライアントライブラリが必要になります。この Db2 クライアントライブラリをインストールするには、無料版の Db2 Community Edition をダウンロードしてインストールする必要があり、ここで急にハードルが上がってしまいます。

そこでこれらの手間を省くために別の動作確認方法を考えました。それは「Db2 のコンテナインスタンスをもう1つ作って、そこからリモートサーバーにアクセスする」という方法です。マネージドサービスと異なり、Db2 サーバーのコンテナイメージからはライブラリセットアップ済みの CLI も使えるのです。そのための Db2 コンテナをもう1つ追加で作ってクライアントとして使うことで(手間という意味では)比較的容易に動作確認用クライアント環境が用意できると思いました。

というわけで、先程の yaml ファイルを少し変更(サンプルDB 作らず、mydb DBも作らず、ポート番号は 30501 に変更、コンテナの名称も db2 から db2cli に変更)した yaml ファイルを使って、(機能的にはサーバー機能もインストール済みですが)新しい Db2 クライアントコンテナを作成します:
$ kubectl apply -f https://github.com/dotnsf/yamls_for_iks/blob/main/db2cli_deployment_nodeport.yaml

作成後にクラスタ内の様子を kubectl コマンドで確認すると、新しい db2cli インスタンスが起動しているはずです:
$ kubectl get all
NAME                            READY   STATUS    RESTARTS   AGE
pod/db2-54ff649944-w7d24        1/1     Running   0          2d14h
pod/db2cli-86d476659d-m7c6d     1/1     Running   0          9s
pod/hostname-7b4f76fd59-c8b2l   1/1     Running   0          8d

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)           AGE
service/db2          NodePort    172.21.54.211           50000:30500/TCP   2d14h
service/db2cli       NodePort    172.21.112.13           50000:30501/TCP   11s
service/hostname     NodePort    172.21.105.49           8080:30080/TCP    8d
service/kubernetes   ClusterIP   172.21.0.1              443/TCP           8d

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/db2        1/1     1            1           2d14h
deployment.apps/db2cli     1/1     1            1           11s
deployment.apps/hostname   1/1     1            1           8d

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/db2-54ff649944        1         1         1       2d14h
replicaset.apps/db2cli-86d476659d     1         1         1       11s
replicaset.apps/hostname-7b4f76fd59   1         1         1       8d

ここからは db2cli コンテナからの作業となります。まずは db2cli コンテナ上のシェルにログインする必要があります。まずは IBM Cloud の k8s サービスからダッシュボードにアクセスします:
2022011801


ダッシュボード画面左メニューから「ポッド」を選び、ポッド一覧から "db2cli" で始まる名前のポッドを探し、画面右のメニューから「実行」を選択します:
2022011802


これで該当 Pod のシェルをブラウザからリモート操作できるようになります:
2022011803


以下の手順で(最初に作った方の)Db2 サーバーに接続して SQL を実行する、という動作確認をします。

まずは Db2 を利用可能なユーザー(db2inst1)にスイッチします(プロンプトが # から $ へ切り替わります):
# su - db2inst1
$

次に Db2 コマンドをインタラクティブに実行できるシェルに切り替えます(プロンプトが $ から db2 => に切り替わります):
$ db2
db2 =>

この状態から Db2 サーバーに接続したり、ログインしたり、SQL を実行したりできます。まずは Db2 サーバーへの接続が必要です。Db2 ではリモートサーバーやデータベースを「カタログ」という単位で管理するので、まず最初にリモート Db21 サーバーを db2onk8s という名前でカタログに登録します。以下のコマンドを実行します(1.2.3.4 部分はワーカーノードの IP アドレスに置き換えてください):
db2 => catalog tcpip node db2onk8s remote 1.2.3.4 server 30500

次にこのサーバー上の SAMPLE データベースをカタログに登録します:
db2 => catalog database sample at node db2onk8s

これで Db2 クライアントから Db2 サーバー上の SAMPLE データベースに接続するためのカタログができました。

ではこのデータベースカタログを使って Db2 サーバー上の SAMPLE データベースに接続します。このコマンドを入力した後に db2inst1 ユーザーのパスワードを聞かれるので、Db2 サーバーのコンテナを作成した時に指定した db2inst1 ユーザーのパスワード(変更していない場合は db2inst1)を入力してください:
db2 => connect to sample user db2inst1
Enter current password for db2inst1:


正しいパスワードを入力すると目的のデータベースに接続します。この状態で Db2 クライアントから Db2 サーバーに接続しています:
db2 => connect to sample user db2inst1
Enter current password for db2inst1:

   Database Connection Information

 Database server        = DB2/LINUXX8664 11.5.7.0
 SQL authorization ID   = DB2INST1
 Local database alias   = SAMPLE

db2 => 

SQL を使って Db2 サーバーの SAMPLE データベースのテーブルからレコードを取り出したり、追加・変更・削除といった操作が可能です:
db2 => select * from employee;

EMPNO  FIRSTNME     MIDINIT LASTNAME        WORKDEPT PHONENO HIREDATE   JOB      EDLEVEL SEX BIRTHDATE  SALARY      BONUS       COMM       
------ ------------ ------- --------------- -------- ------- ---------- -------- ------- --- ---------- ----------- ----------- -----------
000010 CHRISTINE    I       HAAS            A00      3978    01/01/1995 PRES          18 F   08/24/1963   152750.00     1000.00     4220.00
000020 MICHAEL      L       THOMPSON        B01      3476    10/10/2003 MANAGER       18 M   02/02/1978    94250.00      800.00     3300.00
000030 SALLY        A       KWAN            C01      4738    04/05/2005 MANAGER       20 F   05/11/1971    98250.00      800.00     3060.00
000050 JOHN         B       GEYER           E01      6789    08/17/1979 MANAGER       16 M   09/15/1955    80175.00      800.00     3214.00
000060 IRVING       F       STERN           D11      6423    09/14/2003 MANAGER       16 M   07/07/1975    72250.00      500.00     2580.00
000070 EVA          D       PULASKI         D21      7831    09/30/2005 MANAGER       16 F   05/26/2003    96170.00      700.00     2893.00
000090 EILEEN       W       HENDERSON       E11      5498    08/15/2000 MANAGER       16 F   05/15/1971    89750.00      600.00     2380.00
000100 THEODORE     Q       SPENSER         E21      0972    06/19/2000 MANAGER       14 M   12/18/1980    86150.00      500.00     2092.00
000110 VINCENZO     G       LUCCHESSI       A00      3490    05/16/1988 SALESREP      19 M   11/05/1959    66500.00      900.00     3720.00
000120 SEAN                 O'CONNELL       A00      2167    12/05/1993 CLERK         14 M   10/18/1972    49250.00      600.00     2340.00
000130 DELORES      M       QUINTANA        C01      4578    07/28/2001 ANALYST       16 F   09/15/1955    73800.00      500.00     1904.00
000140 HEATHER      A       NICHOLLS        C01      1793    12/15/2006 ANALYST       18 F   01/19/1976    68420.00      600.00     2274.00
000150 BRUCE                ADAMSON         D11      4510    02/12/2002 DESIGNER      16 M   05/17/1977    55280.00      500.00     2022.00
000160 ELIZABETH    R       PIANKA          D11      3782    10/11/2006 DESIGNER      17 F   04/12/1980    62250.00      400.00     1780.00
000170 MASATOSHI    J       YOSHIMURA       D11      2890    09/15/1999 DESIGNER      16 M   01/05/1981    44680.00      500.00     1974.00
000180 MARILYN      S       SCOUTTEN        D11      1682    07/07/2003 DESIGNER      17 F   02/21/1979    51340.00      500.00     1707.00
000190 JAMES        H       WALKER          D11      2986    07/26/2004 DESIGNER      16 M   06/25/1982    50450.00      400.00     1636.00
000200 DAVID                BROWN           D11      4501    03/03/2002 DESIGNER      16 M   05/29/1971    57740.00      600.00     2217.00
000210 WILLIAM      T       JONES           D11      0942    04/11/1998 DESIGNER      17 M   02/23/2003    68270.00      400.00     1462.00
000220 JENNIFER     K       LUTZ            D11      0672    08/29/1998 DESIGNER      18 F   03/19/1978    49840.00      600.00     2387.00
000230 JAMES        J       JEFFERSON       D21      2094    11/21/1996 CLERK         14 M   05/30/1980    42180.00      400.00     1774.00
000240 SALVATORE    M       MARINO          D21      3780    12/05/2004 CLERK         17 M   03/31/2002    48760.00      600.00     2301.00
000250 DANIEL       S       SMITH           D21      0961    10/30/1999 CLERK         15 M   11/12/1969    49180.00      400.00     1534.00
000260 SYBIL        P       JOHNSON         D21      8953    09/11/2005 CLERK         16 F   10/05/1976    47250.00      300.00     1380.00
000270 MARIA        L       PEREZ           D21      9001    09/30/2006 CLERK         15 F   05/26/2003    37380.00      500.00     2190.00
000280 ETHEL        R       SCHNEIDER       E11      8997    03/24/1997 OPERATOR      17 F   03/28/1976    36250.00      500.00     2100.00
000290 JOHN         R       PARKER          E11      4502    05/30/2006 OPERATOR      12 M   07/09/1985    35340.00      300.00     1227.00
000300 PHILIP       X       SMITH           E11      2095    06/19/2002 OPERATOR      14 M   10/27/1976    37750.00      400.00     1420.00
000310 MAUDE        F       SETRIGHT        E11      3332    09/12/1994 OPERATOR      12 F   04/21/1961    35900.00      300.00     1272.00
000320 RAMLAL       V       MEHTA           E21      9990    07/07/1995 FIELDREP      16 M   08/11/1962    39950.00      400.00     1596.00
000330 WING                 LEE             E21      2103    02/23/2006 FIELDREP      14 M   07/18/1971    45370.00      500.00     2030.00
000340 JASON        R       GOUNOT          E21      5698    05/05/1977 FIELDREP      16 M   05/17/1956    43840.00      500.00     1907.00
200010 DIAN         J       HEMMINGER       A00      3978    01/01/1995 SALESREP      18 F   08/14/1973    46500.00     1000.00     4220.00
200120 GREG                 ORLANDO         A00      2167    05/05/2002 CLERK         14 M   10/18/1972    39250.00      600.00     2340.00
200140 KIM          N       NATZ            C01      1793    12/15/2006 ANALYST       18 F   01/19/1976    68420.00      600.00     2274.00
200170 KIYOSHI              YAMAMOTO        D11      2890    09/15/2005 DESIGNER      16 M   01/05/1981    64680.00      500.00     1974.00
200220 REBA         K       JOHN            D11      0672    08/29/2005 DESIGNER      18 F   03/19/1978    69840.00      600.00     2387.00
200240 ROBERT       M       MONTEVERDE      D21      3780    12/05/2004 CLERK         17 M   03/31/1984    37760.00      600.00     2301.00
200280 EILEEN       R       SCHWARTZ        E11      8997    03/24/1997 OPERATOR      17 F   03/28/1966    46250.00      500.00     2100.00
200310 MICHELLE     F       SPRINGER        E11      3332    09/12/1994 OPERATOR      12 F   04/21/1961    35900.00      300.00     1272.00
200330 HELENA               WONG            E21      2103    02/23/2006 FIELDREP      14 F   07/18/1971    35370.00      500.00     2030.00
200340 ROY          R       ALONZO          E21      5698    07/05/1997 FIELDREP      16 M   05/17/1956    31840.00      500.00     1907.00

  42 record(s) selected.


この時点で k8s ダッシュボード画面はこのようになっています。必要に応じて続けてコマンドを実行することもできます:
2022011800


操作を終了して接続を切るには quit コマンドを実行します:
db2 => quit

$ exit

# 


改めて k8s 上にデプロイした Db2 サーバーコンテナ(とクライアントコンテナ)が期待通りに動いていることが確認できました。


まず最初に、自分がもともと今回勝利する技術に興味を持ったきっかけは 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

2022 年最初のブログエントリとなります。遅ればせながら本年もよろしくお願いいたします。

今年最初のエントリは React や Angular といったフロントエンドフレームワークにまつわるネタです。最近流行りのこれらのフロントエンドフレームワークを使うことで、比較的簡単に SPA(Single Page Application) を作ることができます。SPA 化のメリット/デメリットを理解した上で作る必要があるとは思いますが、SPA 化することによる大きなメリットの1つに「Amazon S3 などのオブジェクトストレージや Github ページといった、安価かつ滅多にサービスダウンしない環境でフロントエンドのウェブホスティングができる」ことがあると思っています。

この点を少し補足しておきます。「自分が作って管理しているサービスやアプリケーションを安定運用したい」というのは誰でも思うことだと思っています。ただ現実にはこれは簡単なことではありません。サービスやアプリケーションは利用者が直接参照するフロントエンド部分に加えて、データベースなどのバックエンド部分、そしてこれらを繋ぐ API サーバーなどから成り立っていて、これら全てを安定運用するのは簡単なことではありません。特にフロントエンドや API サーバーについては最近よく耳にするコンテナオーケストレーションなどによって比較的安価に安定運用することもできるようになりました。ただフロントエンド部がアプリケーションサーバーを持たないシンプルな静的ウェブコンテンツであれば、上述したような Amazon S3 のウェブコンテンツ機能を使ったり、Github ページ機能を使うことで、滅多にサービスダウンしないウェブページとして公開するという方法もあります。現実問題として、この方法だとフロントエンドページの公開は簡単ですが、API サーバーなどのバックエンドとの通信時に CORS を考慮する必要が生じたりして、これはこれで面倒な設定も必要になるのですが、一方で「ほぼ無料」レベルの安価で利用できるウェブサーバーにしてはかなり安定しているのも事実で、用途によっては(「面倒な設定」の面倒レベルがあまり高くならないケースであれば)運用方法の考慮に含めてもいいのではないかと思っています。補足は以上。


一方、最近のウェブアプリケーション開発において IDaaS の利用ケースもよくあります(個人的にも業務で多く使っている印象です)。ログインや認証、ユーザー管理といった ID 周りの機能がマネージドサービスとして提供されていて、自分で面倒な開発をしなくても API や SDK から利用可能になる、というものです。具体的なサービスとしては AWS CognitoAuth0 などが有名どころでしょうか。IBM Cloud からも AppID という IDaaS が提供されています。

そしてフロントエンドアプリケーションと IDaaS の組み合わせというのが実は相性が悪かったりします(苦笑)。多くの IDaaS では OAuth と呼ばれるプロトコルでの認証/認可が主流となっているのですが、アプリケーションサーバーを持たない静的なフロントエンドアプリケーションでは OAuth のコールバックの仕組みとの相性が悪く、実装が非常に難しいという事情があります。その結果として、各 IDaaS ベンダーから認証専用の JavaScript SDK が提供され、それらを使って認証機能を実装することになります。IBM Cloud の AppID も専用の JavaScript SDK が用意され、React などで作ったフロントエンドに組み込むことで AppID へのログインや認証を実現できます(この SDK では PKCE と呼ばれる方法を使ってログインを実現しています)。

で、ここまでのアプリ開発方法に関する内容についてはこちらのページでも紹介されているのですが、問題はこの先にありました。この方法で作った React のフロントエンドアプリを Github ページにホスティングして運用しようとすると・・・結論としては少し特殊なリダイレクト URI の設定が必要でした。これを知らないと Github ページでの運用時にトラブルが起こりそうだと思ったので、将来の自分への備忘録の意味も含めてブログに設定内容を記載しておくことにしました。

前段部分の長いブログですが、といった背景の中で「IBM AppID の JavaScript SDK を使って作った SPA アプリを Github ページで動かす」ためのアプリ開発手順と設定内容について、順に紹介します。


【React で SPA アプリを作成する】
この部分は上述ページでも紹介されている内容ではありますが、一般的な内容でもあるので、特にコメントに色をつけずに紹介します。以下のコマンドで react-appid というフォルダに React のサンプルアプリを作成します:
$ npx create-react-app react-appid

$ cd react-appid

ここまでは普通の React アプリと同じです。ここから下で AppID の認証に対応したり、Github ページで運用する場合特有の設定を加えていきます。


【IBM AppID サービスインスタンスの準備を行う】
ここは上述ページでも触れられてはいるのですが、あまり詳しくは紹介されていないので、ここで改めて手順を紹介します。

IBM Cloud にログインして AppID サービスを作成後にインスタンスを開き、「アプリケーション」タブから「アプリケーションの追加」ボタンで対象アプリケーションを追加します。その際にアプリケーションのタイプを「単一ページ・アプリケーション」(SPA) として追加するようにしてください:
2022011301


追加したアプリケーションを選択して、内容を確認します。type の値が "singlepageapp" となっていることを確認してください。確認できたらこの中の "clientId" 値と "discoveryEndpoint" 値を後で使うことになるのでメモしておきます:
2022011303


まだ AppID にユーザーを登録していない場合はここでユーザーも登録しておきましょう。メニューの「クラウド・ディレクトリー」から「ユーザー」を選択し、「ユーザーの追加」からログイン可能なユーザー(の ID とパスワード)を登録しておきます:
2022011302


またリダイレクト URL を登録しておきましょう。「認証の管理」から「認証設定」を選択して、リダイレクト URL に http://localhost:3000/ を追加しておきます:
2022011303



IBM AppID 側で準備する内容は以上です。取得した情報を使ってアプリ開発を続けます。


【React アプリに IBM AppID モジュールを追加して、AppID のログイン機能を追加する】
ここは上述ページでも詳しく記載されている内容です。まずは IBM AppID を利用するためのライブラリを追加します:
$ npm install ibmcloud-appid-js

そしてソースコードの src/App.js をテキストエディタで開き、以下の内容に書き換えます(詳しい内容は上述ページを参照してください)。この時に太字で示している clientId の値と discoveryEndpoint の値には先程 AppID の画面で確認した clientId 値と discoveryEndpoint 値をコピペして指定してください:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import AppID from 'ibmcloud-appid-js';
function App() {
  const appID = React.useMemo(() => {
    return new AppID()
  }, []);
  const [errorState, setErrorState] = React.useState(false);
  const [errorMessage, setErrorMessage] = React.useState('');
  (async () => {
    try {
      await appID.init({
        clientId: 'AppID の clientID の値',
        discoveryEndpoint: 'AppID の discoveryEndpoint の値'
      });
    } catch (e) {
      setErrorState(true);
      setErrorMessage(e.message);
    }
  })();
  const [welcomeDisplayState, setWelcomeDisplayState] = React.useState(false);
  const [loginButtonDisplayState, setLoginButtonDisplayState] = React.useState(true);
  const [userName, setUserName] = React.useState('');
  const loginAction = async () => {
    try {
      const tokens = await appID.signin();
      setErrorState(false);
      setLoginButtonDisplayState(false);
      setWelcomeDisplayState(true);
      setUserName(tokens.idTokenPayload.name);
    } catch (e) {
      setErrorState(true);
      setErrorMessage(e.message);
    }
  };
  return (
    <div  classname="App">
    <header  classname="App-header">
      <img  alt="logo" classname="App-logo" src="{logo}">
        {welcomeDisplayState && <div> Welcome {userName}! You are now authenticated.</div>}
        {loginButtonDisplayState && <button  onclick="{loginAction}" id="login" style="{{fontSize:">Login</button>}
        {errorState && <div  style="{{color:">{errorMessage}</div>}
    </header>
    </div>
  );
}
export default App;

この状態で一度動作確認します:
$ npm start

ウェブブラウザが起動して http://localhost:3000/ が開き、以下のような画面が表示されます:
2022011301


"Login" と書かれた箇所をクリックするとログイン画面がポップアップ表示されます。ここで AppID に登録済みのユーザー ID とパスワードを指定してサインインします:
2022011304


正しくログインできると先程の画面に戻り、登録したユーザーの氏名が表示とともに "You are now authenticated." と表示され、ログインが成功したことが明示されます:
2022011305


【React アプリをビルドして、SPA アプリでもログインできることを確認する】
今作成した React アプリをビルドして index.html にまとめた SPA アプリにしたあとでも AppID でログインできることを確認します。nginx などの HTTP サーバーがあればそれを使ってもいいのですが、ここでは Node.js のシンプルなウェブサーバーを使って動作確認します。以下のような内容で app.js を作成し、react-appid フォルダ直下(README.md などと同じ階層)に保存します:
var express = require( 'express' ),
    app = express();

app.use( express.static( __dirname + '/build' ) );

var port = process.env.PORT || 3000;
app.listen( port );
console.log( "server starting on " + port + " ..." );

↑ build/ フォルダをコンテキストルートにして 3000 番ポートで HTTP リクエストを待ち受けるだけのシンプルなコードです。

app.js が準備できたら、まずは一度 React コードをビルドして SPA アプリ化します:
$ npm run build

ビルドが完了すると React アプリが SPA 化されて圧縮されて build/ フォルダにまとめられますので、これを app.js を使って起動します:
$ node app

改めてウェブブラウザで http://localhost:3000/ にアクセスして同じアプリが起動していることを確認し、Login から AppID アカウントでログインできることを確認します。アプリケーション・サーバーを持たない SPA アプリでも IBM AppID を使って認証できることが確認できました:
2022011305


【React アプリをビルドした SPA アプリを Github ページでも動くように調整する】
ここまでできれば後は build/ フォルダを Github ページで公開するだけで動くんじゃないか? と思うかもしれませんが、実は期待通りに動くようになるまではいくつかの落とし穴があります。1つずつ解いていきましょう。

(1)絶対パス→相対パスへの書き換え

React の SPA アプリをただビルドしただけだと、コンテキストルート直下で動く想定のアプリになってしまいます。どういうことかというと、例えば http://localhost:3000/ とか https://xxx.xxxxxxx.com/ といったサブディレクトリを持たない GET / というリクエストに対して動くアプリになります(要はビルドで作成される index.html 内で参照される外部 CSS や JavaScript は "/" ではじまる絶対パスになっています)。一方、Github ページで動かす際は https://dotnsf.github.io/react-appid/ のようなサブディレクトリ上で動くことになります。ここがコンフリクトになってしまい、絶対パスで指定された CSS や JavaScript のロードエラーが発生してしまうのでした。これを回避するために index.html 内の該当箇所を絶対パス指定から相対パス指定に変更する必要があるのでした。

具体的にはこの下の(3)までの作業後に改めて $ npm run build を実行してから、build/index.html の以下の <head> 部分で頭に . を付けて、絶対パス指定から相対パス指定に書き換えてください:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" href="./favicon.ico"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="theme-color" content="#000000"/>
<meta name="description" content="Web site created using create-react-app"/>
<link rel="apple-touch-icon" href="./logo192.png"/>
<link rel="manifest" href="./manifest.json"/>
<title>React App</title>
<script defer="defer" src="./static/js/main.85d3d620.js"></script>
<link href="./static/css/main.073c9b0a.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>



(2)回転する画像の URL を絶対指定

(1)と似た内容ですが、SPA アプリ内で表示される画像も絶対パスのままだと正しく表示されません。ただこちらは JavaScript 内で {logo} と指定されている画像なので単純に絶対パスを相対パスにすればよいという対処が使えないのでした。 まあアプリケーションとしては必須ではないので単純に画像ごと削除してしまってもいいのですが、残したい場合は Git リポジトリ上の画像 URL を直接指定する、などの方法で対処する必要があります。よくわからない人は以下の例でそのまま書き換えてください:
    :
  return (
    <div className='App'>
    <header className='App-header'>
      <img src="https://raw.githubusercontent.com/dotnsf/react-appid/main/src/logo.svg" className='App-logo' alt='logo' />
        {welcomeDisplayState && <div> Welcome {userName}! You are now authenticated.</div>}
        {loginButtonDisplayState && <button style={{fontSize: '24px', backgroundColor: 'skyblue', 
          border: 'none', cursor: 'pointer'}} id='login' onClick={loginAction}>Login</button>}
        {errorState && <div style={{color: 'red'}}>{errorMessage}</div>}
    </header>
    </div>
  );
    :


(3)デフォルトの .gitignore を変更

$ npx create-react-app コマンドで作成した React プロジェクトにははじめから .gitignore ファイルが含まれています。が、この .gitignore では /build フォルダを除外するよう記述されています。今回は build/ フォルダを Github ページで運用することが目的なので、/build フォルダが除外されてしまっては意味がありません。.gitignore ファイルを編集して、/build フォルダを含めるよう(コメントアウトするなど)してください:
  :
  :
#/build
  :
  :

(1)でも紹介しましたが、ここまでの変更を行ったら再度 SPA アプリケーションをビルドし、その後に build/index.html ファイルに対して(1)の変更を行ってください。


(4)AppID のリダイレクト URL の設定が特殊

これまではアプリケーションを http://localhost:3000/ という開発時専用の URL で実行していたので、AppID のリダイレクト URL も http://localhost:3000 だけを登録して動作確認できました。では実際に Github ページでアプリケーションを動かす際にはどのようなリダイレクト URL を指定すればいいのでしょうか? 実はここがくせ者でした。

例えば私(Github ID は dotnsf )が react-appid という github リポジトリを作って、このリポジトリを Github ページとして公開して運用しようとすると、アプリケーションの URL は以下のようになります:
 https://dotnsf.github.io/react-appid

2022011306


ということは AppID に設定するリダイレクト URL にもこの値を指定するべき・・・だと思っていたのですが、なんとリポジトリ名部分が不要で、 https://dotnsf.github.io/ を指定するのが正しい設定内容のようでした:
2022011308


この URL がリダイレクト URL として設定されていれば AppID SDK が正しく動作して、認証も正しく実行できるようになりました(自分は実はここで結構つまずきました):
2022011307


(5)build フォルダを Github ページとして公開する方法
最後に作成したプロジェクトを Github に登録して、build フォルダを Github ページで公開する方法についてです。root フォルダや docs フォルダを Github ページで公開する場合は選択肢から選ぶだけなんですが、任意のフォルダを Github ページで公開するのは少しコツが必要です。

例えば react-appid というリポジトリを使う場合は、まず Github 上でこのリポジトリを公開設定で作成します。そして普通に main リポジトリに登録します:
$ git init

$ git add .

$ git commit -m 'first commit'

$ git branch -M main

$ git remote add origin https://github.com/dotnsf/react-appid.git

$ git push -u origin main

その後、build フォルダを Github ページで公開するには以下のコマンドを実行します:
$ git subtree push --prefix build origin gh-pages

これで該当フォルダが Github ページとして公開されます。実際に以下のページはこの設定を使って公開しています:

https://dotnsf.github.io/react-appid/

2022011306


ID: username1@test.com, パスワード: password1 でログインできるので、よければ試してみてください:
2022011307


React で作成した SPA アプリケーションの認証をどうするか、という問題を IBM AppID (と SDK)で解決する方法と、ビルドした SPA アプリを Github ページで運用する場合の特殊な設定やコマンドについて紹介しました。




このページのトップヘ