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

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

この記事の続きです:
nsf2dxl2web の挑戦(1)


上記記事では自分が開発中(公開予定)のツール nsf2dxl2web の概要を紹介しました。第2回目である今回は技術編と位置づけ、 nsf2dxl2web が動作する仕組みについて紹介します。現時点(2023-11-07)ではまだツールそのものを公開しておらず、試すことができないため仕組みの紹介もわかりにくい点があるかもしれませんが、どのような仕組みで、どのような変換を行うのか、その理解の上で必要な知識である XSLT も交えて紹介したいと思います。


【2つのツール+オプション】
nsf2dxl2web は主に nsf2dxldxl2web という2つのツールから構成されていて、これらに加えオプションとして更にいくつかの補助ツールが用意されています。ツール名に含まれる "nsf" というのは(ノーツデータベースファイルの拡張子が "nsf" であることから)ノーツデータベースのことを指しています。ノーツデータベースに対してまず nsf2dxl(NSF to DXL) の処理を行うことでノーツデータベースを DXL(Domino XML Language) という XML の派生フォーマットに変換します。 その結果である DXL に対して dxl2web(DXL to Web) の処理を行ってウェブ化(ウェブコンテンツ化)する、という2段階の処理を続けて行います。この NSF to DXL と DXL to Web の2段階の処理をそれぞれ nsf2dxl と dxl2web を使って行うことになります。



【DXL】
上述の説明内にも出てきた DXL(Domino XML Language) は知らない人も多いと思うので補足しておきます。DXL は Domino(ノーツサーバー)が管理するデータベース内の文書データや設計要素を domino.dtd のルールに従って XML フォーマットで記述したものです。ノーツの拡張機能である LotusScript や Java を使ってデータベース(の全体または一部)を DXL フォーマットで出力したり、DXL で記述された情報からバイナリデータであるノーツデータベースに変換することができます。

nsf2dxl2web を使う上で DXL の最大の特徴は「テキストファイル」であることです。バイナリデータであるノーツデータベースファイルはノーツクライアントまたはノーツサーバーを使って正しい権限の ID でログインした上でないと開いて中身を確認することもできませんが、DXL(つまり XML)になっていればテキストエディタで開くこともできるようになります。

nsf2dxl2web はこの名前の通り、nsf(ノーツデータベース)をノーツの機能を使って一旦 dxl(XML テキスト)に変換(nsf2dxl)した上で、再度 dxl を web コンテンツに変換(dxl2web)することで一連の変換処理が完了します。


【XML と XSL(と XSLT)】
上述の DXL の説明内で「nsf を dxl に変換」し、「dxl を web コンテンツに変換」すると書きました。前者の処理はノーツが持つ DXL エクスポート機能を使うことでできるのですが、後者の処理(UI を持たない XML を UI 付きの HTML へ変換)はどのようにするのでしょう? この答が「XSL を使って XML を HTML に変換する」です。

XSL(eXtensible Stylesheet Language)  は XML というフォーマットに従って作られた文書(XML 文書)に対して、その構造や表示方法を印刷および閲覧に適した状態に整える、その見栄えを定義するマークアップ言語です。XSL 自体も XML の拡張として定義されています。また「XML を XSL を使って印刷および閲覧に適した状態に整える」この変換処理のことを XSLT(eXtensible Stylesheet Language Transformation) と呼びます。

具体例があった方がわかりやすいと思うので、簡単な例を紹介します。まず以下のような XML データ(test01.xml)があったとします:
<?xml version="1.0" ?>
<成績表>
  <見出し>成績表</見出し>
  <結果 名前="佐藤" 国語="50" 数学="90"/>
  <結果 名前="鈴木" 国語="70" 数学="60"/>
  <結果 名前="田中" 国語="80" 数学="40"/>    
</成績表>

この程度であれば UI がなくてもなんとなく全容が見えてくる XML データだと思っています。クラスの科目別成績表のようなデータが XML フォーマットで格納されている、と思っていただくのがいいでしょう。

この XML データに対して、以下のような XSL (test01.xsl)を適用することを考えてみます:
<?xml version="1.0" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:template match="/">
<html>
<head>
<title><xsl:value-of select="成績表/見出し" /></title>
</head>
<body>
<h1><xsl:value-of select="成績表/見出し" /></h1>
<table id="mytable">
<thead>
<tr><th>名前</th><th>国語</th><th>数学</th></tr>
</thead>
<tbody>
<xsl:for-each select="成績表/結果">
<tr>
  <td><xsl:value-of select="@名前" /></td>
  <td><xsl:value-of select="@国語" /></td>
  <td><xsl:value-of select="@数学" /></td>
