クリアフィルター
記事
Tomohiro Iwamoto · 2020年6月8日
VMware vSphereで実行する大規模な本番データベースのCPUキャパシティプランニングについて、お客様やベンダー、または社内のチームから説明するように頼まれることが良くあります。
要約すると、大規模な本番データベースのCPUのサイジングには、いくつかの単純なベストプラクティスがあります。
物理CPUコア当たり1つのvCPUを計画する。
NUMAを考慮し、CPUとメモリをNUMAノードに対してローカルに維持できるようVMの理想的なサイズを決定する。
仮想マシンを適正化する。 vCPUは必要な場合にのみ追加する。
このことから、通常いくつかの一般的な疑問が生まれます。
ハイパースレッディングにより、VMwareでは物理CPUの2倍の数でVMを作成できます。 これはキャパシティが2倍になるということか? できるだけ多くのCPUを使ってVMを作成すべきではないのか?
NUMAノードとは? NUMAに配慮する必要があるのか?
VMを適正化する必要があるが、どうすれば適正化されたことがわかるのか?
こういった疑問につては、下の例を使って答えることにします。 ただし、ベストプラクティスは決定事項ではありません。 ときには妥協することも必要です。 たとえば、大規模なデータベースVMはNUMAノードに収まらない可能性が高く、それはそれでも良いのです。 ベストプラクティスは、アプリケーションと環境に合わせて評価・検証する必要のあるガイドラインです。
この記事は、InterSystemsデータプラットフォームで実行するデータベースを例として書いていますが、概念とルールは、一般的にあらゆる大規模(モンスター)VMのキャパシティとパフォーマンスプランニングにも適用されます。
仮想化のベストプラクティスとパフォーマンスとキャパシティ計画に関するその他の記事: 「InterSystemsデータプラットフォームとパフォーマンス」シリーズの「その他の記事」セクションを参照してください。
モンスターVM
この記事は主に、「ワイドVM」と呼ばれることもあるモンスターVMのデプロイについて書いています。 高トランザクションデータベースのCPUリソース要件は、こういったデータベースがモンスターVMにデプロイされることが多い事を意味します。
モンスターVMとは、単一の物理NUMAノードよりも多い仮想CPUまたはメモリを備えたVMです。
CPUアーキテクチャとNUMA
現行のIntelプロセッサアーキテクチャには、NUMA(Non-Uniform Memory Architecture)アーキテクチャがあります。 たとえば、この記事の検証用に使用しているサーバーは次の構成になっています。
CPUソケット2個。各ソケットに12コアのプロセッサを装着(Intel E5-2680 v3)。
256 GBメモリ(16 x 16 GB RDIMM)
各12コアプロセッサには、独自のローカルメモリ(128 GBのRDIMMとローカルキャッシュ)があり、同じホスト内のほかのプロセッサのメモリにもアクセスできます。 CPU、CPUキャッシュ、および128 GB RDIMMメモリを1つのパッケージとした12コアをそれぞれNUMAノードと呼びます。 別のプロセッサのメモリにアクセスするために、NUMAノードは高速インターコネクトで接続されます。
ローカルRDIMMとキャッシュメモリにアクセスするプロセッサで実行するプロセスは、別のプロセッサのリモートメモリにアクセスするためにインターコネクトを経由する場合に比べ、レイテンシが低くなります。 インターコネクト経由のアクセスではレイテンシが高くなるため、パフォーマンスが一定しません。 同じ設計は3つ以上のソケットを持つサーバーにも適用されます。 ソケットが4つあるIntelサーバーには、4つのNUMAノードがあります。
ESXiは物理NUMAを認識し、ESXi CPUスケジューラはNUMAシステムのパフォーマンスを最適化するように設計されています。 ESXiがパフォーマンスを最大化する方法の1つは、物理NUMAノードにデータの局所性を作成することです。 この記事の例では、vCPUが12でメモリが128 GB未満のVMがある場合、ESXiはそのVMを物理NUMAノードの1つで実行するように割り当てます。 このことから次のルールについて言うことができます。
可能であれば、CPUとメモリをNUMAノードに対してローカルに維持できるようにVMのサイズを決定する。
NUMAノードより大きなモンスターVMが必要な場合は、それでもかまいません。ESXiは非常に優れた機能によって、要件を自動的に計算して管理することができます。 たとえば、ESXiは、最適なパフォーマンスを得るために、物理NUMAノードにインテリジェントにスケジュールする仮想NUMAノード(vNUMA)を作成します。 vNUMA構造は、オペレーティングシステムに公開されます。 たとえば、2つの12コアプロセッサを搭載したホストサーバーと16 vCPUを備えたVMがある場合、ESXiは8つの物理コアを2つのプロセッサに使用して、VM vCPUをスケジュールするため、オペレーティングシステム(LinuxまたはWindows)に2つのNUMAノードが表示されます。
また、VMを適正化して、必要以上のリソースを割り当てないようにすることも重要です。リソースが無駄になり、パフォーマンスが低下する可能性があります。 VMを適正化すればNUMAのサイズ設定にも役立つだけでなく、特に低から中程度のCPU使用率のある24 vCPUのVMよりもCPU使用率の高い(ただし安全な)12 vCPUのVMの方が、より効率的でパフォーマンスの改善が得られます。特にこのホストに、スケジュールされる必要がありリソースを争っているほかのVMがある場合はなおさらです。 このこともルールをさらに強化する要因といえます。
仮想マシンを適正化する。
注意: NUMAのIntel実装とAMD実装には違いがあります。 AMDには、プロセッサごとに複数のNUMAがあります。 顧客のサーバーでAMDプロセッサを見たのはしばらく前になりますが、それを使用している場合は、計画の一環としてNUMAのレイアウトを確認してください。
ワイドVMとライセンス
最適なNUMAスケジュール設定を得る、ワイドVMの構成は 2017年6月訂正: ソケットあたり1 vCPUでVMを構成します。 たとえば、24 vCPUのVMは、デフォルトでそれぞれが1つのコアを持つ24 CPUソケットとして構成されます。
VMwareのベストプラクティスのルールに従ってください。
例については、VMwareブログに記載のこちらの記事を参照してください。
VMwareのブログ記事には詳細が説明されていますが、これを執筆したMark Achtemichukは次の経験則を推奨しています。
高度なvNUMA設定はたくさんあるが、デフォルトの設定を変更しなければいけないのは、まれなケースのみ。
仮想マシンvCPU数は、単一の物理NUMAノードの物理コア数を超えるまでは、必ず、ソケットあたりのコア数が反映されるように構成すること。
NUMAノードにある物理コア数以上のvCPUを構成する必要がある場合は、最少数のNUMAノード間でvCPU数を均一に分割すること。
仮想マシンのサイズが単一の物理NUMAノードを超える場合、奇数のvCPU数を割り当てないこと。
vCPUホットアド(Hot-add)は、vNUMAを無効にしてもかまわない場合にのみ有効にすること。
ホストの物理コアの合計数以上に大きいVMを作成しないこと。
Cachéライセンスはコアを計数するため問題ではありませんが、Caché以外のソフトウェアやデータベースの場合は、VMに24ソケットを指定するとソフトウェアライセンスに違いが生じる場合があるため、ベンダーと確認する必要があります。
ハイパースレッディングとCPUスケジューラ
ハイパースレッディング(HT)の話が持ち上がるとよく聞くのが「ハイパースレッディングはCPUコア数を2倍にする」ということです。 これは明らかに物理的に可能なことではありません。コア数が物理的に増えたり減ったりすることはありえません。 ハイパースレッディングは有効にすべきものであり、有効になればシステムのパフォーマンスを向上させることができます。 期待値はおそらくアプリケーションパフォーマンスにおいて20%増かもしれませんが、実際の量はアプリケーションとワークロードによって決まります。 が、それでも2倍ではありません。
VMwareのベストプラクティスの記事で説明したように、大規模な本番データベースVMをサイジングするには、vCPUに完全専用の物理コアがサーバーにあると仮定することから始まります。基本的に、キャパシティ計画を実施する際はハイパースレッディングを無視するということです。 たとえば:
24コアのホストサーバーの場合、利用可能なヘッドルームがあることを考えて、本番データベースに合計で最大24 vCPUを計画します。
ピーク処理期間のアプリケーション、オペレーティングシステム、およびVMwareのパフォーマンスをしばらく監視したら、より高い統合が可能であるかどうかを判断することができます。 ベストプラクティスの記事では、ルールを次のように述べました。
1つの物理CPU(ハイパースレッディングを含む)= 1つのvCPU(ハイパースレッディングを含む)
ハイパースレッディングでCPUが2倍にならない理由
Intel Xeonプロセッサ上のHTは、1つの物理コア上に2つの論理CPUを作成する方法です。 オペレーティングシステムは、2つの論理プロセッサに対して効率的にスケジュールを設定できます。論理プロセッサ上のプロセスまたはスレッドがIOなどを待機している場合、物理CPUリソースは、別の論理プロセッサによって使用されます。 ある時点において進行できるのは1つの論理プロセッサのみであるため、物理コアがより効果的に使用されていても、パフォーマンスは2倍にならないのです。
ホストBIOSでHTが有効化されている場合、VMを作成する際に、HT論理プロセッサあたりのvCPUを構成することができます。 たとえば、HTが有効になっている24物理コアのサーバーで、最大48 vCPUのVMを作成することができます。 ESXi CPUスケジューラは、最初に個別の物理コアで(NUMAを考慮しながら)VMプロセスを実行することにより、処理を最適化します。 モンスターデータベースVMに物理コア数以上のvCPUを割り当てるとスケーリングにメリットがあるのかについては、この記事の後の方で説明することにします。
co-stopとCPUスケジューリング
ホストとアプリケーションのパフォーマンスを監視した後、ホストCPUリソースのオーバーコミットが可能であると判断する場合があります。 これがよいことかどうかは、アプリケーションとワークロードに大きく依存します。 スケジューラと監視すべき重要なメトリックを理解することで、ホストリソースをオーバーコミットしていないことを確認できます。
たまに、VMが進行するには、VMのvCPUと同じ数の空き論理CPUが必要だということを聞くことがあります。 たとえば、12 vCPUのVMは、実行が進む前に12個の論理CPUが「利用可能」になるまで「待機」する必要があります。 ただし、バージョン3以降のESXiではそうでないことに注意してください。 ESXiは、アプリケーションのパフォーマンスを向上させるために効率的なスケジューリングを使用しています。
複数の協調動作するスレッドまたはプロセスは頻繁に同期するため、一緒にスケジューリングしない場合、操作のレイテンシが増加する可能性があります。 たとえば、スピンループ内で別のスレッドがスケジュールされるのを待機しているスレッドです。 最良のパフォーマンスを得るために、ESXiはできる限り多くの兄弟vCPUを一緒にスケジュールしようとします。 ただし、CPUスケジューラは、統合環境で複数のVMがCPUリソースを争う場合に、vCPUを柔軟にスケジュールすることができます。 兄弟vCPUが進行しないにも関わらず一部のvCPUが進行してしまうと大きな時間差(これをスキューと呼ぶ)が生まれてしまうため、この場合は先行のvCPUがそれ自体を停止するかどうかを決定します(co-stop)。 co-stop(またはco-start)するのは、VM全体ではなくvCPUであることに注意してください。 これは、リソースのオーバーコミットがある場合でもうまく機能しますが、CPUのオーバーコミットが多すぎると必然的にパフォーマンスに影響します。 後の例2で、オーバーコミットとco-stopの例を示します。
これは、VM間のCPUリソースの争奪戦ではありません。ESXiのCPUスケジューラの仕事は、CPUのシェア数、予約、限界といったポリシーを確実に守りながら、CPU使用率を最大化し、公平性、スループット、応答性、スケーラビリティを確保することにあります。 予約とシェアを使用して本番ワークロードに優先順を付ける話はこの記事の目的から外れてしまい、アプリケーションやワークロードの組み合わせによって異なります。 Caché固有の推奨事項がある場合は、これについて後で触れるかもしれません。 CPUスケジューラにはさまざまな要因がかかわっているため、このセクションではその表面をざっと紹介しています。 詳細については、この記事の最後に記載する参考情報にあるVMwareのホワイトペーパーやその他のリンクを参照してください。
例
さまざまなvCPU構成を説明するために、高トランザクションレートのブラウザベースの病院情報システムアプリケーションを使って、一連のベンチマークを実行しました。 VMwareが作成したDVDストアデータベースベンチマークの概念に似ています。
ベンチマークのスクリプトは、ライブの病院実装から得た観察とメトリックを基に作成されており、使用率の高いワークフロー、トランザクション、およびシステムリソースを最も使用するコンポーネントが含まれています。 ほかのホスト上のドライバーVMは、ランダム化された入力データを使用したスクリプトを設定されたワークフロートランザクションレートで実行することで、Webセッション(ユーザー)をシミュレートします。 レート 1x のベンチマークがベースラインです。 レートは段階的に増減できます。
データベースとオペレーティングシステムのメトリックに加え、ベンチマークデータベースVMのパフォーマンスを測定するのに適したメトリックは、サーバーで測定されるコンポーネント(またはトランザクション)の応答時間です。 コンポーネントとは、エンドユーザー画面の一部分などを指します。 コンポーネントの応答時間が増加すると、ユーザー側においてはアプリケーションの応答時間に悪影響がでます。 パフォーマンスの高いデータベースシステムは、一貫した高いパフォーマンスをエンドユーザーに提供できるものでなければなりません。 次のグラフでは、最も遅く使用頻度の高いコンポーネントの応答時間の平均を算出し、一貫したテストパフォーマンスとエンドユーザーエクスペリエンスの指標を測定しています。 平均的なコンポーネント応答時間は1秒未満であり、ユーザー画面は、1つのコンポーネント、または複雑な画面の場合は多くのコンポーネントで構成されています。
常にピーク時のワークロードと、アクティビティの予定外の急増に対応するためのバッファに対してサイジングを行っていることを覚えておきましょう。 私の場合は通常、平均80%のピークCPU使用率を目指しています。
ベンチマークのハードウェアとソフトウェアの全リストはこの記事の最後にあります。
例1. 適正化 - ホストあたり1つのモンスターVM
たとえば、24個の物理コアホスト上の24 vCPUのVMといったように、ホストサーバーのすべての物理コアを使用できるようにサイジングされたデータベースVMを作成することができます。 HAを得るためにCachéデータベースミラーで「ベアメタル」サーバーを実行したりオペレーティングシステムのフェイルオーバークラスタリングといった複雑なものを導入したりせずとも、データベースVMは、DRSやVMware HAなどの管理・HA向けvSphereクラスタに含まれます。
古い考え方で、5年後のハードウェア使用期間の最後に期待されるキャパシティに対してプライマリデータベースVMのサイズを決定するお客様を見てきましたが、上記で説明したように、適正化する方が有効と言えます。VMのサイズが過大に設定されていなければ、パフォーマンスと統合が改善され、HAの管理もより簡単になります。メンテナンスやホスト障害が発生した場合は、テトリスのように、データベースのモンスターVMを別のホストに移行して、そこで再起動しなければなりません。 トランザクションレートが大幅に増加すると予測される場合は、定期メンテナンス中に前もってvCPUを追加することができます。
「ホットアド」CPUオプションはvNUMAを無効にするため、モンスターVMには使用しないでください。
24コアホストでの一連の検証を示す次のグラフを考察してみましょう。 3x トランザクションレートは、この24コアシステムのスイートスポットであり、キャパシティプランニングの目標です。
ホストでは単一のVMが実行している。
12、24、36、48 vCPUでのパフォーマンスを示すために4つのVMサイズを使用している。
各VMサイズに対し、トランザクションレート(1x、2x、3x、4x、5x)が実行された(可能な場合)。
コンポーネント応答時間としてパフォーマンス/ユーザーエクスペリエンスを示している(棒グラフ表示)。
平均CPU%使用率はゲストVM(線グラフ表示)
すべてのVMサイズのホストCPU使用率は、4x レートで100%に達した(赤い破線)。
このグラフには多くのことが示されていますが、2つのことに注目できます。
24 vCPUのVM(オレンジ)は、目標の3x トランザクションレートにスムーズにスケールアップしています。 3x レートにおいては、ゲスト内のVMは平均76%CPUに達成しています(ピーク時は約91%)。 ホストCPUの使用率はゲストVMとあまり変わりません。 コンポーネント応答時間は3xまでほぼ一定しているため、ユーザーは満足しています。 目標のトランザクションレートに関して言えば、このVMは適正化されていると言えます。
適正化についてはわかりましたが、vCPUの増加についてはどうでしょうか。これは、ハイパースレッディングを使用するということです。 パフォーマンスとスケーラビリティを2倍にすることは可能でしょうか。 短く言えば、答えは「ノー!」です。
この場合、この答えは、4x 以降のコンポーネント応答時間を見るとわかります。 割り当てられる論理コア数(vCPU)が多いほどパフォーマンスは「向上」しますが、一定しておらず、3x までのような一貫性がありません。 割り当てられるvCPU数が増えても、ユーザーは4x での応答時間が遅くなっていることを報告するでしょう。 vSphereが示したように、4xでは、ホストはすでに100%CPU使用率で横ばいになっていたことを思い出しましょう。 vCPU数が高い場合、ゲスト内CPUメトリック(vmstat)が100%使用率未満を示していたとしても、これは物理リソースには当てはまりません。 ゲストオペレーティングシステムは仮想化されていることを認識しないため、それに与えられたリソースに対して示しているだけであることに注意してください。 また、ゲストオペレーティングシステムはHTスレッドも認識しないため、すべてのvCPUは物理コアとして示されます。
ポイントは、データベースプロセス(3x トランザクションレートのCachéプロセスは200個以上ある)は非常にビジーであり、プロセッサを非常に効率的に使用しますが、ほかの作業をスケジュールするかこのホストにほかのVMを統合するための論理プロセッサにはあまり余裕がないということです。 たとえば、Caché処理の大部分はメモリ内で行われるため、IOの待機時間はあまりありません。 そのため、物理コアより多いvCPUを割り当てることはできますが、ホストの使用率がすでに100%に達しているため、あまり得がないのです。
Cachéは高いワークロードの処理に非常に優れています。 ホストやVMが100%のCPU使用率に達していても、アプリケーションは実行し続けており、トランザクションレートも増加し続けます。スケーリングは線形ではなく、応答時間が長引けば、ユーザーエクスペリエンスに影響があります。ただし、アプリケーションが「崖から落ちる」わけではなく、快適な場所でないにしても、ユーザーは作業を続行できるでしょう。 応答時間の影響をそれほど受けないアプリケーションであれば、限界まで押したとしても、Cachéは安全に動作し続けます。
データベースVMやホストを100%CPUで実行するのは好ましいことではないことを覚えておいてください。 VMには予定外の急増や予測していなかった増加に対応できるキャパシティが必要であり、ESXiハイパーバイザーには、すべてのネットワーキング、ストレージなどでのアクティビティに使用するリソースが必要です。
私は常にピーク時のCPU使用率を80%で計画するようにしていますが、 それでも、vCPUのサイズを物理コア数までに設定するようにし、極端な状況でも論理スレッド上にESXiハイパーバイザー用のヘッドルームをいくらか残すようにしています。
ハイパーコンバージド(HCI)ソリューションを実行している場合は、HCIのCPU要件もホストレベルで考慮する必要があります。 詳細は、HCIに関する前の記事を参照してください。 HCIにデプロイされたVMの基本的なCPUサイジングは、ほかのVMと変わりません。
独自の環境とアプリケーションですべての設定を確認・検証する必要があることを忘れないでください。
例2. オーバーコミットされたリソース
アプリケーションパフォーマンスは「遅い」にもかかわらず、ゲストオペレーティングシステムではCPUリソースに余裕があることが示される顧客サイトを見たことがあります。
ゲストオペレーティングシステムは仮想化を認識しないことに注意してください。 残念ながらvmstat(pButtonsの場合)が示すゲスト内のメトリックは当てにならないことがあるため、システムの状態とキャパシティを正確に理解するには、ホストレベルのメトリックとESXiのメトリック(esxtopなど)も取得する必要があります。
上のグラフからもわかるように、ホストが100%使用率を示す場合、ゲストVMの使用率は低く示されることがあります。 36 vCPUのVM(赤)は4x レートで80%の平均CPU使用率を示していますが、ホストでは100%となっています。 公開後にほかのVMがホストに移行した場合や、きちんと構成されていないDRSルールによってリソースがオーバーコミットされている場合など、適正化されたVMでもリソース不足となることがあります。
主なメトリックを示すために、この一連の検証では以下のように構成しました。
ホストで実行する2つのデータベースVM
一定の2xトランザクションレートで実行する24vCPU(グラフには表示されていません)
1x、2x、3xレートで実行する24vCPU(グラフに3つのメトリックが表示されています)
リソースを使う別のデータベースでは、3x レートにおけるゲストOS(RHEL 7)のvmstatは86%の平均CPU使用率を示しており、実行キューは平均25です。 ただし、このシステムのユーザーは、プロセスが遅くなるにつれコンポーネント応答時間が急増するため、大きな不満を訴えると考えられます。
次のグラフが示すように、co-stopと準備時間からユーザーパフォーマンスがこれほど悪い理由を読み取ることができます。 準備時間(%Ready)とco-stop(%CoStop)メトリックは、CPUリソースが目標の3xレートで大幅にオーバーコミットされていることを示しています。 ホストの実行レートが2x(other VM)、かつこのデータベースVMは3xレートであるわけですから、特に驚くことでもないでしょう。
このグラフは、ホスト上の合計CPU負荷が増加すると準備時間が増加することを示しています。
準備時間とは、VMの実行準備は整っているが、CPUリソースが利用できないために実行できない時間です。
co-stopも増加します。 データベースVMを進行させられるのに十分な空き論理CPUがありません(上記のHTの項目で説明しました)。 その結果、物理CPUリソースの競合により処理が遅延しています。
pButtonsとvmstatのサポートビューに、仮想オペレーティングシステムのみが表示されるという状況を顧客サイトで見たことがあります。 vmstatはCPUヘッドルームを示していましたが、ユーザーのパフォーマンスエクスペリエンスはひどいものでした。
ここでの教訓は、ESXiメトリックとホストレベルビューが利用できるようになるまで実際の問題が診断されなかったということです。一般的なクラスタCPUリソース不足によってCPUリソースのオーバーコミットが発生し、状況を悪化すべく、粗末なDRSルールによって高トランザクションデータベースVMがまとめてホストリソースに移行され、ホストリソースを圧迫していたのです。
例3. オーバーコミットされたリソース
この例では、3xトランザクションレートで実行する24 vCPUデータベースVMをベースラインとし、一定した3xトランザクションレートで2つの24 vCPUデータベースVMを使用しました。
平均ベースラインCPU使用率(上の例1を参照)は、VMが76%でホストが85%でした。 1つの24 vCPUデータベースVMが24個のの物理プロセッサをすべて使用しています。 2つの24 vCPU VMを実行すると、VMはリソースを争い、サーバーで48個の論理実行スレッドすべてを使用することになります。
単一のVMではホストは100%使用されていませんでしたが、2つの非常にビジーな24 vCPU VMがホスト上の24個の物理コアを使用しようとすると(HTを使用してでさえも)、スループットとパフォーマンスが大幅に低下するのがわかります。 Cachéは利用可能なCPUリソースを非常に効率よく使用できますが、VMあたりのデータベーススループットは16%低下し、さらに重要なことに、コンポーネント(ユーザー)応答時間が50%以上増加しています。
最後に
この記事の目的は、一般的な疑問にお答えすることでした。 CPUホストリソースとVMware CPUスケジューラの詳細については、下の参考情報を参照してください。
システムの最後の一滴までパフォーマンスを絞り出す、特別に高度な調整やESXi設定がたくさんありますが、基本的なルールは非常にシンプルです。
大規模な本番データベースの場合:
物理CPUコアあたり1 vCPUで計画する。
NUMAを考慮し、CPUとメモリをNUMAノードに対してローカルに維持できるようVMの理想的なサイズを決定する。
仮想マシンを適正化する。 vCPUは必要な場合にのみ追加する。
VMを統合する場合は、大規模なデータベースは非常にビジーであり、ピーク時にCPU(物理と論理)を大量に使用することに注意してください。 監視で安全だということがわかるまで、オーバーサブスクライブしてはいけません。
参考情報
VMwareブログ「When to Overcommit vCPU:pCPU for Monster VMs」
Introduction 2016 NUMA Deep Dive Series
The CPU Scheduler in VMware vSphere 5.1
検証について
この記事で使用した例は、オールフラッシュアレイに接続された2つのDell R730プロセッサで構成されるvSphereクラスタで実行しました。 例では、ネットワークまたはストレージにボトルネックはありませんでした。
Caché 2016.2.1.803.0
PowerEdge R730
2x Intel(R) Xeon(R) CPU E5-2680 v3 @ 2.50 GHz
16x 16 GB RDIMM、2133 MT/秒、デュアルランク、x4 データ幅
SAS 12 Gbps HBA外部コントローラ
ハイパースレッディング(HT)有効
PowerVault MD3420、12G SAS、2U-24ドライブ
24x 24 960 GBソリッドステートドライブSAS読み取り処理集中型MLC 12 Gbps 2.5インチホットプラグドライブ、PX04SR
2 コントローラー、12G SAS、2U MD34xx、8Gキャッシュ
VMware ESXi 6.0.0ビルド2494585
VMはベストプラクティスに合わせて構成されています。VMXNET3、PVSCSIなど
RHEL 7
LargePages
ベースラインの1x レートの平均は700,000 glorefs/秒(データベースアクセス/秒)でした。 5x レートの平均は、24 vCPUで3,000,000 glorefs/秒超でした。 検証では、一貫したパフォーマンスが達成されるまで慣らし運転を行い、その後で15分間、サンプルを取得し平均を算出しました。
これらの例は理論を説明することだけを目的としているため、独自アプリケーションでは必ず検証する必要があります。
また、前の記事「InterSystemsデータプラットフォームとパフォーマンス - VMバックアップとCachéのFreeze/Thawスクリプト」もお読みください。
記事
Toshihiko Minamoto · 2021年8月31日
[最初の記事](https://jp.community.intersystems.com/node/501166)では、Caché Webアプリケーションのテストとデバッグを外部ツールを用いて行うことについて説明しました。 2回目となるこの記事では、Cachéツールの使用について説明します。
以下について説明します。
* CSP GatewayとWebappの構成
* CSP Gatewayのロギング
* CSP Gatewayのトレース
* ISCLOG
* カスタムロギング
* セッションイベント
* デバイスへの出力
### CSP GatewayとWebappの構成
まず初めに、フロントエンドアプリケーションをデバッグする場合、特にそれを開発している場合は、キャッシュは必要ありません。 本番システムでは役立ちますが、開発中には不要です。 Webアプリケーションのロギングを無効にするには、システム管理ポータル → メニュー → Webアプリケーションの管理 → <あなたのWebアプリ>に移動して、Serve Files Timeoutを0に設定します。 そして[保存]ボタンを押します。
次に、webアプリケーションの既存のキャッシュを消去する必要があります。 これを行うには、システム管理ポータル → システム管理 → 構成 → CSP Gatewayの管理 → システムステータスに移動します。 「Cached Forms」テーブルを探し、その最後の行にある「Total」の消去ボタン(ドット)を押してWebアプリケーションのキャッシュを消去します。

### CSP Gatewayのロギング
CSP Gatewayに関しては、受信リクエストのロギングがサポートされています(ドキュメント)。 [デフォルトのパラメーター]タブで、希望するログレベル(v9aなど)を指定して、変更を保存します。 v9aは、すべてのHTTPリクエストをGatewayホームディレクトリのhttp.logに記録します(ほかのオプションについてはドキュメントをご覧ください)。 リクエストは次のようにキャプチャされます。
GET /forms/form/info/Form.Test.Person HTTP/1.1
Host: localhost:57772
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:57772/formsui/index.html
Cookie: CSPSESSIONID-SP-57772-UP-forms-=001000000000yxiocLLb8bbc9SVXQJC5WMU831n2sENf4OGeGa; CSPWSERVERID=144zwBTP
Dnt: 1
Connection: keep-alive
Cache-Control: max-age=0
また、パフォーマンスを記録するオプションもあります。 結果はファイルに出力されるか、[イベントログの表示]タブで閲覧することができます。
### CSP Gatewayのトレース
そして最後に、CSP Gatewayの[HTTPトレースの表示]タブでリクエストとレスポンスをトレースすることができます。 トレースを有効にすると、リクエストのキャプチャがすぐに開始します。 デバッグを終えたら、忘れずに無効にしてください。 デバッグセッションは次のように行われます。

注意: トレースのやり方がわかっている場合は、エラーを簡単に理解し、問題を再現することができます。 ロギングは統計収集、パフォーマンスプロファイリングなどに使用します。
また、ほとんどのWebサーバーにも、ロギングとパフォーマンス追跡ツールが備わっています。
### ISCLOG
CSP Gatewayは、ネットワークの問題の特定とパフォーマンスの追跡には役立ちますが、Caché内で起きていることをログするにほかのツールが必要となります。 そのようなツールの中でも汎用性に優れているのがISCLOGです。 [ドキュメント](http://docs.intersystems.com/latestj/csp/docbook/DocBook.UI.Page.cls?KEY=GCSP_logging)をご覧ください。
これは、現在のリクエスト処理に関する情報を保存できるグローバル変数です。 ロギングを開始するには、以下を実行します。
set ^%ISCLOG = 2
ロギングを終了するには、以下を実行します。
set ^%ISCLOG = 0
リクエストは次のようにロギングされます。
^%ISCLOG=0
^%ISCLOG("Data")=24
^%ISCLOG("Data",1)=$lb(2,"CSPServer","Header from CSP Size:3744 CMD:h IdSource:3","4664","FORMS","2017-06-07 10:49:21.341","%SYS.cspServer2","","")
^%ISCLOG("Data",1,0)="^h30000 "_$c(14,0,0)_"A"
^%ISCLOG("Data",2)=$lb(2,"CSPServer","[UpdateURL] Looking up: //localhost/forms/form/info path found: //localhost/forms/ Appl= "_$c(2,1,3,4)_"@"_$c(3,4,1,2,1,9,1)_"/forms/"_$c(2,1,3,4,1,2,1,2,1,2,4,3,4,1,2,1,9,1,7,1)_":%All"_$c(8,1)_"/forms"_$c(7,1)_"FORMS"_$c(2,1,2,1,3,4,1,2,1,2,1,3,4,1,2,1,4,4,132,3,3,4,2,3,4,2,2,1,4,4,16,14,2,4,3,4,1,3,4,1,2,1,3,4,1,2,1,16,1)_"Form.REST.Main"_$c(2,4,2,4),"4664","FORMS","2017-06-07 10:49:21.342","%CSP.Request.1","124","L3DfNILTaE")
^%ISCLOG("Data",3)=$lb(2,"CSPServer","[UpdateURL] Found cls: Form.REST.Main nocharsetconvert: charset:UTF-8 convert charset:UTF8","4664","FORMS","2017-06-07 10:49:21.342","%CSP.Request.1","124","L3DfNILTaE")
^%ISCLOG("Data",4)=$lb(2,"CSPServer","[HTML] Determined request type","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer2","124","L3DfNILTaE")
^%ISCLOG("Data",4,0)=$lb("usesession",1,"i%Class","Form.REST.Main","i%Service","REST","NOLOCKITEM","","i%GatewayError","")
^%ISCLOG("Data",5)=$lb(2,"CSPSession","[%LoadData] Loading CSP session, nosave=0","4664","FORMS","2017-06-07 10:49:21.342","%CSP.Session.1","","L3DfNILTaE")
^%ISCLOG("Data",5,0)=$lb(900,,0,5567742244,$c(149)_"Ù"_$c(3)_"ó»à"_$c(127)_",½"_$c(149,10)_"\"_$c(18)_"v"_$c(128,135)_"3Vô"_$c(11)_"*"_$c(154)_"PÏG¥"_$c(140,157,145,10,131)_"*",2,"FORMS","001000000000L3DfNILTaE1cDBJNjyQdyLwKq4wCXP82ld8gic",,0,"ru","L3DfNILTaE",2,1,"/forms/",$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737),"","","","2017-06-07 10:48:51","2017-06-07 10:49:04","Basic ZGV2OjEyMw==","Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0","","",0,"/forms/","","","",4,"","","","","http://localhost:57772/formsui/index.html")
^%ISCLOG("Data",6)=$lb(2,"CSPServer","[CSPDispatch]Requested GET /forms/form/info","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",7)=$lb(2,"CSPServer","[CSPDispatch] ** Start processing request newSes=0","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",7,0)="/forms/form/info"
^%ISCLOG("Data",8)=$lb(2,"CSPServer","[CSPDispatch] Service type is REST has-soapaction=0 nosave=0","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",9)=$lb(2,"CSPServer","[CSPDispatch]About to run page: Form.REST.Main","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",9,0)=$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737)
^%ISCLOG("Data",10)=$lb(2,"CSPServer","[callPage] url=/forms/form/info ; Appl: /forms/ newsession=0","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",11)=$lb(2,"CSPServer","[callPage]Imported security context ; User: UnknownUser ; Roles: %All,%Developer","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",12)=$lb(2,"CSPServer","[OutputCSPGatewayData]: chd=1;","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Response.1","","L3DfNILTaE")
^%ISCLOG("Data",13)=$lb(2,"CSPResponse","[WriteHTTPHeaderCookies] Session cookie: CSPSESSIONID-SP-57772-UP-forms-=001000000000L3DfNILTaE1cDBJNjyQdyLwKq4wCXP82ld8gic; path=/forms/; httpOnly;","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Response.1","124","L3DfNILTaE")
^%ISCLOG("Data",14)=$lb(2,"CSPServer","[callPage] Return Status","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",14,0)=1
^%ISCLOG("Data",15)=$lb(2,"CSPServer","[OutputCSPGatewayData]: chd=1;","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Response.1","","L3DfNILTaE")
^%ISCLOG("Data",16)=$lb(2,"CSPServer","[Cleanup]Page EndSession=0; needToGetALicense=-1; nosave=0; loginredirect=0; sessionContext=1","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",17)=$lb(2,"CSPSession","[Cleanup] EndSession=0 nosave=0","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","124","L3DfNILTaE")
^%ISCLOG("Data",18)=$lb(2,"CSPSession","[%SaveData] Saved: ","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Session.1","","L3DfNILTaE")
^%ISCLOG("Data",18,0)=$lb(900,,0,5567742261,$c(149)_"Ù"_$c(3)_"ó»à"_$c(127)_",½"_$c(149,10)_"\"_$c(18)_"v"_$c(128,135)_"3Vô"_$c(11)_"*"_$c(154)_"PÏG¥"_$c(140,157,145,10,131)_"*",2,"FORMS","001000000000L3DfNILTaE1cDBJNjyQdyLwKq4wCXP82ld8gic",,0,"ru","L3DfNILTaE",2,1,"/forms/",$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737),"","","","2017-06-07 10:48:51","2017-06-07 10:49:21","Basic ZGV2OjEyMw==","Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0","","",0,"/forms/","","","",5,"","","","","http://localhost:57772/formsui/index.html")
^%ISCLOG("Data",19)=$lb(2,"CSPServer","[Cleanup] Restoring roles before running destructor","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","","L3DfNILTaE")
^%ISCLOG("Data",19,0)=$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737)
^%ISCLOG("Data",20)=$lb(2,"CSPServer","[Cleanup] End","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","","L3DfNILTaE")
^%ISCLOG("Data",20,0)=""
^%ISCLOG("Data",21)=$lb(2,"GatewayRequest","[CSPGWClientRequest] GWID: ed-pc:57772; Request: sys_get_system_metricsTimeout: 5","11112","%SYS","2017-06-07 10:49:23.141","%SYS.cspServer3","","")
^%ISCLOG("Data",22)=$lb(2,"GatewayRequest","[CSPGWClientRequest] GWID: 127.0.0.1:57772; Request: sys_get_system_metricsTimeout: 5","11112","%SYS","2017-06-07 10:49:23.141","%SYS.cspServer3","","")
^%ISCLOG("Data",23)=$lb(2,"GatewayRequest","[SendSimpleCmd:Server:Failed] WebServer: 127.0.0.1:57772; Gateway Server Request Failed","11112","%SYS","2017-06-07 10:49:23.141","%CSP.Mgr.GatewayMgrImpl.1","","")
^%ISCLOG("Data",23,0)=0
^%ISCLOG("Data",24)=$lb(2,"GatewayRequest","[GetMetrics]","11112","%SYS","2017-06-07 10:49:23.141","%CSP.Mgr.GatewayMgrImpl.1","","")
^%ISCLOG("Data",24,0)=""
また、以下のような簡単なスクリプトを使用して、グローバルをファイルに出力することができます。
set p="c:\temp\isclog.txt"
open p:"NW"
use p zw ^%ISCLOG
close p
### カスタムロギング
デフォルトのロギングツールも非常に優れていますが、いくつかの問題があります。
* 内容が一般的であり、アプリケーションが認識されない
* より詳細なオプションを使うと、パフォーマンスに影響する
* 構造化されていないため、情報の抽出が困難な場合がある
そのため、より具体的なケースを網羅するために、独自のカスタムロギングシステムを作成することができます。 以下は、%requestオブジェクトの一部を記録する永続クラスのサンプルです。
/// Incoming request
Class Log.Request Extends %Persistent
{
/// A string indicating HTTP method used for this request.
Property method As %String;
/// A string containing the URL up to and including the page name
/// and extension, but not including the query string.
Property url As %String(MAXLEN = "");
/// A string indicating the type of browser from which the request
/// originated, as determined from the HTTP_USER_AGENT header.
Property userAgent As %String(MAXLEN = "");
/// A string indicating the MIME Content-Type of the request.
Property contentType As %String(MAXLEN = "");
/// Character set this request was send in, if not specified in the HTTP headers
/// it defaults to the character set of the page it is being submitted to.
Property charSet As %String(MAXLEN = "");
/// A %CSP.Stream containing the content submitted
/// with this request.
Property content As %Stream.GlobalBinary;
/// True if the communication between the browser and the web server was using
/// the secure https protocol. False for a normal http connection.
Property secure As %Boolean;
Property cgiEnvs As array Of %String(MAXLEN = "", SQLPROJECTION = "table/column");
Property data As array Of %String(MAXLEN = "", SQLPROJECTION = "table/column");
ClassMethod add() As %Status
{
set request = ..%New()
quit request.%Save()
}
Method %OnNew() As %Status [ Private, ServerOnly = 1 ]
{
#dim %request As %CSP.Request
#dim sc As %Status = $$$OK
quit:'$isObject($g(%request)) $$$ERROR($$$GeneralError, "Not a web context")
set ..charSet = %request.CharSet
if $isObject(%request.Content) {
do ..content.CopyFromAndSave(%request.Content)
} else {
set ..content = ""
}
set ..contentType = %request.ContentType
set ..method = %request.Method
set ..secure = %request.Secure
set ..url = %request.URL
set ..userAgent = %request.UserAgent
set cgi = ""
for {
set cgi=$order(%request.CgiEnvs(cgi))
quit:cgi=""
do ..cgiEnvs.SetAt(%request.CgiEnvs(cgi), cgi)
}
// Only gets first data if more than one data with the same name is present
set data = ""
for {
set data=$order(%request.Data(data))
quit:data=""
do ..data.SetAt(%request.Get(data), data)
}
quit sc
}
}
Log.Request テーブルに新しいレコードを追加するには、コードに次の呼び出しを追加します。
do ##class(Log.Request).add()
これは非常に基本的なサンプルであり、必要に応じてコメント、変数、またはほかの様々なものを記録するように拡張することができ、そうすることが推奨されます。 このアプローチの主なメリットは、記録されたデータに対してSQLクエリを実行できることにあります。 独自のロギングシステムの構築に関する詳細は、[こちらの記事](https://community.intersystems.com/post/logging-using-macros-intersystems-cach%C3%A9)をご覧ください。
### セッションイベント
イベントクラスは、[%CSP.Session](http://docs.intersystems.com/latestj/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25CSP.Session)オブジェクトの寿命中に呼び出されるインターフェースを定義するクラスです。 これを使用するには、 [%CSP.SessionEvents](http://docs.intersystems.com/latestj/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25CSP.SessionEvents)をサブクラス化して、実行するメソッドコードを実装する必要があります。 次に、CSPアプリケーション構成内で、イベントクラスを作成したクラスに設定します。
次のコールバックを利用できます。
OnApplicationChange
OnEndRequest
OnEndSession
OnLogin
OnLogout
OnStartRequest
OnStartSession
* OnTimeout
たとえば上記のカスタムロギングは、これらのメソッドから呼び出すことができます。
### デバイスへの出力
最も単純なオプションの1つは、すべてのオブジェクトをレスポンスとして出力するCSPユーティリティメソッドです。 これをコードの任意の場所に追加するだけで出力できます。
set %response.ContentType = "html"
do ##class(%CSP.Utils).DisplayAllObjects()
return $$$OK
### まとめ
Webアプリケーションのデバッグにはさまざまなツールを使用することができます。 手元のタスクに最適なものを選択するようにしてください。
皆さんは、CachéからWebアプリケーションをデバッグする際には、どのようなヒントやトリックを使用していますか?
記事
Toshihiko Minamoto · 2020年12月16日
今回は、InterSystems IRIS に特有のことではなく、職場で Windows 10 Pro または Enterprise を搭載した PC またはノートパソコンがサーバーとして使用されている環境で Docker を使用する場合に重要と思われる点について触れたいと思います。
ご存知かと思いますが、コンテナテクノロジーは基本的に Linux の世界で生まれ、最近では Linux のホストで使用されており、その最大のポテンシャルを伺わせています。 Windows を普段から使用するユーザーの間では、Microsoft と Docker 両社によるここ数年の重大な試みにより、Windows のシステムで Linux イメージを基にコンテナを実行することがとても簡単になったと理解されています。しかし、生産システムでの使用がサポートされておらず、それが大きな問題となっています。特に、Windows と Linux のファイルシステムに大きな違いがあるため、安心してホストシステム内でコンテナの外に持続データを保管するということができないのです。 ついには、コンテナを実行するために、_Docker for Windows _自体で Linux の小さな仮想マシン (_MobiLinux_) が使用されるようになり、Windows ユーザーに対しては透過的に実行されます。また、先ほど述べたように、データベースの存続がコンテナよりも短くて構わないのであれば、パーフェクトに動作します。
では、何が言いたいかというと、問題を回避して処理を単純化するには、完全な Linux システムが必要になるが、Windows ベースのサーバーを使用していると、仮想マシンを使用する以外に方法がない場合が多い、ということです。 少なくとも、Windows の WSL2 がリリースされるまでの話ですが、それはまた別の機会に触れたいと思います。もちろん、十分堅牢に動作するまでは時間がかかるでしょう。
この記事では、必要に応じて、Windows サーバーの Ubuntu システムで Docker コンテナを使用できる環境をインストールする方法について分かりやすく説明します。 それでは、始めましょう。
1. Hyper-V を有効にする
まだ有効にしていない方は、`Windows Features`を追加し、Hyper-V を有効にしてください。 そして再起動します (現在の私のロケールにより、テキストはスペイン語になっています。 ドン・キホーテの言語をご存知ない方は手順を参照しながら「解明」できることを願っています 😉 )
.png)
2. Hyper-V 上で Ubuntu の仮想マシンを作成する
これ以上簡単に仮想マシン (VM) を作成する方法はないと思います。 `Hyper-V Manager` のウィンドウを開き、 Quick Create... オプション (画面一番上近く) を選択します。そして、画面に表示されている Ubuntu バージョンのいずれかを使って仮想マシンを作成します (どの Linux の場合でも、_iso _file をダウンロードすれば、別のディストリビューションの VM を作成できます)。 私は、使用可能な Ubuntu の最新リリース 19.10 を選択しました。 ここで表示されるものはすべて 18.04 に対応しています。 イメージのダウンロードにかかる時間によりますが、15~20 分ほどで新しい VM が作成され、使用を開始できます。
重要: Default Switch オプションはそのままにしておいてください。. ホストと仮想マシンの両方からインターネットにアクセスできることが保証されます。

3. ローカルサブネットを作成する
仮想マシンを使用するときによく目にする問題の 1 つに、ネットワーク設定に関連して起こるものがあります。動作が不安定であったり、WiFi に接続しているときは動作してもケーブルだと駄目な場合があったり、もしくはその逆の場合もあります。また、Windows ホストで VPN を確立すると、VM のインターネットアクセスを失ったり、VM (Linux) とホスト (Windows) 間のコミュニケーションが遮断されたりすることもあり、はっきり言って大変なんです! ノートパソコンで開発作業を行ったり、簡単なデモやプレゼン資料を作成したりするとき、つまりインターネットにアクセスできることよりも、自分のホストと VM が確実に通信できることの方が重要であるときに、自分の作業環境を信頼できなくなってしまいます。
アドホックのローカルサブネットを Windows ホストと仮想マシン間で共有すれば、それを解決できます。 相互に通信し合えるようにするには、そのサブネットを使用します。 ホストと VM に特定の IP を割り当てれば準備完了です。
その方法はいたって簡単。 まずは、`Hyper-V Manager` にある Virtual Switch Manager... に移動します。
.png)
そこで、_New Virtual Switch __ _オプションを選択します (VM の新しいネットワークカードとして機能します)。
.png)
_内部ネットワーク _ として定義し、好きな名前を選び、他のオプションはデフォルトのままにしておきます。
.png)
_`Windows Control Panel --> Network and Sharing Center`_ と移動すれば、作成したばかりのスイッチが既に表示されています。
.png)
4. ホストと仮想マシン間で共有されるローカルサブネットを設定する
この時点で、新しいローカルネットワークの設定を完了します。 そのためには、_Mi Nuevo Conmutador LOCAL_ にカーソルを合わせて、Properties をクリックし、それから IPv4 protocol へと移動して固定 IP アドレスを割り当てます。
.png)
重要: ここで割り当てる IP は、このローカルサブネットのホスト (Windows) IP となります。
5. 新しいローカルネットワークを設定し、仮想マシンにリンクする
ここで、 `Hyper-V Manager` に戻ります。 VM が実行中であれば、停止してください。 停止したら、その設定に移動し、新しい内部仮想スイッチを追加します。
.png)
_(注意: 画像には他のスイッチ Hyper-V Conmutador INTERNO が表示されています。 これは私のもう 1 つのサブネット用のもので、 この設定には必要ありません) _
「Add」をクリックしたら、 前回作成したスイッチを選択します。
.png)
これが済んだら、「Apply」、「Accept」と順にクリックして準備完了です! 仮想マシンを起動し、もう一度ログインしてから内部接続の設定を完了します。 そうするには、VM が起動した後に画面右上のネットワークアイコンをクリックします。すると 2 つのネットワーク _eth0_ y _eth1_ が表示されます。 _eth1_ は、この時点では切断された状態で表示されます。
.png)
Ethernet (eht1) の設定に移動し、このローカルサブネットに固定 IP を割り当てます (例: IP _155.100.101.1_、サブネットマスク _ 255.255.255.0_)。
.png)
これで完了です。 これで仮想マシンが出来上がり、ホストと同じサブネットを共有する IP 155.100.101.1 として識別されます。
7. 仮想マシンから Windows 10 へのアクセスを許可する
大抵の場合、Windows 10 のデフォルト設定では、他のサーバーからのアクセスが許可されていません。Windows システムについては、たった今作成した VM がまさにそのサーバー (外部の潜在的に危険なサーバー) に該当します。このため、こういった仮想マシンからホストに接続するには、ファイアウォールにルールを追加する必要があります。 どうするのかって? いたって簡単です。`Windows Control Panel` の中から `Windows Defender Firewall` を見つけて、Advance Configuration に移動し、新規の *Entry Rule* を作成します。
.png)
1 つのポートまたは 1 つ以上の範囲を設定できます... (すべてのポートに対しルールを設定することもできます)...
.png)
ここで役に立つのが _Allow Connection_ です。
.png)
_ネットワークのすべてのタイプ_オプションを選択し...
.png)
ルールに名前を付けます。
.png)
ここで**重要**なことがあります。上記の手順が終わったら、新しく作成したルールのプロパティをもう一度開き、ルールをローカルサブネット内の接続に対してのみ適用できるよう*適用範囲を制限*しておきます。
.png)
8. 準備。 Docker と他に使用するアプリケーションを新しい Ubuntu 仮想マシンにインストールする
インストールプロセスを完了し、新しい VM を最新の状態に整え、インターネットアクセスなども準備ができたら、 好きなアプリをインストールできます。 少なくとも、元々の目的である Docker、また会社のネットワークへの接続に必要であれば VPN クライアント、さらには VS Code、Eclipse+Atelier などのアプリもインストールできます。
具体的に、VM 内で Docker をインストールする場合は、こちらの手順に従ってください: 。
Docker ランタイムの動作確認、テストイメージのダウンロードなどが済んだら、準備完了です。
_**¡You're all set!**_ が表示されたら、Ubuntu 仮想マシンでコンテナを (ハードウェアのキャパシティ内で) 制限なしに動作させることができます。同仮想マシンには、Windows 10 ホストやブラウザー、アプリなどから接続したり、またはその逆、Ubuntu VM から Windows 10 ホストに接続したりもできます。 このすべてに共有ローカルサブネット内で設定した IP アドレスが使用されるため、VPN を確立するしないに関わらず、またインターネットへのアクセスに Wi-Fi アダプターを使用するのか、イーサネットケーブルを使用するのかに関わらず、接続に失敗することはありません。
それから、アドバイスがもう 1 つだけあります。 Windows 10 と仮想マシンの間でファイルを交換する場合は、[WinSCP](https://winscp.net/eng/download.php) を使用すると簡単で便利です。 無料ですし、とても良く機能します。
もちろん、使える設定は他にもありますが、私は一番確実な方法としてこれを使っています。 お役に立つことを期待しています。 皆さんの悩みの種を取り除くことができれば、この記事を書いた価値があったと言えるでしょう。
¡ハッピーコーディング!
Windows10 ホストで実行される Hyper-V Ubuntu 仮想マシンで Docker を使用できるように環境を設定する
今回は、InterSystems IRIS に特有のことではなく、職場で Windows 10 Pro または Enterprise を搭載した PC またはノートパソコンがサーバーとして使用されている環境で Docker を使用する場合に重要と思われる点について触れたいと思います。
ご存知かと思いますが、コンテナテクノロジーは基本的に Linux の世界で生まれ、最近では Linux のホストで使用されており、その最大のポテンシャルを伺わせています。 Windows を普段から使用するユーザーの間では、Microsoft と Docker 両社によるここ数年の重大な試みにより、Windows のシステムで Linux イメージを基にコンテナを実行することがとても簡単になったと理解されています。しかし、生産システムでの使用がサポートされておらず、それが大きな問題となっています。特に、Windows と Linux のファイルシステムに大きな違いがあるため、安心してホストシステム内でコンテナの外に持続データを保管するということができないのです。 ついには、コンテナを実行するために、_Docker for Windows _自体で Linux の小さな仮想マシン (_MobiLinux_) が使用されるようになり、Windows ユーザーに対しては透過的に実行されます。また、先ほど述べたように、データベースの存続がコンテナよりも短くて構わないのであれば、パーフェクトに動作します。
では、何が言いたいかというと、問題を回避して処理を単純化するには、完全な Linux システムが必要になるが、Windows ベースのサーバーを使用していると、仮想マシンを使用する以外に方法がない場合が多い、ということです。 少なくとも、Windows の WSL2 がリリースされるまでの話ですが、それはまた別の機会に触れたいと思います。もちろん、十分堅牢に動作するまでは時間がかかるでしょう。
この記事では、必要に応じて、Windows サーバーの Ubuntu システムで Docker コンテナを使用できる環境をインストールする方法について分かりやすく説明します。 それでは、始めましょう。
1. Hyper-V を有効にする
まだ有効にしていない方は、`Windows Features`を追加し、Hyper-V を有効にしてください。 そして再起動します (現在の私のロケールにより、テキストはスペイン語になっています。 ドン・キホーテの言語をご存知ない方は手順を参照しながら「解明」できることを願っています 😉 )
.png)
2. Hyper-V 上で Ubuntu の仮想マシンを作成する
これ以上簡単に仮想マシン (VM) を作成する方法はないと思います。 `Hyper-V Manager` のウィンドウを開き、 Quick Create... オプション (画面一番上近く) を選択します。そして、画面に表示されている Ubuntu バージョンのいずれかを使って仮想マシンを作成します (どの Linux の場合でも、_iso _file をダウンロードすれば、別のディストリビューションの VM を作成できます)。 私は、使用可能な Ubuntu の最新リリース 19.10 を選択しました。 ここで表示されるものはすべて 18.04 に対応しています。 イメージのダウンロードにかかる時間によりますが、15~20 分ほどで新しい VM が作成され、使用を開始できます。
重要: Default Switch オプションはそのままにしておいてください。. ホストと仮想マシンの両方からインターネットにアクセスできることが保証されます。

