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

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

タグ:rest

このネタを見た時は「やられた!」と思いました:
手持ちの画像を「Excel」ファイルにしてしまうWebアプリ「Image to Excel」が爆誕

画像をアップロードして、そのピクセル情報を解析して(よりによって)エクセルファイル化する・・・ 素晴らしい変態アプリだと思いました(褒めてます)。 これをアイデアだけでなく実装して公開する行動力まで含めて素晴らしい姿勢だと感じました。自分もこうでありたい。。

で、その衝撃が強すぎたのか、「これ自分だったらどう作るだろうか??」と考え、特にセルの属性を変更したエクセルファイルとして出力する所は何をどうやれば実現できそうか・・ なども調べた上で、なんとか同じような機能を実装することができたと思っています。自分の場合はアプリケーションというよりは REST API として実装したので、この機能を外部アプリからも使えるような形で仕上げています(Swagger ドキュメントも用意したのでウェブブラウザの UI からも使えるはずです)。実装したものは先日このブログでも紹介した無料の PaaS 環境を使って公開しています:

ソースコード
 https://github.com/dotnsf/image2xlsx
公開した REST API(の Swagger ドキュメント)
 https://image2xlsx.140.238.50.190.sslip.io/ (使い方も後述します)


で、この機能を作る上で xlsx-js-style という npm ライブラリを使っています。このライブラリは Node.js やフロントエンド JavaScript からエクセルファイルを生成する機能を持ったライブラリです。もともとは SheetJS というライブラリ(無料)があったのですが、有料版の提供を境にして無料版のサポートが少なくなってしまったのか、更新が途絶えてしまいました。そこで SheetJS からフォークして様々な後継ライブラリが作られていったのですが、xlsx-js-style はその1つです。ただ詳しい使い方がウェブ上でもあまり見つけることができず、一部では「事実上、読み取り用のライブラリ」とも書かれていました。実際はそんなことはなくて、現に今回紹介するようなエクセルブックファイルを新規作成する上での最低限の機能を持っていました。一方で情報が少ないことも事実で、今回はソースコードの中身を含めて自分でも色々調べながらの作業となりました。その調べて分かった内容も含めて本ブログエントリにまとめておこうと思います。

なお以下で紹介する内容は 2023/12/08 時点で公開しているサービスと、そのソースコードについて紹介しています。今後の更新で多少実装内容が変わる可能性もあることをご了承ください。


【image2xlsx (の Swagger ドキュメント)の使い方】
何はともあれ公開している機能の使い方を紹介します。作ったのは REST API なんですが、Swagger ドキュメントも併せて公開しているので curl などを使わなくてもウェブブラウザで実際の挙動を確認できるように(つまり手持ちの画像をアップロードして画像化したエクセルファイルをダウンロードすることができるように)しています。ウェブブラウザでこの URL にアクセスしてみてください:
https://image2xlsx.140.238.50.190.sslip.io/

2023120701


↑上のような画面が表示されれば成功です。https スキーマを使ってアクセスしているはずなので、実際に使う前に Schemas と書かれた欄(上図の赤枠部分)を "HTTP" から "HTTPS" に変更しておいてください(このサイトのサービスを使う場合、あらかじめ HTTPS に変更しておかないと正しく実行できません)。

この Swagger ドキュメントで提供している REST API は1つだけで、"POST /image" です。緑色の "POST" と書かれた部分をクリックして展開してみましょう:
2023120702


省略されていた部分が展開されて上のような画面になったはずです。この画面で "POST /image" API にはいくつかのパラメータが指定できることがわかります。が、まずは実際に API を実行してみたいので "Try it out" と書かれたボタンをクリックします:
2023120703


パラメータ部分が編集可能状態に切り替わり、一番下には "Execute" と書かれた青いボタンが現れたはずです。これでパラメータを指定して "Execute" ボタンをクリックすると実際に REST API が動く状態になりました。パラメータは4つありますが、入力必須なのは一番下の file フィールドだけです。他はいったん無視して file フィールドのボタンをクリックして適当な画像ファイル(.jpg, .gif, .png など)を指定してください(2023/12/08 時点では 10MB 以下の画像ファイルを受け付けることができます。もっと大きなファイルを扱えるようにしたい場合は是非ソースコードをダウンロードして自分で改良してみてください)。この際に日本語名のファイルを指定するとダウンロードするエクセルファイル名が文字化けしてしまうので、(ファイル名の文字化け程度を許容できる場合はそのままでもいいですが)できれば半角英数字名だけで構成されたファイル名の画像ファイルを指定してください:
hachi
(↑hachi.jpg というこの画像ファイルを指定してみました)