</tr>
</xsl:for-each>
</tbody>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

XSL にあまり詳しくなくても、この例をみると何となく結果がイメージできるのではないでしょうか? XSL の中に <xsl:template match="/"> ~ </xsl:template> で囲われた箇所があり、この中に変換元である XML(上述の test01.xml)の "/" にマッチする部分(つまり全体)が適用されて変換されます。 そしてこの箇所には <xsl:value-of select="成績表/見出し" /> と書かれた部分が2か所存在しています。この部分は変換元 XML(test01.xml)の "/" にマッチした部分(つまり全体)の中の、"成績表/見出し" 要素(つまり "/成績表/見出し" 要素)の値が適用されることになります。変換元 XML (test01.xml)を見ると "/成績表/見出し" 要素の値というのは「成績表」という文字列です。つまりこの例だと <xsl:value-of select="成績表/見出し" /> 部分には「成績表」という文字列が入ることになります。

また、この XSL 内には <xsl:for-each select="成績表/結果"> ~ </xsl:for-each> で囲われた箇所もあります。変換元 XML(test01.xml)の "/成績表/結果" は複数存在しています。つまり全ての "/成績表/結果" 要素に対してループを行っている、ということが何となくわかるのではないかと思います。

そしてそのループの中では上述と同様の <xsl:value-of match="@名前"/> 、<xsl:value-of match="@国語"/> 、<xsl:value-of match="@数学"/> という値を取り出している箇所がありますが、これらはそれぞれループ内の要素から「名前属性値」、「国語属性値」、「数学属性値」をそれぞれ取り出して適用する、という内容が定義されています。

そして、この XML(test01.xml)に XSL(test01.xsl)を適用した XSLT の結果は以下のような HTML になります:
<html>
<head>
<title> 成績表 </title>
</head>
<body>
<h1> 成績表 </h1>
<table id="mytable">
<thead>
<tr><th>名前</th><th>国語</th><th>数学</th></tr>
</thead>
<tbody>
<tr>
  <td> 佐藤 </td>
  <td> 50 </td>
  <td> 90 </td>
</tr>
<tr>
  <td> 鈴木 </td>
  <td> 70 </td>
  <td> 60 </td>
</tr>
<tr>
  <td> 田中 </td>
  <td> 80 </td>
  <td> 40 </td>
</tr>
</tbody>
</table>
</body>
</html>

↑これが XML と XSL による XSLT です。XSLT という処理がノーツの文書とフォームの関係に近い処理をしていることがお分かりいただけるでしょうか。

そして XSLT が理解できているとなんとなく想像できると思いますが、nsf2dxl2web というツールを使うことで、
・ノーツの文書 DXL (XML) データとフォームの DXL データを取り出し、
・ノーツ文書の DXL データと
・フォームの XSL データを使って、
・これらを XSLT することで、ノーツ文書の HTML を生成する

という方法でノーツデータベースのウェブ化を実現しています。そのための準備として、上述の dxl2web というツールを使って、
・フォームの DXL(XML) から XSL を作る
・ノーツ文書の DXL(XML) を HTML 向けに微調整する
という処理を行います。この「微調整」の意味ですが、ノーツの機能を使って DXL エクスポートされた文書データのリッチテキストフィールドには HTML に近い(けど異なる)フォーマットでデータが格納されています。この似て非なるフォーマットを HTML で表示する前提となるフォーマットに変換する、という前処理を行っています。なお文書データをウェブ化、つまり HTML 化する XSLT の処理はこのフォーマットが微調整された文書 DXL に対して(表示時に)動的に行います。このためフォームの XSL を変更すれば、そのフォームを使う全文書の UI を変えるというカスタマイズを(後からでも)行うことができるような仕組みにしています。

理想は単独のツールだけで NSF をウェブ化できるのが理想と思いつつも、カスタマイズの要素を残そうとすると単独ツールで最後まで処理してしまうことは必ずしも正しくないと判断し、nsf2dxl2web は2回(カスタマイズ内容によってもう数回)のツール実行でウェブ化するような設計にしています。