3. ローカルサブネットを作成する
仮想マシンを使用するときによく目にする問題の 1 つに、ネットワーク設定に関連して起こるものがあります。動作が不安定であったり、WiFi に接続しているときは動作してもケーブルだと駄目な場合があったり、もしくはその逆の場合もあります。また、Windows ホストで VPN を確立すると、VM のインターネットアクセスを失ったり、VM (Linux) とホスト (Windows) 間のコミュニケーションが遮断されたりすることもあり、はっきり言って大変なんです! ノートパソコンで開発作業を行ったり、簡単なデモやプレゼン資料を作成したりするとき、つまりインターネットにアクセスできることよりも、自分のホストと VM が確実に通信できることの方が重要であるときに、自分の作業環境を信頼できなくなってしまいます。
アドホックのローカルサブネットを Windows ホストと仮想マシン間で共有すれば、それを解決できます。 相互に通信し合えるようにするには、そのサブネットを使用します。 ホストと VM に特定の IP を割り当てれば準備完了です。
その方法はいたって簡単。 まずは、`Hyper-V Manager` にある Virtual Switch Manager... に移動します。
.png)
そこで、_New Virtual Switch __ _オプションを選択します (VM の新しいネットワークカードとして機能します)。
.png)
_内部ネットワーク _ として定義し、好きな名前を選び、他のオプションはデフォルトのままにしておきます。
.png)
_`Windows Control Panel --> Network and Sharing Center`_ と移動すれば、作成したばかりのスイッチが既に表示されています。
.png)
4. ホストと仮想マシン間で共有されるローカルサブネットを設定する
この時点で、新しいローカルネットワークの設定を完了します。 そのためには、_Mi Nuevo Conmutador LOCAL_ にカーソルを合わせて、Properties をクリックし、それから IPv4 protocol へと移動して固定 IP アドレスを割り当てます。
.png)
重要: ここで割り当てる IP は、このローカルサブネットのホスト (Windows) IP となります。
5. 新しいローカルネットワークを設定し、仮想マシンにリンクする
ここで、 `Hyper-V Manager` に戻ります。 VM が実行中であれば、停止してください。 停止したら、その設定に移動し、新しい内部仮想スイッチを追加します。
.png)
_(注意: 画像には他のスイッチ Hyper-V Conmutador INTERNO が表示されています。 これは私のもう 1 つのサブネット用のもので、 この設定には必要ありません) _
「Add」をクリックしたら、 前回作成したスイッチを選択します。
.png)
これが済んだら、「Apply」、「Accept」と順にクリックして準備完了です! 仮想マシンを起動し、もう一度ログインしてから内部接続の設定を完了します。 そうするには、VM が起動した後に画面右上のネットワークアイコンをクリックします。すると 2 つのネットワーク _eth0_ y _eth1_ が表示されます。 _eth1_ は、この時点では切断された状態で表示されます。
.png)
Ethernet (eht1) の設定に移動し、このローカルサブネットに固定 IP を割り当てます (例: IP _155.100.101.1_、サブネットマスク _ 255.255.255.0_)。
.png)
これで完了です。 これで仮想マシンが出来上がり、ホストと同じサブネットを共有する IP 155.100.101.1 として識別されます。
7. 仮想マシンから Windows 10 へのアクセスを許可する
大抵の場合、Windows 10 のデフォルト設定では、他のサーバーからのアクセスが許可されていません。Windows システムについては、たった今作成した VM がまさにそのサーバー (外部の潜在的に危険なサーバー) に該当します。このため、こういった仮想マシンからホストに接続するには、ファイアウォールにルールを追加する必要があります。 どうするのかって? いたって簡単です。`Windows Control Panel` の中から `Windows Defender Firewall` を見つけて、Advance Configuration に移動し、新規の *Entry Rule* を作成します。
.png)
1 つのポートまたは 1 つ以上の範囲を設定できます... (すべてのポートに対しルールを設定することもできます)...
.png)
ここで役に立つのが _Allow Connection_ です。
.png)
_ネットワークのすべてのタイプ_オプションを選択し...
.png)
ルールに名前を付けます。
.png)
ここで**重要**なことがあります。上記の手順が終わったら、新しく作成したルールのプロパティをもう一度開き、ルールをローカルサブネット内の接続に対してのみ適用できるよう*適用範囲を制限*しておきます。
.png)
8. 準備。 Docker と他に使用するアプリケーションを新しい Ubuntu 仮想マシンにインストールする
インストールプロセスを完了し、新しい VM を最新の状態に整え、インターネットアクセスなども準備ができたら、 好きなアプリをインストールできます。 少なくとも、元々の目的である Docker、また会社のネットワークへの接続に必要であれば VPN クライアント、さらには VS Code、Eclipse+Atelier などのアプリもインストールできます。
具体的に、VM 内で Docker をインストールする場合は、こちらの手順に従ってください: 。
Docker ランタイムの動作確認、テストイメージのダウンロードなどが済んだら、準備完了です。
_**¡You're all set!**_ が表示されたら、Ubuntu 仮想マシンでコンテナを (ハードウェアのキャパシティ内で) 制限なしに動作させることができます。同仮想マシンには、Windows 10 ホストやブラウザー、アプリなどから接続したり、またはその逆、Ubuntu VM から Windows 10 ホストに接続したりもできます。 このすべてに共有ローカルサブネット内で設定した IP アドレスが使用されるため、VPN を確立するしないに関わらず、またインターネットへのアクセスに Wi-Fi アダプターを使用するのか、イーサネットケーブルを使用するのかに関わらず、接続に失敗することはありません。
それから、アドバイスがもう 1 つだけあります。 Windows 10 と仮想マシンの間でファイルを交換する場合は、[WinSCP](https://winscp.net/eng/download.php) を使用すると簡単で便利です。 無料ですし、とても良く機能します。
もちろん、使える設定は他にもありますが、私は一番確実な方法としてこれを使っています。 お役に立つことを期待しています。 皆さんの悩みの種を取り除くことができれば、この記事を書いた価値があったと言えるでしょう。
¡ハッピーコーディング!
記事
Toshihiko Minamoto · 2021年1月21日
InterSystems Caché のグローバルは、デベロッパーにとって非常に便利な機能を提供します。 しかし、グローバルが高速な上に効率が良いのはなぜでしょう?
### 理論
基本的に、Caché データベースとは、データベースと同じ名前を持ち、CACHE.DAT ファイルを含んだカタログのことです。 Unix システムでは、このデータベースを普通のディスクパーティションにすることもできます。
Caché のデータはすべてブロックとして保管され、バランスド B\* ツリーとして整理されます。 基本的にすべてのグローバルがツリーに保管されると考えると、グローバルのサブスクリプトはツリーの枝を意味する一方で、グローバルのサブスクリプトの値はツリーの葉として保管されると言えます。 バランスド B\* ツリーと通常の B ツリーの違いは、前者の枝には [$Order](http://docs.intersystems.com/latestj/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_forder) と [$Query](http://docs.intersystems.com/latestj/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fquery) の両関数を使ってサブスクリプト (この記事ではグローバル) のイテレーションをスピーディに実行するのに役立つ適切なリンクがあり、ツリーの幹に戻る必要がないという点です。
デフォルトで、データベースファイルの各ブロックのサイズは 8,192 バイトに固定されています。 既存のデータベースのブロックサイズは変更できません。 新しいデータベースを作成する場合は、保管するデータ型に合わせて、16kB、32 kB、または 64 kB をブロックサイズに選択できます。 但し、すべてのデータはブロック毎に読み取られることを覚えておきましょう。つまり、1 バイトの値を 1 つだけリクエストする場合でも、システムはリクエストされたデータブロックの前にいくつかのブロックを読み取ります。 また、Caché はデータを再利用するためにデータベースブロックをメモリに格納するグローバルバッファを使うことと、ブロックサイズと同じサイズとグローバルバッファを使用することを覚えておきましょう。 対応するブロックサイズのグローバルバッファがシステムにない場合は、そのブロックサイズの既存のデータベースをマウントしたり、新しいデータベースを作成したりはできません。 実際使用するブロックサイズでメモリサイズを定義することをおすすめします。 データベースブロックよりも大きいバッファブロックを使用することは可能ですが、その場合、各バッファブロックには 1 つのデータベースブロック、もしくはさらに小さいブロックしか保管されません。

この画像では、8 kB のグローバルバッファのメモリがそれぞれ 8 kB のブロックで構成されるデータベースに割り当てられています。 このデータベースのブロックのうち、空でないものは、それぞれのマップが 62,464 個のブロック (それぞれ 8 kB のブロック) を定義するかたちでマップに定義されています。
### ブロックタイプ
システムは複数のブロックタイプに対応しています。 各レベルにおいて、ブロックの適切なリンクは同じタイプのブロックまたはデータの終わりを意味する Null ブロックにポイントしている必要があります。
* **タイプ 9**: グローバルカタログのブロック。 通常このブロックでは、既存のすべてのグローバルがそれぞれのパラメーターと併せて説明されます。このパラメーターには、グローバルのサブスクリプトのコレーション (照合順序) が含まれています。大切なパラメーターの 1 つで、グローバル作成後には変更できません。
* **タイプ 66**: 高レベルポインタのブロック。 このブロックの上に置けるのは、グローバルカタログのブロックのみです。
* **タイプ 6**: 低レベルポインタのブロック。 このブロックの上に置けるのは高レベルポインタのブロックのみで、これより下に置けるのはデータブロックのみです。
* **タイプ 70**: 高レベルポインタと低レベルポイントの両方で構成されるブロック。 このブロックは、対応するグローバルに保管される値の数が少ないため、複数のレベルのブロックを設ける必要がない場合に使用されます。 このブロックは通常、グローバルカタログのブロックと同様、データブロックにポイントします。
* **タイプ 2**: 比較的大きなグローバルを保管するポインターのブロック。 値を複数のデータブロックに均等に割り当てるには、ポインタのブロックにレベルを追加すると便利です。 このブロックは通常、ポインタのブロックの間に置かれます。
* **タイプ 8**: データブロック。 通常このブロックには、1 つのノードの値ではなく、複数のグローバルノードの値が保管されます。
* **タイプ 24**: ラージストリングのブロック。 1 つのグローバルの値がブロックサイズよりも大きい場合、その値はラージストリングの特殊ブロックに記録される一方で、データブロックのノードにはラージストリングのブロック一覧へのリンクが合計サイズと共に保管されます。
* **タイプ 16**: マップブロック。 このブロックは、未割り当てのブロックに関する情報を保管するようにデザインされています。
典型的な Caché データベースの最初のブロックには、データベースファイルそのものに関するサービス情報、2 つ目のブロックには複数のブロックからなるマップが含まれます。 最初のカタログブロックは 3 番目 (ブロック #3) に位置付けされ、1 つのデータベースに複数のカタログブロックを保管できます。 この後は、ポインタブロック (枝)、データブロック (葉)、そしてラージストリングのブロックが続きます。 先ほども触れましたが、グローバルカタログのブロックには、データベースやグローバル設定に存在するすべてのグローバルに関する情報が保管されます (そうでない場合は、そのようなグローバルに置かれます)。 この場合、そのようなグローバルを説明するノードには、低レベルの Null ポインタが割り当てられます。 既存のグローバルの一覧は、管理ポータルのグローバルカタログから表示できます。 また、このポータルでは、削除したグローバルをカタログに保存 (照合順序を保存するなど) したり、デフォルトの、またはカスタマイズしたコレ―ションを設定した新しいグローバルを作成したりできます。


一般的に、ブロックのツリーは下の画像のように描くことができます。 赤く表示されているのは、ブロックへのリンクです。

### データベースの整合性
Caché の現在のバージョンでは、データベースの最も重大な問題を解決しましたので、データベースのパフォーマンスが低下するリスクは極めて低くなっています。 それでも、定期的に ^Integrity ツールを使って自動整合性チェックを実行することをおすすめします。同ツールは、データベースページもしくはタスクマネージャーのマネジメントポータルを使って、%SYS ネームスペースのターミナルで起動できます。 自動整合性チェックは、デフォルトでセットアップされており、事前に定義もされているので、有効化するだけで実行できます。


整合性チェックでは、OSI モデルの下位層でのリンク検証、ブロックタイプの確認、適切なリンクの分析、グローバルノードと適用される照合順序のマッチングが行われます。 整合性チェックの最中にエラーが出る場合は、%SYS ネームスペースから ^REPAIR ツールを実行できます。 このツールを使えば、どのブロックでも表示できる上に、必要であれば、データベースを修正するなどの変更を施すことができます。
### 実践
しかし、ここまで紹介した内容は理論にすぎません。 グローバルとそのブロックの実態を見極めるのは未だに容易なことではありません。 現時点で、ブロックを表示するには、上述した ^REPAIR ツールを使う方法しかありません。 下に示すのがこのプログラムの典型的な出力です。

1 年前、私は新しいツールを開発するプロジェクトに着手しました。それは、データベースを破損させるリスクなしにブロックのツリーをイテレーションし、それらのブロックをウェブ UI に視覚化し、その視覚化されたイメージを SVG または PNG に保存するオプションを提供するというものです。 このプロジェクトは名付けて CacheBlocksExplorer。ソースコードは、[Github](https://github.com/daimor/CacheBlocksExplorer) からダウンロードしていただけます。

実装した機能には以下の通りです。
* システム内の構成済みのデータベースや単純にマウントされたデータベースを表示する。
* 情報をブロック別、ブロックタイプ別、右ポインタ別、リンク付ノードの一覧別に表示する。
* 前述のレベルよりも低いレベルのブロックにポイントしているノードに関する詳細を表示する。
* ブロックへのリンクを削除することにより、そのブロックを非表示にする (そのブロックに保管されているデータを損傷することはありません)。
To-Do リスト:
適切なリンクを表示する: 現在のバージョンでは、適切なリンクはブロックに関する情報の中に表示されていますが、矢印として表示する方がベターでしょう。
ラージストリングのブロックを表示する: 現在のバージョンでは表示されていない。
グローバルカタログのブロックを3 番目のブロックだけでなく、すべて表示する。
ツリー全体も表示したいのですが、何十万個もあるブロックとそれぞれのリンクを一緒にすばやくレンダリングできるライブラリは、まだ見つかっていません。現在のライブラリは、ウェブブラウザーにレンダリングしていますが、そのスピードは Caché が構造全体を読み取るスピードよりもかなり遅いです。
次回の記事では、私の自作ツール Cache Block Explorer を実際に使い、その機能をさらに詳しく説明するほか、使用例をいくつか紹介し、グローバルやブロックに関する実行可能なデータを数多く取得する方法を披露したいと思います。
記事
Toshihiko Minamoto · 2022年4月14日
IRIS ベースのアプリケーションを GCP Kubernetes で実行する方法については、すでに「InterSystems IRIS ソリューションを CircleCI を使用して GCP Kubernetes Cluster GKE へデプロイする」で検討しました。 また、IRIS ベースのアプリケーションを AWS Kubernetes で実行する方法については、「Amazon EKS を使用したシンプルな IRIS ベースの Web アプリケーションのデプロイ」で確認しました。 そこで今回は、アプリケーションを Azure Kubernetes Service(AKS)にデプロイする方法を説明することにします。
Azure
この記事では、Azure の無料サブスクリプションを使用します。 価格の詳細については、Azure の価格表ページをご覧ください。
登録が完了すると、Microsoft Azure ポータルが表示されます。
便利なポータルではありますが、この記事では使用しません。 代わりに、Azure コマンドラインインターフェースをインストールしましょう。 執筆時点での最新バージョンは 2.30.0 です。
$ az version
{
"azure-cli": "2.30.0",
"azure-cli-core": "2.30.0",
"azure-cli-telemetry": "1.0.6",
"extensions": {}
}
それでは、Azure にログインしましょう。
$ az login
CircleCI パイプライン
強力な CircleCI の CI/CD を使用して、AKS のセットアップと IRIS アプリケーションのインストールを行います。 つまり、GitHub ベースのプロジェクトを使って、コードとしてのインフラストラクチャと共にいくつかのパイプラインファイルを追加し、GitHub に変更をプッシュし直して、その結果を使いやすい CircleCI UI で確認します。
GitHub アカウントを使えば、簡単に CircleCI との統合を作成できます。 詳細については、「Seamless integration with GitHub」の記事をご覧ください。
では、「Amazon EKS を使用したシンプルな IRIS ベースの Web アプリケーションのデプロイ 」で使用したプロジェクトの更新バージョンを使用しましょう。 つまり、secured-rest-api です。 それを開き、Use this Template をクリックして、新しいリポジトリ内にバージョンを作成します。 この記事では、そこに含まれるコードサンプルを参照します。
ローカルにリポジトリを Clone し、以下に示すファイルを含む .circleci/ ディレクトリを作成します。
$ tree .circleci/
.circleci/
├── config.yml
└── continue.yml
[ダイナミックコンフィグ](https://circleci.com/docs/ja/2.0/configuration-cookbook/?section=examples-and-guides#dynamic-configuration)と[パスのフィルタリング](https://circleci.com/developer/ja/orbs/orb/circleci/path-filtering)を使用して、変更のあるファイルに応じて全体または一部を実行するようにパイプラインを設定します。 この例では、Terraform コードに変更がある場合にのみ Terraform ジョブを実行します。 最初の [config.yml](https://github.com/myardyas/secured-rest-api/blob/master/.circleci/config.yml) ファイルは単純です。 2 つ目の .circleci/continue.yml を呼び出し、Terraform コードが最新である場合に特定のブール値パラメーターを渡します。
$ cat config.yml
version: 2.1
# Enable CircleCI's dynamic configuration feature
setup: true
# Enable path-based pipeline
orbs:
path-filtering: circleci/path-filtering@0.1.0
workflows:
Generate dynamic configuration:
jobs:
- path-filtering/filter:
name: Check updated files
config-path: .circleci/continue.yml
base-revision: master
mapping: |
terraform/.* terraform-job true
2 つ目の [continue.yml](https://github.com/myardyas/secured-rest-api/blob/master/.circleci/continue.yml) ファイルを説明する前に、この secured-rest-app プロジェクトを CircleCI に追加して、.circleci/config.yml の変更を GitHub にプッシュしましょう。
$ git add .circleci/config.yml
$ git commit -m "Add circleci config.yml"
$ git push
そして、[CircleCI Projects ページ](https://app.circleci.com/projects)を開いて、プロジェクトを選択し、Set Up Project をクリックします。

提示される推奨事項に従って、Setup Workflow を有効にします(詳細は、「[CircleCI のダイナミック コンフィグの使用を開始する](https://circleci.com/docs/ja/2.0/dynamic-config/#getting-started-with-dynamic-config-in-circleci)」をご覧ください。

これで、2 つ目の continue.yml ファイルに進む準備が整いました。 このファイルの構造は次のようになっています。
Version: CircleCI パイプラインのバージョンです。
Parameters: Terraform が実行中であるかどうかを決定する変数です。
Orbs: 他の人が作成した再利用可能な構成の一部です。
Executors: 一部のジョブに使用する Asure コマンドラインを含む docker イメージです。
Jobs: 実際のデプロイステップです。
Workflows: Terraform の有無に関係なくパイプラインを実行するためのロジックです。
Jobs セクションには、次のジョブが含まれます。
Build and push Docker image to ACR: このジョブは、az コマンドラインツールがインストールされた docker イメージ内で実行します。 Azure にログインし、イメージをビルドして Azure Container Registry(ACR)にプッシュします。
Terraform: このジョブは、Terraform orb を使用してインフラストラクチャを作成します。 詳細は、以下の Terraform に関するセクションをご覧ください。
Setup packages: このジョブは、IRIS アプリケーションといくつかのサービスアプリケーションをインストールします。 詳細は、以下の「パッケージのセットアップ」セクションをご覧ください。
Terraform
インフラストラクチャの作成には、infrastructure as code アプローチを使用して、Terraform の力を利用します。 Terraform は Azure プラグインを使って AKS と対話します。 ラッパーの役割を果たし、リソース作成を単純化する AKS Terraform モジュール を使用すると便利です。
Terraform を使用して AKS リソースを使用する例は、「Creating a Kubernetes Cluster with AKS and Terraform」にあります。 ここでは、Terraform がデモと単純化の目的でですべてのリソースを管理するように、Owner ロールを割り当てます。 アプリケーションとしての Terraform は Service Principal を使用して Azure に接続します。 厳密には、「Create an Azure service principal with the Azure CLI」に説明されているとおりに、Owner ロールを Service Principal に割り当てます。
ローカルマシンでコマンドをいくつか実行してみましょう。 Azure サブスクリプション ID を環境変数に保存します。
$ export AZ_SUBSCRIPTION_ID=$(az account show --query id --output tsv)
$ az ad sp create-for-rbac -n "Terraform" --role="Owner" --scopes="/subscriptions/${AZ_SUBSCRIPTION_ID}"
…
{
"appId": "<appId>",
"displayName": "<displayName>",
"name": "<name>",
"password": "<password>",
"tenant": "<tenant>"
}
後で、Service Principals をリスト表示して Terraform という表示名を探すと、appId と tenantId を見つけ出すことができます。
$ az ad sp list --display-name "Terraform" | jq '.[] | "AppId: \(.appId), TenantId: \(.appOwnerTenantId)"'
ただし、この方法ではパスワードが表示されません。 パスワードを忘れた場合には、資格情報をリセットするしかありません。
パイプラインでは、AKS の作成には、一般に公開されている Azure Terraform モジュールと Terraform バージョン 1.0.11 を使用します。
取得した、Terraform が Azure への接続に使用する資格情報を使用して、CircleCI project 設定に環境変数を設定します。 また、DOMAIN_NAME 環境変数も設定します。 このチュートリアルは demo-iris.myardyas.club ドメイン名を使用していますが、実際にはユーザーが登録したドメイン名を使用します。 パイプラインでこの変数を使用して、IRIS アプリケーションへの外部アクセスを有効にします。 CircleCI 変数と az create-for-rbac コマンドのマッピングは次のとおりです。
ARM_CLIENT_ID: appId
ARM_CLIENT_SECRET: password
ARM_TENANT_ID: tenant
ARM_SUBSCRIPTION_ID: Value of environment variable AZ_SUBSCRIPTION_ID
DOMAIN_NAME: your domain name
 [Terraform Remote の状態](https://www.terraform.io/docs/language/state/remote.html)を有効にするには、[Azure Storage の Terraform の状態](https://docs.microsoft.com/ja-jp/azure/developer/terraform/store-state-in-azure-storage?tabs=azure-cli)を使用します。 これを実現するために、ローカルマシンで次のコマンドを実行してみましょう。
$ export RESOURCE_GROUP_NAME=tfstate
$ export STORAGE_ACCOUNT_NAME=tfstate14112021 # Must be between 3 and 24 characters in length and use numbers and lower-case letters only
$ export CONTAINER_NAME=tfstate
# Create resource group
$ az group create --name ${RESOURCE_GROUP_NAME} --location eastus
# Create storage account
$ az storage account create --resource-group ${RESOURCE_GROUP_NAME} --name ${STORAGE_ACCOUNT_NAME} --sku Standard_LRS --encryption-services blob
# Enable versioning. Read more at https://docs.microsoft.com/ja-jp/azure/storage/blobs/versioning-overview
$ az storage account blob-service-properties update --account-name ${STORAGE_ACCOUNT_NAME} --enable-versioning true
# Create blob container
$ az storage container create --name ${CONTAINER_NAME} --account-name ${STORAGE_ACCOUNT_NAME}
Terraform ディレクトリに配置した Terraform コードです。 これは、次の 3 つのファイルに分割されています。
provider.tf: Azure プラグインのバージョンと、Terraform 状態を保存するリモートストレージへのパスを設定します。
variables.tf: Terraform モジュールの入力データです。
main.tf: 実際のリソースの作成です。
Azure リソースグループ、パブリック IP、Azure コンテナレジストリなどを作成します。 ネットワーキングと Azure Kubernetes サービスについては、一般に公開されている Terraform モジュールを活用します。
パッケージのセットアップ
新たに作成された AKS クラスタにインストールするものは、[helm](https://github.com/myardyas/secured-rest-api/tree/master/helm) ディレクトリにあります。 説明的な [Helmfile](https://github.com/roboll/helmfile) アプローチを使用することで、アプリケーションとその設定を [helmfile.yaml](https://github.com/myardyas/secured-rest-api/blob/master/helm/helmfile.yaml) ファイルに定義することができます。
セットアップは、単一の helmfile sync コマンドで実行します。 このコマンドによって、IRIS アプリケーションと 2 つの追加アプリケーション、cert-manager、および ingress-nginx がインストールされ、外部からアプリケーションを呼び出せるようになります。 詳細については、GitHub の「releases」セクションをご覧ください。
IRIS アプリケーションは、「CircleCI ビルドで GKE の作成を自動化する」の説明と同じ Helm チャートを使用してインストールします。 単純化するために、deployment を使用します。 つまり、ポッドの再起動時に、データは保持されません。 永続させる場合は、Statefulset またはより優れた Kubernetes IRIS Operator(IKO)を使用することをお勧めします。 IKO デプロイメントの例は、iris-k8s-monitoring リポジトリにあります。
パイプラインの実行
.circleci/、terraform/、および helm/ ディレクトリを追加したら、それらを GitHub にプッシュします。
$ git add .
$ git commit -m "Setup everything"
$ git push
すべて問題がなければ、以下のような CircleCI UI 画面が表示されます。

ドメインレジストラでの A レコードの設定
後もう一つ残っているのは、Terraform が Azure で作成したパブリック IP とDomain Registrar 近ソースのドメイン名の A レコードでバインディングを作成することです。
クラスタに接続しましょう。
$ az aks get-credentials --resource-group demo --name demo
ingress-nginx で公開されるパブリック IP アドレスを定義します。
$ kubectl -n ingress-nginx get service ingress-nginx-controller -ojsonpath='{.spec.loadBalancerIP}'
x.x.x.x
次のようにして、この IP をドメインレジストラ([GoDaddy](https://godaddy.com/)、[Route53](https://en.wikipedia.org/wiki/Amazon_Route_53)、[GoogleDomains](https://domains.google/) など)に設定します。
YOUR_DOMAIN_NAME = x.x.x.x
ここで、DNS の変更が世界中に伝搬されるまでしばらく待ってから、結果を確認します。
$ dig +short YOUR_DOMAIN_NAME
応答は x.x.x.x となります。
テスト
ドメイン名を demo-iris.myardyas.club と仮定して、手動テストを実施します。 [Let's Encrypt のステージング用](https://letsencrypt.org/docs/staging-environment/)証明書を使用しているため、ここでは、証明書の確認は省略しましょう。 本番環境では、[lets-encrypt-production](https://github.com/myardyas/secured-rest-api/blob/master/helm/cert-manager/lets-encrypt-production.yaml) [(こちら)](https://github.com/myardyas/secured-rest-api/blob/master/helm/iris/values.yaml#L30)に置き換える必要があります。 また、メールアドレス([こちら](https://github.com/myardyas/secured-rest-api/blob/master/helm/cert-manager/lets-encrypt-production.yaml#L7))を example@gmail.com ではないものに設定することもお勧めします。
$ curl -sku Bill:ChangeMe https://demo-iris.myardyas.club/crudall/_spec | jq .
…
人(ユーザー)を作成する:
$ curl -ku John:ChangeMe -XPOST -H "Content-Type: application/json" https://demo-iris.myardyas.club/crud/persons/ -d '{"Name":"John Doe"}'
人が作成されたかどうかを確認する:
$ curl -sku Bill:ChangeMe https://demo-iris.myardyas.club/crud/persons/all | jq .
[
{
"Name": "John Doe"
}
...
まとめ
これで以上です! Terraform と CircleCI ワークフローを使用して、Azure クラウドに Kubernetes クラスタを作成する方法を確認しました。 IRIS インストールでは、最も簡単な Helm チャートを使用しました。 本番環境では、このチャートを拡張し、少なくともデプロイを Statefulset に置き換えるか、IKO を使用することをお勧めします。
作成したリソースが不要になったら、忘れずに削除しましょう。 Azure には、無料利用枠が用意されており、AKS は無料ですが、AKS クラスタの実行用に設計されたリソースは有料です。
記事
Toshihiko Minamoto · 2022年11月17日
こんにちは!
Sergei Sakisian と申します。InterSystems で 7 年以上、Angular フロントエンドを作成しています。 Angular は非常に人気のあるフレームワークであるため、開発者、お客様、そしてパートナーの皆さんは、アプリケーションのスタックの 1 つとして Angular を選択することがよくあります。
概念、ハウツー、ベストプラクティス、高度なトピックなど、Angular のさまざまな側面を網羅する記事の連載を始めたいと思います。 この連載は、すでに Angular に精通しており、基本概念の説明がいらない方が対象となります。 連載記事のロードマップを作成しているところであるため、まずは、一番新しい Angular リリースの重要な機能をいくつか紹介することから始めることにします。
## 厳格な型指定のフォーム
これはおそらく、過去 2 年間で最も要望の多かった Angular 機能です。 Angular 14 では、Angular リアクティブフォームを使って、TypeScript のすべての厳格な型チェック機能を使用できるようになりました。
FormControl クラスはジェネリクスになったため、それが保持する値の型を取ることができます。
```typescript
/* Angular 14 より前*/
const untypedControl = new FormControl(true);
untypedControl.setValue(100); // 値を設定、エラーなし
// 現在
const strictlyTypedControl = new FormControl(true);
strictlyTypedControl.setValue(100); // ここで型チェックエラーメッセージが表示されます
// Angular 14
const strictlyTypedControl = new FormControl(true);
strictlyTypedControl.setValue(100); // ここで型チェックエラーメッセージが表示されます
```
ご覧のとおり、最初の最後の例はほぼ同じですが、結果が異なります。 これは、Angular 14 では、新しい FormControl クラスが、開発者が指定した初期値から型を推論しているためです。 したがって、`true` が指定された場合、Angular はこの FormControl の型を `boolean | null` に設定します。 `.reset()` メソッドには、値が指定されていない場合に値を null にする Nullable 値が必要です。
以前の型なしの FormControl クラスは、`UntypedFormControl` に変換されています(`UntypedFormGroup`、`UntypedFormArray`、および `UntypedFormBuilder` についても同様)が、実質的に `FormControl` のエイリアスです。 以前のバージョンの Angular からアップグレードしている場合、`FormControl` クラスのすべてのメンションは、Angular CLI によって `UntypedFormControl` クラスに置き換えられます。
Untyped* のクラスは、以下のような特定の目的に使用されます。
1. アプリを、以前のバージョンから移行される前とまったく同じように動作させる(新しい FormControl は、初期値から型を推論することに注意してください)。
2. すべての `FormControl` を意図的に使用する。 そのため、すべての UntypedFormControl を手動で `FormControl` に変更する必要があります。
3. 開発者にもっと自由度を与える(これについては、後の方で説明します)。
初期値が `null` である場合、FormControl の型を明示的に指定する必要があることに注意してください。 また、TypeScript には、初期値が `false` の場合に同じことを行う必要のあるバグが存在します。
フォームの Group については、インターフェースを定義することも可能です。このインターフェースを FormGroup の型として渡すだけです。 この場合、TypeScript は FormGroup 内のすべての型を推論します。
```typescript
interface LoginForm {
email: FormControl;
password?: FormControl;
}
const login = new FormGroup({
email: new FormControl('', {nonNullable: true}),
password: new FormControl('', {nonNullable: true}),
});
```
手動で FormGroup を作成した上記の例のように、FormBuilder の `.group()` メソッドに、事前に定義されたインターフェースを受け入れられるジェネリクス属性が追加されました。
```typescript
interface LoginForm {
email: FormControl;
password?: FormControl;
}
const fb = new FormBuilder();
const login = fb.group({
email: '',
password: '',
});
```
このインターフェースにはプリミティブな非 nullable 型しかないため、新しい `nonNullable` FormBuilder プロパティ(`NonNullableFormBuilder` クラスインスタンスを含み、直接作成することも可能)を使って以下のように単純化できます。
```typescript
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
```
❗ 非 nullable 型の FormBuilder を使用する場合、または FormControl に非 nullable 型のオプションを設定する場合、`.reset()` メソッドを呼び出す際に、リセット値として初期の FormControl 値が使用されることに注意してください。
また、`this.form.value` のすべてのプロパティがオプションとしてマークされることに注意することも非常に重要です。 以下に例を示します。
```typescript
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
// login.value
// {
// email?: string;
// password?: string;
// }
```
これは、FormGroup 内のいずれかの FormControl を無効にする際に、この FormControl の値が `form.value` から削除されるために発生します。
```typescript
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
login.get('email').disable();
console.log(login.value);
// {
// password: ''
// }
```
フォームオブジェクト全体を取得するには、`.getRawValue()` メソッドを使用する必要があります。
```typescript
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
login.get('email').disable();
console.log(login.getRawValue());
// {
// email: '',
// password: ''
// }
```
厳格に型付けされたフォームのメリット:
1. FormControl / FormGroup の値を返すすべてのプロパティとメソッドが厳格に型付けされるようになった。 例: `value`、`getRawValue()`、`valueChanges`
2. FormControl 値を変更するすべてのメソッドが型安全になった。`setValue()`、`patchValue()`、`updateValue()`
3. FormControl が厳格に型付けされた。 このことは、FormGroup の `.get()` メソッドにも適用されます。 これにより、コンパイル時に存在しない FormControl へのアクセスも防止されます。
### 新しい FormRecord クラス
新しい `FormGroup` クラスの欠点は、その動的な性質が失われたことです。 一度定義されると、オンザフライで FormControl を追加または削除することはできません。
この問題を解決するために、Angular は新たに `FormRecord` クラスを追加しました。 `FormRecord` は実質的に `FormGroup` と同じですが、動的であり、そのすべての FormControl に同じ型が使用されます。
```typescript
folders: new FormRecord({
home: new FormControl(true, { nonNullable: true }),
music: new FormControl(false, { nonNullable: true })
});
// グループに新しい FormContol を追加する
this.foldersForm.get('folders').addControl('videos', new FormControl(false, { nonNullable: true }));
// コントロールの型が異なるため、これにより、コンパイルエラーが発生する
this.foldersForm.get('folders').addControl('books', new FormControl('Some string', { nonNullable: true }));
```
ご覧のとおり、これには別の制限があります。すべての FormControl は同じ型でなければなりません。 動的と異種の両方を兼ね備えた FormGroup がどうしても必要な場合は、`UntypedFormGroup` クラスを使用してフォームを定義することをお勧めします。
## モジュールレス(スタンドアロン)コンポーネント
これは未だ実験的とされている機能ではありますが、興味深い機能です。 コンポーネント、ディレクティブ、およびパイプをモジュールに含めることなく、これらを定義することができます。
この概念はまだ完全に練られてはいませんが、すでに ngModule を使用せずにアプリケーションをビルドすることができるようになっています。
スタンドアロンコンポーネントを定義するには、Component/Pipe/Directive デコレーターで新しい `standalone` プロパティを使用する必要があります。
```typescript
@Component({
selector: 'app-table',
standalone: true,
templateUrl: './table.component.html'
})
export class TableComponent {
}
```
この場合、このコンポーネントはどの ngModule にも宣言されませんが、 ngModule やその他のスタンドアロンコンポーネントにインポートすることは可能です。
各スタンドアロンコンポーネント/パイプ/ディレクティブには、その依存関係を直接デコレーターにインポートするメカニズムが備えられています。
```typescript
@Component({
standalone: true,
selector: 'photo-gallery',
// 既存のモジュールは直接スタンドアロンコンポーネントにインポートされる
// CommonModuleは、*ngIf などの標準の Angular ディレクティブを使用するために直接インポートされる
// 上記に宣言されるスタンドアロンコンポーネントも直接インポートされる
imports: [CommonModule, MatButtonModule, TableComponent],
template: `
...
Next Page
`,
})
export class PhotoGalleryComponent {
}
```
前述のとおり、スタンドアロンコンポーネントは、既存の ngModule にインポート可能です。 sharedModule 全体をインポートする必要がなく、本当に必要な物だけをインポートできます。 新しいスタンドアロンコンポーネントを使用し始めるのに適したストラテジーでもあります。
```typescript
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, TableComponent], // import our standalone TableComponent
bootstrap: [AppComponent]
})
export class AppModule {}
```
スタンドアロンコンポーネントは、Angular CLI を使って以下を入力すると作成できます。
```bash
ng g component --standalone user
```
### モジュールレスアプリケーションをブートストラップ
アプリケーションにあるすべての ngModule を排除する場合は、別の方法でアプリをブートストラップする必要があります。 Angular にはこのための新しい関数があり、それを main.ts ファイルで呼び出す必要があります。
```typescript
bootstrapApplication(AppComponent);
```
この関数の 2 つ目のパラメーターを使って、アプリ全体で必要なプロバイダーを定義できます。 通常プロバイダーのほとんどはモジュール内に存在するため、Angular は(現時点では)それに新しい `importProvidersFrom` 抽出関数を使用する必要があります。
```typescript
bootstrapApplication(AppComponent, { providers: [importProvidersFrom(HttpClientModule)] });
```
### スタンドアロンコンポーネントの遅延読み込みルート:
Angular には、`loadComponent` という新しい遅延読み込みルート関数があります。これは、スタンドアロンコンポーネントを読み込むためだけに存在する関数です。
```typescript
{
path: 'home',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
}
```
`loadChildren` は、ngModule を遅延読み込みできるようにするだけでなく、ルートファイルから直接、子ルートも読み込めるようになっています。
```typescript
{
path: 'home',
loadChildren: () => import('./home/home.routes').then(c => c.HomeRoutes)
}
```
### 記事の執筆時点におけるいくつかの注意事項
- スタンドアロンコンポーネント機能は、現在も実験的段階にあります。 将来的に、Webpack の代わりに Vite ビルダーに移行し、ツーリングの改善、ビルド時間の高速化、アプリアーキテクチャの強化、テスト方法の改善などを通じて、機能が大幅に改善されるでしょう。 現時点では、こういったものが多数欠けているため、全パッケージを受け取っていません。いずれにせよ、少なくともこの新しい Angular パラダイムを念頭に、アプリを開発し始めることは可能です。
- IDE と Angular ツールはまだ、新しいスタンドアロンエンティティを静的に解析する準備を整えていません。 すべての依存関係を各スタンドアロンエンティティにインポートする必要があるため、何かを見逃した場合、コンパイラーもそれを見逃し、ランタイム時に失敗する可能性があります。 これは今後改善されていきますが、現時点ではインポートの際に開発者側の注意が必要です。
- 現時点では Angular にグローバルインポート機能がないため(Vue などで行われるように)、各スタンドアロンエンティティで、依存関係を確実に 1 つずつインポートする必要があります。 この機能の主な目標は、私が思うところ、ボイラープレートを減らして物事を簡単に実行できるようにすることにあるため、今後のバージョンで解決されることを期待しています。
#
今日は、これで以上です。 それではまた!
記事
Mihoko Iijima · 2023年1月5日
これは InterSystems FAQ サイトの記事です。
システムモニタの中の「アプリケーションモニタ」を利用することで、ユーザが定義した特定の監視対象に対してチェックを行い特定の条件に合致した場合に通知を行ったり、メッセージログ(コンソールログ)に情報を出力したり、ユーザが定義するアクションを指定できます。
<メモ>アプリケーションモニタはインストールにより準備されますが、ユーザが付属のアプリケーション・モニタ・クラスを有効化するまで動作しないモニタです。付属のアプリケーションモニタには、監査のカウントやイベント詳細を収集するもの、ディスクの容量を監視するものなどが含まれます。
詳細は、以下ドキュメントをご参照ください。【IRIS】アプリケーション・モニタのメトリックアプリケーション・モニタのメトリック
作成手順は以下の通りです。
%SYSネームスペースにアプリケーションモニタ用クラスを作成する
作成した1のクラスを、システムモニタのアプリケーションモニタ有効化メニューで有効化する
収集のインターバルを設定する(秒単位)
システムモニタを再起動する
以下例で追加するユーザ定義のアプリケーションモニタクラスは、監査ログに「ログイン失敗」のイベントが記録された場合に、メッセージログ(コンソールログ)にイベント詳細を出力する流れを記述しています。(システムに監査ログに関するアプリケーションモニタクラスが用意されていますが、特定のイベントに対する監視ではないため、ユーザ定義のアプリケーションモニタを用意します。)
具体的な流れは以下の通りです。
1、%SYSネームスペースにアプリケーションモニタ用クラスを作成する
%Monitor.Adaptorを継承するクラスを%SYSネームスペースに作成します。継承された GetSample()メソッドが指定の間隔(デフォルト30秒)で動作するため、GetSample()メソッドをオーバーライドし、情報収集と通知の判断、出力の命令を記述します。例の情報収集はSQLを利用していますが、グローバル変数にある情報を利用することもできます。※GetSample()メソッドは、%Statusを戻り値に設定していますので、必ず戻り値に%Statusの情報を返してください。
例では、監査イベントが記録される %SYS.Audit テーブルを利用して、ログイン失敗のイベント(Event='LoginFailure')を抽出しています。(前回調査した時刻よりも新しい時刻で記録されているものを抽出しています)
Class MyMetric.CheckTable Extends %Monitor.Adaptor
{
Property AuditEventTC As %String;
Method GetSample() As %Status
{
#dim ex As %Exception.AbstractException
set status=$$$OK
try {
//前回の検索時刻より後の物だけ(分単位に検査する)
/*
select EventData,UserName ,{fn TIMESTAMPADD(SQL_TSI_HOUR,9,UTCTimeStamp)} from %SYS.Audit
where Event='LoginFailure' And ({fn TIMESTAMPDIFF('SQL_TSI_MINUTE',{fn TIMESTAMPADD(SQL_TSI_HOUR,9,UTCTimeStamp)},?)}<0)
*/
set sql="select EventData,UserName ,{fn TIMESTAMPADD(SQL_TSI_HOUR,9,UTCTimeStamp)} from %SYS.Audit"
set sql=sql_" where Event='LoginFailure' And ({fn TIMESTAMPDIFF('SQL_TSI_MINUTE',{fn TIMESTAMPADD(SQL_TSI_HOUR,9,UTCTimeStamp)},?)}<=0)"
//set ^isjdebug("sql")=sql_"-"_..AuditEventTC
set stmt=##class(%SQL.Statement).%New()
set st=stmt.%Prepare(sql)
set rset=stmt.%Execute(..AuditEventTC) //現在のタイムスタンプに再セット
set ..AuditEventTC=$ZDATETIME($system.Util.UTCtoLocalWithZTIMEZONE($ZTIMESTAMP),3)
//set ^isjdebug("TC")=..AuditEventTC
while rset.%Next() {
set message=rset.%Get("Username")_"のアクセスでエラーが発生"_$C(13,10)
set message=message_rset.%Get("EventData")
//set ^isjdebug("msg",$H)=message
$$$THROWONERROR(ex,##class(%SYS.System).WriteToConsoleLog(message,1,2))
}
}
catch ex {
set status=ex.AsStatus()
}
return status
}
/// 必要に応じてオーバーライドしてください。
/// このメソッドはモニタクラスの初期化処理として実行されます。
/// 例のように、情報の抽出に必要な初期データがある場合この中でセットできます。
/// このメソッドの戻り値も%Statusが定義されています。$$$OKを戻すとモニタクラスが起動し、$$$OK以外を戻すとモニタクラスは動作しません
Method Initialize() As %Status
{
//現在のタイムスタンプを取得($ZTIMESTAMPはUTCタイム)
if ..AuditEventTC="" {
set ..AuditEventTC=$ZDATETIME($system.Util.UTCtoLocalWithZTIMEZONE($ZTIMESTAMP),3)
}
Quit $$$OK
}
}
例の中では、メッセージログ(コンソールログ)に出力するため %SYS.SystemクラスのWriteToConsoleLog()メソッドを使用しています。
<注意>
Caché/Ensemble の場合、日本語が含まれるメッセージを出力すると文字化けしますので、英数字のみのメッセージを出力するようにしてください。
IRIS/IRIS for Healthでは、日本語が含まれるメッセージをメッセージログ(messages.log)に出力できますが、メッセージログの中身を参照できる管理ポータルのメニュー(管理ポータル > システムオペレーション > システムログ > メッセージログ)では、化けて見えます。 メッセージログファイル(messages.log)を「BOM付きUTF8」で保存し直すことで、管理ポータルから日本語を参照できるようになります。
また、アプリケーションモニタクラスの中では、定期的に動作するGetSample()メソッドの中で定期実行時に使用する情報をプロパティとして記録しておくことができます。例では、プロパティ AuditEventTC を作成し、GetSample()を実行した時のタイムスタンプを記録しています。(次回起動時に前回のタイムスタンプより新しい情報を抽出するために使用しています。)
AuditEventTCプロパティの初期値は、モニタクラスが開始するときに実行される Initialize()メソッドに記載しています。
2、作成した1のクラスを、システムモニタのアプリケーションモニタ有効化メニューで有効化する
%SYSネームスペースに接続したターミナルを用意し、%SYSMONMGRルーチンを起動し、1で作成したクラス(MyMetric.CheckTable)を有効化します。
%SYS>do ^%SYSMONMGR // ルーチンを実行します。
1) Start/Stop System Monitor
2) Set System Monitor Options
3) Configure System Monitor Classes
4) View System Monitor State
5) Manage Application Monitor
6) Manage Health Monitor
7) View System Data
8) Exit
Option? 5 // 5を入力
1) Set Sample Interval
2) Manage Monitor Classes
3) Change Default Notification Method
4) Manage Email Options
5) Manage Alerts
6) Exit
Option? 2 //2を入力
1) Activate/Deactivate Monitor Class
2) List Monitor Classes
3) Register Monitor System Classes
4) Remove/Purge Monitor Class
5) Set Class Sample Interval
6) Debug Monitor Classes
7) Exit
Option? 1 //1を入力
Class? ? // ?を入力
Num MetricsClassName Activated
1) %Monitor.System.HistoryMemory N
2) %Monitor.System.HistoryPerf N
3) %Monitor.System.HistorySys N
4) %Monitor.System.HistoryUser N
5) %Monitor.System.AuditCount N
6) %Monitor.System.AuditEvents N
7) %Monitor.System.Clients N
8) %Monitor.System.Diskspace N
9) %Monitor.System.Freespace N
10) %Monitor.System.Globals N
11) %Monitor.System.Journals N
12) %Monitor.System.License N
13) %Monitor.System.LockTable N
14) %Monitor.System.Processes N
15) %Monitor.System.Routines N
16) %Monitor.System.Servers N
17) %Monitor.System.CSPGateway N
18) MyMetric.CheckTable N
Class? 18 MyMetric.CheckTable //18を入力
Activate class? Yes => Yes // EnterまたはYesを入力
1) Activate/Deactivate Monitor Class
2) List Monitor Classes
3) Register Monitor System Classes
4) Remove/Purge Monitor Class
5) Set Class Sample Interval
6) Debug Monitor Classes
7) Exit
Option?
ターミナルはこのままの状態にします。
3、収集のインターバルを設定する(秒単位)(先ほどの手順の続き)
1) Activate/Deactivate Monitor Class
2) List Monitor Classes
3) Register Monitor System Classes
4) Remove/Purge Monitor Class
5) Set Class Sample Interval
6) Debug Monitor Classes
7) Exit
Option? 5 // 5を入力
Class? ? Num MetricsClassName Activated
1) %Monitor.System.HistoryMemory N
2) %Monitor.System.HistoryPerf N
3) %Monitor.System.HistorySys N
4) %Monitor.System.HistoryUser N
5) %Monitor.System.AuditCount N
6) %Monitor.System.AuditEvents N
7) %Monitor.System.Clients N
8) %Monitor.System.Diskspace N
9) %Monitor.System.Freespace N
10) %Monitor.System.Globals N
11) %Monitor.System.Journals N
12) %Monitor.System.License N
13) %Monitor.System.LockTable N
14) %Monitor.System.Processes N
15) %Monitor.System.Routines N
16) %Monitor.System.Servers N
17) %Monitor.System.CSPGateway N
18) MyMetric.CheckTable Y
Class? 18 MyMetric.CheckTable // 18を入力
Class Sample Interval? 0 (Enter '-' to reset) => 10 // 10を入力(=10秒間隔で収集します)
1) Activate/Deactivate Monitor Class
2) List Monitor Classes
3) Register Monitor System Classes
4) Remove/Purge Monitor Class
5) Set Class Sample Interval
6) Debug Monitor Classes
7) Exit
Option? // Enterを入力
1) Set Sample Interval
2) Manage Monitor Classes
3) Change Default Notification Method
4) Manage Email Options
5) Manage Alerts
6) Exit
Option? // Enterを入力
1) Start/Stop System Monitor
2) Set System Monitor Options
3) Configure System Monitor Classes
4) View System Monitor State
5) Manage Application Monitor
6) Manage Health Monitor
7) View System Data
8) Exit
Option?
4、システムモニタを再起動する
(先ほどの手順の続き)
1) Start/Stop System Monitor
2) Set System Monitor Options
3) Configure System Monitor Classes
4) View System Monitor State
5) Manage Application Monitor
6) Manage Health Monitor
7) View System Data
8) Exit
Option? 1 // 1を入力
1) Start System Monitor
2) Stop System Monitor
3) Exit
Option? 2 // 2を入力
Stopping System Monitor... System Monitor stopped
1) Start System Monitor
2) Stop System Monitor
3) Exit
Option? 1 // 1を入力
Starting System Monitor... System Monitor started
1) Start System Monitor
2) Stop System Monitor
3) Exit
Option? // Enterを入力
1) Start/Stop System Monitor
2) Set System Monitor Options
3) Configure System Monitor Classes
4) View System Monitor State
5) Manage Application Monitor
6) Manage Health Monitor
7) View System Data
8) Exit
Option? // Enterを入力
%SYS>
以上で手続き終了です。
テストとして、管理ポータルやターミナルにログインする際のユーザ名、パスワードを不正な状態で入力し、ログインを失敗させます(※1)。しばらくたった後で、メッセージログ(コンソールログ)にログイン失敗時のメッセージが記録されていれば、成功です。
メッセージログ(IRIS)は <インストールディレクトリ>\mgr\messages.log コンソールログ(Caché/Ensemble)は <インストールディレクトリ>\mgr\cconsole.log
をご参照ください。
メッセージ例
07/13/22-12:11:26:480 (1528) 2 [Utility.Event] abcdのアクセスでエラーが発生
エラーメッセージ: エラー #798: パスワード 認証が失敗しました
エラー #838: ユーザ abcd が存在しません
CSPアプリケーション /csp/sys
$I: |TCP|51774|1912
$P: |TCP|51774|1912
アプリケーションモニタを無効化する場合も、%SYSMONMGRルーチンを使用します(2の手続きとほぼ同様の手順です)。モニタの状態を変えた場合は、必ずシステムモニタを再起動し、設定を反映させてください。
無効化手続きは以下の通りです。(MyMetric.CheckTableクラスを無効化する手続き)
%SYS>do ^%SYSMONMGR // ルーチンを実行します。
1) Start/Stop System Monitor
2) Set System Monitor Options
3) Configure System Monitor Classes
4) View System Monitor State
5) Manage Application Monitor
6) Manage Health Monitor
7) View System Data
8) Exit
Option? 5 // 5を入力
1) Set Sample Interval
2) Manage Monitor Classes
3) Change Default Notification Method
4) Manage Email Options
5) Manage Alerts
6) Exit
Option? 2 //2を入力
1) Activate/Deactivate Monitor Class
2) List Monitor Classes
3) Register Monitor System Classes
4) Remove/Purge Monitor Class
5) Set Class Sample Interval
6) Debug Monitor Classes
7) Exit
Option? 1 //1を入力
Class? ? // ?を入力
Num MetricsClassName Activated
1) %Monitor.System.HistoryMemory N
2) %Monitor.System.HistoryPerf N
3) %Monitor.System.HistorySys N
4) %Monitor.System.HistoryUser N
5) %Monitor.System.AuditCount N
6) %Monitor.System.AuditEvents N
7) %Monitor.System.Clients N
8) %Monitor.System.Diskspace N
9) %Monitor.System.Freespace N
10) %Monitor.System.Globals N
11) %Monitor.System.Journals N
12) %Monitor.System.License N
13) %Monitor.System.LockTable N
14) %Monitor.System.Processes N
15) %Monitor.System.Routines N
16) %Monitor.System.Servers N
17) %Monitor.System.CSPGateway N
18) MyMetric.CheckTable Y
Class? 18 MyMetric.CheckTable //18を入力
Deactivate class? Yes => Yes // EnterまたはYesを入力
1) Activate/Deactivate Monitor Class
2) List Monitor Classes
3) Register Monitor System Classes
4) Remove/Purge Monitor Class
5) Set Class Sample Interval
6) Debug Monitor Classes
7) Exit
Option? // Enterを入力
1) Set Sample Interval
2) Manage Monitor Classes
3) Change Default Notification Method
4) Manage Email Options
5) Manage Alerts
6) Exit
Option? // Enterを入力
1) Start/Stop System Monitor
2) Set System Monitor Options
3) Configure System Monitor Classes
4) View System Monitor State
5) Manage Application Monitor
6) Manage Health Monitor
7) View System Data
8) Exit
Option? 1 //1を入力
1) Start System Monitor
2) Stop System Monitor
3) Exit
Option? 2 //2を入力
Stopping System Monitor... System Monitor stopped
1) Start System Monitor
2) Stop System Monitor
3) Exit
Option? 1 //1を入力
Starting System Monitor... System Monitor started
1) Start System Monitor
2) Stop System Monitor
3) Exit
Option? // Enterを入力
1) Start/Stop System Monitor
2) Set System Monitor Options
3) Configure System Monitor Classes
4) View System Monitor State
5) Manage Application Monitor
6) Manage Health Monitor
7) View System Data
8) Exit
Option? // Enterを入力
%SYS>
以上で無効化手続きは終了です。
※1 インストール時に初期セキュリティの設定を「最小」とした場合、管理ポータル/ターミナル/スタジオ起動時、認証なしアクセスでログインします。 テストのため、ターミナルをパスワード認証に変更する方法をご説明します。
管理ポータル > システム管理 > セキュリティ > サービス を開き、Windowsの場合は %Service_Consoleサービス、Windows以外の場合は %Service_Terminal を開きます。
「許可された認証方法」以下にあるチェックボックスの中から、「パスワード」にチェックを追加し、保存ボタンをクリックすると、新しいプロセスからパスワード認証が有効になります。
テストを終え、元に戻す場合は、対象サービス名の設定から「パスワード」のチェックを外し、保存ボタンをクリックするだけで元に戻ります。
記事
Mihoko Iijima · 2020年7月6日
前回はシンプルなIRISアプリケーション をGoogleクラウドにデプロイしました。 今回は、同じプロジェクトを Amazon Web Services(アマゾンウェブサービス) のElastic Kubernetes Service (EKS)を使って、デプロイします。
IRISプロジェクトをあなた自身のプライベート・リポジトリにすでにFORKしていると想定します。この記事では<username>/my-objectscript-rest-docker-templateという名前にしています。 <root_repo_dir>は、そのルートディレクトリです。
開始する前に、 AWSコマンドラインインターフェースと、Kubernetesクラスタ作成用のシンプルなCLIユーティリティeksctlをインストールします。 AWSの場合 aws2 の使用を試すことができますが、ここで説明するようにkube設定ファイルでaws2の使用法を設定する必要があります 。
AWS EKS
一般的なAWSリソースと同様に、EKSは無料ではありません 。 ただし、無料利用枠のアカウントを作成して、AWSの機能を試すことができます。 ただし、試してみたい機能のすべてが無料枠に含まれているわけではないことに注意してください。 ですから、現在の予算を管理し、金銭的な問題を理解するには、 これと これを読んでください。
ここでは既にAWSアカウントとrootアクセス権があり、このrootアクセス権を使用せず、管理者権限のあるユーザーが作成されていると想定します。 このユーザーのアクセスキーと秘密キーを [dev] プロファイル(またはあなたがつけたプロファイル名)の下のAWS認証情報ファイルに配置する必要があります。
$ cat ~/.aws/credentials[dev]aws_access_key_id = ABCDEFGHIJKLMNOPQRSTaws_secret_access_key = 1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234
今回は、 AWS「eu-west-1」リージョンにリソースを作成しますが、あなたが今いる場所に最も近いリージョンを選択し、以下に記載されている「eu-west-1」のすべてを選択したリージョンで置き換えてください。
ちなみに、必要なすべてのファイル(.circleci 、eks/、k8s/)も、ここに保存されており 、簡単にコピーと貼り付けができます。 必要なすべてのEKSリソースは最初から作成されます。 Amazon EKS Workshop は、良いリソースだと思います。
次に、AWSへのアクセスを確認します(ここではダミーのアカウントを使用しています)。
$ export AWS_PROFILE=dev
$ aws sts get-caller-identity{ "Account": "012345678910", "UserId": " ABCDEFGHIJKLMNOPQRSTU", "Arn": "arn:aws:iam::012345678910:user/FirstName.LastName"}
$ eksctl version[ℹ] version.Info{BuiltAt:"", GitCommit:"", GitTag:"0.10.2"}
すべてのデフォルト設定が適切であるという事実に従い、「 eksctl create cluster <cluster_name> --region eu-west-1 」を実行することもできますし、設定ファイルを作成して独自の設定を管理することもできます。
後者は、そのようなファイルをバージョン管理システム(VCS)に保存できるため、よりよい方法です。設定の例はここにあります。 さまざまな設定に関するここの記述を読んだら、独自の設定を作成してみましょう。
mkdir <root_repo_dir>/eks; cd <root_repo_dir>/eks
$ cat cluster.yaml
apiVersion: eksctl.io/v1alpha5kind: ClusterConfig
metadata: name: dev-cluster region: eu-west-1 version: '1.14'
vpc: cidr: 10.42.0.0/16 nat: gateway: Single clusterEndpoints: publicAccess: true privateAccess: false
nodeGroups: - name: ng-1 amiFamily: AmazonLinux2 ami: ami-059c6874350e63ca9 # AMI is specific for a region instanceType: t2.medium desiredCapacity: 1 minSize: 1 maxSize: 1
# Worker nodes won't have an access FROM the Internet # But have an access TO the Internet through NAT-gateway privateNetworking: true
# We don't need to SSH to nodes for demo ssh: allow: false
# Labels are Kubernetes labels, shown when 'kubectl get nodes --show-labels' labels: role: eks-demo # Tags are AWS tags, shown in 'Tags' tab on AWS console' tags: role: eks-demo
# CloudWatch logging is disabled by default to save money# Mentioned here just to show a way to manage it#cloudWatch: # clusterLogging:# enableTypes: []
「nodeGroups.desiredCapacity = 1」は本番環境では意味がありませんが、デモでは問題ありません。また、AMIイメージはリージョン間で異なる可能性があることに注意してください。 「amazon-eks-node-1.14」を探し、最新の1つを選択します。
次に、クラスター(コントロールプレーンとワーカーノード)を作成します。
$ eksctl create cluster -f cluster.yaml
ちなみに、クラスターが不要になった場合は、以下を使用してクラスターを削除できます。
$ eksctl delete cluster --name dev-cluster --region eu-west-1 --wait
クラスターの作成には約15分かかります。 この間、eksctlの出力を確認できます。
CloudFormationコンソールを参照すると、2つのスタックがあります。それぞれにドリルダウンして、[リソース] タブを参照すると、何が作成されるかを正確に確認でき、[イベント] タブで、リソース作成の現在の状態を確認できます。
クラスターは正常に作成されましたが、eksctl の出力で「EKSクラスターでkubectlを使用できません」というメッセージがあり、接続に問題があったことがわかります。aws-iam-authenticator(IAM)をインストールしてkubeコンテキストを作成し、これを解決しましょう。
$ which aws-iam-authenticator/usr/local/bin/aws-iam-authenticator
$ aws eks update-kubeconfig --name dev-cluster --region eu-west-1
$ kubectl get nodesNAME STATUS ROLES AGE VERSIONip-10-42-140-98.eu-west-1.compute.internal Ready <none> 1m v1.14.7-eks-1861c5
これで動作するはずですが、管理者権限を持つユーザーでクラスターを作成しました。 CircleCIからの通常のデプロイ処理では、プログラムによるアクセスのみで、次のポリシーが付与されている特別なAWSユーザー(この例ではCircleCIと名付けられたユーザー)を作成する と良いでしょう。
最初のポリシーはAWSに組み込まれているため、それを選択するだけで済みます。 2つ目は自分で作成する必要があります。 作成プロセスの説明はここにあります。 ポリシー「 AmazonEKSDescribePolicy 」は次のようになります。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "eks:DescribeCluster", "eks:ListClusters" ], "Resource": "*" } ]}
ユーザーの作成後、ユーザーのアクセスキーと秘密のアクセスキーを保存します。これらのキーはすぐに必要になります。
また、この記事で説明されているように、Kubernetesクラスター内でこのユーザー権限を付与したいと考えています。 つまり、EKSクラスターを作成した後は、IAMユーザー、すなわち作成者のみがそれにアクセスできます。 CircleCIユーザーを追加するには、クラスターのAWS認証設定(configmap aws-auth、 'data'セクション)のデフォルトの空の「mapUsers」セクションをkubectl editを使って(‘01234567890’の代わりに自分のアカウントIDを使います)次の行に置き換える必要があります。
$ kubectl -n kube-system edit configmap aws-auth...data:... mapUsers: | - userarn: arn:aws:iam::01234567890:user/CircleCI username: circle-ci groups: - system:masters
以前の記事ののKubernetesマニフェストを使用します (「Googleクラウドの前提条件」のセクションを参照)。以前のやり方と違う点は、デプロイのイメージフィールドでプレースホルダを使うということだけです。 これらのマニフェストを<root_repo_dir>/k8sディレクトリに保存します。 デプロイファイルの名前がdeployment.tplに変更されたことに注意してください。
$ cat <root_repo_dir>/k8s/deployment.tpl...spec:containers:- image: DOCKER_REPO_NAME/iris-rest:DOCKER_IMAGE_TAG...
CircleCI
CircleCI側のデプロイ処理は、GKEに使用される処理に似ています。
リポジトリをPullする
Dockerイメージをビルドする
Amazon クラウドで認証する
イメージをAmazon Elastic Container Registry(ECR)にアップロードする
このイメージを基にしたAWS EKSでコンテナを実行する
前回と同様に、作成およびテスト済みのCircleCI構成テンプレートorbsを利用します。
イメージを構築してECRにPushするためのaws-ecr orb
AWS認証のためのaws-eks orb
Kubernetesマニフェストのデプロイのためのkubernetes orb
デプロイ構成は次のようになります。
$ cat <root_repo_dir>/.circleci/config.ymlversion: 2.1orbs: aws-ecr: circleci/aws-ecr@6.5.0 aws-eks: circleci/aws-eks@0.2.6 kubernetes: circleci/kubernetes@0.10.1
jobs: deploy-application: executor: aws-eks/python3 parameters: cluster-name: description: | Name of the EKS cluster type: string aws-region: description: | AWS region type: string account-url: description: | Docker AWS ECR repository url type: string tag: description: | Docker image tag type: string steps: - checkout - run: name: Replace placeholders with values in deployment template command: | cat k8s/deployment.tpl |\ sed "s|DOCKER_REPO_NAME|<< parameters.account-url >>|" |\ sed "s|DOCKER_IMAGE_TAG|<< parameters.tag >>|" > k8s/deployment.yaml; \ cat k8s/deployment.yaml - aws-eks/update-kubeconfig-with-authenticator: cluster-name: << parameters.cluster-name >> install-kubectl: true aws-region: << parameters.aws-region >> - kubernetes/create-or-update-resource: action-type: apply resource-file-path: "k8s/namespace.yaml" show-kubectl-command: true - kubernetes/create-or-update-resource: action-type: apply resource-file-path: "k8s/deployment.yaml" show-kubectl-command: true get-rollout-status: true resource-name: deployment/iris-rest namespace: iris - kubernetes/create-or-update-resource: action-type: apply resource-file-path: "k8s/service.yaml" show-kubectl-command: true namespace: irisworkflows: main: jobs: - aws-ecr/build-and-push-image: aws-access-key-id: AWS_ACCESS_KEY_ID aws-secret-access-key: AWS_SECRET_ACCESS_KEY region: AWS_REGION account-url: AWS_ECR_ACCOUNT_URL repo: iris-rest create-repo: true dockerfile: Dockerfile-zpm path: . tag: ${CIRCLE_SHA1} - deploy-application: cluster-name: dev-cluster aws-region: eu-west-1 account-url: ${AWS_ECR_ACCOUNT_URL} tag: ${CIRCLE_SHA1} requires: - aws-ecr/build-and-push-image
ワークフローのセクションにはジョブリストが含まれ、各ジョブはaws-ecr/build-and-push-imageなどのorbから呼び出すか、構成で「deploy-application」を使って直接定義できます。
次のコードは、aws-ecr/build-and-push-imageジョブが終了した後で、deploy-applicationジョブが呼び出されることを意味します。
requires:- aws-ecr/build-and-push-image
[ジョブ] セクションには、デプロイ・アプリケーションジョブの説明と、次のような定義された手順のリストが含まれています。
checkoutで、GitリポジトリからPullする
runで、Docker-imageリポジトリとタグを動的に設定するスクリプトを実行する
aws-iam-authenticatorを使用する aws-eks /update-kubeconfig-with-authenticatorを使用して Kubernetesへの接続を設定する
CircleCIから「kubectl apply」を実行する方法として数回使用されるkubernetes/create-or-update-resource
変数を使用しますが、もちろんそれらはCircleCIの「環境変数」タブで定義する必要があります。
次の表は、使用される変数の意味を示しています。
AWS_ACCESS_KEY_ID
CircleCI IAMユーザーのアクセスキー
AWS_SECRET_ACCESS_KEY
CircleCI IAMユーザーの秘密キー
AWS_REGION
eu-west-1、この場合
AWS_ECR_ACCOUNT_URL
01234567890.dkr.ecr.eu-west-1.amazonaws.comなどのAWS ECR Docker レジストリのURL
「01234567890」がアカウント IDの場合
デプロイ処理をトリガーする方法は次のとおりです。
$ git add .circleci/ eks/ k8s/$ git commit -m “AWS EKS deployment”$ git push
これにより、このワークフローにおける2つのジョブが表示されます。
どちらのジョブもクリック可能であり、これにより、実行した手順の詳細を確認できます。デプロイには数分かかります。 完了したら、KubernetesリソースとIRISアプリケーション自体のステータスを確認できます。
$ kubectl -n iris get pods -w # Ctrl+C to stop $ kubectl -n iris get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEiris-rest LoadBalancer 172.20.190.211 a3de52988147a11eaaaff02ca6b647c2-663499201.eu-west-1.elb.amazonaws.com 52773:32573/TCP 15s
DNSレコードが反映されるまで数分かかります。 それまでは、curlを実行すると「ホストを解決できませんでした」というエラーが表示されます。
$ curl -XPOST -H "Content-Type: application/json" -u _system:SYS a3de52988147a11eaaaff02ca6b647c2-663499201.eu-west-1.elb.amazonaws.com:52773/person/ -d '{"Name":"John Dou"}' $ curl -XGET -u _system:SYS a3de52988147a11eaaaff02ca6b647c2-663499201.eu-west-1.elb.amazonaws.com:52773/person/all[{"Name":"John Dou"},]
まとめ
一見するとAWS EKSへのデプロイはGKEへのデプロイよりも複雑に見えますが、それほど大きな違いはありません。 組織でAWSを使用している場合は、Kubernetesをスタックに追加する方法を理解されたと思います。
最近、EKS APIが拡張され、管理グループをサポートできるようになりました。これにより、コントロールプレーンとデータプレーンを全体としてデプロイでき、これは将来有望と思われます。 さらに、コンテナ用のAWSサーバーレスコンピューティングエンジンであるFargateが利用可能になりました。
最後に、AWS ECRに関する簡単な注意事項を記します:イメージにライフサイクルポリシーを設定することを忘れないでください。
InterSystems Open Exchangeで関連アプリケーション をご確認ください。
記事
Toshihiko Minamoto · 2020年10月5日
CachéとCosFakerを使ったテスト駆動開発の簡単な紹介
**読了****目安時間**: 6分
皆さん、こんにちは。
私がTDDに初めて出会ったのは約9年前のことです。すぐに夢中になってしまいました。
最近は非常に人気が出てきているようですが、残念ながら多くの企業ではあまり使われていないようです。 また、主に初心者の方ではありますが、一体それがなんであるのか、どのように使うのかといったことさえも知らない開発者もたくさんいます。
#### 概要
この記事は、%UnitTestでTDDを使用する方法を紹介することを目標としています。 ワークフローを示し、私の最初のプロジェクトであった[cosFaker](https://github.com/henryhamon/cosfaker)の使用方法を説明します。これはCachéを使って作成したものであり、最近になって[OpenExchange](https://openexchange.intersystems.com/package/CosFaker)にアップロードしたものです。
では、ベルトを締めて出発しましょう。
#### TDDとは?
テスト駆動開発(TDD)は、自動テストが失敗した場合に、開発者に新しいコードの書き方のみを示すプログラミング実践として定義できます。
このメリットに関する記事、講義、講演などは数多く存在しますが、どれもが正しい内容です。
コードはテスト済みで生成されること、過度なエンジニアリングを避けるために定義された要件に、システムが実際に適合していることを確認できること、継続的にフィードバックを得ることが挙げられます。
では、TDDを使用しない理由は何でしょうか。 TDDにはどのような問題があるのでしょうか。 答えは単純です。そう、コストです! とにかくコストがかかります!
TDDではより多くの行のコードを記述する必要があるため、その処理には時間がかかります。 しかし、TDDを使用すると、製品を作成するための最終コストは現時点で発生し、後で追加コストをかける必要がありません。
常にテストを実行すれば、早期にエラーを検出できるため、修正にかかるコストが削減されるのです。
というわけで、私からのアドバイスは、ただ実行に移しましょう!
#### セットアップ
InterSystemsは、%UnitTestの使用方法に関するドキュメントとチュートリアルを用意しています。[こちらからお読みください。](https://irisdocs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=TUNT)
開発にはvscodeを使用します。 この方法で、テスト用に別のフォルダを作成し、 UnitTestRootにプロジェクトコードパスを追加して、テストを実行する際に、テストサブフォルダの名前を渡します。 そして必ず、修飾子loadudlを渡します。
Set ^UnitTestRoot = "~/code"
Do ##class(%UnitTest.Manager).RunTest("myPack","/loadudl")
#### 手順
おそらく、「レッド ➡グリーン➡リファクタリング」という有名なTDDサイクルを耳にしたことがあるでしょう。 失敗するテストを書き、合格する単純なプロダクションコードを書き、そしてそのプロダクションコードをリファクタリングするというサイクルです。
では、実際に手を動かして、計算を行うクラスとそれをテストする別のクラスを作成することにしましょう。 後者のクラスは、%UnitTest.TestCaseを拡張します。
では、整数の2乗を返すClassMethodを作成しましょう。
Class Production.Math
{
ClassMethod Square(pValue As %Integer) As %Integer
{
}
}
そして、2を渡すとどうなるかテストします。 4を返すはずです。
Class TDD.Math Extends %UnitTest.TestCase
{
Method TestSquare()
{
Do $$$AssertEquals(##class(Production.Math).Square(2), 4)
}
}
次のコードを実行します。
Do ##class(%UnitTest.Manager).RunTest("TDD","/loadudl")
テストは失敗します。

レッド! 次のステップは、これをグリーンにすることです。
グリーンにするために、Squareメソッドの実行結果として4を返すようにしましょう。
Class Production.Math
{
ClassMethod Square(pValue As %Integer) As %Integer
{
Quit 4
}
}
そしてテストを再実行します。

1つのシナリオでしか機能しないのですから、このソリューションでは、おそらくあまり嬉しくないのではないでしょうか。 わかりました! では次のステップに進みましょう。 別のシナリオを作成することにします。今度は負の数を渡してみます。
Class TDD.Math Extends %UnitTest.TestCase
{
Method TestSquare()
{
Do $$$AssertEquals(##class(Production.Math).Square(2), 4)
}
Method TestSquareNegativeNumber()
{
Do $$$AssertEquals(##class(Production.Math).Square(-3), 9)
}
}
テストを実行します。

また失敗してしまうので、プロダクションコードをリファクタリングしましょう。
Class Production.Math
{
ClassMethod Square(pValue As %Integer) As %Integer
{
Quit pValue * pValue
}
}
そして、テストを再実行します。

これですべてがうまく機能するようになりました... これが凝縮版のTDDサイクルです。
なぜこの手順に従う必要があるのか、と思っていることでしょう。 なぜテストを失敗させる必要があるのか、と。
私はプロダクションコードを記述したチームで作業したことがありますが、テストを作成したのはその後でした。 それでも、この小さな一歩に従う方を好むのには、次の理由があります。
ボブおじさん(Robert C. Martin)は、コードを書いた後でテストを書くことは「TDD」ではなく「時間の無駄」だと呼んだのです。
もう少し述べれば、テストが失敗し、次に合格することから、テストをテストしていることになります。
このテストはコードに変わりなく、誤りが含まれることもあります。 そして、それをテストする方法こそ、失敗して合格する必要のある場合に失敗と合格を保証することになります。 つまり、「テストをテストした」ということになります。
#### cosFaker
適切なテストを作成するには、最初にテストデータを生成する必要があります。 これを行う方法の1つが、データのダンプを生成して、テストに使うという方法です。
別の方法には、[cosFaker](https://openexchange.intersystems.com/package/CosFaker)を使用して、必要なときに偽のデータを簡単に作り出す手があります。
xml ファイルをダウンロードするだけです。その後、管理ポータル -> システムエクスプローラ -> Classes -> Importに移動します。 インポートする xml ファイルを選択するか、そのファイルを Studio にドラッグします。
また、ターミナルを使用してインポートすることもできます。
Do $system.OBJ.Load("yourpath/cosFaker.vX.X.X.xml","ck")
##### ローカリゼーション
cosFakerは、デフォルトのCSPアプリケーションフォルダにロケールファイルを追加します。 現時点では、英語とブラジルポルトガル語(私の母国語)の2言語しかありません。
データの言語は、Cachéの構成に応じて選択されます。
cosFakerのローカリゼーションは進行中のプロセスです。ご協力いただける方は、ぜひ、自身のロケール向けにローカライズされるプロバイダを作成してプルリクエストを提出してください。
cosFakerを使用すれば、ランダムな語、段落、電話番号、名前、住所、メール、価格、製品名、日付、16進色コードなどを生成することができます。
すべてのメソッドは、クラスでサブジェクトごとにグループ化されます。つまり、Latitudeを生成するには、AddressクラスのLatitudeメソッドを呼び出します。
Write ##class(cosFaker.Address).Latitude()
-37.6806
また、テスト用のJsonを生成することもできます。
Write ##class(cosFaker.JSON).GetDataJSONFromJSON("{ip:'ipv4',created_at:'date.backward 40',login:'username', text: 'words 3'}")
{
"created_at":"2019-03-08",
"ip":"95.226.124.187",
"login":"john46",
"text":"temporibus fugit deserunt"
}
以下は、**cosFaker**クラスとメソッドの全リストです。
* **cosFaker.Address**
* StreetSuffix
* StreetPrefix
* PostCode
* StreetName
* Latitude
* _出力:_ -54.7274
* Longitude
* _出力:_ -43.9504
* Capital( Location = “” )
* State( FullName = 0 )
* City( State = “” )
* Country( Abrev = 0 )
* SecondaryAddress
* BuildingNumber
* **cosFaker.App**
* FunctionName( Group= “”, Separator = “” )
* AppAction( Group= “” )
* AppType
* **cosFaker.Coffee**
* BlendName
* _出力:_ Cascara Cake
* Variety
* _出力:_ Mundo Novo
* Notes
* _出力:_ crisp, slick, nutella, potato defect!, red apple
* Origin
* _出力:_ Rulindo, Rwanda
* **cosFaker.Color**
* Hexadecimal
* _出力:_ #A50BD7
* RGB
* _出力:_ 189,180,195
* Name
* **cosFaker.Commerce**
* ProductName
* Product
* PromotionCode
* Color
* Department
* Price( Min = 0, Max = 1000, Dec = 2, Symbol = “” )
* _出力:_ 556.88
* CNPJ( Pretty = 1 )
* CNPJはブラジルの法人用税務登記番号です
* _出力:_ 44.383.315/0001-30
* **cosFaker.Company**
* Name
* Profession
* Industry
* **cosFaker.Dates**
* Forward( Days = 365, Format = 3 )
* Backward( Days = 365, Format = 3 )
* **cosFaker.DragonBall**
* Character
* _出力:_ Gogeta
* **cosFaker.File**
* Extension
* _出力:_ txt
* MimeType
* _出力:_ application/font-woff
* Filename( Dir = “”, Name = “”, Ext = “”, DirectorySeparator = “/” )
* _出力:_ repellat.architecto.aut/aliquid.gif
* **cosFaker.Finance**
* Amount( Min = 0, Max = 10000, Dec = 2, Separator= “,”, Symbol = “” )
* _出力:_ 3949,18
* CreditCard( Type = “” )
* _出力:_ 3476-581511-6349
* BitcoinAddress( Min = 24, Max = 34 )
* _出力:_ 1WoR6fYvsE8gNXkBkeXvNqGECPUZ
* **cosFaker.Game**
* MortalKombat
* _出力:_ Raiden
* StreetFighter
* _出力:_ Akuma
* Card( Abrev = 0 )
* _出力:_ 5 of Diamonds
* **cosFaker.Internet**
* UserName( FirstName = “”, LastName = “” )
* Email( FirstName = “”, LastName = “”, Provider = “” )
* Protocol
* _出力:_ http
* DomainWord
* DomainName
* Url
* Avatar( Size = “” )
* _出力:_ http://www.avatarpro.biz/avatar?s=150
* Slug( Words = “”, Glue = “” )
* IPV4
* _出力:_ 226.7.213.228
* IPV6
* _出力:_ 0532:0b70:35f6:00fd:041f:5655:74c8:83fe
* MAC
* _出力:_ 73:B0:82:D0:BC:70
* **cosFaker.JSON**
* GetDataOBJFromJSON( Json = “” // JSON template string to create data )
* _パラメーターの例_: "{dates:'5 date'}"
* _出力_: {"dates":["2019-02-19","2019-12-21","2018-07-02","2017-05-25","2016-08-14"]}
* **cosFaker.Job**
* Title
* Field
* Skills
* **cosFaker.Lorem**
* Word
* Words( Num = “” )
* Sentence( WordCount = “”, Min = 3, Max = 10 )
* _出力:_ Sapiente et accusamus reiciendis iure qui est.
* Sentences( SentenceCount = “”, Separator = “” )
* Paragraph( SentenceCount = “” )
* Paragraphs( ParagraphCount = “”, Separator = “” )
* Lines( LineCount = “” )
* Text( Times = 1 )
* Hipster( ParagraphCount = “”, Separator = “” )
* **cosFaker.Name**
* FirstName( Gender = “” )
* LastName
* FullName( Gender = “” )
* Suffix
* **cosFaker.Person**
* cpf( Pretty = 1 )
* CPFはブラジルの社会保障番号です
* _出力:_ 469.655.208-09
* **cosFaker.Phone**
* PhoneNumber( Area = 1 )
* _出力:_ (36) 9560-9757
* CellPhone( Area = 1 )
* _出力:_ (77) 94497-9538
* AreaCode
* _出力:_ 17
* **cosFaker.Pokemon**
* Pokemon( EvolvesFrom = “” )
* _出力:_ Kingdra
* **cosFaker.StarWars**
* Characters
* _出力:_ Darth Vader
* Droids
* _出力:_ C-3PO
* Planets
* _出力:_ Takodana
* Quotes
* _出力:_ Only at the end do you realize the power of the Dark Side.
* Species
* _出力:_ Hutt
* Vehicles
* _出力:_ ATT Battle Tank
* WookieWords
* _出力:_ nng
* WookieSentence( SentenceCount = “” )
* _出力:_ ruh ga ru hnn-rowr mumwa ru ru mumwa.
* **cosFaker.UFC**
* Category
* _出力:_ Middleweight
* Fighter( Category = “”, Country = “”, WithISOCountry = 0 )
* _出力:_ Dmitry Poberezhets
* Featherweight( Country = “” )
* _出力:_ Yair Rodriguez
* Middleweight( Country = “” )
* _出力:_ Elias Theodorou
* Welterweight( Country = “” )
* _出力:_ Charlie Ward
* Lightweight( Country = “” )
* _出力:_ Tae Hyun Bang
* Bantamweight( Country = “” )
* _出力:_ Alejandro Pérez
* Flyweight( Country = “” )
* _出力:_ Ben Nguyen
* Heavyweight( Country = “” )
* _出力:_ Francis Ngannou
* LightHeavyweight( Country = “” )
* _出力:_ Paul Craig
* Nickname( Fighter = “” )
* _出力:_ Abacus
ユーザー名を返すメソッドでユーザーのクラスを作成してみましょう。ユーザー名はFirstNameとLastNameを連結したものになります。
Class Production.User Extends %RegisteredObject
{
Property FirstName As %String;
Property LastName As %String;
Method Username() As %String
{
}
}
Class TDD.User Extends %UnitTest.TestCase
{
Method TestUsername()
{
Set firstName = ##class(cosFaker.Name).FirstName(),
lastName = ##class(cosFaker.Name).LastName(),
user = ##class(Production.User).%New(),
user.FirstName = firstName,
user.LastName = lastName
Do $$$AssertEquals(user.Username(), firstName _ "." _ lastName)
}
}

リファクタリング:
Class Production.User Extends %RegisteredObject
{
Property FirstName As %String;
Property LastName As %String;
Method Username() As %String
{
Quit ..FirstName _ "." _ ..LastName
}
}

アカウントの有効期限を追加して、検証することにします。
Class Production.User Extends %RegisteredObject
{
Property FirstName As %String;
Property LastName As %String;
Property AccountExpires As %Date;
Method Username() As %String
{
Quit ..FirstName _ "." _ ..LastName
}
Method Expired() As %Boolean
{
}
}
Class TDD.User Extends %UnitTest.TestCase
{
Method TestUsername()
{
Set firstName = ##class(cosFaker.Name).FirstName(),
lastName = ##class(cosFaker.Name).LastName(),
user = ##class(Production.User).%New(),
user.FirstName = firstName,
user.LastName = lastName
Do $$$AssertEquals(user.Username(), firstName _ "." _ lastName)
}
Method TestWhenIsNotExpired() As %Status
{
Set user = ##class(Production.User).%New(),
user.AccountExpires = ##class(cosFaker.Dates).Forward(40)
Do $$$AssertNotTrue(user.Expired())
}
}

リファクタリング:
Method Expired() As %Boolean
{
Quit ($system.SQL.DATEDIFF("dd", ..AccountExpires, +$Horolog) > 0)
}

では、アカウントの有効期限が切れた場合をテストしてみましょう。
Method TestWhenIsExpired() As %Status
{
Set user = ##class(Production.User).%New(),
user.AccountExpires = ##class(cosFaker.Dates).Backward(40)
Do $$$AssertTrue(user.Expired())
}

すべてがグリーンです。
これらはあまり大したことのない例かもしれませんが、このようにすることで、コードだけでなくクラスの設計も単純にすることができます。
#### まとめ
この記事では、テスト駆動開発と%UnitTestクラスの使用方法について少しだけ学習しました。
また、cosFakerとテスト用の偽のデータの生成方法についても説明しました。
テストとTDDについては、これらの実践をレガシーコードで使用する方法、統合テスト、受け入れテスト駆動開発など、ほかにも学習することがたくさんあります。
詳細については、次の2冊が私のイチオシです。
[『Test Driven Development Teste e design no mundo real com Ruby』Mauricio Aniche著](https://www.amazon.com/Test-driven-development-Teste-design-Portuguese-ebook/dp/B01B6MSVBK/ref=sr_1_3?qid=1553909895&refinements=p_27%3AMauricio+Aniche&s=digital-text&sr=1-3&text=Mauricio+Aniche) - これについては英語版が出版されているかわかりません。 Java、C#、Ruby、およびPHP版があります。 あまりの素晴らしさに感銘を受けた一冊です。
そしてもちろん、Kent Beckの『[Test Driven Development by Example](https://www.amazon.com.br/Test-Driven-Development-Kent-Beck/dp/0321146530/ref=pd_sbs_14_2/131-5080621-0921627?_encoding=UTF8&pd_rd_i=0321146530&pd_rd_r=23604a58-528d-11e9-8a77-f188713467c0&pd_rd_w=TSR2y&pd_rd_wg=JBqE6&pf_rd_p=80c6065d-57d3-41bf-b15e-ee01dd80424f&pf_rd_r=CJ3QPX0H0P6H0EMNFZYJ&psc=1&refRID=CJ3QPX0H0P6H0EMNFZYJ)』。
コメントやご質問はお気軽にどうぞ。
これで、おしまいです。
記事
Toshihiko Minamoto · 2021年9月9日
## はじめに
この連載の最初の記事では、リレーショナルデータベースの[EAV(Entity–Attribute–Value)モデル](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model)を見て、それがどのように使用されて、何に役立つのかを確認しましょう。 その上で、EAVモデルの概念とグローバル変数と比較します。
原則として検索する必要のある、フィールド数、または階層的にネストされたフィールドの数が不明なオブジェクトがある場合があります。
たとえば、多様な商品群を扱うオンラインストアを考えてみましょう。 商品群ごとに固有の一意のプロパティセットがあり、共通のプロパティもあります。 たとえば、SSDとHDDドライブには共通の「capacity」プロパティがありますが、SSDには「Endurance, TBW」、HDDには「average head positioning time」という一意のプロパティもあります。
場合によっては、同じ商品でも別のメーカーが製造した場合には、それぞれに一意のプロパティが存在します。
では、50種の商品群を販売するオンラインストアがあるとしましょう。 各商品群には、数値またはテキストの固有のプロパティが5つあります。
実際に使用するのは5個だけであっても、各商品に250個のプロパティがあるテーブルを作成するのであれば、ディスク容量の要件が大幅に増える(50倍!)だけでなく、有用性のない空のプロパティによってキャッシュが詰まってしまうため、データベースの速度特性が大幅に減少してしまいます。
さらに、それだけではありません。 固有のプロパティを持つ新しい商品群を追加するたびに、**ALTER TABLE**コマンドを使用してテーブルの構造を変更する必要があります。 大規模なテーブルであれば、この操作には数時間、さらには数日間かかる可能性もあり、ビジネスでは許容しかねます。
これを注意深く読んでいる方は「商品群ごとに異なるテーブルを用意しては?」と言うでしょう。 もちろんその通りではありますが、このアプローチを使用すると、大型ストアの場合には、数万個ものテーブルでデータベースを作成することになり、管理が困難になります。 さらに、サポートする必要のあるコードがますます複雑化してしまいます。
一方、新しい商品群を追加するときに、データベースの構造を変更する必要はありません。 新しい商品群向けの新しいテーブルを追加すればよいだけだからです。
いずれにせよユーザーは、ストア内の商品を簡単に検索できること、現在のプロパティを示す便利な表形式で商品を表示できること、そして商品を比較できることが必要です。
ご想像のとおり、商品群の5個のプロパティのみが必要であるにもかかわらず、商品テーブルにはさまざまなプロパティを示す250個のフィールドがあれば不便であるのと同様に、250個のフィールドを使った検索フォームは、ユーザーにとって非常に不便です。 これは商品の比較にも当てはまります。
マーケティングデータベースも別の有用な例と言えるでしょう。 それに格納されている人ごとに、絶えず追加、変更、または削除される可能性のある多数のプロパティが必要です(多くの場合はネストされています)。 過去にある商品を特定の数量で購入した、特定の商品群を購入した、何かに参加した、どこかで勤務した、親戚がいる、この都市に住む、特定の社会階級に属する、などのプロパティがあります。 フィールド数は数千個にもなり、変化も絶えないでしょう。 マーケターは常に、さまざまな顧客グループを区別して魅力的な特別オファーを提供する方法を考えています。
これらの問題を解決すると同時に、明確で確定的なデータベース構造を得るために、Entity-Attribute-Valueアプローチが編み出されました。
## EAVアプローチ
EAVアプローチの本質は、エンティティ、属性、および属性値を個別に保存することにあります。 一般的に、EAVアプローチを説明するために、Entity、Attribute、およびValueという3つのテーブルのみが使用されます。

保存するデモデータの構造。

## テーブルを使用したEAVアプローチの実装
5つ(最後の2つのテーブルを1つに統合することにした場合は4つ)のテーブルを使用したより複雑な例を考察してみましょう。

最初のテーブルは`Сatalog`です。
CREATE TABLE Catalog (
id INT,
name VARCHAR (128),
parent INT
);
このテーブルは実際、EAVアプローチのエンティティに対応しています。 階層的な商品カタログのセクションを保存します。
2つ目のテーブルは ****`Field`です。
CREATE TABLE Field (
id INT,
name VARCHAR (128),
typeOf INT,
searchable INT,
catalog_id INT,
table_view INT,
sort INT
);
このテーブルでは、属性の名前、型、および属性が検索可能であるかどうかを指定します。 また、プロパティが属する商品を保持しているカタログのセクションも指定します。 catalog_id以下のカタログセクションにあるすべての商品には、このテーブルに保存されているさまざまなプロパティがある場合があります。
3つ目のテーブルは`Good`です。 商品を、商品の価格、商品の合計数量、商品の予約数量、および商品名とともに保存するように設計されています。 厳密にはこのテーブルは必要ではありませんが、個人的には、商品用に別のテーブルを用意しておくと便利だと思います。
CREATE TABLE Good (
id INT,
name VARCHAR (128),
price FLOAT,
item_count INT,
reserved_count,
catalog_id INT
);
4つ目のテーブル(`TextValues`)と5つ目のテーブル(`NumberValues`)は、商品のテキストの値と数値属性を保存するように設計されており、構造も似ています。
CREATE TABLE TextValues (
good_id INT,
field_id INT,
fValue TEXT
);
CREATE TABLE NumberValues (
good_id INT,
field_id INT,
fValue INT
);
テキスト値と数値に個別のテーブルを使用する代わりに、次の構造で単一のCustomeValuesテーブルを使用することもできます。
CREATE TABLE CustomValues (
good_id INT,
field_id INT,
text_value TEXT,
number_value INT
);
データ型ごとに個別に保存しておけば、速度が向上し、容量を節約できるため、私は別々に保存する方を好んでいます。
## EAVアプローチを使用したデータへのアクセス
SQLを使用して、カタログ構造マッピングを表示してみましょう。
SELECT * FROM Catalog ORDER BY id;
これらの値からツリーを作成するには、個別のコードが必要となります。 PHPでは、次のようになります。
$stmt = $ pdo-> query ('SELECT * FROM Catalog ORDER BY id');
$aTree = [];
$idRoot = NULL;
while ($row = $ stmt->fetch())
{
$aTree [$row ['id']] = ['name' => $ row ['name']];
if (! $row['parent'])
$idRoot = $row ['id'];
else
$aTree [$row['parent']] ['sub'] [] = $row['id'];
}
将来的には、ルートノードの $aTree[$ idRoot] から始めると、ツリーを簡単に描画できるようになります。
では、特定の商品のプロパティを取得しましょう。
まず、この商品に固有のプロパティのリストを取得し、その後で、それらのプロパティとデータベースにあるプロパティを接続します。 実際には、示されるすべてのプロパティが入力されているわけではないため、LEFT JOINを使用する必要があります。
SELECT * FROM
(
SELECT g. *, F.name, f.type_of, val.fValue, f.sort FROM Good as g
INNER JOIN Field as f ON f.catalog_id = g.catalog_id
LEFT JOIN TextValues as val ON tv.good = g.id AND f.id = val.field_id
WHERE g.id = $ nGood AND f.type_of = 'text'
UNION
SELECT g. *, F.name, f.type_of, val.fValue, f.sort FROM Good as g
INNER JOIN Field as f ON f.catalog_id = g.catalog_id
LEFT JOIN NumberValues as val ON val.good = g.id AND f.id = val.field_id
WHERE g.id = $nGood AND f.type_of = 'number'
) t
ORDER BY t.sort;
数値とテキスト値の両方を保存するために1つのテーブルのみを使用すると、クエリを大幅に簡略化できます。
SELECT g. *, F.name, f.type_of, val.text_value, val.number_value, f.sort FROM Good as g
INNER JOIN Field as f ON f.catalog = g.catalog
LEFT JOIN CustomValues as val ON tv.good = g.id AND f.id = val.field_id
WHERE g.id = $nGood
ORDER BY f.sort;
では、$nCatalogカタログセクションに含まれる商品を表形式で取得します。 まず、カタログのこのセクションのテーブルビューに反映する必要があるプロパティのリストを取得します。
SELECT f.id, f.name, f.type_of FROM Catalog as c
INNER JOIN Field as f ON f.catalog_id = c.id
WHERE c.id = $nCatalog AND f.table_view = 1
ORDER BY f.sort;
次に、テーブルを作成するクエリを構築します。 表形式ビューには、3つの追加プロパティ(Goodテーブルのプロパティのほかに)が必要だとします。 クエリを単純化するために、次を前提としています。
SELECT g.if, g.name, g.price,
f1.fValue as f1_val,
f2.fValue as f2_val,
f3.fValue as f3_val,
FROM Good
LEFT JOIN TextValue as f1 ON f1.good_id = g.id
LEFT JOIN NumberValue as f2 ON f2.good_id = g.id
LEFT JOIN NumberValue as f3 ON f3.good_id = g.id
WHERE g.catalog_id = $nCatalog;
## EAVアプローチの長所と短所
EAVアプローチは明らかに柔軟性のメリットがあります。 テーブルなどの固定されたデータ構造を使用すると、オブジェクトの広範なプロパティセットを保存することが可能になります。 また、データベースのスキーマを変更せずに、別のデータ構造を保存することができます。
また、非常に多くの開発者に馴染みのあるSQLも使用することができます。
最も明白なデメリットは、データの論理構造と物理ストレージの不一致であり、これによって様々な問題が引き起こされます。
さらに、プログラミングには、非常に複雑なSQLクエリが伴うこともよくあります。 EAVデータの表示には標準的に使用されていないツールの作成が必要となるため、デバッグが困難になることがあります。 また、LEFT JOINクエリを使用する必要がある場合があるため、データベースの速度が低下してしまいます。
## グローバル変数: EAVの代替
私はSQLの世界とグローバル変数の世界の両方に精通しているため、EAVアプローチが解決するタスクにグローバルを使用する方がはるかに魅力的になるのではないかと考えました。
グローバル変数はまばらで階層的な情報を保存できるデータ構造です。 グローバル変数は階層情報を保存するために慎重に最適化されているというのが非常に重要なポイントです。 グローバル変数自体はテーブルよりも低レベルの構造であるため、テーブルよりもはるかに素早く動作します。
同時に、グローバル構造自体をデータ構造に従って選択できるため、コードを非常に単純で明確にすることができます。
## デモデータを保存するためのグローバル構造
グローバル変数はデータを保存する上で非常に柔軟でエレガントな構造であるため、1つのグローバル変数を管理するだけでカタログセクション、プロパティ、および商品などのデータを保存することができます。

グローバル構造がデータ構造にどれほど似ているのかに注目してください。 このコンプライアンスによって、コーディングとデバッグが大幅に簡略化されます。
実際には、全ての情報を1つのグローバルに保存したい気持ちが非常に強くても、複数のグローバルを使用することをお勧めします。 インデックス用に別のグローバルを作成することが合理的です。 また、ディレクトリのパーティション構造のストレージを商品から分離することもできます。
## この続きは?
この連載の2つ目の記事では、EAVモデルに従う代わりに、InterSystems Irisのグローバルにデータを保存する方法の詳細とメリットについて説明します。
記事
Shintaro Kaminaka · 2021年11月3日
開発者の皆さん、こんにちは。
この記事は、FHIRの関連技術として、FHIRプロファイル作成ツールであるSUSHIの使い方を紹介するシリーズの第2弾です。パート2である今回まで半年の期間が経ってしまいました。
前回の[パート1](https://jp.community.intersystems.com/node/493351)では、FHIRとは?FHIRプロファイルとは?FHIR Shorthandとは?そしてSUSHIとはどのようなツールなのか?どのような物を作成できるのか?について、サンプルの成果物のスクリーンショットを交えながら説明しました。
今回の記事では、SUSHIで作成したプロファイルの実際の活用例として、SUSHIを使ってPatientリソースに **Extension** を追加し、さらにそのExtensionの項目に対する新しい **SearchParameter** を定義し、IRIS for HealthのFHIR Repositoyで新しいSearchParameterが使えるようになるまで、をご紹介します。
## SUSHIのアップグレード
いきなり本筋とそれて恐縮ですが、SUSHIを久しぶりに触る私のような方はSUSHIのアップグレードを行いましょう。
この半年の間もSUSHIは精力的な機能Enhancementが行われており、8月にはversion 2.0.0がリリースされています。この記事の執筆段階の最新バージョンは [SUSHI 2.1.1](https://github.com/FHIR/sushi/releases) でした。
このリンク先でも紹介されている通り、アップグレードはインストール同様以下のコマンドです。
```Bash
$ npm install -g fsh-sushi
```
sushi -versionを実行すればバージョンが確認できます。
同様に、SUSHIで生成されたProfileをベースに、実装ガイドのHTMLファイル群を作成してくれるIG Publisherツールも、**_updatePublisher** コマンドを実行してアップグレードすることができます。
## FISHファイルの作成
まずは、前回同様 ```sushi --init``` コマンドを使って、プロジェクトを作成します。この記事ではテンプレートで生成される、**patient.fsh** ファイルを修正していきます。
今回は、出身都道府県を表現するstring型の **birthPlace** のExtensionを追加し、さらにそのbirthPlaceに対するSearchParameterも定義することで、その患者の出身都道府県で検索できるような拡張を行います!
# Extensionを追加する
まず、Extensionを追加するために以下の定義を追加します。
US CoreやJP Coreのように、通常はAddress型を使うことが多いですが、ここでは単純にstring型にしています。
```
Extension: BirthPlace
Id: birthPlace
Title: "出身地"
Description: "生まれた場所をstring型で表現する"
* ^url = "http://isc-demo/fhir/StructureDefinition/patient-birthPlace"
* value[x] only string
```
各項目は以下のようにExtensionのStructureDefinitionに対応しています。項目によっては複数の箇所に設定されます。ベースとなるfhirのバージョンや、このExtension自体のバージョンなどの情報は、`sushi-config.yml`ファイルから取得されているものもあります。
| SUSHIの項目 | 対応するStructureDefinitionの項目 |
|:----------|:------------|
| Extensions | name |
| Id | id |
| Title | title/differencial.element[id=Extension].short |
| Desctiption | description/differencial.element[id=Extension].definition |
| ^url | url//differencial.element[id=Extension.url].fixedUri |
| value[x] | differencial.element[id=Extension.value[x]].type.code |
実際に生成されたExtensionのStructureDefinitionです。
手書きでこれを1から作るのは大変ですが、SUSHIを使えば比較的簡単です。
```json
{
"resourceType": "StructureDefinition",
"id": "birthPlace",
"url": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace",
"version": "0.1.0",
"name": "BirthPlace",
"title": "出身地",
"status": "active",
"description": "生まれた場所をstring型で表現する",
"fhirVersion": "4.0.1",
"mapping": [
{
"identity": "rim",
"uri": "http://hl7.org/v3",
"name": "RIM Mapping"
}
],
"kind": "complex-type",
"abstract": false,
"context": [
{
"type": "element",
"expression": "Element"
}
],
"type": "Extension",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/Extension",
"derivation": "constraint",
"differential": {
"element": [
{
"id": "Extension",
"path": "Extension",
"short": "出身地",
"definition": "生まれた場所をstring型で表現する"
},
{
"id": "Extension.extension",
"path": "Extension.extension",
"max": "0"
},
{
"id": "Extension.url",
"path": "Extension.url",
"fixedUri": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace"
},
{
"id": "Extension.value[x]",
"path": "Extension.value[x]",
"type": [
{
"code": "string"
}
]
}
]
}
}
```
このExtensionで追加したPatientリソースへのExtensionデータは、実際はこのようなデータになります。
```json
"extension": [
{
"url": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace",
"valueString": "鹿児島"
}
],
```
# SearchParamterを追加する
次は、先ほど追加したExtensionの項目をキーにして、リソースを検索できるように、**SearchParamter** を追加します。FHIRの場合、各リソースには構造化された要素(エレメント)が定義されていますが、 **そのすべての要素で検索ができるわけではなく、SearchParamterに定義された項目(≒要素)でのみ検索することができます** 。ここがSQLのテーブルとは少し異なる点ですね。
SearchParamter名は要素名とは別に定義されており、Patientリソースで言えば、genderのように要素名=SearchParameter名で一致するものもあれば、要素名=address.country -> SearchParamter名=address-country のように構造化された要素では一致しないものもあります。
Extensionに追加される項目は当然ながら(何がはいってくるかわからないので)デフォルトではSearchParameterにはならないわけですが、あえてExtensionを定義して格納する方針を定めるようなExtensionは重要な項目であることも多いですよね。
以下のようなSearchParameter定義を生成するための内容をpatient.fshファイルに追加します。
```
Instance: BirthPlaceSearchParameter
InstanceOf: SearchParameter
Usage: #definition
* url = "http://isc-demo/fhir/SearchParameter/patient-birthPlace"
* version = "0.0.1"
* name = "birthPlace"
* status = #active
* description = "出身地検索のパラメータ"
* code = #birthPlace
* base = #Patient
* type = #string
* expression = "Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value"
* comparator = #eq
```
SearchParameterで生成されるStructureDefinitionはこちらです。
比較的シンプルな定義なので、上記SUSHIの情報とのマッピングは理解しやすいと思います。
```json
{
"resourceType": "SearchParameter",
"id": "BirthPlaceSearchParameter",
"url": "http://isc-demo/fhir/SearchParameter/patient-birthPlace",
"version": "0.0.1",
"name": "birthPlace",
"status": "active",
"description": "出身地検索のパラメータ",
"code": "birthPlace",
"base": [
"Patient"
],
"type": "string",
"expression": "Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value",
"comparator": [
"eq"
]
}
```
特にSearchParameterの定義として、重要になるのは **expression** の項目と **comparator** になります。
expressionには対象となるSearchParameterへの **FHIRPath** 式を記述します。FHIRPathも詳しく説明すると長くなるので興味のある方は[こちらの公式ページ](http://hl7.org/fhir/R4/fhirpath.html)をご覧ください。
今回の定義で使っている
`Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value"`
こちらの式は、PatientリソースのJson構造に従って、階層順にPatient.extensionと指定し、複数存在する可能性があるExtensionの中から、url=(省略) である今回のExtensionを絞り込み、そのvalueを指定しています。
comparatorはどのような比較式が使えるかを指定します。詳細は[こちら](https://www.hl7.org/fhir/valueset-search-comparator.html)をご覧下さい。
# Patientに作成したExtension定義を追加する
もう一つ大事な変更があります。Patientリソースでこの作成した BirthPlace Extensionを追加することです。元々自動生成されたPatientリソースのProfile定義MyProfileを以下のように変更します。name要素のCardinalityの変更はコメントアウトしました。
```
Profile: MyPatient
Parent: Patient
Description: "An example profile of the Patient resource."
//* name 1..* MS
* extension contains BirthPlace named birthPlace 0..1
```
先ほど追加した"BirthPlace"という名前のExtensionを、Patientリソース内にbirthPlaceという名前でCardinality 0..1 で追加しています。
# ついでにテスト用リソースを作成
SUSHIでは、例示用などの目的で使用できるリソースのInstanceを作成することもできます。テストのためにこちらも利用しておきましょう。今定義したExtensionも含めることができます。
```
Instance: KamiExample
InstanceOf: MyPatient
Description: "Patientリソースのサンプル"
* name.family = "山田"
* extension[BirthPlace].valueString = "鹿児島"
```
どんなデータができたかは最後のテストでご覧いただきたいと思います。
## Let's SUSHI!
FSHファイルの用意ができました!それでは SUSHIコマンドで、fshファイルから各定義ファイルを生成しましょう!
**sushi** コマンドを実行し、以下のように2つのProfile(拡張されたPatientとExtension)、二つのInstance(SearchParameterとサンプルリソース)が生成されたら成功です。
```PowerShell
C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject>sushi .
info Running SUSHI v2.1.1 (implements FHIR Shorthand specification v1.2.0)
info Arguments:
info C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject
info No output path specified. Output to .
info Using configuration file: C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject\sushi-config.yaml
info Importing FSH text...
info Preprocessed 1 documents with 0 aliases.
info Imported 2 definitions and 2 instances.
info Checking local cache for hl7.fhir.r4.core#4.0.1...
info Found hl7.fhir.r4.core#4.0.1 in local cache.
info Loaded package hl7.fhir.r4.core#4.0.1
(node:27132) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
(node:27132) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency
info Converting FSH to FHIR resources...
info Converted 2 FHIR StructureDefinitions.
info Converted 2 FHIR instances.
info Exporting FHIR resources as JSON...
info Exported 4 FHIR resources as JSON.
info Assembling Implementation Guide sources...
info Generated ImplementationGuide-myprofileproject.json
info Assembled Implementation Guide sources; ready for IG Publisher.
╔════════════════════════ SUSHI RESULTS ══════════════════════════╗
║ ╭───────────────┬──────────────┬──────────────┬───────────────╮ ║
║ │ Profiles │ Extensions │ Logicals │ Resources │ ║
║ ├───────────────┼──────────────┼──────────────┼───────────────┤ ║
║ │ 1 │ 1 │ 0 │ 0 │ ║
║ ╰───────────────┴──────────────┴──────────────┴───────────────╯ ║
║ ╭────────────────────┬───────────────────┬────────────────────╮ ║
║ │ ValueSets │ CodeSystems │ Instances │ ║
║ ├────────────────────┼───────────────────┼────────────────────┤ ║
║ │ 0 │ 0 │ 2 │ ║
║ ╰────────────────────┴───────────────────┴────────────────────╯ ║
║ ║
╠═════════════════════════════════════════════════════════════════╣
║ FSHing for compliments? Super job! 0 Errors 0 Warnings ║
╚═════════════════════════════════════════════════════════════════╝
C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject>
```
`fsh-generated\resource` フォルダには以下のような成果物が作成されました。
| ファイル名 | 内容 |
|:----------|:------------|
| ImplementationGuide-myprofileproject.json | 今回の全ての内容を取りまとめたImplemamtionGuide |
| StructureDefinition-MyPatient.json | PatientにExtensionを追加したStructureDefinition |
| StructureDefinition-birthPlace.json | Extension birthPlaceの定義を含むStructureDefinition |
| SearchParameter-BirthPlaceSearchParameter.json | birthPlace SearchParameterの定義ファイル |
| Patient-KamiExample.json | Patientのサンプルインスタンス |
## IRIS for HealthにFHIR Profileをインポートしてテストしてみる
# IRIS for Health のFHIRリポジトリへの適用
前回の記事ではこの後、_updatePublisherを実行してIGファイル群を生成しましたが、今回は、このStuructureDefinitino/SearchParameterファイルをIRIS for HealthのFHIRリポジトリに取り込んで、新しいSearchParameterで検索できるようになるところを見ていきましょう。
FHIR Profileのインポート等について詳細は、こちらの[開発者コミュニティ記事 FHIRプロファイル](https://jp.community.intersystems.com/node/495321)をご覧章ください。
FHIRリポジトリの構築方法などは、[こちらの記事](https://jp.community.intersystems.com/node/480231)も参考になると思います。
インポートの対象となるのは、先ほど生成された5つのファイルのうち、
- StructureDefinition-MyPatient.json
- StructureDefinition-birthPlace.json
- SearchParameter-BirthPlaceSearchParameter.json
の3つです。これを別のフォルダにコピーし、さらにパッケージ全体の情報を管理するための `package.json`ファイルを用意します。
**package.json**
```json
{
"name": "SUSHI Demo",
"title": "SUSHI Demo",
"version": "0.0.1",
"author": {
"name": "ISC"
},
"fhirVersions": [
"4.0.1"
],
"bundleDependencies": false,
"date": "20201208205547",
"dependencies": {
"hl7.fhir.r4.core": "4.0.1"
},
"deprecated": false
}
```
nameやtitle,author,dateなどの項目は適宜変更して問題ありません。
(注意)各プロファイルを変更してIRISに再インポートする場合は、versionを適切に変更していく(上げていく)必要があります。
(現在のバージョン2021.1ではFHIRリポジトリには、プロファイルを削除する機能がないため、テスト環境で適切に動作確認した上で、本番環境への適用は最小の回数に抑えるなど、本番環境等でプロファイルが増えすぎないように注意をする必要があります。)
IRISの管理ポータルからHealth -> FHIR Configuration -> Package Configurationと進み、Import Packageから上記4ファイルを含むフォルダを選ぶと以下のような画面になります。

Importをクリックして、IRISへのインポートを完了します。
次に Server Configuration画面で、新規FHIRリポジトリを作成します。
(既存のFHIRリポジトリへ追加することも可能です。)

## POSTMANからテストする
先ほどSUSHIで生成された、テスト用リソースをPOSTします。検証のためには他の値のbirthPlaceを含むデータや、そもそもbirthPlaceを含まないPatientリソースなども生成するほうが良いかもしれません。

FHIRリポジトリのSearchParameter に正しく birthPlaceが追加されていれば、以下のGETリクエストでこの患者情報を取得できるはずです!
```http
GET http://localhost:52785/csp/healthshare/sushi/fhir/r4/Patient?birthPlace=鹿児島
```
正しく結果を取得できるようになったでしょうか?
新しいSearchParameterである birthPlaceが正しく追加されていない場合は、GETリクエストの応答の最初に以下の「birthPlaceというパラメータが認識されていません」というエラー情報をが記述されたOperationOutcomeリソースの情報が含まれています。このメッセージがでていないか応答メッセージを確認してみてください。
```json
{
"resource": {
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "invalid",
"diagnostics": "ParameterNotSupported",
"details": {
"text": "Unrecognized parameter 'birthPlace'. 鹿児島"
}
}
]
},
"search": {
"mode": "outcome"
}
},
```
# まとめ
SUSHIを使ってFHIRのProfile(StructureDefinition/SearchParameter)を作成し、IRIS for HealthのFHIRリポジトリにインポートして機能を拡張する流れをみていただきました。
今回は、Extensionに追加した項目をSearchParameterに追加しましたが、FHIR標準仕様で存在するが、SearchParameterにはなっていない要素(エレメント)に対して、SearchParameterを追加するということも可能です。
自由度の高いFHIRの開発では、このように機能を拡張することが可能になっていますが、一方ではInteroperabilityを担保するためにどのような拡張を行ったかという情報の共有、つまりImplemantationGuide等の作成も重要になってきます。
このシリーズのPart1,2で見てきたようにSUSHIはその両面をカバーすることができる非常にユニークで強力なオープンソースのツールです。
このようなツールとIRIS for Healthを組み合わせて、新しいFHIRソリューションが構築されることを期待しています。
今回の記事で使用したSUSHIのfshファイルおよび、生成されたStructureDefinition/SearchParameterのサンプルファイルは[こちら](https://github.com/Intersystems-jp/FHIR_SUSHI)からダウンロードすることができます。
記事
Minoru Horita · 2020年6月3日
前のパート(1、2)では、ツリーとしてのグローバルを話題に取り上げました。 この記事では、それらを疎な配列と見なします。
疎な配列は、ほとんどの値が同一であると想定される配列の種類です。
疎な配列は実際には非常に大きいため、同一の要素でメモリを占有することには意味がありません。 したがって、疎な配列を整理し、重複した値の格納にメモリが浪費されないようにすることには意味があります。
疎な配列は、J、MATLABなど一部のプログラミング言語では言語の一部になっています。 他の言語では、疎な配列を使用できるようにする特別なライブラリが存在します。 C++の場合は、Eigenなどがあります。
次の理由により、グローバルは疎な配列を実装するのに適した候補であると言えます。
特定のノード値のみを保存し、未定義のノード値を保存しないこと。
ノード値のアクセスインターフェースが、多くのプログラミング言語が多次元配列の要素にアクセスするために提供しているものとよく似ていること。
Set ^a(1, 2, 3)=5
Write ^a(1, 2, 3)
グローバルはデータを格納するためにかなり低レベルの構造を採用しているため、優れたパフォーマンス特性を備えていること(ハードウェアによっては毎秒数十万から数千万のトランザクションを処理可能、1をご覧ください)。
グローバルは永続的な構造であるため、グローバル用に十分なメモリを確保できることが事前に分かっている場合のみ、グローバルに基づいて疎な配列を作成する意味があること。
疎な配列の実装には、未定義の要素を処理する場合にデフォルトで特定の値を返すようにするという意味合いもあること。
これは、COSの$GET関数を使用して実装できます。 この例では、次のような3次元配列を見てみましょう。
SET a = $GET(^a(x,y,z), defValue)
では、どのような種類のタスクで疎な配列が必要になり、どのようにグローバルは役立つのでしょうか?
隣接行列
このような行列はグラフを表すために使用されています。
グラフが大きいほど、行列に含まれるゼロが多くなることは明らかです。 例えば社会的ネットワークのグラフをこの種の行列で表すと、ほとんどがゼロで構成されることになります。つまり、疎な配列になります。
Set ^m(id1, id2) = 1
Set ^m(id1, id3) = 1
Set ^m(id1, id4) = 1
Set ^m(id1) = 3
Set ^m(id2, id4) = 1
Set ^m(id2, id5) = 1
Set ^m(id2) = 2
....
この例では隣接行列と各ノードのエッジ数(誰とつながっているか、およびつながりの数)を ^m グローバルに保存します。
グラフの要素数が2,900万を超えない場合(この数は、8 * 最大文字列長で計算されます)、このような行列を格納するには、ビット文字列を使うのがより経済的です。ビット文字列は大きなギャップを特別な方法で最適化するからです。
ビット文字列の操作は、$BIT 関数を使用して実行されます。
; ビットの設定
SET $BIT(rowID, positionID) = 1
; ビットの取得
Write $BIT(rowID, positionID)
FSMスイッチのテーブル
FSMスイッチのグラフは通常のグラフであるため、FSMスイッチのテーブルは基本的に上述したのと同じ隣接行列です。
セル・オートマトン
最も有名なセル・オートマトンである「ライフ」ゲームでは、そのルール(セルに多数の隣接セルがある場合、セルが死ぬ)によって本質的に疎な配列が形成されます。
スティーブン・ウルフラム氏は、セル・オートマトンを新しい科学分野であると考えています。 スティーブン氏は2002年に「新しい種類の科学」と呼ばれる1,280ページの本を出版しました。同著には、セル・オートマトンの分野での成果は分離されていないものの、非常に安定しており、すべての科学分野にとって重要であることが記されています。
コンピューターで処理できるアルゴリズムがセル・オートマトンを使用して実装できることも証明されています。 セル・オートマトンは動的な環境やシステムのシミュレーション、アルゴリズムの問題の解決、その他の目的に使用されています。
巨大なフィールドがあり、セル・オートマトンのすべての中間状態を登録する必要がある場合、グローバルの使用は合理的であると言えます。
地図の作成
疎な配列の使用に関して最初に思い浮かぶのは、地図の作成です。
一般的に、地図には何も無い空間がたくさんあります。 世界地図が大きなピクセルで構成されていると想定した場合、地球上の全ピクセルの71%が海を表す疎な配列で占められていることになります。 また、地図に人工的な構造物を追加するだけの場合、95%超は何も無い空間になります。
もちろん、地図をビットマップ配列として保存する人などいません。誰もが代わりにベクトル表現を使用しています。[文字列の折り返しの区切り]しかし、ベクトル地図とは何でしょうか? これは、ポリラインとポリゴンを備えたある種の構造物です。[文字列の折り返しの区切り]本質的には、ポイントとこれらの関係を記録したデータベースです。
地図作成で最も困難な作業の1つには、ガイア宇宙望遠鏡が実行している銀河地図の作成が挙げられます。 例えて言えば、銀河は1つの巨大な疎な配列になっています。 その99,999999.......%は完全に空の空間です。 銀河地図の保存には、グローバルに基づくデータベースであるCacheが選ばれました。
このプロジェクトにおけるグローバルの正確な構造は分かりませんが、おそらく次のようなものであると想定できます。
Set ^galaxy(b, l, d) = 1; 星のカタログ番号(存在する場合)
Set ^galaxy(b, l, d, "name") = "太陽"
Set ^galaxy(b, l, d, "type") = "普通" ; 他のオプションには、ブラックホール、クエーサー、赤色矮星などがあります。
Set ^galaxy(b, l, d, "weight") = 14E50
Set ^galaxy(b, l, d, "planetes") = 7
Set ^galaxy(b, l, d, "planetes", 1) = "水星"
Set ^galaxy(b, l, d, "planetes", 1, weight) = 1E20
...
b、l、dは銀河座標であり、それぞれ緯度、経度、太陽からの距離を表しています。
グローバルベースのデータベースはスキーマレスであるため、グローバルの柔軟な構造を活かして星と惑星の特性を保存することができます。
Cacheは柔軟性に優れているというだけでなく、一連のデータを素早く保存しながら同時にインデックスグローバルを作成できるため、高速な検索を実行できるということが理由で選ばれました。
地球に話を戻すと、グローバルはこのような地図に特化したプロジェクトであるOpenStreetMap XAPI やOpenStreetMapのフォークであるFOSMで使用されていました。
つい最近のCachéハッカソンでは、ある開発者のグループがこの技術を使用して地理空間インデックスを実装していました。 詳細については、こちらの記事をご覧ください。
OpenStreetMap XAPIでグローバルを使用した地理空間インデックスの実装
イラストはこちらのプレゼン資料から引用しています。
地球全体を正方形に分割し、それをさらに小さな正方形に再帰的に分割していきます。 最終的には、グローバルを作成するための階層構造を取得します。
これで、いつでも素早く任意の正方形を要求したり、空にしたりできます。その場合はその正方形の子孫となるすべての正方形も返されるか、空になります。
グローバルに基づく詳細なスキームは、以下のようにいくつかの方法で実装できます。
方法1:
Set ^m(a, b, a, c, d, a, b,c, d, a, b, a, c, d, a, b,c, d, a, 1) = idPointOne
Set ^m(a, b, a, c, d, a, b,c, d, a, b, a, c, d, a, b,c, d, a, 2) = idPointTwo
...
方法2:
Set ^m('abacdabcdabacdabcda', 1) = idPointOne
Set ^m('abacdabcdabacdabcda', 2) = idPointTwo
...
どちらの場合も、COS/Mで任意のレベルの正方形にあるポイントを要求するのはそれほど面倒ではありません。 最初の方法では、任意のレベルで正方形の空間断片を消去するのが多少簡単になりますが、この処理が必要になることはほとんどありません。
下位レベルの正方形の例:
そして、こちらはXAPIプロジェクトで使用されているグローバルの例です。グローバルに基づいてインデックスが表現されています。
^way グローバルは、ポリライン(道路、小川など)やポリゴン(建物や森林などの閉じられた空間)の頂点を格納するために使用されています。
グローバルでの疎な配列の使用方法の大まかな分類
一部のオブジェクトの座標とその状態(地図作成、セル・オートマトン)を格納します。
疎行列を格納します。
方法2)では特定の座標が要求され、要素に値が割り当てられていない場合、疎な配列の要素のデフォルト値を取得する必要があります。
グローバルに多次元行列を格納するメリット
文字列、面、立方体などの複数の空間断片を素早く削除または選択できます。整数インデックスの場合、文字列、面、立方体などの複数の空間断片を素早く削除または選択できると便利です。
Killコマンドは、単独の要素、文字列、さらには面全体を削除できます。 グローバルにはプロパティがあるため、要素ごとに削除するよりも1000倍高速に削除できます。
この図は、グローバル ^a の3次元配列とさまざまな削除処理を表現しています。
既知のインデックスで空間断片を選択するには、Merge コマンドを使用できます。
行列の列をColumn変数に抜き出します。
; 3x3x3の疎な3次元配列を定義します
Set ^a(0,0,0)=1,^a(2,2,0)=1,^a(2,0,1)=1,^a(0,2,1)=1,^a(2,2,2)=1,^a(2,1,2)=1
Merge Column = ^a(2,2)
; Column変数を出力します
Zwrite Column
出力:
Column(0)=1
Column(2)=1
興味深いことに、$GET 経由でアドレスを指定できるColumn変数に疎な配列が含まれています。これは、デフォルト値が格納されていないためです。
$Order 関数を使用する小さなプログラムを使用して空間断片を選択することもできます。 これは、量子化されていないインデックスがある空間で特に便利です(地図の作成)。
まとめ
今日の現実は、新しい課題を提起しています。 グラフは数十億の頂点で構成でき、地図は数十億のポイントで構成できます。セル・オートマトン(1、2)に基づいて独自の世界を作りたいと考えている人もいるかもしれません。
疎な配列のデータ量はRAMに収まる大きさには圧縮できませんが、それでも疎な配列を処理する必要がある場合は、グローバルとCOSを使用してそのようなプロジェクトを実装することを検討する必要があります。
最後までお読みいただき、ありがとうございました! コメント欄で質問やリクエストをお待ちしています。
免責事項:この記事と記事に対する筆者(英語原文はSergey Kamenev氏によるものです)のコメントは単なる筆者の私見であり、InterSystemsの公式見解とは関係ありません。
また、前のパート「グローバルはデータを保存するための魔法の剣です パート2 - ツリー」も確認してください。
記事
Toshihiko Minamoto · 2021年11月3日
この連載の最初の記事では、大きなチャンクのデータをHTTP POSTメソッドのRaw本体から読み取って、それをクラスのストリームクラスとしてデータベースに格納する方法を説明しました。 2つ目の記事では、ファイルとファイル名をJSON形式にラップして送信する方法を説明しました。
それでは、大きなファイルを分割してサーバーに送るという構想を詳しく見ていきましょう。 これを行うために使用できるアプローチにはいくつかあるのですが、 この記事では、Transfer-Encodingヘッダーを使用してチャンク転送を指示する方法を説明します。 Transfer-EncodingヘッダーはHTTP/1.1仕様で導入されたものです。RFC 7230第4.1項では説明されているものの、HTTP/2仕様からはその説明が無くなっています。
Transfer-Encoding(転送符号法)ヘッダー
Transfer-Encodingヘッダーは、ペイロード本体をユーザーに安全に転送するために使用されるエンコードの形式を指定することを目的としています。 主に、動的に生成されたペイロードを正確に区切るため、そして選択されたリソースの特性から、転送効率のためのペイロードエンコードであるのか、セキュリティのためのペイロードエンコードであるのかを区別するために使用します。
このヘッダーでは次の値を使用できます。
Chunked
Compress
Deflate
gzip
Transfer-EncodingがChunkedである場合
Transfer-EncodingをChunkedに設定した場合、メッセージの本文は不特定の数の通常のチャンク、終了チャンク、トレーラー、および最後の行頭復帰・改行(CRLF)シーケンスで構成されます。
各部分は、16進数で表現されるチャンクサイズで始まり、オプションの拡張とCRLFが続きます。 その後には、チャンクの本体と最後にCRLFが続きます。 拡張にはチャンクのメタデータが含まれます。 たとえば、メタデータには署名、ハッシュ、メッセージの中途を制御する情報などが含まれることがあります。 終了チャンクは長さがゼロの通常のチャンクです。 (おそらく空の)ヘッダーフィールドで構成されるトレーラーは、終了チャンクの後に続きます。
想像しやすくするために、以下に「Transfer-Encoding = chunked」を使ってメッセージの構造を示します。
簡単なチャンク化メッセージの例は次のようになります。
13\r\n
Transferring Files \r\n
4\r\n
on\r\n
1A\r\n
community.intersystems.com
0\r\n
\r\n
このメッセージ本文は、3つの有意義なチャンクで構成されています。 最初のチャンクの長さは19オクテット、2つ目は4オクテット、そして3つ目は26オクテットです。 チャンクの終わりを示す末尾のCRLFはこのチャンクサイズに含まれないことがわかるでしょう。 ただし、CRLFを行末(EOL)マーカーとして使用する場合は、そのCRLFはメッセージの一部として考慮され、2オクテットとなります。 デコードされたメッセージは次のようになります。
Transferring Files on
community.intersystems.com
IRISでのチャンク化メッセージの作成
このチュートリアルでは、最初の記事で作成したサーバーのメソッドを使用します。 つまり、ファイルのコンテンツを直接POSTメソッドの本体に送信することになります。 ファイルのコンテンツを本体で送信するため、POSTをhttp://webserver/RestTransfer/fileに送信します。
では、IRISでチャンク化メッセージを作成する方法を見てみましょう。 HTTP/1.1を使用しているのであれば、「チャンク化リクエストの送信」のセクションの「HTTPリクエストの送信」で説明されるとおり、HTTPリクエストをチャンクで送信することができます。 このプロセスの最も良いところは、%Net.HttpRequestがメッセージ本文全体のコンテンツの長さをサーバー側で自動的に計算するため、サーバー側で何かを変更する必要がまったくないことです。 したがって、チャンク化されたリクエストを送信するには、クライアントでのみ次の手順に従う必要があります。
最初のステップは、%Net.ChunkedWriterのサブクラスを作成してOutputStreamメソッドを実装することです。 このメソッドはデータのストリームを取得し、それを調べて、分割するかどうかと分割の方法を決定し、継承されたクラスのメソッドを呼び出して出力に書き込みます。 この場合、クラスRestTransfer.ChunkedWriterを呼び出します。
次に、データの送信を行うクライアント側のメソッド(ここでは「SendFileChunked」と呼びます)で、RestTransfer.ChunkedWriterクラスのインスタンスを作成して、送信するリクエストデータを入力する必要があります。 ファイルを送信しようとしているので、面倒な作業はすべてRestTransfer.ChunkedWriterクラスで行うようにします。 Filename As %Stringというプロパティと「MAXSIZEOFCHUNK = 10000」というパラメーターを追加します。 もちろん、チャンクの最大許容サイズをプロパティとして設定し、ファイルまたはメッセージごとに設定することもできます。
最後に、%Net.HttpRequestのEntityBodyプロパティが作成したRestTransfer.ChunkedWriterクラスのインスタンスと等しくなるように設定すれば、準備完了です。
これらの手順は、ファイルをサーバーに送信する既存のメソッドに書き込んだり置換したりする必要のある新しいコードにすぎません。
このメソッドは次のようになります。
ClassMethod SendFileChunked(aFileName) As %Status
{
Set sc = $$$OK
Set request = ..GetLink()
set cw = ##class(RestTransfer.ChunkedWriter).%New()
set cw.Filename = aFileName
set request.EntityBody = cw
set sc = request.Post("/RestTransfer/file")
Quit:$System.Status.IsError(sc) sc
Set response=request.HttpResponse
do response.OutputToDevice()
Quit sc
}
%Net.ChunkedWriterクラスは、インターフェースを提供し、いくつかの実装済みメソッドとプロパティを持つ抽象ストリームクラスです。 ここでは、次のプロパティとメソッドを使用します。
プロパティTranslateTable as %Stringは、チャンクを出力ストリーム(EntityBody)に書き込むときに、チャンクの自動変換を強制します。 Rawデータを受け取ることを期待しているため、TranslateTableを “RAW” に設定する必要があります。
メソッドOutputStreamは、すべてのチャンク化操作を行うために、サブクラスによってオーバーライドされる抽象メソッドです。
メソッドWriteSingleChunk(buffer As %String)は、Content-Length HTTPヘッダーとそれに続くエンティティ本体を単一のチャンクとして書き込みます。 ファイルのサイズがMAXSIZEOFCHUNKメソッドよりも小さいかどうかを確認し、小さい場合には、このメソッドを使用します。
メソッドWriteFirstChunk(buffer As %String)は、Transfer-Encodingヘッダーとそれに続く最初のチャンクを書き込みます。 必ず存在する必要があります。 この後にさらにチャンクを書き込むため、0回以上の呼び出しが行われる可能性がありますが、その後、空の文字列を持つ最後のチャンクを書き込む強制的な呼び出しが行われます。 ファイルの長さがMAXSIZEOFCHUNKメソッドを超えることを確認したら、このメソッドを呼び出します。
メソッドWriteChunk(buffer As %String)は結果として得たチャンクを書き込みます。 最初のチャンクの後の残りのファイルが依然としてMAXSIZEOFCHUNKを上回るかを確認してから、このメソッドを使用してデータを送信します。 ファイルの最後の部分のサイズがMAXSIZEOFCHUNKよりも小さくなるまで、この作業を繰り返します。
メソッドWriteLastChunk(buffer As %String)は、最後のチャンクと、それに続く長さゼロのチャンクを書き込み、データの終わりをマークします。
上記のすべてを基にすると、クラスRestTransfer.ChunkedWriterは次のようになります。
Class RestTransfer.ChunkedWriter Extends %Net.ChunkedWriter
{
Parameter MAXSIZEOFCHUNK = 10000;
Property Filename As %String;
Method OutputStream()
{
set ..TranslateTable = "RAW"
set cTime = $zdatetime($Now(), 8, 1)
set fStream = ##class(%Stream.FileBinary).%New()
set fStream.Filename = ..Filename
set size = fStream.Size
if size < ..#MAXSIZEOFCHUNK {
set buf = fStream.Read(.size, .st)
if $$$ISERR(st)
{
THROW st
} else {
set ^log(cTime, ..Filename) = size
do ..WriteSingleChunk(buf)
}
} else {
set ^log(cTime, ..Filename, 0) = size
set len = ..#MAXSIZEOFCHUNK
set buf = fStream.Read(.len, .st)
if $$$ISERR(st)
{
THROW st
} else {
set ^log(cTime, ..Filename, 1) = len
do ..WriteFirstChunk(buf)
}
set i = 2
While 'fStream.AtEnd {
set len = ..#MAXSIZEOFCHUNK
set temp = fStream.Read(.len, .sc)
if len<..#MAXSIZEOFCHUNK
{
do ..WriteLastChunk(temp)
} else {
do ..WriteChunk(temp)
}
set ^log(cTime, ..Filename, i) = len
set i = $increment(i)
}
}
}
}
これらのメソッドがファイルをどのように分割しているかを確認するために、次の構造でグローバル^logを追加します。
//単一のチャンクで転送する場合
^log(time, filename) = size_of_the_file
//複数のチャンクで転送する場合
^log(time, filename, 0) = size_of_the_file
^log(time, filename, idx) = size_of_the_idx’s_chunk
プログラミングが完了したので、これら3つのアプローチがさまざまなファイルでどのように機能するのかを見てみましょう。 サーバーを呼び出すための単純なクラスメソッドを記述します。
ClassMethod Run()
{
// まず、グローバルを削除します。
kill ^RestTransfer.FileDescD
kill ^RestTransfer.FileDescS
// 次に、送信するファイルのリストを作成します
for filename = "D:\Downloads\wiresharkOutput.txt", // 856 バイト
"D:\Downloads\wiresharkOutput.pdf", // 60 134 バイト
"D:\Downloads\Wireshark-win64-3.4.7.exe", // 71 354 272 バイト
"D:\Downloads\IRIS_Community-2021.1.0.215.0-win_x64.exe" //542 370 224 bytes
{
write !, !, filename, !, !
// そしてデータをサーバー側に送信する3つのメソッドをすべて呼び出します。
set resp1=##class(RestTransfer.Client).SendFileChunked(filename)
if $$$ISERR(resp1) do $System.OBJ.DisplayError(resp1)
set resp1=##class(RestTransfer.Client).SendFile(filename)
if $$$ISERR(resp1) do $System.OBJ.DisplayError(resp1)
set resp1=##class(RestTransfer.Client).SendFileDirect(filename)
if $$$ISERR(resp1) do $System.OBJ.DisplayError(resp1)
}
}
クラスメソッドRunを実行した後、最初の3つのファイルの出力では、ステータスは正常となりました。 しかし、最後のファイルでは、最初と最後の呼び出しは動作するにもかかわらず、真ん中の呼び出しはエラー: 5922を返しました。これは応答待ちのタイムアウトです。 globalsメソッドを見ると、コードが11番目のファイルを保存しなかったことがわかります。 つまり、##class(RestTransfer.Client).SendFile(filename)が失敗しています。正確に言えば、JSONからデータを取り出すメソッドが成功しなかったということです。
ここで、ストリームを見ると、正常に保存されたファイルのサイズがすべて正しいことがわかります。
^logグローバルを見ると、各ファイルに対してコードが作成したチャンク数がわかります。
おそらく、実際のメッセージの本文を確認したいところでしょう。 Eduard Lebedyukは、「Webをデバッグする」という記事の中で、CSP ゲートウェイロギングとトレーシングを使用できると提案しています。
イベントログで2つ目のチャンクファイルを見ると、Transfer-Encodingヘッダーの値が実際に「chunked」となっていることがわかります。 残念ながら、サーバーはすでにメッセージを接合してしまっているため、実際のチャンクを確認することはできません。
トレース機能を使用しても、それ以上の情報はあまり表示されませんが、最後から2番目と最後のリクエストの間にギャップがあることが明らかになります。
メッセージの実際の部分を確認するには、クライアントを別のコンピューターにコピーして、ネットワークスニファーを使用します。 ここでは、Wiresharkを選択しました。これは無料のツールで、必要な機能が揃っているためです。 コードがファイルをチャンクに分割する方法をわかりやすく示すには、MAXSIZEOFCHUNKの値を100に変更して、小さなファイルを送信することができます。 すると、次のような結果が表示されます。
最後の2つのチャンクを除くすべてのチャンクの長さがHEXの64(DECの100)に等しく、データのある最後のチャンクは21 DEC(HEXでは15)であることがわかります。また、最後のチャンクのサイズがゼロであることを確認できます。 すべては正常にみえるため、仕様に合致しています。 ファイルの全長さは421(4x100+1x21)に等しく、これをグロバールで確認することもできます。
まとめ
総合的に、このアプローチは動作し、大きなファイルを問題なくサーバーに送信できることがわかります。 さらに、大量のデータをクライアントに送信する場合は、Web ゲートウェイの動作と構成の「アプリケーション・パスの構成パラメータ」セクションにあるパラメーター「応答サイズの通知」をよく読むことをお勧めします。 これは、使用するHTTPのバージョンに応じて、大量のデータを送信する際のWebゲートウェイの動作を指定するパラメーターです。
このアプローチのコードは、GitHubとInterSystems Open Exchangeにある、この例の前のバージョンに追加されています。
ファイルをチャンクで送信するというトピックでは、Transfer-Encodingヘッダーの有無に関わらずContent-Rangeヘッダーを使用して、データのどの部分が転送されているのかを示すことも可能です。 さらに、HTTP/2仕様で利用できる、まったく新しいストリームの概念を使用することができます。
いつものように、質問や提案があれば、お気軽にコメントセクションに書き込んでください。
記事
Toshihiko Minamoto · 2021年5月12日
これは、SQLインデックスに関する2部構成の記事の前半です。
第1部 - インデックスを理解する
インデックスとは?
最後に図書館に行った時のことを思い出してください。 通常そこには、分野別(そして作者順と題名順)に整理された本が並び、それぞれの棚には、本の分野を説明したコードが記載された本立てがあります。 特定の分野の本を収集する場合、すべての通路を歩いて一冊ずつ本の表紙を読む代わりに、目的の分野の本棚に直接向かって選ぶことができるでしょう。
SQLインデックスにもこれと同じ機能があります。テーブルの各行にフィールドの値へのクイック参照を提供することで、パフォーマンスを向上させています。
インデックスの設定は、最適なSQLパフォーマンスを得られるようにクラスを準備する際の主なステップの1つです。
この記事では、次のことについて説明します。
1. インデックスとは何か。いつ、なぜそれを使用するか。
2. どのようなインデックスが存在するか、どのようなシナリオに適しているのか。
3. インデックスの例
4. 作成方法
インデックスが存在する場合、どのように扱うのか。
この記事では、Sampleスキーマのクラスを参照します。 このスキーマは以下に示すGitHubリポジトリにあります。また、CachéとEnsembleでインストールされるSamplesネームスペースでも提供されています。
https://github.com/intersystems/Samples-Data
基本
永続プロパティと、永続データから確実に計算されるプロパティにインデックスを作成できます。
Sample.CompanyのTaxIDプロパティにインデックスを作成するとしましょう。 StudioまたはAtelierで、以下のコードをクラス定義に追加します。
Index TaxIDIdx On TaxID;
これに相当するDDL SQLステートメントは、次のようになります。
CREATE INDEX TaxIDIdx ON Sample.Company (TaxID);
デフォルトのグローバルインデックス構造は、次のようになります。
^Sample.CompanyI("TaxIDIdx ",<TaxIDValueAtRowID>,<RowID>) = ""
通常のデータグローバルのフィールドより、読み取るサブスクリプトが少ないところに注目してください。
「SELECT Name,TaxID FROM Sample.Company WHERE TaxID = 'J7349'」というクエリを見てみましょう。 論理的に単純なクエリです。このクエリを実行するためのクエリプランは、これを反映しています。
このプランは基本的に、指定されたTaxID値を持つ行のインデックスグローバルをチェックし、データグローバル(「マスターマップ」)を参照して一致する行を取得するように指定しています。
ここで、同じクエリを、TaxIDXにインデックスを使わずに考察してみましょう。 クエリプランの効率は、予想どおり、低下します。
インデックスがない場合、IRISの基盤のクエリ実行は、メモリを読み取って、テーブルの各行にWHERE句の条件を適用します。論理的に言って、TaxIDを共有する会社はないと思うため、この作業をたった1行のためだけに行っているのです!
もちろん、インデックスを使用するということは、インデックスと行データがディスクにあるということですので、 条件の内容とテーブルに含まれるデータの量によっては、インデックスを作成してデータを入力する際に、それ固有の問題が生じる可能性もあります。
では、プロパティにはいつインデックスを追加すればよいのでしょうか。
とされることの多いエクステントビットマップは、クラスのIDにおけるビットマップインデックスです。IRISはこれを使って行が存在するのかをすばやく検出し、COUNTクエリまたはサブクラスのクエリに役立てています。 これらのインデックスは、ビットマップインデックスがクラスに追加される際に自動的に生成されますが、次のように、クラス定義にビットマップエクステントインデックスを手動で作成することも可能です。
Index Company [ Extent, SqlName = "$Company", Type = bitmap ];
DDLのBITMAPEXTENTキーワードを使うこともできます。
CREATE BITMAPEXTENT INDEX "$Company" ON TABLE Sample.Company
複合 - 2つ以上のプロパティに基づくインデックス
Index OfficeAddrIDX On (Office.City, Office.State);
複合インデックスは通常、2つ以上のプロパティを条件とするクエリが頻繁に発生する場合に使用できます。
インデックスはグローバルレベルで格納されるため、複合インデックスでは、プロパティの順序が重要になります。 インデックスグローバルの最初のディスク読み取りは保存されるため、選択する頻度の高いプロパティを最初に指定すると、高いパフォーマンス効率を得ることができます。この例では、米国の州の数より都市の数の方が多いため、Office.Cityが最初に指定されています。
あまり選択しないプロパティを最初に指定すると、スペースの効率性が高くなります。 グローバル構造に焦点を当てれば、Stateを最初に指定すると、インデックスツリーのバランスがより高まります。 考えてみれば、各州には多数の市がありますが、1つの州にしか存在しない市もあるのです。
また、いずれかのプロパティのみを条件としたクエリを頻繁に実行するのかどうかを検討することもお勧めします。別のインデックスを定義する手間を省けるからです。
複合インデックスのグローバル構造の例を以下に示します。
^Sample.PersonI("OfficeAddrIDX"," BOSTON"," MA",100115)="~Sample.Employee~"
余談: 複合インデックスかビットマップインデックスか
複数のプロパティで条件付けするクエリの場合、個別のビットマップインデックスを使った方が1つの複合インデックスよりも効果的かどうかを検討することもできます。
ビットマップインデックスが各プロパティに適切に適合するのであれば、2つの異なるインデックスに対してビット演算した方が効率的になる可能性があります。
複合ビットマップインデックスを作成することもできます。これらはユニーク値が、インデックスを作成している複数のプロパティの共通した値となるビットマップインデックスです。 前のセクションで示したテーブルを考察してみましょう。ただし、州の代わりに、州と市のすべての可能な組み合わせ(マサチューセッツ州ボストン、マサチューセッツ州ケンブリッジ、マサチューセッツ州ロサンゼルスなど)を用いたテーブルです。両方の値に適合する行のセルは1となります。
コレクション - コレクションプロパティに基づくインデックス
次のように定義されたFavoriteColorsプロパティがあります。
Property FavoriteColors As list Of %String;
実演の目的で、インデックスは次のように定義されています。
Index fcIDX1 On FavoriteColors(ELEMENTS);Index fcIDX2 On FavoriteColors(KEYS);
ここでは、複数の値を含む単一セルのプロパティをより広く参照するために、「コレクション」を使用しています。 ここでは、List OfとArray Ofプロパティが重要で、必要に応じて区切り付きの文字列も指定できます。
コレクションプロパティは自動的に解析され、インデックスが構築されます。 電話番号などの区切り付きのプロパティでは、このメソッドを明示的に <PropertyName>BuildValueArray(value, .valueArray) と定義する必要があります。
上記のFavoriteColorsの例で考えると、お気に入りの色が青と白であるPersonのfcIDX1は、次のようになります。
^Sample.PersonI("fcIDX1"," BLUE",100115)="~Sample.Employee~"
(…)
^Sample.PersonI("fcIDX1"," WHITE",100115)="~Sample.Employee~"
そしてfcIDX2は次のようになります。
^Sample.PersonI("fcIDX2",1,100115)="~Sample.Employee~"
^Sample.PersonI("fcIDX2",2,100115)="~Sample.Employee~"
この場合、FavoriteCoslorsはListコレクションであるため、キーに基づくインデックスの有用性は、要素に基づくインデックスよりも低くなります。
コレクションプロパティのインデックスの作成と管理に関するより詳しい考慮事項については、ドキュメントをご覧ください。
https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_indices#GSQLOPT_indices_collections
ビットスライス - 数値データのビット文字列表現のビットマップ表現
Index SalaryIDX On Salary [ Type = bitslice ]; //In Sample.Employee
ビットスライスインデックスは、どの行に特定の値が含まれるかを表すフラグを含むビットマップインデックスとは異なり、最初に数値を10進数から2進数に変換し、その後で2進数値の各桁にビットマップを作成します。
上記の例を見てみましょう。現実的に考えられるよう、Salaryを$1000単位として単純化します。つまり、従業員の給与が65であれば、65,000ドルということになります。
ID 1のEmployeeのSalaryを15、ID 2のSalaryを40、ID 3のSalaryを64、ID 4のSalaryを130とします。 この場合、対応するビット値は次のようになります。
15
0
0
0
0
1
1
1
1
40
0
0
1
0
1
0
0
0
64
0
1
0
0
0
0
0
0
130
1
0
0
0
0
0
1
0
ビット文字列は8桁を超えています。 対応するビットマップ表現(ビットスライスインデックス値)は、基本的に次ように格納されます。
^Sample.PersonI("SalaryIDX",1,1) = "1000" ; Row 1 has value in 1’s place
^Sample.PersonI("SalaryIDX",2,1) = "1001" ; Rows 1 and 4 have values in 2’s place
^Sample.PersonI("SalaryIDX",3,1) = "1000" ; Row 1 has value in 4’s place
^Sample.PersonI("SalaryIDX",4,1) = "1100" ; Rows 1 and 2 have values in 8’s place
^Sample.PersonI("SalaryIDX",5,1) = "0000" ; etc…
^Sample.PersonI("SalaryIDX",6,1) = "0100"
^Sample.PersonI("SalaryIDX",7,1) = "0010"
^Sample.PersonI("SalaryIDX",8,1) = "0001"
Sample.Employeeまたはその行の給与を変更する演算(INSERT、UPDATES、DELETE)では、これらの各グローバルノードまたはビットスライスを更新する必要があることに注意してください。 ビットスライスインデックスをテーブルの複数のプロパティまたは頻繁に変更されるプロパティに追加すると、パフォーマンスにリスクが生じる可能性があります。 一般的に、ビットスライスインデックスの管理には、標準またはビットマップインデックスの管理よりもコストがかかります。
ビットスライスインデックスは非常に特殊であるため、ユースケースも特殊であり、SUM、COUTN、またはAVGなどの集計計算を実行する必要のあるクエリで使用します。
さらに、数値に対してのみ効果を発揮するため、文字列は2進数の0に変換されます。
クエリの条件をチェックするために、インデックスではなくデータテーブルを読み取る必要がある場合、クエリの実行にビットスライスインデックスは選択されません。 Sample.PersonのNameにインデックスがないとします。 Smithという姓の従業員の平均給与を計算する場合(SELECT AVG(Salary) FROM Sample.Employee WHERE Name %STARTSWITH 'Smith,' )、WHERE条件を適用するためにデータ行を読み取る必要があるため、ビットスライスは実際には使用されません。
行が頻繁に作成または削除されるテーブルのビットスライスとビットマップインデックスについても、同様のストレージに関する懸念があります。
データ - グローバルノードに格納されているデータのインデックス。
Index QuickSearchIDX On Name [ Data = (SSN, DOB, Name) ];
前のいくつのかの例で、「~Sample.Employee~」という文字列がノード自体に値として格納されていることに気づいたかもしれません。 Sample.Employeeは、Sample.Personからインデックスを継承していることを思い出してください。 特にEmployeesをクエリする場合、プロパティ条件に一致するインデックスノードの値を読み取り、そのPersonがEmployeeでもあることを確認します。
また、格納する値を明示的に定義することもできます。 インデックスグローバルノードでデータを定義すると、データグローバルの読み取りも保存できます。頻繁な選択クエリや順序付けクエリに役立ちます。
上記のインデックスを例とすると、 名前の全部または一部を指定された人物に関する識別情報を取得する場合(フロントデスクアプリケーションでクライアントの情報を検索する場合など)、「SELECT SSN, Name, DOB FROM Sample.Person WHERE Name %STARTSWITH 'Smith,J' ORDER BY Name」というクエリを実行できます。 Nameをクエリ条件としており、取得しようとしている値はすべてQuickSearchIDXグローバルノード内に格納されているため、このクエリを実行するには、グローバルのみを読み取る必要があります。
データ値は、ビットマップまたはビットスライスインデックスと保存できないことに注意してください。
^Sample.PersonI("QuickSearchIDX"," LARSON,KIRSTEN A.",100115)=$lb("~Sample.Employee~","555-55-5555",51274,"Larson,Kirsten A.")
iFindインデックス
このようなインデックスを聞いたことがあるでしょうか? 私にもありません。iFindインデックスは、ストリームプロパティで使用されますが、これを使用するには、クエリにキーワードで名前を指定する必要があります。
もう少し説明することもできますが、このことについては、Kyle Baxterがすでに有用な記事を執筆しています。
フリーテキスト検索:SQL開発者が秘密にしているテキストフィールドの検索方法
[最終更新日: 2020年4月16日 - 可読性を調整。]
記事
Tomohiro Iwamoto · 2023年4月3日
本記事は、あくまで執筆者の見解であり、インターシステムズの公式なドキュメントではありません。
IRISのoAuth2機能関連の情報発信は既に多数ありますが、本稿では
- 手順(ほぼ)ゼロでひとまず動作させてみる
- 設定の見通しを良くするために、役割ごとにサーバを分ける
- 目に見えない動作を確認する
- クライアント実装(PythonやAngular,CSPアプリケーション等)と合わせて理解する
- シングルサインオン/シングルログアウトを実現する
ということを主眼においています。
コミュニティ版で動作しますので、「とりあえず動かす」の手順に従って、どなたでもお試しいただけます。
> 現状、使用IRISバージョンはIRIS 2023.1のプレビュー版になっていますが、[ソースコード](https://github.com/IRISMeister/iris-oauth2)は適宜変更します。
手順に沿ってコンテナを起動すると下記の環境が用意されます。この環境を使用して動作を確認します。

ユーザエージェント(ブラウザ)やPython/curlからのアクセスは、全てApache (https://webgw.localdomain/) 経由になります。青枠の中のirisclient等の文字はコンテナ名(ホスト名)です。
例えば、irisclientホストの/csp/user/MyApp.Login.clsにアクセスする場合、URLとして
```
https://webgw.localdomain/irisclient/csp/user/MyApp.Login.cls
```
と指定します。
> つまり、各エンドポイントは同一のorigin (https://webgw.localdomain) を持ちます。そのため、クロスサイト固有の課題は存在しません(カバーされません)が、仮に各サーバが別のドメインに存在しても基本的には動作するはずです。
oAuth2/OIDC(OpenID Connect)の利用シーンは多種多様です。
本例は、認証・認可サーバ,クライアントアプリケーション,リソースサーバの全てがIRISで実行されるクローズドな環境(社内や組織内での使用)を想定して、認可コードフロー(Authorization Code Flow)を実現します。分かりやすい解説が、ネットにたくさんありますので、コードフロー自身の説明は本稿では行いません。
>認証・認可サーバの候補はIRIS, WindowsAD, Azure AD, AWS Cognito, Google Workspace, keycloak, OpenAMなどがあり得ます。個別に動作検証が必要です。
クライアントアプリケーション(RP)は、昨今はSPAが第一候補となると思いますが、利用環境によっては、SPA固有のセキュリティ課題に直面します。
IRISには、Confidential Clientである、従来型のWebアプリケーション(フォームをSubmitして、画面を都度再描画するタイプのWebアプリケーション)用のoAuth2関連のAPI群が用意されています。
そこで、Webアプリケーション(CSP)を選択することも考えられますが、クライアント編では、よりセキュアとされるSPA+BFF(Backend For Frontend)の構成を実現するにあたり、Webアプリケーション用APIをそのまま活用する方法をご紹介する予定です。
> 以下、サーバ編の動作確認には、CSPアプリケーションを使用しています。これは、新規開発にCSP(サーバページ)を使用しましょう、という事ではなく、BFF実現のために必要となる機能を理解するためです。BFFについては、クライアント編で触れます。BFFについては、[こちら](https://dev.to/damikun/web-app-security-understanding-the-meaning-of-the-bff-pattern-i85)の説明がわかりやすかったです。
リソースサーバの役割はデータプラットフォームであるIRISは最適な選択肢です。医療系用のサーバ機能ですがFHIRリポジトリはその良い例です。本例では、至極簡単な情報を返すAPIを使用しています。
> 少しの努力でFHIRリポジトリを組み込むことも可能です。
サーバ編とクライアント編に分けて記載します。今回はサーバ編です。
> とはいえ、クライアントとサーバが協調動作する仕組みですので、境界は少しあいまいです
---------
# 使用環境
- Windows10
ブラウザ(Chrome使用)、curl及びpythonサンプルコードを実行する環境です。
- Liunx (Ubuntu)
IRIS, WebGateway(Apache)を実行する環境です。Windows10上のwsl2、仮想マシンあるいはクラウドで動作させる事を想定しています。
参考までに私の環境は以下の通りです。
---------
|用途|O/S|ホストタイプ|
|:--|:--|:--|
|クライアントPC|Windows10 Pro|物理ホスト|
|Linux環境|ubuntu 22.04.1 LTS|上記Windows10上のwsl2|
---------
Linux環境はVMでも動作します。VMのubuntuは、[ubuntu-22.04-live-server-amd64.iso](https://releases.ubuntu.com/22.04/ubuntu-22.04.1-live-server-amd64.iso
)等を使用して、最低限のサーバ機能のみをインストールしてあれば十分です。
# Linux上に必要なソフトウェア
実行にはjq,openssl,dockerが必要です。
私の環境は以下の通りです。
```
$ jq --version
jq-1.6
$ openssl version
OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)
$ docker version
Client: Docker Engine - Community
Version: 23.0.1
```
# とりあえず動かす
下記手順でとりあえず動かしてみることが出来ます。
- 以下は、Linuxで実行します。
```bash
git clone https://github.com/IRISMeister/iris-oauth2.git --recursive
cd iris-oauth2
./first-run.sh
```
この時点で下記をLinuxで実行し、OpenIDプロバイダーのメタデータを取得できる事を確認してください。[こちら](https://github.com/IRISMeister/iris-oauth2/blob/main/docs/openid-configuration.json)のような出力が得られるはずです。
```bash
curl http://localhost/irisauth/authserver/oauth2/.well-known/openid-configuration
```
- 以下はWindowsで実行します。
クライアントPC(Windows)にホスト名(webgw.localdomain)を認識させるために、%SystemRoot%\system32\drivers\etc\hostsに下記を追加します。
wsl2使用でかつlocalhostForwarding=Trueに設定してある場合は下記のように設定します。
```
127.0.0.1 webgw.localdomain
```
VM使用時は、LinuxのIPを指定します。
```
192.168.11.48 webgw.localdomain
```
次に、httpsの設定が正しく機能しているか確認します。作成された証明書チェーンをWindows側のc:\tempにコピーします。
```
cp ssl/web/all.crt /mnt/c/temp
```
> VMの場合は、scp等を使用してssl/web/all.crtを c:\temp\all.crtにコピーしてください。以後、WSL2のコマンドのみを例示します。
PCからcurlでリソースサーバの認証なしのRESTエンドポイントにアクセスします。ユーザ指定(-u指定)していないことに注目してください。
```DOS
curl --cacert c:\temp\all.crt --ssl-no-revoke -X POST https://webgw.localdomain/irisrsc/csp/myrsc/public
{"HostName":"irisrsc","UserName":"UnknownUser","sub":"","aud":"","Status":"OK","TimeStamp":"03/28/2023 17:39:17","exp":"(1970-01-01 09:00:00)","debug":{}}
```
認証なしのRESTサービスですので成功するはずです。次にアクセストークン/IDトークンによる認証・認可チェック処理を施したエンドポイントにアクセスします。
```DOS
curl --cacert c:\temp\all.crt --ssl-no-revoke -X POST https://webgw.localdomain/irisrsc/csp/myrsc/private
{
"errors":[ {
"code":5035,
"domain":"%ObjectErrors",
"error":"エラー #5035: 一般例外 名前 'NoAccessToken' コード '5001' データ ''",
"id":"GeneralException",
"params":["NoAccessToken",5001,""
]
}
],
"summary":"エラー #5035: 一般例外 名前 'NoAccessToken' コード '5001' データ ''"
}
```
こちらは、期待通りエラーで終了します。
次に、ブラウザで[CSPベースのWEBクライアントアプリケーション](https://webgw.localdomain/irisclient/csp/user/MyApp.Login.cls)を開きます。
> プライベート認証局発行のサーバ証明書を使用しているため、初回はブラウザで「この接続ではプライバシーが保護されません」といったセキュリティ警告が出ます。アクセスを許可してください。

「oAuth2認証を行う」ボタンを押した際に、ユーザ名、パスワードを求められますので、ここではtest/testを使用してください。

権限の要求画面で「許可」を押すと各種情報が表示されます。

ページ先頭に「ログアウト(SSO)」というリンクがありますので、クリックしてください。最初のページに戻ります。
IRISコミュニティエディションで、接続数上限に達してしまうと、それ以後は[Service Unavailable]になったり、認証後のページ遷移が失敗したりしますので、ご注意ください。その場合、下記のような警告メッセージがログされます。
```
docker compose logs irisclient
iris-oauth2-irisclient-1 | 03/24/23-17:14:34:429 (1201) 2 [Generic.Event] License limit exceeded 1 times since instance start.
```
しばらく(10分ほど)待つか、終了・起動をしてください。
- 以下は、Linuxで実行します。
終了させるには下記を実行します。
```bash
./down.sh
```
# 主要エンドポイント一覧
下図は、コード認可フローを例にした、各要素の役割になります。用語としてはoAuth2を採用しています。

OIDCはoAuth2の仕組みに認証機能を載せたものなので、各要素は重複しますが異なる名称(Authorization serverはOIDC用語ではOP)で呼ばれています。
> CLIENT SERVERという表現は「何どっち?」と思われる方もおられると思いますが、Client's backend serverの事で、サーバサイドに配置されるロジック処理機能を備えたWebサーバの事です。描画を担うJavaScriptなどで記述されたClient's frontendと合わせて単にClientと呼ぶこともあります。
-----
|要素|サービス名|OIDC用語|oAuth2用語|エンドポイント|
|:--|:--|:--|:--|:--|
|ユーザエージェント|N/A|User Agent|User Agent|N/A|
|Web Gateway|webgw|N/A|N/A|[/csp/bin/Systems/Module.cxw](http://webgw.localdomain/csp/bin/Systems/Module.cxw)|
|認可サーバの管理|irisauth|N/A|N/A|[/irisauth/csp/sys/%25CSP.Portal.Home.zen](http://webgw.localdomain/irisauth/csp/sys/%25CSP.Portal.Home.zen)|
|リソースサーバ#1の管理|irisrsc|N/A|N/A|[irisrsc/csp/sys/%25CSP.Portal.Home.zen](https://webgw.localdomain/irisrsc/csp/sys/%25CSP.Portal.Home.zen)|
|リソースサーバ#1|irisrsc|N/A|Resource server|[/irisrsc/csp/myrsc/private](https://webgw.localdomain/irisrsc/csp/myrsc/private)|
|リソースサーバ#2の管理|irisrsc2|N/A|N/A|[/irisrsc2/csp/sys/%25CSP.Portal.Home.zen](https://webgw.localdomain/irisrsc2/csp/sys/%25CSP.Portal.Home.zen)|
|リソースサーバ#2|irisrsc2|N/A|Resource server|[/irisrsc2/csp/myrsc/private](https://webgw.localdomain/irisrsc2/csp/myrsc/private)|
|WebApp 1a,1bの管理|irisclient|N/A|N/A|[/irisclient/csp/sys/%25CSP.Portal.Home.zen](http://webgw.localdomain/irisclient/csp/sys/%25CSP.Portal.Home.zen)|
|WebApp 1a|irisclient|RP|Client server|[/irisclient/csp/user/MyApp.Login.cls](https://webgw.localdomain/irisclient/csp/user/MyApp.Login.cls)|
|WebApp 1b|irisclient|RP|Client server|[/irisclient2/csp/user/MyApp.AppMain.cls](https://webgw.localdomain/irisclient/csp/user2/MyApp.Login.cls)|
|WebApp 2の管理|irisclient2|N/A|N/A|[/irisclient2/csp/sys/%25CSP.Portal.Home.zen](http://webgw.localdomain/irisclient2/csp/sys/%25CSP.Portal.Home.zen)|
|WebApp 2|irisclient2|RP|Client server|[/irisclient2/csp/user/MyApp.AppMain.cls](https://webgw.localdomain/irisclient2/csp/user/MyApp.AppMain.cls)|
> エンドポイントのオリジン(https://webgw.localdomain)は省略しています
-----
組み込みのIRISユーザ(SuperUser,_SYSTEM等)のパスワードは、[merge1.cpf](https://github.com/IRISMeister/iris-oauth2/blob/master/cpf/merge1.cpf)のPasswordHashで一括で"SYS"に設定しています。管理ポータルへのログイン時に使用します。
# 導入手順の解説
first-run.shは、2~5を行っています。
1. ソースコード入手
```bash
git clone https://github.com/IRISMeister/iris-oauth2.git --recursive
```
2. SSL証明書を作成
```
./create_cert_keys.sh
```
[apache-ssl](https://github.com/IRISMeister/apache-ssl.git)に同梱のsetup.shを使って、鍵ペアを作成し、出来たsslフォルダの中身を丸ごと、ssl/web下等にコピーしています。コピー先と用途は以下の通りです。
|コピー先|使用場所|用途|
|:--|:--|:--|
|ssl/web/| ApacheのSSL設定およびクライアントアプリ(python)| Apacheとのhttps通信用|
|irisauth/ssl/auth/|認可サーバ| 認可サーバのクライアント証明書|
|irisclient/ssl/client/|CSPアプリケーション#1a,1b| IRIS(CSP)がクライアントアプリになる際のクライアント証明書|
|irisclient2/ssl/client/|CSPアプリケーション#2| IRIS(CSP)がクライアントアプリになる際のクライアント証明書|
|irisrsc/ssl/resserver/|リソースサーバ| リソースサーバのクライアント証明書|
|irisrsc2/ssl/resserver/|リソースサーバ#2| リソースサーバのクライアント証明書|
3. PCにクライアント用の証明書チェーンをコピー
all.crtには、サーバ証明書、中間認証局、ルート認証局の情報が含まれています。curlやpythonなどを使用する場合、これらを指定しないとSSL/TLSサーバ証明書の検証に失敗します。
```bash
cp ssl/web/all.crt /mnt/c/temp
```
>備忘録
>下記のコマンドで内容を確認できます。
>```bash
>openssl crl2pkcs7 -nocrl -certfile ssl/web/all.crt | openssl pkcs7 -print_certs -text -noout
>```
4. Web Gatewayの構成ファイルを上書きコピー
```bash
cp webgateway* iris-webgateway-example/
```
5. コンテナイメージをビルドする
```bash
./build.sh
```
>各種セットアップは、各サービス用のDockerfile以下に全てスクリプト化されています。iris関連のサービスは、原則、##class(MyApps.Installer).setup()で設定を行い、必要に応じてアプリケーションコードをインポートするという動作を踏襲しています。例えば、認可サーバの設定はこちらの[Dockefile](https://github.com/IRISMeister/iris-oauth2/blob/master/irisauth/Dockerfile)と、インストーラ用のクラスである[MyApps.Installer](https://github.com/IRISMeister/iris-oauth2/blob/master/irisauth/src/MyApps/Installer.cls)(内容は後述します)を使用しています。
6. ブラウザ、つまりクライアントPC(Windows)にホスト名webgw.localdomainを認識させる
上述の通りです。
# 起動方法
```
./up.sh
```
up時に表示される下記のようなjsonは、後々、pythonなどの非IRISベースのクライアントからのアクセス時に使用する事を想定しています。各々client/下に保存されます。
```json
{
"client_id": "trwAtbo5DKYBqpjwaBu9NnkQeP4PiNUgnbWU4YUVg_c",
"client_secret": "PeDUMmFKq3WoCfNfi50J6DnKH9KlTM6kHizLj1uAPqDzh5iPItU342wPvUbXp2tOwhrTCKolpg2u1IarEVFImw",
"issuer_uri": "https://webgw.localdomain/irisauth/authserver/oauth2"
}
```
コンテナ起動後、ブラウザで下記(CSPアプリケーション)を開く。
https://webgw.localdomain/irisclient/csp/user/MyApp.Login.cls
# 停止方法
```bash
./down.sh
```
# 認可サーバの設定について
## カスタマイズ内容
多様なユースケースに対応するために、[認可サーバの動作をカスタマイズする機能](https://docs.intersystems.com/irislatestj/csp/docbook/DocBook.UI.Page.cls?KEY=GOAUTH_authz#GOAUTH_authz_code)を提供しています。
特に、%OAuth2.Server.Authenticateはプロダクションには適さない可能性が高いのでなんらかのカスタマイズを行うように[注記](https://docs.intersystems.com/irislatestj/csp/docbook/DocBook.UI.Page.cls?KEY=GOAUTH_authz#GOAUTH_authz_oauth2serverauthenticate)されていますのでご注意ください。
本例では、認証関連で下記の独自クラスを採用しています。
- 認証クラス
[%ZOAuth2.Server.MyAuthenticate.cls](https://github.com/IRISMeister/iris-oauth2/blob/master/irisauth/src/%25ZOAuth2/Server/MyAuthenticate.cls)
下記を実装しています。
BeforeAuthenticate() — 必要に応じてこのメソッドを実装し、認証の前にカスタム処理を実行します。
ドキュメントに下記の記載があります。本例ではscope2が要求された場合には、応答に必ずscope99も含める処理を行っています。
>通常、このメソッドを実装する必要はありません。ただし、このメソッドの使用事例の1つとして、FHIR® で使用される launch と launch/patient のスコープを実装するのに利用するというようなものがあります。この事例では、特定の患者を含めるようにスコープを調整する必要があります。
AfterAuthenticate() — 必要に応じてこのメソッドを実装し、認証の後にカスタム処理を実行します。
ドキュメントに下記の記載があります。本例ではトークンエンドポイントからの応答にaccountno=12345というプロパティを付与する処理を行っています。
>通常、このメソッドを実装する必要はありません。ただし、このメソッドの使用事例の1つとして、FHIR® で使用される launch と launch/patient のスコープを実装するのに利用するというようなものがあります。この事例では、特定の患者を含めるようにスコープを調整する必要があります。
トークンエンドポイントからの応答はリダイレクトの関係でブラウザのDevToolでは確認できません。[pythonクライアント](https://github.com/IRISMeister/python-oauth2-client)で表示出来ます。
```
{ 'access_token': '...........',
'accountno': '12345',
'expires_at': 1680157346.845698,
'expires_in': 3600,
'id_token': '...........',
'refresh_token': '..........',
'scope': ['openid', 'profile', 'scope1', 'scope2', 'scope99'],
'token_type': 'bearer'
}
```
- ユーザクラスを検証(ユーザの検証を行うクラス)
[%ZOAuth2.Server.MyValidate.cls](https://github.com/IRISMeister/iris-oauth2/blob/master/irisauth/src/%25ZOAuth2/Server/MyValidate.cls)
下記を実装しています。
ValidateUser() — (クライアント資格情報を除くすべての付与タイプで使用)
ここでは、トークンに含まれる"aud"クレームのデフォルト値を変更したり、カスタムクレーム(customer_id)を含める処理を行っています。
```
{
"jti":"https://webgw.localdomain/irisauth/authserver/oauth2.UQK89uY7wBdysNvG-fFh44AxFu8",
"iss":"https://webgw.localdomain/irisauth/authserver/oauth2",
"sub":"test",
"exp":1680156948,
"aud":[
"https://webgw.localdomain/irisrsc/csp/myrsc",
"https://webgw.localdomain/irisrsc2/csp/myrsc",
"pZXxYLRaP8vAOjmMetLe1jBIKl0wu4ehCIA8sN7Wr-Q"
],
"scope":"openid profile scope1",
"iat":1680153348,
"customer_id":"RSC-00001",
"email":"test@examples.com",
"phone_number":"01234567"
}
```
これらの独自クラスは、下記で設定しています。

## リフレッシュトークン
「パブリッククライアント更新を許可」をオンにしています。
この設定をオンにすると、client_secretを含まない(つまりpublic clientの要件を満たすクライアント)からのリフレッシュトークンフローを受け付けます。そもそもPublic Clientにはリフレッシュトークンを発行しない、という選択もありますが、ここでは許可しています。
また、「リフレッシュ・トークンを返す」項目で「常にリフレッシュトークンを返す」を設定しています。
> 「scopeに"offline_access"が含まれている場合のみ」のように、より強めの制約を課すことも可能ですが、今回は無条件に返しています
## ユーザセッションをサポート
認可サーバの"ユーザセッションをサポート"を有効に設定しています。この機能により、シングルサインオン(SSO)、シングルログアウト(SLO)が実現します。
> ユーザセッションをユーザエージェントとRP間のセッション維持に使用する"セッション"と混同しないよう
この設定を有効にすると、認可時に使用したユーザエージェントは、以後、ユーザ名・パスワードの再入力を求めることなくユーザを認証します。以下のように動作を確認できます。
1. [CSPベースのアプリケーション#1a](https://webgw.localdomain/irisclient/csp/user/MyApp.Login.cls)をブラウザで開きます。ユーザ名・パスワードを入力し、認証を行います。
2. 同じブラウザの別タブで、異なるclient_idを持つ[CSPベースのアプリケーション#1b](https://webgw.localdomain/irisclient/csp/user2/MyApp.Login.cls)を開きます。本来であれば、ユーザ名・パスワード入力を求められますが、今回はその工程はスキップされます。
3. 上記はほぼ同じ表示内容ですが$NAMESPACE(つまり実行されているアプリケーション)が異なります。
> アプリケーションが最初に認可されたスコープと異なるスコープを要求した場合、以下のようなスコープ確認画面だけが表示されます。
> 
この時点で認可サーバで下記を実行すると、現在1個のセッションに属する(同じGroupIdを持つ)トークンが2個存在することが確認できます。
```bash
$ docker compose exec irisauth iris session iris -U%SYS "##class(%SYSTEM.SQL).Shell()"
[SQL]%SYS>>SELECT * FROM OAuth2_Server.Session
ID AuthTime Cookie Expires Scope Username
6Xks9UD1fm8HU6u6FYf5eRtlyv8IU44LM4vGEkqbI60 1679909215 6Xks9UD1fm8HU6u6FYf5eRtlyv8IU44LM4vGEkqbI60 1679995615 openid profile scope1 test
[SQL]%SYS>>SELECT ClientId, GroupId, Scope, Username FROM OAuth2_Server.AccessToken
ClientId GroupId Scope Username
qCIoFRl1jtO0KpLlCrfYb8TelYcy_G1sXW_vav_osYU 6Xks9UD1fm8HU6u6FYf5eRtlyv8IU44LM4vGEkqbI60 openid profile scope1 test
vBv3V0_tS3XEO5O15BLGOgORwk-xYlEGQA-48Do9JB8 6Xks9UD1fm8HU6u6FYf5eRtlyv8IU44LM4vGEkqbI60 openid profile scope1 test
```
4. 両方のタブでF5を何度か押して、%session.Data("COUNTER")の値が増えて行くことを確認します。
> セッションを持つアプリケーションの動作という見立てです。
5. 1個目のタブ(CSPベースのアプリケーション#1a)でログアウト(SSO)をクリックします。ログアウトが実行され、最初のページに戻ります。
6. 2個目のタブ(CSPベースのアプリケーション#1b)でF5を押します。「認証されていません! 認証を行う」と表示されます。
これで、1度のログアウト操作で、全てのアプリケーションからログアウトするSLOが動作したことがが確認できました。
同様に、[サンプルのpythonコード](https://github.com/IRISMeister/python-oauth2-client)も、一度認証を行うと、それ以降、何度実行してもユーザ名・パスワード入力を求めることはありません。これはpythonが利用するブラウザに"ユーザセッション"が記録されるためです。
```
redirect
ブラウザ --> 認可サーバ
| (ユーザセッション)
+--> リソースサーバ
```
この設定が有効の場合、認可サーバはユーザエージェントに対してCSPOAuth2Sessionという名称のクッキーをhttpOnly, Secure設定で送信します。以後、同ユーザエージェントが認可リクエストを行う際には、このクッキーが使用され、(認可サーバでのチェックを経て)ユーザを認証済みとします。

CSPOAuth2Sessionの値は、発行されるIDトークンの"sid"クレームに含まれます。
```
{
"iss":"https://webgw.localdomain/irisauth/authserver/oauth2",
"sub":"test",
"exp":1679629322,
"auth_time":1679625721,
"iat":1679625722,
"nonce":"M79MJF6HqHHDKFpK4ZZJkaD3moE",
"at_hash":"AFeWfbXALP78Y9KEhlKnp_5LJmEjthJQlJDGXh_eLPc",
"aud":[
"https://webgw.localdomain/irisrsc/csp/myrsc",
"https://webgw.localdomain/irisrsc2/csp/myrsc",
"SrGSiVPB8qWvQng-N7HV9lYUi5WWW_iscvCvGwXWGJM"
],
"azp":"SrGSiVPB8qWvQng-N7HV9lYUi5WWW_iscvCvGwXWGJM",
"sid":"yxGBivVOuMZGr2m3Z5AkScNueppl8Js_5cz2KvVt6dU"
}
```
詳細は[こちら](https://docs.intersystems.com/irislatest/csp/docbookj/DocBook.UI.Page.cls?KEY=GOAUTH_authz#GOAUTH_authz_config_ui_server)
の「ユーザ・セッションのサポート」の項目を参照ください。
## PKCE
認可コード横取り攻撃への対策である、[PKCE](https://www.rfc-editor.org/rfc/rfc7636)(ピクシーと発音するそうです)関連の設定を行っています。そのため、PublicクライアントはPKCEを実装する必要があります。
- 公開クライアントにコード交換用 Proof Key (PKCE) を適用する: 有効
- 機密クライアントにコード交換用 Proof Key (PKCE) を適用する: 無効
## ログアウト機能
OpenID Connectのログアウト機能について再確認しておきます。
実に、様々なログアウト方法が提案されています。メカニズムとして、postMessage-Based Logout,HTTP-Based Logoutがあり、ログアウト実行の起点によりRP-Initiated, OP-Initiatedがあり、さらにHTTP-Based LogoutはFront-Channel, Back-Channelがありと、利用環境に応じて様々な方法が存在します。
> postMessageとはクロスドメインのiframe間でデータ交換する仕組みです
目的は同じでシングルログアウト(SLO)、つまり、シングルサインオンの逆で、OP,RP双方からログアウトする機能を実現することです。
### 本例での設定
HTTP-Basedを使用したほうがクライアント実装が簡単になる事、バックチャネルログアウトは現在IRISでは未対応であることから、本例では、フロントチャネルログアウトをRP-Initiatedで実行しています。
ユーザセッションが有効なクライアント(irisclient)のログアウト用のリンクをクリックすると下記のようなJavaScriptを含むページが描画されます。
```
・
・
function check(start) {
個々のiframeの実行完了待ち
if (完了) doRedirect()
}
function doRedirect() {
post_logout_redirect_uriへのリダイレクト処理
}
```
> 表示がおかしくなってしまうので、scriptをscr1ptに変更しています。インジェクション攻撃扱いされています...?
JavaScriptが行っていることは、iframe hiddenで指定された各RPログアウト用のエンドポイント(複数のRPにログインしている場合、iframeも複数出来ます)を全て呼び出して、成功したら、doRedirect()で、post_logout_redirect_urisで指定されたURLにリダイレクトする、という処理です。これにより、一度の操作で全RPからのログアウトとOPからのログアウト、ログアウト後の指定したページ(本例では最初のページ)への遷移が実現します。
> 内容を確認したい場合、ログアウトする前に、ログアウト用のリンクのURLをcurlで実行してみてください。
>
>```
>curl -L --insecure "https://webgw.localdomain/irisclient/csp/sys/oauth2/OAuth2.PostLogoutRedirect.cls?register=R3_wD-F5..."
>```
一方、ユーザセッションが無効の場合は、ログアウトを実行したクライアントのみがfrontchannel_logout対象となります。
> つまり、ユーザセッションを使用して、2回目以降にユーザ名・パスワードの入力なしで、認証されたアプリケーション群が、SLOでログアウトされる対象となります。
フロントチャネルログアウト実現のために、認可サーバの設定で、下記のログアウト関連の設定を行っています。
- HTTPベースのフロントチャネルログアウトをサポート:有効
- フロントチャネルログアウトURLとともに sid (セッションID) クレームの送信をサポート:有効
また、認可サーバ(irisauth)に以下のcookie関連の設定を行っています。
[ドキュメント](https://docs.intersystems.com/iris20223/csp/docbook/Doc.View.cls?KEY=GOAUTH_authz)に従って、irisauthの/oauth2のUser Cookie Scopeをlaxとしています。
>Note:
For an InterSystems IRIS authorization server to support front channel logout, the User Cookie Scope for the /oauth2 web application must be set to Lax. For details on configuring application settings, see Create and Edit Applications.

> 本例は同じオリジンで完結している(Chromeであれば、ログアウト実行時に関わるhttpアクセスのRequest Headersに含まれるsec-fetch-site値がsame-originになっていることで確認できます)ので、この設定は不要ですが、備忘目的で設定しています。
また、クライアント(irisclient)に以下の設定を行っています。
1. Session Cookie Scopeの設定
[ドキュメント](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GOAUTH_client)に従って、irisclientの/csp/userのSession Cookie Scopeをnoneとしています。
>Note:
For an InterSystems IRIS client to support front channel logout, the Session Cookie Scope of the client application to None. For details on configuring application settings, see Create and Edit Applications.

> 本例は同じオリジンで完結している(Chromeであれば、ログアウト実行時に関わるhttpアクセスのRequest Headersに含まれるsec-fetch-site値がsame-originになっていることで確認できます)ので、この設定は不要ですが、備忘目的で設定しています。
2. "frontchannel_logout_session_required"をTrueに設定しています。
3. "frontchannel_logout_uri"に"https://webgw.localdomain/irisclient/csp/user/MyApp.Logout.cls"を設定しています。
管理ポータル上には下記のように表示されています。このURLに遷移する際は、IRISLogout=endが自動付与されます。
>If the front channel logout URL is empty, the client won't support front channel logout.
'IRISLogout=end' will always be appended to any provided URL.
IRISドキュメントに記述はありませんが、Cache'の同等機能の記述は[こちら](https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GCSP_sessions)です。IRISLogout=endは、CSPセッション情報の破棄を確実なものとするためと理解しておけば良いでしょう。
> 一般論として、ログアウト時のRP側での処理は認可サーバ側では制御不可能です。本当にセッションやトークンを破棄しているか知るすべがありません。IRISLogout=endはRPがCSPベースである場合に限り、それら(cspセッションとそれに紐づくセッションデータ)の破棄を強制するものです。非CSPベースのRPにとっては意味を持ちませんので、無視してください。
# 各サーバの設定について
各サーバの設定内容とサーバ環境を自動作成する際に使用した各種インストールスクリプトに関する内容です。
## 認可サーバ
認可サーバ上の、oAUth2/OIDC関連の設定は、[MyApps.Installer](https://github.com/IRISMeister/iris-oauth2/blob/master/irisauth/src/MyApps/Installer.cls)にスクリプト化してあります。
下記の箇所で、「OAuth 2.0 認可サーバ構成」を行っています。
```
Set cnf=##class(OAuth2.Server.Configuration).%New()
・
・
Set tSC=cnf.%Save()
```
これらの設定は、[認可サーバ](http://webgw.localdomain/irisauth/csp/sys/sec/%25CSP.UI.Portal.OAuth2.Server.Configuration.zen#0)で確認できます。

## 認可サーバ上のクライアントデスクリプション
下記のような箇所が3か所あります。これらは「 OAuth 2.0 サーバ クライアントデスクリプション」で定義されている、python, curl, angularのエントリに相当します。
```
Set c=##class(OAuth2.Server.Client).%New()
Set c.Name = "python"
・
・
Set tSC=c.%Save()
```
> これらに続くファイル操作は、利便性のためにclient_idなどをファイル出力しているだけで、本来は不要な処理です。
> これらはコンテナイメージのビルド時に実行されます。
これらの設定は、[認可サーバ](http://webgw.localdomain/irisauth/csp/sys/sec/%25CSP.UI.Portal.OAuth2.Server.ClientList.zen?)で確認できます。

## CSPベースのWebアプリケーション
実行内容の説明は、クライアント編で行います。
CSPベースのWebアプリケーションの設定は、[MyApps.Installer](https://github.com/IRISMeister/iris-oauth2/blob/master/irisclient/src/MyApps/Installer.cls)にスクリプト化してあります。
oAUth2/OIDC関連の設定(クライアントの動的登録)は、irisclient用の[RegisterAll.mac](https://github.com/IRISMeister/iris-oauth2/blob/master/irisclient/src/MyApp/RegisterAll.mac)、およびirisclient2用の[RegisterAll.mac](https://github.com/IRISMeister/iris-oauth2/blob/master/irisclient2/src/MyApp/RegisterAll.mac)にスクリプト化してあります。
> これらは[register_oauth2_client.sh](https://github.com/IRISMeister/iris-oauth2/blob/master/register_oauth2_client.sh)により、コンテナ起動後に実行されます。
これらの設定は、[クライアント用サーバ](http://webgw.localdomain/irisclient/csp/sys/sec/%25CSP.UI.Portal.OAuth2.Client.Configuration.zen?PID=USER_CLIENT_APP&IssuerEndpointID=1&IssuerEndpoint=https%3A%2F%2Fwebgw.localdomain%2Firisauth%2Fauthserver%2Foauth2#0)で確認できます。

動的登録を行った時点で、これらの内容が認可サーバに渡されて、認可サーバ上に保管されます。その内容は、認可サーバで確認できます。
> ビルド時に生成されるclient_idがURLに含まれるため、リンクを用意できません。画像イメージのみです。

## リソースサーバ
リソースサーバの設定は、[MyApps.Installer](https://github.com/IRISMeister/iris-oauth2/blob/master/irisrsc/src/MyApps/Installer.cls)にスクリプト化してあります。
リソースサーバのRESTサービスは、IRISユーザUnknownUserで動作しています。
リソースサーバは、受信したトークンのバリデーションをするために、[REST APIの実装](https://github.com/IRISMeister/iris-oauth2/blob/master/irisrsc/src/API/REST.cls)で、下記のAPIを使用しています。
アクセストークンをhttp requestから取得します。
```objectscript
set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.tSC)
```
アクセストークンのバリデーションを実行します。この際、..#AUDがアクセストークンのaudクレームに含まれていることをチェックしています。
```objectscript
if '(##class(%SYS.OAuth2.Validation).ValidateJWT($$$APP,accessToken,,..#AUD,.jsonObjectJWT,.securityParameters,.tSC)) {
```
署名の有無の確認をしています。
```objectscript
Set sigalg=$G(securityParameters("sigalg"))
if sigalg="" {
set reason=..#HTTP401UNAUTHORIZED
$$$ThrowOnError(tSC)
}
```
(べた書きしていますが)受信したアクセストークンのSCOPEクレーム値がscope1を含まない場合、http 404エラーを返しています。
```objectscript
if '(jsonObjectJWT.scope_" "["scope1 ") { set reason=..#HTTP404NOTFOUND throw }
```
oAUth2/OIDC関連の設定(クライアントの動的登録)は、[Register.mac](https://github.com/IRISMeister/iris-oauth2/blob/master/irisrsc/src/API/Register.mac)にスクリプト化してあります。
> これらは[register_oauth2_client.sh](https://github.com/IRISMeister/iris-oauth2/blob/master/register_oauth2_client.sh)により、コンテナ起動後に実行されます。
これらの設定は、[リソースサーバ](http://webgw.localdomain/irisrsc/csp/sys/sec/%25CSP.UI.Portal.OAuth2.Client.Configuration.zen?PID=RESSERVER_APP&IssuerEndpointID=1&IssuerEndpoint=https%3A%2F%2Fwebgw.localdomain%2Firisauth%2Fauthserver%2Foauth2#0)で確認できます。

動的登録を行った時点で、これらの内容が認可サーバに渡されて、認可サーバ上に保管されます。その内容は、認可サーバで確認できます。
> ビルド時に生成されるclient_idがURLに含まれるため、リンクを用意できません。画像イメージのみです。

## 署名(JWK)
認可サーバをセットアップすると、一連の暗号鍵ペアが作成されます。これらはJWTで表現されたアクセストークンやIDトークンを署名する(JWS)ために使用されます。
鍵情報は認可サーバのデータべースに保存されています。参照するにはirisauthで下記SQLを実行します。
```
$ docker compose exec irisauth iris session iris -U%SYS "##class(%SYSTEM.SQL).Shell()"
SELECT PrivateJWKS,PublicJWKS FROM OAuth2_Server.Configuration
```
PrivateJWKSの内容だけを見やすいように整形すると[こちら](https://github.com/IRISMeister/iris-oauth2/blob/main/docs/PrivateJWKS.json)のようになります。
実際にアクセストークンを https://jwt.io/ で確認してみます。ヘッダにはkidというクレームが含まれます。これはトークンの署名に使用されたキーのIDです。
```
{
"typ": "JWT",
"alg": "RS512",
"kid": "3"
}
```
これで、このトークンはkid:3で署名されていることがわかります。
この時点で、Signature Verifiedと表示されていますが、これはkid:3の公開鍵を使用して署名の確認がとれたことを示しています。
> 公開鍵は[公開エンドポイント](https://webgw.localdomain/irisauth/authserver/oauth2/jwks)から取得されています
次に、エンコード処理(データへのJWSの付与)を確認するために、ペーストしたトークンの水色の部分(直前のピリオドも)をカットします。Invalid Signatureに変わります。
さきほどSQLで表示したPrivateJWKSの内容のkid:3の部分だけ(下記のような内容)を抜き出して下のBOXにペーストします。
```
{
"kty": "RSA",
"n": "....",
"e": "....",
"d": "....",
・
・
・
"alg": "RS512",
"kid": "3"
}
```

水色部分が復元され、再度、Signature Verifiedと表示されるはずです。また、水色部分は元々ペーストしたアクセストークンのものと一致しているはずです。
> 本当に大切な秘密鍵はこういう外部サイトには張り付けないほうが無難かも、です
# ログ取得方法
各所でのログの取得方法です。
## 認可サーバ(IRIS)
認可サーバ上の実行ログを取得、参照出来ます。クライアントの要求が失敗した際、多くの場合、クライアントが知りえるのはhttpのステータスコードのみで、その理由は明示されません。認可サーバ(RPがIRISベースの場合は、クライアントサーバでも)でログを取得すれば、予期せぬ動作が発生した際に、原因のヒントを得ることができます。
- ログ取得開始
```bash
./log_start.sh
```
これ以降、発生した操作に対するログが保存されます。ログは^ISCLOGグローバルに保存されます。
- ログを出力
ログは非常に多くなるので、いったんファイルに出力してIDE等で参照するのが良いです。
```bash
./log_display.sh
```
[Webアプリケーション1a](https://webgw.localdomain/irisclient/csp/user/MyApp.Login.cls)をユーザエージェント(ブラウザ)からアクセスした際のログファイルの出力例は[こちら](https://github.com/IRISMeister/iris-oauth2/blob/master/docs/logging.txt)です。
- ログを削除
ログを削除します。ログ取得は継続します。
```bash
./log_clear.sh
```
- ログ取得停止
ログ取得を停止します。ログ(^ISCLOGグローバル)は削除されません。
```bash
./log_end.sh
```
## IRISサーバのログ確認
IRISサーバが稼働しているサービス名(認可サーバならirisauth)を指定します。
IRISコミュニティエディション使用時に、接続数オーバ等を発見できます。
```
docker compose logs -f irisauth
```
## WebGWのログ確認
WebGWコンテナ内で稼働するapacheのログを確認できます。
全体の流れを追ったり、エラー箇所を発見するのに役立ちます。
```
docker compose logs -f webgw
```