ファイルを選択したら "Execute" ボタンをクリックします:
2023120704


すると指定したファイルがアップロードされ、エクセルファイル化が実行されます(実行中は "LOADING" と表示されます):
2023120705


正しく処理が完了すると "Responses" 欄が現れます。ここには実際に実行した REST API の内容(curl コマンドで実行した時のコマンドの内容)やその実行結果のレスポンスに関する情報が表示されます。処理が成功していると "Download file" と書かれたリンクが表示されていて、ここをクリックすると実行結果のエクセルファイルをダウンロードできます:
2023120706


実際に "Download file" をクリックします。ダウンロードは一瞬のはずです。ちなみにエクセルファイル名は元の画像ファイル名の拡張子部分が "xlsx" になったものです(hachi.jpg → hachi.xlsx):
2023120707


ダウンロードしたファイルを(ダブルクリックするなどして)エクセルやエクセル互換ツールで開いてみると期待通りのエクセルシートが表示されるはずです(シート名は元の画像ファイル名です)。元祖の Image to Excel と同じような結果になっているはずです:
2023120708
(↑シートに画像が貼られているのではなく、細かなセルの色を変えることで実現しています)


なお今回はアップロードする画像以外のパラメータを無視していました。これらのパラメータの意味とデフォルト値について簡単に説明しておきます:
2023120709


px パラメータは「エクセルファイル側で画像の1ピクセルを何ピクセルのセルで表現するか」という数値です。デフォルト値は「2」です。つまり何も指定しなければ画像の各ピクセルは「横 2px 縦 2px」のサイズのセルとして表示されます。この値を変えたい場合は px パラメータになんらかの数値(1とか3あたり)を入力してください。

width パラメータは「エクセルファイル側で画像の横幅を何個ぶんのセルを使って表現するか」の数です。デフォルト値は「100」です。つまり何も指定しなければアップロードされた画像の横幅サイズを 100 に縮小(または拡大)します。

同様に height パラメータは「エクセルファイル側で画像の縦幅を何個ぶんのセルを使って表現するか」の数です。デフォルト値はなく、何も指定されなかった場合は縦横比を保ったまま横サイズ(こちらのデフォルト値は 100)に合わせて縦幅が拡大縮小されます。

なお height パラメータのみ指定されて width パラメータが指定されていなかった場合は、縦横比を保ったまま縦サイズ(=height)に合わせて横幅が拡大縮小されます。

このような仕様となっていました。なので、これらを何も指定せずに実行した場合は、
・画像の1ピクセルは 2x2 px サイズのセルで表示される(そのピクセルの色がセルの色になる)
・横幅は 100px に拡大縮小され、縦幅は同じ比率で拡大縮小される
ということになります。何も指定しなければ横幅は 100 になるので、巨大な画像をアップロードしても、そこまで巨大なエクセルファイルにはならないはずです。よかったら(必要に応じてパラメータも変えたりしながら)使ってみてください。


【image2xlsx REST API の使い方】
ここからは Swagger ドキュメントの使い方というよりは REST API の使い方を説明します。改めて実行結果の Responses 部分の、"Curl" と書かれた部分を見てみます:
2023120701


file 以外のパラメータを指定せずに実行した場合はこんな感じに書かれているはずです:
curl -X POST "https://image2xlsx.140.238.50.190.sslip.io/image" -H "accept: application/vnd.ms-excel" -H "Content-Type: multipart/form-data" -F "file=@hachi.png;type=image/png"

ちなみにパラメータを指定した場合(例えば width パラメータを 200 に指定して実行した場合)はこのようになります(-F "width=200" が追加されています):
curl -X POST "https://image2xlsx.140.238.50.190.sslip.io/image" -H "accept: application/vnd.ms-excel" -H "Content-Type: multipart/form-data" -F "width=200" -F "file=@hachi.png;type=image/png"