(過去記事や続きはこちらです)
nsf2web2dxl の挑戦(2)



ブログ更新が2か月ほど滞っておりました。いろいろなアプリ開発にいそしんでいたり、業務が忙しかったりもしたのですが、まあ言い訳はやめます。失礼しました。

で、その「いろいろなアプリ開発」の中の1つを何回かにわけて紹介したいと思います。もともと公開前提では作ってなかったのですが、友人の助言などから考えを変えて、とりあえずツールとしては近い将来に最初は招待制みたいな感じで(無料で)公開しようと思っています。

で、そのツールは名前を nsf2dxl2web(読み方は「えぬえすえふ・つー・でぃーえっくすえる・つー・うぇぶ」)です。正式名称ではないので公開時には変えるかもしれませんが、現在はこの名前でいくつもりです。この名称が用途を表しているともいえるのですが nsf2dxl2web は「ノーツデータベースのウェブ化」ツールの1つです。ただユースケースは限られていると思っています。

このシリーズの第一回目は紹介編と位置づけ、 nsf2dxl2web の持つ機能と、自分が考えているそのユースケースを紹介します。

【機能】
本当はまず開発背景を紹介したい気持ちもあるのですが、まずは nsf2dxl2web がどんなツールなのかを簡単に紹介します。以下のような機能と特徴を持ったツールです:

・指定したノーツデータベース (.nsf) をウェブ化する(ツール利用後は Domino サーバーは不要で、nginx などの HTTP サーバーで動きます)
・ノーツデータベースはウェブ用に作られている必要はない(実行時に HTTP タスク不要、というか Domino サーバー不要)。ノーツクライアント向けの UI をウェブで可能な限り再現することを目指しています
・対象ノーツデータベースの設計を理解している必要はない。標準データベースでも独自データベースでもノーツデータベースの設計内容から UI を再現します
・ウェブ化対象コンテンツはビュー/フォルダ一覧、全ビュー/フォルダ、ビュー/フォルダからリンクされた文書だけ(文書が参照するフォーム/サブフォーム/共有フィールドも対象、ページやアウトライン、ナビゲーター、エージェントなどは対象外)。オプションで検索エンジンによるコンテンツの検索が可能になる
・文書やフォーム内のリッチテキストは画像や添付ファイルを含めて再現される(添付ファイルはウェブ化後もダウンロード可)
・文書内の DB リンク、ビューリンク、文書リンクも再現される。リンク先が他 DB の場合、その DB も nsf2dxl2web でウェブ化されていれば各種リンクが有効になる
・UI はカスタマイズ可。文書の UI はウェブ化後のフォームの UI データをカスタマイズすることで全文書の UI が変わる
ウェブ化されたコンテンツは ACL がなく、リードオンリーとなる

2023110601



2023-11-06 の現時点で上記はすべて実現できています。ノーツのウェブ化ツールは数あれど、誰が作ったかもわからないような(作った人と連絡が取れなくなっているような)カスタムデータベースまでツール一発でウェブ化できるというのはかなり珍しいはず。

2023110505

(上がノーツ、下がブラウザで同じビューの同じ文書を見ている様子です。以下同様)

2023110506


またノーツの超便利な機能といえるリッチテキストは可能な限り再現することを心掛けています。テーブルやタブ、セクション、フォントなどの情報はもちろん、画像(添付されていたり、イメージリソースだったり、画像データのコピペだったり、・・)や添付ファイルも再現します。添付ファイルはクリックすればダウンロードできる形で表示されます:

2023110501

2023110502


DB リンクやビューリンク、文書リンクも再現され、リンク先が同じデータベース内である必要はありません(ただし外部データベースの場合、そのリンク先のデータベースも nsf2dxl2web でウェブ化されている必要があります):

2023110503

2023110504


ただし「ノーツをウェブで完全に再現するツール」ではありません。この辺りは↓の背景で補足します。自分の思いが含まれていて長いので(笑)、背景に興味はなく単に使ってみたいという人は読み飛ばしてください。


【背景】
そもそも何故この nsf2dxl2web を作ろうと思い立ったのか? 理由は1つだけではないのですが、最も大きな理由を紹介します。

まず私自身が元ノーツの製品開発者として働いていた時期があり、ノーツはその設計思想含めて非常に素晴らしい製品であると思っています(今もです)。

そして様々な理由でノーツデータベースは「塩漬け」と呼ばれるような運用形態になることがあります。ノーツからウェブに移行したいけど技術的なものも含めた様々な理由からあきらめるか、膨大な移行コストを覚悟する必要が生じてしまい、「それならやっぱりノーツで」と判断されるケースです。といっても引き続き有効活用されるというよりは「過去のデータ資産を捨てるという決断ができない」ために仕方なく使い続けるような形態です(この状態を「塩漬け」と呼んでいます)。これは利用者にとっても残念ですが、以前ノーツを提供する立場にあった自分としても非常に残念な運用形態です。積極的に使いたいわけではないのに移行できないから、参照のためだけであってもノーツを使い続けなければならない、というのは誰もが不幸な状態であって、そのような状態を解決できないだろうか、とずっと考えていたのでした。

このような背景の中で nsf2dxl2web を設計しています。つまりこれが nsf2dxl2web の設計思想の1つです。このような背景があるため、上述の機能一覧の最後に記述されている「ウェブ化されたコンテンツは ACL がなく、リードオンリーとなる」があります。今でもノーツを使って業務で新規に文書データを作ったり、編集したりしてる人に向けたウェブアプリケーション化ツールではありません。そのようなケースの場合は(今でもノーツの機能を使っている場合は)私自身はノーツを使い続けることが正しいと考えています。


【パフォーマンス】
ノーツの .nsf ファイル(データベースファイル)を nsf2dxl2web ツールを使ってウェブで(HTTP サーバーで)参照できるようになるまでに必要な時間は従来の方法と比べて劇的に改善されていると思っています。数字は私の手元にあるノーツのメールデータベース(文書数=約 75,000 、ファイルサイズ=約 12 GB)を私が使っている Windows 11 PC (AMD Ryzen 5 Pro 6650U + 16GB メモリ)で変換した場合の参考速度だと思ってほしいのですが、約4時間弱でした(作った私の感覚ですが、このツールに関して GPU は高速化にあまり役立ってないかも)。これは文書数が多いことに加えて設計自体がかなり複雑なデータベースでしたが、それでも4時間あればなんとかなる、ということです。ここまで文書数が多くなく、1000 程度の普通(?)の独自設計ノーツデータベースであれば変換に必要な時間は軒並み一瞬でした。

我ながらかなりの高パフォーマンスが実現できていると思います。12 GB のノーツデータベースの設計なんて調べるのも嫌なレベル(苦笑)だと思っていますし、そんなノーツデータベースのビューなんて何も考えずに <table> などでウェブ化したら、ビューを開こうにも(ブラウザが) out of memory エラーを多発することになるはずです。それをコマンドを数回実行して4時間待つだけ、でウェブ移行できるならかなり楽ですよね。


【カスタマイズ】
残念ながらすべてのノーツデータベースのすべての文書でフォーム定義された通りの UI を完全再現することはできていません。最大の理由は式言語やスクリプトといったマクロでカスタマイズされているケースが多く、例えば画面内に表示される値がマクロの実行結果に依存していると(このツールはマクロの実行エンジンを再現しているわけではないので)、ツールで変換しただけでは表示結果が期待通りにならないことがあります。


2023110601

ノーツ標準のディスカッションテンプレートから作ったデータベースの例。上がノーツで下が nsf2dxl2web で変換後。式言語を使った複雑なフォーム定義を完全に変換することができず、表示が乱れてしまっています:

2023110602


このような標準ツールだけでは UI が乱れてしまうケースのためにカスタマイズを可能にしています。nsf2dxl2web ではノーツの(サブフォームや共有フィールドを含む)フォームに相当する設計ファイルを設計から自動生成し、文書データを表示する際には、その文書が表示用に使っていたフォームの設計ファイルを参照して文書 UI を動的に生成します(具体的にな仕組みには XML と XSL を使っているのですが、そのあたりはまたいずれ詳しく・・)。なので、フォームの XSL をカスタマイズすることでそのフォームを使う全文書の見栄えをまとめて変更することができるようにしています。この辺りはノーツの設計思想を残しつつウェブ移行していて、これによってカスタマイズ作業そのものを容易にできています(ノーツ標準のメール、ディスカッション、ドミノディレクトリーのカスタマイズサンプルを提供します):
2023110603