これがこの画像ファイルをエクセルファイル化する時に実行された curl コマンドそのものでもあります。REST API としては以下のような仕様です:

・エンドポイント URL は "https://image2xlsx.140.238.50.190.sslip.io/image"
・HTTP メソッドは "POST"
・HTTP リクエストヘッダは "accept: application/vnd.ms-excel" と "Content-Type: multipart/form-data"
・HTTP リクエストボディは以下を FormData で送信
 - px パラメータでエクセルファイルのセル1つのサイズを何ピクセル平方にするか
 - width パラメータでエクセルファイルのセルを何列で表現するか
 - height パラメータでエクセルファイルのセルを何行で表現するか
 - file パラメータで画像ファイル本体(ここだけ必須)

・HTTP レスポンスはエクセルファイルのバイナリデータ

(需要があるとは思えないのですが)ここを理解した上で外部アプリからこのエンドポイントへファイルをアップロードすればエクセルファイル化してダウンロードすることもできるようになります。REST API 化したので外部連携も可能な形になっています(サーバーがそこまで潤沢な環境ではないので、お手柔らかに使ってください)。


【xslx-js-style をどのように使ったか】
ここまでの内容は「本家の Image to Excel をパクってみました」という内容でした。ここからがこのブログエントリの本体ともいえる部分だと思っていて、「これをどうやって作ったのか」という技術的な解説になります。本家のサービスがどのように作られているのかは分からないのですが、自分はこうやってみた、という紹介です。ちなみにプログラミング言語は Node.js で作っています。

改めて、私が作った image2xlsx のソースコードはこちらで公開しています。実際の実装ソースコードはここからも参照できるので興味ある方は是非どうぞ。

この "POST /image" メソッドは概ね以下のようなフローで作られています:
(1)一時フォルダにアップロードされた画像ファイルを受け取る
(2)画像ファイルを開いて、サイズ(横幅、縦幅)を調べる
(3)受け取ったパラメータなどから出力時のサイズ(横幅、縦幅)を求める
(4)Canvas ライブラリを使って仮想的なキャンバスを作り、画像を出力時のサイズで描画する
(5)エクセルワークブック(ファイル)を新規に作成する
(6)エクセルワークシート(タブ)を新規に作成する
(7)(3)で求めたサイズに合わせて画像の各ピクセルぶんのセルをワークシート内に用意する
(8)(4)の画像データを取り出し、1ピクセルごとに RGB 値を取得し、
(9)(8)の結果を各セルの背景色に指定する
(10)セルの色を指定したワークシートをワークブックに追加する
(11)ワークブックのデータを取り出し、HTTP レスポンスとして返す

以下、このフローの実装内容を順に説明していきます。まず(1)ですが、ここは multer というファイルアップロード機能を作る時の定番ライブラリがあるので multer を使って実装しています。

該当コードとしてはこのような感じです:
var express = require( 'express' ),
    bodyParser = require( 'body-parser' ),
    fs = require( 'fs' ),
    multer = require( 'multer' ),
    { createCanvas, Image } = require( '@napi-rs/canvas' ),
    XLSX = require( 'xlsx-js-style' ),
    app = express();

require( 'dotenv' ).config();
var default_px = 'DEFAULT_PX' in process.env ? parseInt( process.env.DEFAULT_PX ) : 2;
var default_width = 'DEFAULT_WIDTH' in process.env ? parseInt( process.env.DEFAULT_WIDTH ) : 100;

app.use( express.static( __dirname + '/public/_doc' ) );
app.use( bodyParser.urlencoded( { extended: true } ) );
app.use( bodyParser.json( { limit: '10mb' } ) );
app.use( express.Router() );
app.use( multer( { dest: './tmp/' } ).single( 'file' ) );

  :