nsf2dxl2web が標準で提供するサンプルを適用してカスタマイズすると↑のように表示の乱れを修正することができるようになります。自分があまり UI が得意でないこともあって、実際の業務データベースをウェブ移行する際にはこの「カスタマイズ」が必要になるケースもあるのではないかと思っていますが、この辺りを(有償サービスのような形で)支援していただけるような人に使っていただけないかと考えています。ただその場合であってもゼロから移行支援するのではなく、ある程度動くようになっている状態での UI カスタマイズになるので、作業量を減らすことはできるのではないかと考えています。


【ユースケース】
想定ユーザーというか「想定利用シーン」として、「ノーツデータベースをノーツの機能としては使っていないのにデータのウェブ移行ができなくて困っている」というユースケースを想定しています。そのようなケースであればほぼカスタマイズ不要(見栄えを変更したい場合にカスタマイズが必要になるかも)でウェブ化できます。ノーツデータベースをファイルサーバーとして利用しているようなケースであればピッタリだと思っていますが、そのようなケースがどのくらいあるのかは正直よくわかってないです。

逆に「ノーツをノーツとして使っているんだけど、ライセンスのコストを下げたいからウェブ化したい」というケースにはあまり当てはまらないと思っています。このツールでウェブ化したノーツデータベースは参照可能になりますが、新規作成や既存データの編集はできなくなります。繰り返しになりますが、このようなケースは個人的にはノーツを使い続けるべきケースだと思っています。


というわけで、そんなツールを現在進行形で作っています。このブログエントリのタイトルにもありますが、これは自分のプログラマーとしての「(色々な意味での)挑戦」の意味合いが強いものだと思っています。 とはいえ夢物語を語っているつもりはなく、スクリーンショットがあるように一応稼働できる状態には開発できています。次回は nsf2dxl2web がノーツデータベースをウェブ化する上での仕組みについて紹介する予定です。


(続きはこちらです)
nsf2web2dxl の挑戦(1)
nsf2web2dxl の挑戦(2)
nsf2web2dxl の挑戦(3)
nsf2web2dxl の挑戦(4)
nsf2web2dxl の挑戦(5)

これは自分のスキル不足が原因だと思っているのですが、Kubernetes や OpenShift といったコンテナクラスタ環境を使ったウェブアプリケーションのデプロイではトラブルシューティングに時間を費やすことが珍しくありません。過去に成功したのと同じようなパターンでデプロイすることばかりではなく、ストレージ含めた新しいチャレンジをすることが多く、一発で成功することはあまりありません。

この「トラブルシューティング」は、現象としては「デプロイしたが想定していた URL でウェブアプリが動いていない」ことで分かるのですが、この原因は(期待通りに動いていない箇所が)様々です。k8s の用語でいうと Pod, Deployment, Service, Ingress, ... といった構成要素があり、まずはこの中のどこで問題が発生しているのかを見極め、その上でその箇所で発生している問題の原因を特定して解決していく必要があります。

この「どの部分で問題が発生しているか」を特定する作業において、"port-forward" と呼ばれる機能に助けられています。この port-forward 機能について自分へのメモ目的も含めてまとめておきます。


【コンテナクラスタのトラブルシューティング】
k8s を使ったウェブアプリケーションのデプロイ作業は手動であったり(半)自動化されていたりしますが、リソースと呼ばれる以下のようなパーツの単位で作成され、これらが組み合わされて1つのウェブアプリケーションとなります(ウェブアプリケーションがうまく動かない場合は、このいずれか(または複数)が想定していたように動いていない、ということになります):
・デプロイメント(Deployment)
 - アプリケーションイメージを1つ以上インスタンス化し、クラスタ内で稼働しているもの
・ポッド(Pod)
 - インスタンス化されたアプリケーションイメージ1つ1つを指すもの
 - デプロイメントを作成した場合はデプロイメントに含まれる
・サービス(Service)
 - コンテナ内で稼働しているポッドやデプロイメントを外部公開方法を定義するもの
・イングレス(Ingress)
 - 複数のサービスを管理し、外部からのリクエストに対してロードバランサーとなるもの

2023090706


コンテナクラスタ環境にウェブアプリケーションが正しくデプロイされた場合、ユーザーはアプリケーションの URL にアクセスすると、まずイングレスがそのリクエストを受け取って正しいサービスに転送し、サービスを経由してアプリケーションインスタンスであるポッド(デプロイメント)にアクセスします(イングレス→サービス→ポッド)。そしてそのリクエストに対してポッドが返したレスポンスは逆の順序(ポッド→サービス→イングレス)を通って返される、ということになります。


「デプロイしたアプリケーションが期待していたように動かない」というのは、この中のどこかが期待していたように動いていなくて、全体として「動かない」ように見えていることになります。したがってトラブルシューティングにおいて、まずはどこで問題が発生しているのかを特定する必要があります。
2023090706


【port-forward 機能】
kubectl や oc といった CLI ツールの機能である port-forwarding を使うと、ポート番号を使ったフォワーディング機能によりイングレスを経由せずにコンテナクラスタ内に定義されているサービスやポッドに直接アクセスできるようになります。

例えばイングレスを経由せずにサービスにアクセスすると期待していた挙動になる(期待していた結果が返ってくる)のであればイングレスやその設定に問題がある可能性が高いことが考えられ、サービスにアクセスしても動かないがポッドに直接アクセスすれば期待していた挙動になるのであればサービスやその設定に問題がある可能性が高いことが考えられます(それでも動かない場合はポッドに問題があると考えられます)。 このような障害発生個所の見極めにおいては port-forwarding が便利に使えることになります。


【port-forward 機能を使ってみる】
試しにわざと正しく動かないことがわかっている以下のようなマニフェストファイル(deployment.yaml)を使ってアプリケーションをデプロイし、port-forward で確認する、ということをやってみます:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: guestbook
  labels:
    app: guestbook
spec:
  replicas: 1
  selector:
    matchLabels:
      app: guestbook
  template:
    metadata:
      labels:
        app: guestbook
    spec:
      containers:
      - name: guestbook
        image: ibmcom/guestbook:v1
        imagePullPolicy: Always
        env:
          - name: NODE_ENV
            value: production
---
apiVersion: v1
kind: Service
metadata:
  name: guestbook-svc
  labels:
    app: guestbook
spec:
  type: ClusterIP
  ports:
  - protocol: TCP
    port: 3000
    targetPort: 3000
  selector:
    app: guestbook
---
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: guestbook-route
  labels:
    app: guestbook
spec:
  host: guestbook.(アサインされた ingress サブドメイン)
  to:
    kind: Service
    name: guestbook-svc
    weight: 100
  port:
    targetPort: 4000 # 正しくは3000
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect
  wildcardPolicy: None

ちなみに上記マニフェストの正しい記述は下から5行目の Route の spec.port.targetPort の値が 3000 になっているものです(今回はわざと 4000 を指定しています)。

なお以下の作業は IBM Cloud の Redhat OpenShift コンテナクラスタ環境を使って確認しました。なので Ingress 部分の定義は apiVersion が "route.openshift.io/v1" の Route というリソースを使って定義しています。またこの Route 内の spec.host の値には "guestbook.(アサインされた ingress サブドメイン)" と記載していますが、実行前にこの()内の部分を OpenShift 画面に表示されているサブドメイン名に置き換えて保存しておいてください:
2023090701


oc コマンドでログインし、このマニフェストファイルをデプロイ(oc apply)して、その結果を確認(oc get all)します:
$ oc apply -f deployment.yml

$ oc get all

NAME                             READY   STATUS    RESTARTS   AGE
pod/guestbook-59b85f9654-22wh5   1/1     Running   0          13m

NAME                                TYPE           CLUSTER-IP       EXTERNAL-IP                            PORT(S)    AGE
service/guestbook-svc               ClusterIP      172.21.150.230                                          3000/TCP   13m
service/kubernetes                  ClusterIP      172.21.0.1                                              443/TCP    6h7m
service/openshift                   ExternalName              kubernetes.default.svc.cluster.local                    5h46m
service/openshift-apiserver         ClusterIP      172.21.46.95                                            443/TCP    6h6m
service/openshift-oauth-apiserver   ClusterIP      172.21.98.204                                           443/TCP    6h6m

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/guestbook   1/1     1            1           13m

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/guestbook-59b85f9654   1         1         1       13m

NAME                                       HOST/PORT                                                                 PATH   SERVICES        PORT   TERMINATION     WILDCARD
route.route.openshift.io/guestbook-route   guestbook.(ingress サブドメイン).jp-tok.containers.appdomain.cloud             guestbook-svc   4000   edge/Redirect   None