app.post( '/image', function( req, res ){
  var imgpath = req.file.path;
  var outputfilename = '';
  try{
    var imgtype = req.file.mimetype;
    var filename = req.file.originalname;

    :


(2)以降で仮想キャンバス機能を使います。フロントエンド JavaScript では HTML5 の Canvas に相当する機能なのですが、node-Canvas というバックエンド用の互換ライブラリ(のプラットフォーム非互換性を解消した @napi-rs/canvas というライブラリ)を使っています。この canvas でアップロードされた画像ファイルを読み込み、横幅と縦幅を取り出します。

(3)は、画像ファイルと一緒に送られてきたサイズに関するパラメータを使い(送られていない場合はデフォルト設定値を使い)、必要に応じて元画像の縦横比に合わせる形で計算も行って、エクセルファイル内のセルの横幅数と縦幅数を求めておきます。

この(2)と(3)に相当する部分のコードはこのようになっています:
    :
    //. 画像解析
    var data = fs.readFileSync( imgpath );
    var img = new Image;
    img.src = data;

    var width = req.body.width ? parseInt( req.body.width ) : -1;
    var height = req.body.height ? parseInt( req.body.height ) : -1;
    if( width < 0 && height < 0 ){
      width = default_width;  //. サイズの指定が何もない場合、幅100とする
      height = width * img.height / img.width;
    }else if( width < 0 ){
      width = height * img.width / img.height;
    }else if( height < 0 ){
      height = width * img.height / img.width;
    }

    :


(4)では仮想的な Canvas を作って、Canvas 内に元画像を元のサイズから出力時のサイズに拡大・縮小したものを表示(仮想的な Canvas なので実際に表示されるわけではないですが)しています。Canvas に描画されたデータはピクセルごとの情報(今回の場合であれば RGB 値)を取り出して調べることができるようになります。

この部分のコードはこのようにしています:
    :
    //. リサイズして仮想キャンバスに描画
    var canvas = createCanvas( width, height );
    var ctx = canvas.getContext( '2d' );
    ctx.drawImage( img, 0, 0, img.width, img.height, 0, 0, width, height );

    //. 描画した画像のデータを取得
    var imagedata = ctx.getImageData( 0, 0, width, height );
    :

ここまでは(アップロードされた画像を拡大縮小して、各ピクセル毎の情報を取り出すための準備までは) xlsx-js-style ライブラリを使わずに実装しています。ここからが xlsx-js-style ライブラリの出番です。

(5)この後、各ピクセル毎の情報を調べていくのですが、その前にワークブックを新規に作成します。「ワークブック」=「エクセルファイル」と考えると理解しやすいと思いますが、ここでの「新規作成」の意味は「新しいエクセルファイルを作って、まだ保存していない状態にする」と思っていてください。なおこの時点ではワークブック内に1つもタブが存在していない状態で作成されています。

xlsx-js-style でワークブックを新規作成するには utils.book_new() というメソッドを実行します:
    :
    //. ワークブックを作成
    var wb = XLSX.utils.book_new();
    :

(6)ではワークブックに追加することになるワークシート(タブ)を新規に1つ作成します。

xlsx-js-style でワークシートを新規作成するには utils.aoa_to_sheet() というメソッドを実行します。引数にワークシート内の各列の情報を含めることができますが、この時点では特に指定せずに作成しています:
    :
    //. ワークシートを作成
    var row = [[]];
    var ws = XLSX.utils.aoa_to_sheet([row]);
    :


(7)はワークシート内で使うことになるセルの列数と行数(つまり画像の横幅と縦幅)の分だけセルのサイズを調整します。

ここでの「セルのサイズ」とは「REST API 実行時に px パラメータとして指定された値(指定されていない場合は default_px の値(=2)」です。また「セルの列数」とは「Canvas 上に表示されている画像の横幅(=imagedata.width)」で、「セルの行数」とは「Canvas 上に表示されている画像の縦幅(=imagedata.height)」のことです。したがって「セルの列数の分だけセルのサイズを調整する」コードは以下のようになります。具体的には各列の幅を { wpx: (セル幅のピクセル数)} というオブジェクトで定義する配列を用意してワークシートオブジェクトの '!cols' 値として設定し、各行の幅は [ hpx: (セル高さのピクセル数) } というオブジェクトで定義する配列を用意してワークシートオブジェクトの '!rows' 値として設定します。具体的には以下のようなコードになります:
    :
    //. 必要なセルの行数と列数ぶんだけ、幅と高さを指定する
    var px = req.body.px ? parseInt( req.body.px ) : default_px;
    var wscols = [];
    for( var x = 0; x < imagedata.width; x ++ ){
      wscols.push( { wpx: px } );
    }
    var wsrows = [];
    for( var y = 0; y < imagedata.height; y ++ ){
      wsrows.push( { hpx: px } );
    }

    ws['!cols'] = wscols;
    ws['!rows'] = wsrows;
    :

(8)では Canvas 内の各ピクセルにアクセスして RGB 値をそれぞれ取り出し、16進法の "RRGGBB" フォーマットに変換して、

(9)その "RRGGBB" 値をワークシート内の対応するセルの背景色として指定してゆきます。

この(8)と(9)はループ内で以下のように実現しています。ピクセルの RGB 値は Canvas のデータ内に4つごとに(R 値、G 値、B 値、輝値)格納されているのでそれぞれを取り出し、この値を "RRGGBB" というフォーマットに変えて、"A1", "A2", ... といったセルの色として指定しています。また cellname という関数を用意して縦位置、横位置からセル名に変換できるようにしています(この後の(10)でも使っています):
    :
    //. 1ピクセルずつ取り出して色を調べる
    for( var y = 0; y < imagedata.height; y ++ ){
      for( var x = 0; x < imagedata.width; x ++ ){
        var index = ( y * imagedata.width + x ) * 4;
        var rr = imagedata.data[index];    //. 0 <= xx < 256
        var gg = imagedata.data[index+1];
        var bb = imagedata.data[index+2];
        var alpha = imagedata.data[index+3];

        var rrggbb = rgb2hex( rr, gg, bb );
        var cname = cellname( x, y );
        ws[cname] = { v: "", f: undefined, t: 's', s: { fill: { fgColor: { rgb: rrggbb } } } };
      }
    }
    :
    :

function color2hex( c ){
  var x = c.toString( 16 );
  if( x.length == 1 ){ x = '0' + x; }
  return x;
}

function rgb2hex( r, g, b ){
  return color2hex( r ) + color2hex( g ) + color2hex( b );
}

function cellname( x, y ){
  //. String.fromCharCode( 65 ) = 'A';
  var c = '';
  var r = ( y + 1 );

  while( x >= 0 ){
    var m = x % 26;
    x = Math.floor( x / 26 );

    c = String.fromCharCode( 65 + m ) + c;
    if( x == 0 ){
      x = -1;
    }else{
      x --;
    }
  }

  return ( c + r );
}

(10)(7)、(8)、(9)の処理によって変更されたワークシートの内容をワークブックに反映します。これでレスポンスとして返すエクセルファイル(のデータ)が完成しました。

このコードでは実際に使ったセルの範囲を "A1:(最も右下にあるセルの名前)"  という形でワークシートの '!ref' 値に指定してから、作成したワークシートをワークブックに追加する、という処理を実行しています:
    :
    //. 値が適用される範囲を指定
    var cname = cellname( imagedata.width - 1, imagedata.height - 1 );
    ws['!ref'] = 'A1:' + cname;

    //. ワークシートをワークブックに追加
    XLSX.utils.book_append_sheet( wb, ws, filename );
    :


(11)最後に完成したエクセルファイルを HTTP リクエストに対する HTTP レスポンスとして返します。

ここでは、これまでの処理で作成したワークブックのバッファデータを取得して HTTP レスポンスヘッダと合わせる形で返す、ということになるのですが、xlsx-js-style のソースコードを調べてもワークブックのバッファデータを直接取得する関数は用意されていないようでした。というわけで、一旦ワークブックファイルとして書きだし、ワークブックファイルを読み込んだ上で返す(そしてワークブックファイルをアップロード画像と一緒に消す)、というアルゴリズムにしています:
    :
    //. xlsx化
    XLSX.writeFileSync( wb, 'tmp/' + outputfilename );

    res.contentType( 'application/vnd.ms-excel' );
    res.setHeader( 'Content-Disposition', 'attachment; filename="' + outputfilename + '"' );
    var bin = fs.readFileSync( 'tmp/' + outputfilename );
    res.write( bin );
    res.end();
    :

・・と、このような形で xlsx-js-style を使ってエクセルファイルをバックエンドで作成することができました。グラフとかまでは難しそうではありますが、セルのサイズや色を指定したワークシートを作るという程度であれば実現できないことはなさそうで、今後エクセル連携が必要になった場合でも使えそうなライブラリでした。





IBM の新しい AI プラットフォームである IBM watsonx.ai (「ワトソンエックス・エーアイ」)が公開され、IBM Cloud のライトプラン(無料)でもある程度使うことができそうなことがわかったので、まだあまりドキュメントが整備されていなさそうな REST API について調べてみました。そして調べた結果わかった情報を使って、実際に REST API を使って動くサンプルアプリを作り、公開してみました。
watsonx.ai



【事前準備】
このブログエントリに書かれた内容を全て試すためには、IBM Cloud のアカウントが必要です。まだ所有していない場合はこちらから登録してください。なお 2023 年7月時点では新規登録の際にクレジットカード情報の入力が必須です(このブログで紹介する内容は無料範囲でも可能ですが、カードの登録は必須です)。

また今回はプロンプトを使った質問を入力して、その内容に対する回答を取得する、という内容の REST API を実際に動かすのですが、そのためには Watson StudioWatson Machine Learning という2つのサービスを利用する必要があります(いずれも従量課金型のサービスですが、ライトプランを使うことである程度まで無料で使うことができます)。 IBM Cloud のアカウントでログイン後にこれら2つのサービスインスタンスを作っておいてください。


【環境構築】
では実際にプロンプトで指示を行うための環境を構築します。IBM Cloud にログインし、リソースリスト(左メニューの上から2番目)から「AI/機械学習」カテゴリを参照します。他の用途で既に IBM Cloud を使っている場合はここにいくつかのサービスインスタンスが並んでいるかもしれませんが、少なくとも上述の事前準備が完了していれば "Watson Machine Learning" と "Watson Studio" の2つのインスタンスは表示されているはずです。これら2つが表示されていることを確認した上で(実際はどっちでもいいんですが) "Watson Machine Learning" の方をクリックします:
2023071700


クリックした "Watson Machine Learning" インスタンスが表示されます。ここで「IBM Cloud Pak for Data で起動」と書かれた青いボタンをクリックします:
2023071701


初回はこのような画面が表示されます。少し待ちます:
2023071701


少し待つとこのような画面が表示されます。「ML モデルの作成と管理」というダイアログが表示されていますが、実はこの時点では watsonx.ai ではなく IBM Cloud Pak for Data という別プラットフォームの画面になっています。プラットフォームを切り替えるため、この画面では「キャンセル」をクリックします:
2023071702


プラットフォームの切り替えは画面右上のメニューから行うことができます。画面右上から「IBM watsonx.ai」を選択し直します。これでプラットフォームが watsonx.ai 用のものに切り替わります:
2023071703


はじめて watsonx.ai を使う時にはこのような画面が表示されます。利用規約にチェックを入れ、また初回はまだプロジェクトがないので(先に進むためにはプロジェクトが最低1つ必要なので)「サンドボックスプロジェクトの作成」ボタンをクリックします(既に利用済みで、自分のプロジェクトが存在している場合はそのプロジェクトを選択しても構いません):
2023071704


プロジェクトが選択されていると、このような画面(プロジェクトの初期画面)になります。プロンプト指示を試す場合は、一番左の「ファウンデーション・モデルを・・」と書かれたプロンプト・ラボを選択します:
2023071705


プロンプト・ラボを始めて実行するとこのようなダイアログが表示されます。内容を確認して全てチェックします。説明を確認する場合は「ツアーを開始」をクリックしてもよいのですが、飛ばす場合は「ツアーのスキップ」をクリックします。これで環境構築は一通り完了です:
2023071706



【プロンプト実行】
ここまで正しく実行すると下のような画面になり、プロンプト指示を実際に試すことができるようになります。なお特に今回は REST API を操作することを目的としているので、プロンプトを試す前に画面右の「コードの表示」メニューをクリックしておきます:
2023071704


すると画面右側に curl の実行コマンドが表示されます。この後プロンプトで入力をすることになるのですが、その入力処理を curl で実行した場合の実行内容(接続 URL やヘッダ、データの中身)を確認することができるので、この内容を参考に REST API のプログラミングができるようになります:
2023071705


参照ページの例を参考にプロンプトで指示を出してみます。すると実行結果が返ってくるだけでなく、その結果を得るために実行された curl コマンドの REST API パスが /ml/v1-beta/generation/text?version=2023-05-29 であったことや、日本語で指示している場合も特に日本語であることを明示するパラメータが送られていないことを含め、どのような REST API が実行されていたかがわかります:
2023071706


【アクセストークンの取得】
後はこれと同じことを自分のプログラミングの中で実装すればよい、、のですが、この REST API を実行する上で欠くことのできない2つのパラメータがあります:
2023071707


1つは "project_id" というパラメータです。これは名前の通り「プロジェクトのID」で、プロンプト実行前に指定したプロジェクトを一意に示す ID です。この値はプロンプト実行時にブラウザが参照している URL を見ると、URL パラメータの1つとして指定されていることがわかります(つまりブラウザの URL から取得することができます):
2023071708


問題はもう1つの "YOUR_ACCESS_TOKEN" (つまりアクセストークン値)です。これはこの文字列をそのまま使っても正しく実行できません。しかもこの値はウェブブラウザを参照するなどの方法では取得できず、IAM API キーと呼ばれる値とプログラミングによって動的に取得する必要があるものです。この取得方法については本ブログの趣旨とは異なるので詳しくは解説しませんが、詳しくはこちらのドキュメントを参照してください(後述のサンプルでもこの方法でアクセストークンを取得しています)。


【サンプル】
ここまでに記載した情報を使って、実際に動く Node.js のサンプルアプリケーションを作って公開しました:
https://github.com/dotnsf/watsonx


サンプルといっても実体は "POST /api/generate_text" というエンドポイントを1つだけ実装した Swagger ドキュメントベースの API アプリケーションです。起動時に IAM API キーやプロジェクト ID を環境変数に指定することもできますし、API 実行時にパラメータで指定することもできます。

中身を簡単に説明すると、実装はほぼこの app.js ファイル1つだけで、アクセストークンの取得は getAccessToken() 関数で、テキスト生成(プロンプトの実行)は generateText() 関数で実装しています。興味ある方はこれらの関数内の実装部分を参考にしてください(といっても私も上の方法で知った curl コマンドとそのパラメータ指定をそのまま Node.js 内で使ってるだけなんですけど)。

サンプルアプリケーションを実行するには Node.js インストール済みの環境でソースコードを "git clone" して、"npm install" して、.env ファイルに API キーとプロジェクト ID を保存後に "npm start" するとアプリケーションが 8080 番ポートで起動するので、"http://localhost:8080/_doc" にアクセスすると Swagger ドキュメントが開きます:
2023071701


唯一の API である "POST /generate_text" 部分をクリックして開いて "Try it" ボタンをクリックするとパラメータ設定ができる画面になります。API Key や Project ID 、Model ID は環境変数で指定してあればここでは空のままで構いません。必須入力項目といえるのは Input 値くらいで、ここにプロンプトの内容を記載します。最後に "Execute" ボタンで実行します:
2023071702


正しく実行されると、API の実行結果が下部に表示されます。ちなみにこの例では Input が「入力:\nAbout Watson Discovery\\nIBM Watson® Discovery is an intelligent document processing engine that helps you to gain insights from complex business documents.\n翻訳\n」で、その結果が「Watson Discoveryはビジネスドキュメントに関する意見を得るための知能型ドキュメント処理エンジンです。」でした。どこにも「日本語サポート」とも書かれていないし、「日本語で翻訳」とも指定していないのにここまでできるのはそこそこ日本語でもプロンプトに書かれた意図を理解する力があると思っています。なお、このアプリケーションから実行する場合、 REST API 実行時のパラメータで max_new_tokens の値を(デフォルトの 20 から)100 に変更しています。日本語の場合、20 程度だとまともなある程度長い文章を返せなくなってしまうようで、このようにしています:
2023071703


ちなみにこのサービスを無料のライトプランで使う場合、1か月で使えるトークン数は 25000 だそうです。自分がこれまでにどのくらいのトークンを消費しているかは、IBM Cloud のプロジェクト選択画面で「管理」タブから「リソース使用率」を選択した先の画面で確認することができます。ご利用は計画的に:
2023071704


なお REST API のパス(/ml/v1-beta/generation/text)をみても分かると思いますが、現在の API は v1 のベータ版であり、近い将来に仕様含めて変更する可能性が高いと思っています。その辺りもご注意の上で参照してください。


【参照】
IBM watsonx.ai がやってきた
IBM watsonx.ai を試してみた ( コピペ OK )
Documentation ( IBM watsonx.ai 用)



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

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

このページのトップヘ