"oc get all" の結果は上からポッド、サービス、デプロイメント、レプリカセット、そしてイングレスになっています。ポッドのステータスは Running でちゃんと動いていて、サービスも ClusterIP を使って正しく公開され、イングレスもホスト名が割り当てられています。 この結果だけを見ると正しく動いてそうです。

でも実際にブラウザでイングレスの HOST 名に表示されているサーバーにアクセスするとこのように表示されます(知らない人もいるので補足すると、これは期待していたアプリ画面ではなく、アクセスできなかった場合のエラー画面です):
2023090702


エラー無くデプロイできたけどアプリにアクセスできない、、 こんな時に port-forward の出番です。

まずは(上述の結果で Pod が Running になってるので可能性は低そうですが)ポッドのレベルで正しく動いているかどうかを確認するため、port-forwarding でポッドに直結して動作を確認してみます。

ポッドに対して port-forwarding するには kubectl(または oc )コマンドで以下のように実行します:
$ kubectl port-forward (ポッド名) (ホストのポート番号):(ポッド上で動いているポート番号)

今回の例だと(上述の結果より)ポッド名は "guestbook-svc" だとわかります。また実行したマニフェストファイルより、このサービスは 3000 番ポートで動いているはずです(サービスの spec.ports.targetPort の値)。このポッドにホストの(何番でもいいのですが) 8000 番ポートからフォワーディングして接続させてみます。というわけで、今回のケースであれば以下のコマンドを実行します:
$ kubectl port-forward guestbook-59b85f9654-22wh5 8000:3000

コマンド実行後、ウェブブラウザから http://localhost:8000/ にアクセスしてみます:
2023090703


今回は期待通りの画面が表示されました。ということは「ポッドは正しく動いている」ことが推測できました。Ctrl+C で port-forwarding を解除します。


継はサービスのレベルで正しく動いているかどうかを確認してみます。イングレスを経由せずにサービスにアクセスするため、port-forwarding でサービスに直結して動作を確認してみます。この場合は以下のようなコマンドを実行します:
$ kubectl port-forward service/(サービス名) (ホストのポート番号):(サービスで公開しているポート番号)

今回の例だと(上述の結果より)サービス名は "guestbook-59b85f9654-22wh5" だとわかります。また実行したマニフェストファイルより、このポッド(デプロイメント)は 3000 番ポートで動いているはずです(サービスの spec.ports.port の値)。このポッドにホストの(何番でもいいのですが) 9000 番ポートからフォワーディングして接続させてみます。というわけで、今回のケースであれば以下のコマンドを実行します:
$ kubectl port-forward service/guestbook-svc 9000:3000

コマンド実行後、ウェブブラウザから http://localhost:9000/ にアクセスしてみます:
2023090704


今回は期待通りの画面が表示されました。ということは「サービスも正しく動いている」ことが推測できました。Ctrl+C で port-forwarding を解除します。

これらの結果から、
・ポッド(デプロイメント)は正しく動いていそう
・サービスも正しく動いていそう
・ということはイングレスの設定が間違っていそう?

という切り分けをすることができました。実際、マニフェストの Route 内をわざと間違った内容に弄ったことが分かっているので、正しい切り分けができていることになります。

このケースであればイングレスの設定内容を疑ってみることになります。試しにイングレスの設定名称を指定して describe するとこのようになりました(kubectl の場合は "kubectl describe ingress guestbook-route"):
$ oc describe route guestbook-route 

  :
  :
Path:                   
TLS Termination:        edge
Insecure Policy:        Redirect
Endpoint Port:          4000

Service:        guestbook-svc
Weight:         100 (100%)
Endpoints:      

接続先である Endpoints が空になっていて何も表示されていません。ということは設定内容になんらかの誤りがあってサービスに接続できていないのでは・・・ と考えることができます。実際、今回のマニフェストでは targetPort を 3000 と指定すべきところを 4000 と指定していたため接続できていませんでした。仮に 4000 を 3000 に変更して再度適用した上で接続を試みた所、今度は正しく接続できるようになりました:
2023090705


現実問題として、イングレス部分はポッドやサービスとは異なり Kubernetes からはサードパーティ製品となるので色々な実装があり、トラブルシューティングも実装ごとの方法が考えられるため比較的難しいトラブルシューティングとはなります。それでも「どの部分に設定ミスがあったか」を特定することで解決はしやすくなりますし、その特定において port-forwarding は有効な切り分け方であると思っています。




このページのトップヘ