記事 Tomohiro Iwamoto · 11月 13, 2024 7m read

以前、Azure用にOAouth2クライアントをセットアップする記事を書いた時に思ったのですが、各IdPはサンプルコードとしてPythonコードや専用のモジュールを提供しているので、それがそのまま使用できれば効率が良いのにな、と思いました。

IRISが埋め込みPython機能としてWSGIをサポートしたことにより、これが簡単に実現しそうなので、その方法をご紹介したいと思います。

導入方法

今回は、IdPとしてOKTAを使用してAuthorization Codeフローを行います。

OKTAでの設定内容

参考までに、今回使用した環境を後半に記載しています。

アプリケーションの起動

コンテナ化してありますので、コンテナのビルド環境をお持ちの方は、下記を実行してください。

git clone https://github.com/IRISMeister/iris-okta-oidc-wsgi
cd iris-okta-oidc-wsgi

python/.env.templateをpython/.envという名前でコピーを作成して、OKTAで得られる設定値を指定してください。

AUTH0_CLIENT_ID="0oaxxxxxxx"  
AUTH0_CLIENT_SECRET="qUudxxxxxxxxxxx"
AUTH0_DOMAIN="dev-xxxxx.okta.com/oauth2/default"

AUTH0_CLIENT_ID,AUTH0_CLIENT_SECRETは後述の「アプリケーション追加」で使用する画面から取得できます。 AUTH0_DOMAINは、後述の「カスタムSCOPE追加」で使用する画面から取得できる発行者URIを設定します。

docker compose build
docker compose up -d

下記でIRISの管理ポータルにアクセスできます。

http://localhost:8882/csp/sys/%25CSP.Portal.Home.zen
ユーザ名:SuperUser, パスワード:SYS

WSGI環境での実行

まずは、純粋なWSGI環境での実行を行って、設定が正しくできているかを確認します。コードはこちらを使用しました。

元々はAuth0用ですが、ほぼそのままでOKTAでも使用できました

下記のコマンドでFlaskを起動します。

docker compose exec iris python3 /usr/irissys/mgr/python/run.py

ブラウザでメインページにアクセスしてください。

http://127.0.0.1:8889/ ではリダイレクトに失敗します。

「Login」をクリックするとOKTAのログイン画面が表示されます。OKTAサインアップ時に使用した多要素認証(スマホアプリ)を使用してログインしてください。

うまく動作した場合は、取得したトークンの情報等が表示されます。namespace: USERと表示されている通り、IRISへのアクセスも行っています。

Welcome Tomohiro Iwamoto!
Logout

{
    "access_token": "eyJraWQiOi.....",
    "expires_at": 1731482958,
    "expires_in": 3600,
    "id_token": "eyJraWQ......",
    "scope": "email user/*.* profile openid",
    "token_type": "Bearer",
    "userinfo": {
        "amr": [
            "mfa",
            "otp",
            "pwd"
        ],
        "at_hash": "3cRg3plSvDPqGUwEBzefoA",
        "aud": "xxxxx",
        "auth_time": 1731477799,
        "email": "iwamoto@intersystems.com",
        "exp": 1731482958,
        "iat": 1731479358,
        "idp": "xxxxxxxxxx",
        "iss": "https://dev-xxxxxx.okta.com/oauth2/default",
        "jti": "ID.Z0icZKkP61n3WDLgD08q3QxJ4Ags6_rwhrqFX3lAUjs",
        "name": "Tomohiro Iwamoto",
        "nonce": "DYrD0GKQPyXuT6Fni1So",
        "preferred_username": "iwamoto@intersystems.com",
        "sub": "xxxxxx",
        "ver": 1
    }
}
namespace: USER

「Logout」をクリックするとOKTAからのログアウトが実行され、最初のページにリダイレクトされます。

IRIS+WSGI環境での実行

ブラウザでIRIS+WSGI用のメインページにアクセスしてください。以降の操作は同じです。

同じFlask用のコードを使用していますので、全く同じ動作になります。

何が可能になったのか

これは「IRIS+WSGIで何が可能になるか?」という問いと同じですが、本トピックに限定すると、例えばbearer tokenであるアクセストークンを、cookie(flaskのsessionの仕組み)ではなく、IRISのDB上に保存する、他のCSP/RESTアプリケーションに渡す、という応用が考えられます。

元々、CSPやIRISのRESTで作成したアプリケーションがあって、そこにIdP発行のアクセストークンを渡したい、といった用法に向いているアプローチかと思います。

また、IRISでWSGIを実行することにより、gunicornのような運用レベルのWSGI用ウェブサーバを別途立てる必要がなくなります。

OKTAでの設定内容

今回使用した環境です。

こちらのトライアル環境を使用しました。若干画面が変わっていましたが、サインアップ手順はこちらを参考にしました。登録の際にはMFA(多要素認証)としてスマホが必要です。

ログインすると、次のようなメイン画面が表示されます。

以降、アプリケーション追加、カスタムSCOPE追加、認証ポリシー設定などを行っています。

アプリケーション追加

メニューのアプリケーション->アプリケーションで、flask-code-flowという名称でアプリケーションを追加します。

「一般」タブのクライアント資格情報のは下記のようになります。

一般設定は下記のようになります。

ログイン設定は下記のようになります。複数の登録がありますが、これは実行環境に合わせて各々オリジン(ブラウザで指定するURL)が異なるためです。

ログアウト設定は下記のようになります。複数の登録がある理由はログイン設定と同じです。

「サインオン」タブの設定は下記のようになります。サインオン方法としてOpenID Connectが指定されます。

「サインオン」タブの下のほうに「ユーザー認証」というセクションがありますので、そこのポリシーの詳細リンクを押して「認証ポリシー」画面に遷移します。

「認証ポリシー」では、ルールはそのままで、アプリケーションには追加したアプリケーションを「アプリを追加」で追加登録します。

「割り当て」タブのクライアント資格情報のは下記のようになります。

カスタムSCOPE追加

リソースサーバ用にカスタムのSCOPEを追加します。「セキュリティ」->「API」でdefaultを編集して、オーディエンスとして適当なURL(ここでは http://localhost/csp/healthshare/sc/fhir/r4 )を設定します。

defaultをクリックすると下記の画面に遷移します。

「スコープ」タブを選択し、「スコープを追加」を押してカスタムSCOPEを追加します。ここではuser/*.*というスコープを追加しました。

アクセスポリシー設定

アクセスポリシーを追加します。「セキュリティ」->「API」で認可サーバ:defaultを選択します。「アクセスポリシー」タブを選択し、default policyにアプリケーションを追加します。

「ルールを追加」を押して、新しいルールを追加します。以下のような設定にしました。

0
0 68
記事 Tomohiro Iwamoto · 12月 19, 2023 11m read

Debeziumをご存じでしょうか?

グローバルサミット2023にて、Debeziumを題材としたセッション「Near Real Time Analytics with InterSystems IRIS & Debezium Change Data Capture」がありましたので、ご覧になられた方もおられるかと思います。

ご興味がありましたら、グローバルサミット2023の録画アーカイブをご覧ください。

FAQによると、"dee-BEE-zee-uhm"(ディビジウム..ですかね)と読むそうです。元素周期表のように複数のDB(s)を束ねる、というニュアンスみたいです。

CDC(Change data capture)という分野のソフトウェアです。

外部データベースでの変更を追跡して、IRISに反映したいという要望は、インターオペラビリティ機能導入の動機のひとつになっています。一般的には、定期的にSELECT文のポーリングをおこなって、変更対象となるレコード群(差分。対象が少なければ全件)を外部システムから取得する方法が、お手軽で汎用性も高いですが、タイムスタンプや更新の都度に増加するようなバージョンフィールドが元テーブルに存在しない場合、どうしても、各ポーリング間で重複や見落としがでないように、受信側で工夫する必要があります。また、この方法ではデータの削除を反映することはできませんので、代替案として削除フラグを採用するといったアプリケーションでの対応が必要になります。

CDCは、DBMSのトランザクションログをキャプチャすることで、この課題への解決策を提供しています。DebeziumはRedHatが中心となっているCDCのオープンソースプロジェクトです。

CDCの何が良いのか

CDCにはいくつかの利点があります。

  • ポーリングではないので、更新が瞬時に伝わる

  • DELETEも反映できる

  • SourceになるDBMSに対して非侵襲的

    テーブル定義を変更したり専用のテーブルを作成しなくて済む。パフォーマンスへの影響が軽微。

    先進医療っぽい表現ですね。対象に与える影響が軽微というニュアンスだと思います。

  • 受信側(アプリケーション側)の設計が楽

下記は受信側の仕組みに依存する話ですが、例えばIRISのRESTサービスで受信する場合

  • ひとつのハンドラ(Restのディスパッチクラス)で、複数のテーブルを処理できる

    このことはSQLインバウンドサービスがテーブル単位であることと対照的です。

一方、トランザクションログのメカニズムは各DBMS固有なので、DBMSやそのバージョン毎にセットアップ手順、振る舞い、特性が異なる可能性があるというマイナス面があります。

セットアップ作業は、SQLインバウンドアダプタほど簡単ではありません。

Kafkaのコネクタとしての用法

DebeziumはKafkaのSourceコネクタとして使用する用法が一般的です。

引用元: https://debezium.io/documentation/reference/stable/architecture.html

Kafkaのコネクタとしての用法は本稿では扱いません。

今回のメインテーマはKafkaではありませんが、関連するいくつかのKafka用語を確認をしておきたいと思います。

ProducerとConsumer

Kakfaにメッセージを送信するデータの発生元のことをProducer、メッセージを消費する送信先のことをConsumerと呼びます。

SourceとSink

外部システムとの連携用のフレームワークをKafkaコネクトと呼びます。Kafkaコネクトにおいて、外部システムと接続する部分をコネクタと呼び、Producer 側の コネクタ は Sourceコネクタ、Consumer 側の コネクタ は Sinkコネクタと、それぞれ呼びます。

DebeziumはKafkaのSourceコネクタです。

Debeziumのスタンドアロン環境

Kafkaが提供するエンタープライズ級の機能を使いたければ、Kafkaの構成・運用を含めて検討する価値があります。一方、そうでない場合、Debeziumを単体のサーバで動作させることが出来ます。

Debezium Serverと言います。その他の選択肢として、自作のJavaアプリケーションに組み込む方法もあります。

随分とシンプルな構成になります。

KafkaのSinkコネクタを経由しなくても、Debezium自身が様々な送信先に対応しています。Debeziumから見ると、Kafkaは送信先のひとつという位置づけです。

例えば、「POSTGRES上でのデータ更新をCDCして、その内容をhttp serverに送信」したい場合、 POSTGRES用のSourceコネクタと、http Clientを使うことになります。

Debeziumは、SourceとしてこれらのDBMSに対応しています。

残念ながらIRISはSourceになれません。IRISからIRISへのデータの同期であれば非同期ミラリングがお勧めです。

Debezium Serverの起動

今回使用するソースコード一式はこちらにあります。 IRIS環境はコミュニティエディションにネームスペースMYAPPの作成と、3個の空のテーブル作成(01_createtable.sqlを使用)を行ったものになります。

$ git clone https://github.com/IRISMeister/DebeziumServer-IRIS
$ cd DebeziumServer-IRIS
$ cd postgres   (POSTGRESを試す場合。以降POSTGRESを使用します)
あるいは
$ cd mysql      (MYSQLを試す場合)
$ ./up.sh

正常に起動した場合、3個のサービスが稼働中になります。

$ docker composeps ps
NAME                         IMAGE                       COMMAND                                                     SERVICE           CREATED          STATUS                    PORTS
iris                         postgres-iris               "/tini -- /iris-main --ISCAgent false --monitorCPF false"   iris              12 minutes ago   Up 12 minutes (healthy)   2188/tcp, 53773/tcp, 0.0.0.0:1972->1972/tcp, :::1972->1972/tcp, 54773/tcp, 0.0.0.0:52873->52773/tcp, :::52873->52773/tcp
postgres-debezium-server-1   debezium/server:2.4         "/debezium/run.sh"                                          debezium-server   12 minutes ago   Up 12 minutes             8080/tcp, 8443/tcp, 8778/tcp
postgres-postgres-1          debezium/example-postgres   "docker-entrypoint.sh postgres"                             postgres          12 minutes ago   Up 12 minutes             0.0.0.0:5432->5432/tcp, :::5432->5432/tcp

動作確認

初期状態を確認します。起動直後に、POSTGRES上の既存のレコード群がIRISに送信されますのでそれを確認します。端末を2個ひらいておくと便利です。以下(端末1)をPOSTGRESの, (端末2)をIRISのSQL実行に使用します。

(端末1 PG)

$ docker compose exec -u postgres postgres psql
psql (15.2 (Debian 15.2-1.pgdg110+1))
Type "help" for help.

postgres=# select * from inventory.orders;
  id   | order_date | purchaser | quantity | product_id
-------+------------+-----------+----------+------------
 10001 | 2016-01-16 |      1001 |        1 |        102
 10002 | 2016-01-17 |      1002 |        2 |        105
 10003 | 2016-02-19 |      1002 |        2 |        106
 10004 | 2016-02-21 |      1003 |        1 |        107
(4 rows)

postgres=# select * from inventory.products;
 id  |        name        |                       description                       | weight
-----+--------------------+---------------------------------------------------------+--------
 101 | scooter            | Small 2-wheel scooter                                   |   3.14
 102 | car battery        | 12V car battery                                         |    8.1
 103 | 12-pack drill bits | 12-pack of drill bits with sizes ranging from #40 to #3 |    0.8
 104 | hammer             | 12oz carpenter's hammer                                 |   0.75
 105 | hammer             | 14oz carpenter's hammer                                 |  0.875
 106 | hammer             | 16oz carpenter's hammer                                 |      1
 107 | rocks              | box of assorted rocks                                   |    5.3
 108 | jacket             | water resistent black wind breaker                      |    0.1
 109 | spare tire         | 24 inch spare tire                                      |   22.2

(9 rows)
postgres=# select * from inventory.customers;
  id  | first_name | last_name |         email
------+------------+-----------+-----------------------
 1001 | Sally      | Thomas    | sally.thomas@acme.com
 1002 | George     | Bailey    | gbailey@foobar.com
 1003 | Edward     | Walker    | ed@walker.com
 1004 | Anne       | Kretchmar | annek@noanswer.org
(4 rows)

postgres=# \q
$

IRIS上のレコードは下記のコマンドで確認できます。POSTGRES上のレコードと同じになっているはずです。

(端末2 IRIS)

$ docker compose exec iris iris sql iris -Umyapp
[SQL]MYAPP>>set selectmode=odbc
[SQL]MYAPP>>select * from inventory.orders
         出力は省略
[SQL]MYAPP>>select * from inventory.products
[SQL]MYAPP>>select * from inventory.customers

次に、POSTGRESで各種DMLを実行します。

(端末1 PG)

update inventory.orders set quantity=200 where id=10001;
UPDATE 1
postgres=# delete from inventory.orders where id=10002;
DELETE 1
insert into inventory.orders (order_date,purchaser,quantity,product_id) values ('2023-01-01',1003,10,105);
INSERT 0 1
update inventory.products set description='商品説明' where id=101;
UPDATE 1

その結果がIRISに伝わり反映されます。

(端末2 IRIS)

[SQL]MYAPP>>select * from inventory.orders
3.      select * from inventory.orders

id      order_date      purchaser   quantity    product_id
10001   2016-01-16      1001        300         102
10003   2016-02-19      1002        2           106
10004   2016-02-21      1003        1           107
10005   2023-01-01      1003        10          105

4 Rows(s) Affected

[SQL]MYAPP>>select * from inventory.products where id=101
4.      select * from inventory.products where id=101

id      name    description     weight
101     scooter 商品説明        3.14

1 Rows(s) Affected

IRIS側の仕組み

Debezium Serverのhttp clientは、指定したエンドポイントにREST+JSON形式で内容を送信してくれます。エンドポイントにIRISのRESTサービスを指定することで、IRISでその内容をパースし、必要な処理を実行(今回は単純にSQLの実行)しています。

INSERT時には、こちら、UPDATE時には、こちらのようなJSONがPOSTされます。

payload.opにPOSTGRESへの操作の値であるc:Create, u:Update, d:Delete, r:Readが伝わりますので、その内容に基づいて、IRISのRESTディスパッチャークラス(Dispatcher.cls)にて、SQL文を組み立てて実行しています。

r:Readは、初回接続時に実行されるスナップショット取得作業の際に既存のレコード群を読み込み(READ)、それらが送信される場合に使用されます。詳細はこちらをご覧ください。

Debezium Serverについて

Debezium Serverの詳細は公式ドキュメントをご覧ください。

ドキュメントを見ると大量のコーディング例(Java)と構成例が載っており、これ全部理解してプログラムを書かないと使えないのかと思ってしまいますが、幸いコンテナイメージとして公開されていますので、今回はそれを利用しています。ソースコードも公開されています。

明言はされていませんでしたが、グローバルサミット2023のデモは、JavaベースのカスタムアプリケーションサーバからJava APIを使用してDebeziumの機能を使用するスタイルかもしれません

その他

Debezium Serverの欠点といいますか特徴として、接続先が未達になると直ぐ落ちるというのがあります。例えばIRISが停止すると、Debezium Serverが停止(今回の構成では、コンテナが停止)してしまいます。ただ、どこまで処理したかをO/Sファイル(本例ではdata/offsets.dat)に保存していますので、IRIS起動後に、Debezium Serverのコンテナを再開すれば、停止中に発生した更新をキャプチャしてくれます。

停止したコンテナの再開は下記コマンドで行います。

docker compose start debezium-server

「あれ、落ちるんだ」と思いましたが、フェールセーフ思想なのだと思います。 対障害性はKafka Connectに管理してもらう前提になっているためだと思います。

MYSQLもほぼ同じ操作で動作確認が出来ます。./mysqlに必要なファイルがあります。mysql.txtを参照ください。

また、今回は、レコードを同期しているだけですが、GS2023のように組み込みBIのキューブを作成して分析用途にしたり、何某かのビジネスロジックを実行したり、インターオペラビリティ機能に連動させたりといった応用が考えられます。

0
0 784
記事 Tomohiro Iwamoto · 10月 3, 2023 7m read

こちらの内容は、今後のリリースにより変わる(不要になる)可能性があります。

バージョン2023.2以降で、IRISスタジオが非推奨となったこともあり、VSCode拡張機能を評価される方も今後増えるかと思います。

既存のCache'資産をお持ちで、ソース管理をソースコントロールフックで実施されている方などにおかれましては、その際にサーバサイド編集を選択される方もおられるかと思います。

VSCode拡張には、Cache'/IRISスタジオの「ファイルから検索」と同じ要領でサーチを行いたいというご要望に応えるための機能が備わっています。その導入方法が、VSCodeの未公開APIを使用している関係で、ひと手間かかるものとなっているため、解説します。

方法は複数ありますが、手順を簡素化するべく、なるべくGUIを使わない方法をご紹介しています。

導入方法

サーバサイドのサーチ機能は、VSCodeの"Proposed API"であるTextSearchProvider,FileSearchProvideを使用しています。 いずれこれらのAPIが安定化してStableとしてリリースされるまでの措置として、これらを使用している拡張機能はマーケットプレイスからの導入(クリックするだけの簡単インストール)が制限されています。

サーバサイドのサーチ機能を使用する目的でVisual Studio Code Insidersリリースを使用する必要はないようです。

それまでは、マニュアルで導入する必要があります。以降、その導入手順をご紹介します。

InterSystems ObjectScriptのベータ版を導入

使用中のバージョンの「次のバージョン」のbeta.1をインストールします。

VSCode拡張は、日々更新・改良されていますので、常に最新バージョンを保つことをお勧めします。

使用中のバージョンは、下記で確認できます。

この例では2.10.3ですので、2.10.4-beta.1をダウンロードし、ダウンロードされたファイル(拡張子vsix)を、Extentionsにドラッグ&ドロップします。

再起動を促されますので、VSCodeを再起動します。

Proposed APIを有効化

Command Paletteで"Preferences: Configure Runtime Arguments"を選択し、下記を追加します。

	"enable-proposed-api": ["intersystems-community.vscode-objectscript"]

実体はC:\Users\Windowsユーザ名.vscode\argv.jsonにありますので、直接編集しても良いです。

// This configuration file allows you to pass permanent command line arguments to VS Code.
// Only a subset of arguments is currently supported to reduce the likelihood of breaking
// the installation.
//
// PLEASE DO NOT CHANGE WITHOUT UNDERSTANDING THE IMPACT
//
// NOTE: Changing this file requires a restart of VS Code.
{
	// Use software rendering instead of hardware accelerated rendering.
	// This can help in cases where you see rendering issues in VS Code.
	// "disable-hardware-acceleration": true,

	// Allows to disable crash reporting.
	// Should restart the app if the value is changed.
	"enable-crash-reporter": true,

	// Unique id used for correlating crash reports sent from this instance.
	// Do not edit this value.
	"crash-reporter-id": "xxxxxxxxxxxxx",

	"enable-proposed-api": ["intersystems-community.vscode-objectscript"]
}

有効化されると、OUTPUT(ObjectScript)に下記のようなメッセージが表示されるようになります。

VSCodeのワークスペースを作成・保存

VSCodeのワークスペースを作成・保存して、アクセスしたいIRIS環境を追加(感覚的にはマウントする感じです)します。

この作業はUIで行っても良いのですが、ここではワークスペースの設定ファイル(.code-workspace.code-workspace)を直接編集する方法をご紹介します。ワークスペースの設定ファイルをVSCode自身で編集するには、いったん、フォルダとして開き直します。

編集後の.code-workspace.code-workspaceは下記のようになります。

{
	"folders": [
		{
			"name": "local:scdemo",
			"uri": "isfs://local:scdemo/",
		},
		{
			"name": "local:samples",
			"uri": "isfs://local:samples/"
		},
		{
			"path": "."
		}
	],
	"settings": {
		"objectscript.showExplorer": false,
		"intersystems.servers": {
			"local": {
				"webServer": {
					"scheme": "http",
					"host": "localhost",
					"port": 52773
				},
				"username": "SuperUser"
			}
		}
	}
}

上記の意味は

  • local:scdemoという名前(name)で,アクセス先サーバ isfs://local/ のネームスペースscdemoを、ワークスペースにマウントする
  • local:samplesという名前(name)で,アクセス先サーバ isfs://local/ のネームスペースsamplesを、ワークスペースにマウントする
  • フォルダの"."、つまりルートフォルダをワークスペースにマウントする(これはローカルにファイルがなければ不要です)
  • ただしサーバlocalの実体は、http://localhost:52773/ 、IRISユーザ名SuperUserを使用する

となります。IRISサーバのホスト名やポート、ユーザ名は、環境に合わせて変更してください。

nameは、検索対象のフォルダ名の一部として使用されますので、あまり長くすると、サーチ結果を読みにくくなります。

これでサーバサイド編集やサーバサイドサーチを使用する準備が出来ました。ワークスペースとして開き直してください。

ご参考までに、UIで行う方法は下記になります。

接続するIRISサーバの定義と、ネームスペース指定して、仮想的なファイルシステムであるisfsをワークスペースに追加します。

使用

ここまでの作業を行ったフォルダをGitHubで公開してありますので、ご利用ください。git cloneした後に、フォルダ内の.code-workspace.code-workspaceをワークスペースとして開いて使用します。

うまく設定が出来ていれば、Exploreに下記のようなフォルダが表示されます。

この時点で、各フォルダをクリックすれば、IRISサーバ内の要素がツリー表示され、編集、保存(コンパイル)可能になっているはずです。

初めてアクセスする場合はパスワードを聞いてきます。以降、The extenstion 'InterSystems Language Server' wants to sign in using InterSystems Server Credentials.と表示されたら、Allowを押して使用するCredential(SupseUser on localなど)を選択してください。この辺りは、通常のサーバサイド編集時の操作と同じです。

肝心のサーチ機能ですが、Control+シフト+Fを押してファイルサーチ機能を開いてください。普通にVSCodeでファイルサーチを行う要領で、サーチワードを入力すると、ヒットした対象が列挙されます。大量にヒットした場合、(画像のように)Collapse Allアイコンで全体を折りたたんで、まずはヒット件数表示すると良いかもしれません。

files to includeにフォルダ指定するのと同じ要領で、フィルタ設定すると、サーチ対象もフィルタされます。

EnsLibパッケージを対象にする場合。

ワイルドカードを含む場合。

MACのみを対象にする場合。

名前が分かっている場合は、Quick Open機能を使用できます。フォルダツリーを使って、大量に存在するクラスやルーチンの中から、編集対象をピックアップするのが大変な時に役立ちます。

1
0 353
記事 Tomohiro Iwamoto · 4月 20, 2023 18m read

Azure ADをOPとして利用する

元のタイトルから外れますがAzure ADをOPとした場合に、Wepアプリケーション(CSP)とSPA+BFF形式のRPにどのような修正が必要かを調べました。
ある程度の差異は想定はしていましたが、思っていたより違うな、という印象を受けました。RP、リソースサーバ側でこれらの差異を吸収する必要がありました。

個人調べです。誤りがあるかもしれませんが、その際はご容赦ください。また、状況は刻々と変わる可能性があります。

相違点

  • frontchannel_logout_session_supportedをサポートしていない

    オプショナルな機能ではありますが、これが、一番残念でした。 sessionを使用したフロントチャネルログアウトをサポートしていないようです。実際、IDトークンに"sid"クレームが含まれていません。

    "http_logout_supported"はtrueなのでSLOは可能ですが、今回用意したクライアントでは実現していません。趣旨からそれますので、Azure使用時のSLOの実現は断念しました。ログアウト操作の対象は常に、ログアウトを実行したアプリケーション単独になります。

    AD FSはサポートしていると思われます。

  • "revocation_endpoint"をサポートしていない

    OpenId Connectディスカバリーに"revocation_endpoint"がありません(つまりサポートしていません)。

    そもそもSLOが無ければ、Revoke(アプリケーション単独でのログアウトに使用)を用意する意味はありませんので、これも断念しました。

  • Userinfoのエンドポイント

    IDトークンに同じ内容が含まれているので、それらを使うよう推奨されています。Azure AD使用時は、ユーザの情報(名前)をIDトークンから取得するよう変更しました。

    ID トークンの情報は、UserInfo エンドポイントで入手できる情報のスーパーセットです。 UserInfo エンドポイントを呼び出すトークンを取得すると、同時に ID トークンを取得できます。このため、UserInfo エンドポイントを呼び出す代わりに、トークンからユーザーの情報を取得することをお勧めします。

    UserInfoエンドポイントへのアクセスも実際に試してみましたが、エラーが発生しました。どうやらこのエンドポイントにアクセスするには、今回のような独自API(リソースサーバ)用ではなく、Graph API用のアクセストークンが要るようです。

    $ export access_token=...SCOPE['openid','profile','offline_access','api://xxxxx/scope1']で発行されたトークン...
    $ curl --insecure -H "Authorization: Bearer ${access_token}" -H 'Content-Type:application/json;charset=utf-8' https://graph.microsoft.com/oidc/userinfo 
    {"error":{"code":"InvalidAuthenticationToken","message":"Access token validation failure. Invalid audience.","innerError":{"date":"2023-04-20T01:33:53","request-id":"40c464e2-e83f-43e7-bbf5-ec50a9ea3b79","client-request-id":"40c464e2-e83f-43e7-bbf5-ec50a9ea3b79"}}}
    
    $ export access_token=...SCOPE['openid','profile','offline_access']で発行されたトークン...
    $ curl --insecure -H "Authorization: Bearer ${access_token}" -H 'Content-Type:application/json;charset=utf-8' https://graph.microsoft.com/oidc/userinfo 
    {"sub":"dwHvjtAK6XlYA1VJatjT2GY7dWBBXjhAv8ctUlcUcUE","name":"Alex Wilber","family_name":"Wilber","given_name":"Alex","picture":"https://graph.microsoft.com/v1.0/me/photo/$value","email":"AlexW@xxxxx.onmicrosoft.com"}
    
  • Introspectionをサポートしていない

    Introspectionは未サポートのようです。実行しないように修正しました。

  • アクセストークンのSCOPEパラメータ名が異なる

    SCOPEを示すパラメータ名がscpになっています。

      "scp": "scope1",
    
  • 暗黙のSCOPE

    独自(カスタム)API、今回の例ではscope1、が要求時のSCOPEクレームに含まれる場合、明示的に"openid profile offline_access"を含めても、トークンエンドポイントから取得したアクセストークンのSCOPEにはこれらを含まないようです。クライアントディスクリプション作成時に下記を指定することで対応可能です。

    Set c.AcceptNonStandardImplicitIdToken=1
    

    独自のAPIとMS Graph API用のSCOPEを混在させることは出来ないようです。MS Graph APIが優先されてしまいます。例えば

    SCOPE['openid','profile','offline_access','User.Read']の場合 
    => scp": "openid profile User.Read email","aud": "00000003-0000-0000-c000-000000000000"
    SCOPE['openid','profile','offline_access','api://xxxxx/scope1']の場合 
    => "scp": "scope1","aud": "api://xxxxx",
    SCOPE['openid','profile','offline_access','User.Read','api://xxxxx/scope1']の場合 
    => "scp": "openid profile User.Read email","aud": "00000003-0000-0000-c000-000000000000"
    

    Azure ADはAPI(リソースサーバ)ごとに、アクセストークンを使い分けるという設計思想のようです。アプリケーションが、MS Graph APIと独自APIの両方使いたかったらどうするのか、という話もありますが、今回は独自APIだけ(Userinfoのエンドポイント使用は除外したので)なので、良しとしました。

  • アクセストークン,IDトークンで同一クレーム名に異なる値が設定される

    "iss","aud"値がアクセストークン,IDトークンとで値が異なるため、クライアント側でのチェック対象を変える必要があります。

  アクセストークン
  {
    "aud": "api://xxx-xxx-xxx-xxx-xxx",
    "iss": "https://sts.windows.net/d8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416/",
  }

  IDトークン
  {
    "aud": "xxx-xxx-xxx-xxx-xxx",
    "iss": "https://login.microsoftonline.com/d8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416/v2.0",
  }
  • "aud"の値を複数指定できない

    リソースサーバ側で"aud"をチェックする処理で「リソースサーバのURL」の有無をチェックしている処理が通らなくなります。"aud"にはクライアントアプリケーションのCLIENT_IDがセットされています。ひとまずこの値をチェックするように修正をしました。

  • RefreshToken発行時のリクエストにSCOPE指定が必須

    Client_Secret指定時は、Optionalとなっているscopeですが、指定しないと下記のエラーが出ました。こちらの対応と同様にscopeを追加するために ##class(%ZSYS.OAuth2.Authorization).GetAccessTokenRefresh()を追加しました。

    AADSTS90009: Application 'xxx-xxx-xxx-xxx-xxx'(xxx-xxx-xxx-xxx-xxx) is requesting a token for itself. This scenario is supported only if resource is specified using the GUID based App Identifier. 
    
  • OIDCのクライアント動的登録機能をサポートしていない

    OIDCのクライアント動的登録機能は無いようです。

環境

Microsoft 365開発者サブスクリプション を有効化して使用しました。

開発者向けの無償のサブスクリプションです。余談ですが、Exchange Online上のメールの受信(pop3+oAuth2認証)テストにもこの環境を使用しました。

主な選択肢は下記のようにしました。

Set up a new Microsoft 365 developer sandboxを選択。
Instantを選択。
Country: AP
Admin: iwamoto
Password: xxxxxx

認証用にSMS用番号を求められるので入力。

これでiwamoto@xyz.onmicrosoft.comのようなアカウント名が付与されます。以後、このアカウントを使用して管理操作を行います。

ほおっておくと、ログインできなくなるような警告が出ました。
「組織のセキュリティを維持するために、セキュリティの既定値群がオンになっています。Microsoft Authenticator アプリをセットアップして、2 段階認証を使用してください。これが>必要になるまであと 14 日です。」
強制的に設定画面が出たので設定を行いました。スマホアプリのMicrosoft Authenticatorを使って表示されるQRコードを読み込むだけです。

Office365の一通りのアプリケーションの管理作業を行えるようになっています。また、ダミーのユーザが作成されていますので、後でログインユーザとして使用します。一番下に自分が登録されています。

付与されたアカウント(私の場合はiwamoto@xyz.onmicrosoft.com)でAzure Portalにログインします。

作業の流れ

先にお断りしておきますと、Azureでの作業は結構面倒です。

以下のような作業の流れになります。

  1. 最新のソースコードの取得

  2. Azure(OP)にアプリケーション(RP)を登録

  3. Azure(OP)にリソースサーバを登録

  4. IRIS(RP)にサーバデスクリプションを登録

  5. IRIS(RP)にサーバデスクリプション/クライアントを登録

  6. IRIS(リソースサーバ)にサーバデスクリプションを登録

  7. IRIS(リソースサーバ)にサーバデスクリプション/クライアントを登録

最新のソースコードの取得

サーバ環境

以前に、git clone実行されている方は、再度git pullをお願いします。始めて実行される方は、サーバ編をご覧ください。

cd iris-oauth2
git pull

クライアント環境

以前に、git clone実行されている方は、再度git pullをお願いします。始めて実行される方は、クライアント編をご覧ください。

cd angular-oauth2-client
git pull

Azure(OP)にアプリケーション(RP)を登録

こちらの内容に沿って作業を進めます。

アプリケーションの登録

アプリケーションの名前: myapp
サポートされているアカウントの種類: この組織ディレクトリのみに含まれるアカウント (MSFT のみ - シングル テナント)
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls

リダイレクト URI, フロントチャネルのログアウト URL追加

pythonコードでテスト実行をしたいので、2個目のリダイレクト先(https://login.microsoftonline.com/common/oauth2/nativeclient)を追加します
フロントチャネルのログアウト URLに(https://webgw.localdomain/irisclient3/csp/user/MyApp.Logout.cls)を指定します

証明書またはシークレットの追加

新しいクライアント シークレットを追加します。

以下のような情報を取得します。CLIENT_SECRET値はクライアントシークレット作成時にしか見れませんので、このタイミングで必ず書き留めます。

TENANT_ID = 'd8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416' <=[アプリの登録]/[概要]ページの基本で見れる、ディレクトリ (テナント) ID
CLIENT_ID = "f7d8xxx-xxx-xxx-xxx-xxx" <= [アプリの登録]/[概要]ページの基本で見れる、アプリケーション (クライアント) ID
CLIENT_SECRET = "xxxxxxxxxxxxx"  <=クライアントシークレット作成時の「値」のほう。(シークレットIDではない)

これでIssuer エンドポイント(https://login.microsoftonline.com/d8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416/v2.0)が確定します。後で、IRISへの登録時に使用します。

SCOPE追加

「APIの公開」画面で「Scopeの追加」を押してscope1を追加します。既定ではapi://xxxxxというプリフィックスが付きます。

Python + o365 でテスト

Azure AD側の設定が正しく行えているかの事前確認として、Pythonのo365パッケージを使用してトークン取得を行います。

こちらの記事を参考にさせていただきました。

get_token.pyの下記を取得した値に変更して実行します。

TENANT_ID = 'd8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416'
CLIENT_ID = "f7d8xxx-xxx-xxx-xxx-xxx"
CLIENT_SECRET = "xxxxxxxxxxxxx" 

SCOPES = ['openid','profile','offline_access','api://f7d8xxx-xxx-xxx-xxx-xxx/scope1']

実行するとURLが表示されるので、ブラウザにペーストします。(初回実行時は)ログイン実行を促されますので、さきほど取得したアカウント(私の場合、iwamoto@xyz.onmicrosoft.com)でログインします。リダイレクトされて空白ページに移動しますので、そのURLをpythonのプロンプトにペーストして処理を終了します。

C:\git\iris-o365>python get_token.py
Visit the following url to give consent:
https://login.microsoftonline.com/d8f44xxx-xxxx-xxxx-xxxx-xxxxxx2c5416/oauth2/v2.0/authorize?response_type=code&client_id=f7d8xxx-xxx-xxx-xxx-xxx&redirect_uri=https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient&scope=profile+openid+api%3A%2F%2Ff7d8xxx-xxx-xxx-xxx-xxx%2Fscope1&state=K1p7qcbW0PWM29nWpdZRwoMyaWPojA&access_type=offline
Paste the authenticated url here:
https://login.microsoftonline.com/common/oauth2/nativeclient?code=0.AUoAek702LUSWUq0...  [ペースト]
[エンターキーを押下]
Authentication Flow Completed. Oauth Access Token Stored. You can now use the API.
Authenticated!
C:\git\iris-o365>

token.jsonというファイルが出来ますので、access_token, id_tokenをjwt.io等でデコードして内容を確認します。出来ていなければ、何かがおかしいので、設定を見直してください。これが成功しないと、以降の操作も成功しません。

jwt.ioによるとscp値は下記でした。

 "scp": "scope1",

IDTokenの"iss","aud"値がATのそれらと値が異なる事がわかります。これはRP内でATとIDトークンの両方をチェックしようとすると良からぬ影響が出ます。今回はRPではIDトークンのバリデーションだけを行うことで対応しています。

全てのアプリケーションを登録

同様にmyapp2,bff,bff2,という名前でアプリケーションを登録し、それぞれのCLIENT_ID, CLIENT_SECRET, SCOPEを記録しておきます。

面倒です。動的登録が出来ればな...と思いました。WebアプリケーションとSPA+BFF用に、各々2個のクライアント(実行されるIRISネームスペースが異なる)を登録するため、計4通り存在します。ひとまずmyapp(既に登録済み)とbffだけでも良いです。

取得したクライアントID,クライアントシークレット,SCOPE値をIRIS実行環境に反映する必要があります。 本例では、それらをJSONファイル化しておき、実行時にロードするという方法を採用しました。

テンプレートをコピーして使用します。先ほどのmyappの内容を含め、伏字の値を実際の値で置き換えてください。

cd config
cp azure.json.template azure.json
vi azure.json

Azure ADへの登録内容とazure.jsonへの反映箇所の関係

アプリケーションの名前: myapp (今までの作業で登録済みです)
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls
フロントチャネルのログアウト URLに(https://webgw.localdomain/irisclient3/csp/user/MyApp.Logout.cls)
azure.jsonでの名称: USER_CLIENT_APP

アプリケーションの名前: myapp2
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls
フロントチャネルのログアウト URLに(https://webgw.localdomain/irisclient3/csp/user2/MyApp.Logout.cls)
azure.jsonでの名称: USER2_CLIENT_APP

アプリケーションの名前: bff
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls
フロントチャネルのログアウト URLに(https://webgw.localdomain/myapp/#/logout-bff)
azure.jsonでの名称: BFF_BFF_APP

アプリケーションの名前: bff2
リダイレクトURI: Web, https://webgw.localdomain/irisclient3/csp/sys/oauth2/OAuth2.Response.cls
フロントチャネルのログアウト URLに(https://webgw.localdomain/myapp2/#/logout-bff)
azure.jsonでの名称: BFF2_BFF_APP

Azure(OP)にリソースサーバを登録

同様にリソースサーバを登録します。リダイレクトは要りません。

アプリケーションの名前: myrsc
サポートされているアカウントの種類: この組織ディレクトリのみに含まれるアカウント (MSFT のみ - シングル テナント)

同様に新しいクライアント シークレットを追加します。

アプリケーションと同様に、ClientId, ClientSecretの値をazure.jsonの"RESSERVER_APP"下に反映しておきます。

azure.jsonの完成形

伏字だらけで少し分かりにくいですが、全てを埋めたazure.jsonは下記のようになります。

{
	"OP": "azure",
	"tenantID":"d8f44e7a-xxx-xxx-xxx-xxx",
	"issuerEndpoint":"https://login.microsoftonline.com/d8f44e7a-xxx-xxx-xxx-xxx/v2.0",
	"apps":{
		"USER_CLIENT_APP":{
			"ClientId":"e20dd7f3-xxx-xxx-xxx-xxx",
			"ClientSecret":"3bU8Q~9g8xLaAi81WshoTLZuh3rWwDO7NUaDKaa_",
			"SCOPES":"api://e20dd7f3-xxx-xxx-xxx-xxx/scope1",
			"fclouri":"https://webgw.localdomain/{{{HOSTNAME}}}/csp/user/MyApp.Logout.cls"
		},
		"USER2_CLIENT_APP":{
			"ClientId":"53bb346c-xxx-xxx-xxx-xxx",
			"ClientSecret":"oNe8Q~J-5iPyAj_zHd8r3axXxl9ffJRWrVZ0Sa~N",
			"SCOPES":"api://53bb346c-xxx-xxx-xxx-xxx/scope1",
			"fclouri":"https://webgw.localdomain/{{{HOSTNAME}}}/csp/user2/MyApp.Logout.cls"
		},
		"BFF_BFF_APP":{
			"ClientId":"a4ef08b0-xxx-xxx-xxx-xxx",
			"ClientSecret":"D2A8Q~CuxCGHYeXmUAqD7wjtY-gucdQU44Yj4b-U",
			"SCOPES":"api://a4ef08b0-xxx-xxx-xxx-xxx/scope1",
			"fclouri":"https://webgw.localdomain/myapp/#/logout-bff"
		},
		"BFF2_BFF_APP":{
			"ClientId":"dc04f6cd-xxx-xxx-xxx-xxx",
			"ClientSecret":"5Br8Q~h~CzkJW1z2NSWii0uAq0HuPvoW46cvhaKj",
			"SCOPES":"api://dc04f6cd-xxx-xxx-xxx-xxx/scope1",
			"fclouri":"https://webgw.localdomain/myapp2/#/logout-bff"
		}
	},
	"rsc":{
		"RESSERVER_APP": {
			"ClientId":"9842ba63-xxx-xxx-xxx-xxx",
			"ClientSecret":"7vJ8Q~PS7wFw_15SY.V3whxU2p3STBuvAUkTydjH"
		}
	}
}

このファイルを用意することが、下記の操作を行ったことになります。

  • IRIS(RP)にサーバデスクリプションを登録
  • IRIS(RP)にサーバデスクリプション/クライアントを登録
  • IRIS(リソースサーバ)にサーバデスクリプションを登録
  • IRIS(リソースサーバ)にサーバデスクリプション/クライアントを登録

ビルド

サーバ環境

cd iris-oauth2
cp webgateway* iris-webgateway-example/
./build.sh

クライアント

ビルドには、稼働中のサーバ環境が必要なので、この時点で行うことはありません。

実行

サーバ環境

config/azure.jsonを修正済みであることを確認した上で下記を実行してください。

./up-azure.sh
    ・
    ・
Useful links...
Web Gateway | http://webgw.localdomain/csp/bin/Systems/Module.cxw
RSC #1 SMP | http://webgw.localdomain/irisrsc/csp/sys/%25CSP.Portal.Home.zen
RSC #2 SMP | http://webgw.localdomain/irisrsc2/csp/sys/%25CSP.Portal.Home.zen
CSP based client server3 SMP | http://webgw.localdomain/irisclient3/csp/sys/%25CSP.Portal.Home.zen
CSP based client App3-1 | https://webgw.localdomain/irisclient3/csp/user/MyApp.Login.cls
CSP based client App3-2 | https://webgw.localdomain/irisclient3/csp/user2/MyApp.Login.cls
Angular based clien App | https://webgw.localdomain/myapp/ https://webgw.localdomain/myapp2/

クライアント

サーバ環境が起動した事を確認の上、実行します。

cd angular-oauth2-client
./build_and_deploy.sh
あるいは
./ng-start.sh (デバッグ実行)

操作方法

クライアント編と同じです。WebアプリケーションSPA+BFFを実行できます。

前回と異なり、ログインを実行すると、Azure ADのログイン画面が表示されますので、Microsoft 365開発者サブスクリプションで作成されたユーザでログイン(adelev@xxxxx.onmicrosoft.com等)します。

エラー

下記エラーが出た場合、IRISサーバ環境が古いままです。

$ ./build_and_deploy.sh
&#x2714; Browser application bundle generation complete.

Error: src/app/display-info-bff/display-info-bff.component.ts:44:26 - error TS2339: Property 'OP' does not exist on type '{ clientId: string; authUri: string; logoutUri: string; tokenUri: string; userinfoUri: string; redirectUri: string; scope: string; frontchannel_logout_uri: string; post_logout_redirect_uri: string; }'.

44     if (environment.auth.OP==='iris') {
                            ~~

(番外編)SAML認証

同じAD環境を使用して、コミュニティ記事(https://community.intersystems.com/post/work-saml-iris)のSAML認証をAzureで試してみました

ngrok は必要ありません。

IRISでのSAMLのサポートはoAuth2に対するそれほどは手厚くありません。下記コマンドでSAML応答(XML)の署名を確認して、その正当性を確認しています。

Set tSC = ##class(Ens.Util.XML.SecuritySignature).ValidateSAML(tSAML, tValSpec, X509File.Filename, tClockSkew)

実行方法

実行用のCSPアプリケーションを表示。

AD>エンタープライズ アプリケーション>すべてのアプリケーション>+新しいアプリケーション>独自のアプリケーションの作成を選択。

お使いのアプリの名前は何ですか?: myapp-saml ギャラリーに見つからないその他のアプリケーションを統合します (ギャラリー以外) 「作成」を押下。

シングルサインオンの設定「作業の開始」>シングル サインオン方式の選択で「SAML」を選択。

下記の必須項目に、アプリに表示されている内容をそのまま使用する。

基本的な SAML 構成
識別子: https://intersystems.com/saml/E106172E-DB35-11ED-B731-0242C0A82802
応答URL: https://webgw.localdomain/irisclient3/csp/user/SAML.MyApp.cls

[保存]を押下。

識別子はユニークであれば何でも良い。上記はアプリ内で生成した固定文字列"https://intersystems.com/saml/"+GUID。

「属性とクレーム」で「編集」を押し、詳細設定 => SAML クレームの詳細オプション 編集 属性名の形式を含める:有効 <=有効にする [保存]

この作業は不要(今回のケースでは無意味)かもしれない。

「SAML 証明書」から下記を全部ダウンロードする。
証明書 (Base64): myapp-saml.cer
証明書 (未加工): myapp-saml(1).cer
フェデレーション メタデータ XML: myapp-saml.xml

「ユーザとグループ」で「ユーザまたはグループの追加」を押し、ログインするユーザ(誰でも良いです。AlexW@xxxx.onmicrosoft.com)を追加。「割り当て」を押下して追加を完了する。

アプリで「ファイルを選択」を押し、先ほどダウンロードしたメタデータファイル(フェデレーション メタデータXML)を選択し「適用」を押す。 成功するとIdentity providerの情報やSAML要求の内容が表示される。画面最下の「Login」押下すると、Azureの「アカウントを選択する」画面が表示される。 先ほど追加したユーザ(AlexW@xxxx.onmicrosoft.com)で、ログイン。

ログインすると、画面にSAMLResponseの内容が表示される。

Validation: Success
NameID: AlexW@xxxx.onmicrosoft.com

と表示されていればログイン成功です。

オリジナル記事にもあるように、このアプリケーションは、ログインユーザの情報を取得(および署名のチェック)するだけで、アプリケーションのユーザとしての認証の仕組みは備えていません。必要に応じて、上記で得たNameIDを使用して、なんらかの方法でIRISのユーザとして認証してください。

オリジナル(GCP対応)からの修正点

オリジナル(GCP対応)を修正しています。 Azure発行のフェデレーション メタデータ XMLにnameIDFormat属性が含まれていなかったため、決め打ちで設定しています。

フォーマットについて、ドキュメント には、下記のように書いてあるが、emailAddressが返却されたのでemailAddressを採用。

現在 Azure AD では、次の SAML 2.0 のNameID フォーマット URI をサポートしています: urn:oasis:names:tc:SAML:2.0:nameid-format:persistent。

;Azure AD includes no NameIDFormat attribute. So nameIDFormat becomes null.
If $D(nameIDFormat)=0 {
  Set @..#SettingsGN@("nameIDFormat") = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
  #;Set @..#SettingsGN@("nameIDFormat") = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
  
}
Else {
  Set @..#SettingsGN@("nameIDFormat") = nameIDFormat
}
0
0 646
記事 Tomohiro Iwamoto · 4月 7, 2023 21m read

クライアントアプリケーション編

IRISだけでoAuth2/OpenID ConnectのSSO/SLO環境を実現する/サーバ編」 のクライアントアプリケーション編です。サーバ編の構成が既に稼働していることを前提にしています。

既にサーバ編でgit clone実行された方(ありがとうございます)、若干の変更がありますのでgit pullと./build.shの実行をお願いします。

oAuth2クライアントアプリケーション(OICD用語ではRP。以下RPと称します)の形態として、5種類用意しています。


形態ライブラリ登録クライアントタイプSSOSLO
WebアプリケーションIRIS/CSPConfidential実装あり実装あり
SPAAngularPublic実装あり実装なし
SPA+BFFAngular,IRIS/RESTConfidential実装あり実装あり
PythonoauthlibConfidentialN/AN/A
curlN/AConfidentialN/AN/A

PythonやcurlがConfidential(client_secretの秘匿性を十分に保てる)か、というと微妙ですが、あくまで利用者は開発者のみ、という位置づけです。

Webアプリケーション(非SPA)

従来型のWebアプリケーション、Confidential Clientの例です。

                      login/logout
      +-----------------------------------------------------+
      |                                                     | 
ブラウザ ---Apache--> CSPベースのWebアプリケーション +--> 認可サーバ
(ユーザセッション,   (CSP SessionID,AT等)            |    (ユーザセッション)
CSP sessionID)                                       +--> リソースサーバ

IRISに限りませんが、SSOで得たユーザ情報(subクレーム)を、RPやリソースサーバで使用するユーザにどのようにマッピングするか設計・実装する必要があります。ここではRPの実装例として、下記の2種類を用意しています。

  • IRISのユーザ認証をしない

    IRISユーザとの認証(マッピング)は一切行わず、RPのアプリケーションコード内で取得したoAuth2/OIDC関連の情報のみでアクセス制御する形式です。
    サーバ編で使用したWebアプリケーション#1a,1bがこれに該当します。

    内容としては、この記事でGoogleをOPとした例を元に、IRISをOPに変更したものです。
    RPの一部として、認可コードフローの開始を行っています。

    この例ではRP上はIRISユーザ、UnknowUser(RoleはWebアプリケーションで付与)で動作します。

  • ログインフォームをカスタマイズして、IRISユーザとマッピングする

    IRISのログインページのカスタマイズ機能と代行ログインの仕組みを利用して、oAuth2/OIDC関連の情報取得とRPで使用するIRISユーザとの認証(マッピング)を行います。
    RP実行ユーザはSSOログインに使用したユーザ名に"OAuth"を付与した名称を持つ代行ユーザになります。

    例えば、testというユーザでSSOした場合、RP上のIRISユーザ名はAuth2test(RoleはZAUTHENTICATEで付与)になります

    Webアプリケーション#2がこれに該当します。

    RPは、認証が完了した状態からの開始になります。上記のLogin.clsが担っていた役割は、カスタムログインページZAUTHENITICATEに隠ぺいされます。

    現時点で、こちらの方法はあまり深堀りしていません。もう少し製品サイドの機能拡充を待ちたいと思います...。

SPA

Angularで動作する、Public Clientの例です。

                      login/logout
      +-----------------------------------------+
      |                                         | 
ブラウザ(SPA) -----Apache--+--> Webpack         |
(ユーザセッション)         +-------------> 認可サーバ
                           |               (ユーザセッション)           
                           +-------------> リソースサーバ

ng(AngularのCLI)でビルドを行い、ビルド成果物(webpack)をapacheに配布します。 SPAが直接アクセストークンを入手、使用しますので、セキュアな環境が必要な場合、その漏洩対策が必要とされています。

その対策が大変なので、BFFという仕組みが提案されています

現状、SLOは機能しません。正確には、認可サーバからのログアウトは実行されます(その結果、同じユーザセッションに属するトークンは破棄されます)が、SPAベースの各RPに対して、SLOされたというイベントを安全に伝える方法が無いため、各ブラウザタブのsessionStorageに保存されているアクセストークンの破棄などを行うRP側のlogout処理をトリガ出来ないためです。

トークンがExpire(デフォルトで3,600秒)すればRP自身で気づくきっかけになります

RPが定期的にuserinfo等を取得して、ユーザセッションが有効か確認するという方法はあり得ますが、認可サーバへのストレスが大きいです。

SLO実行後、認可サーバ上のユーザセッションは終了していますので、その後のRPでのトークンの更新やログアウト,リソースサーバでのGetIntrospectionは失敗します。

OpenID Connect Session Management 1.0のSession Status Change Notificationがこの仕組みに関する仕様です。
RPの画面にRP用、OP用のiframeをhiddenで用意しておいて、その両者(クロスドメインの可能性あり)でデータ交換することで、ログアウトされた(セッションのステータスが変更した)ことを知る...みたいな内容です。 IRISはOpenID Connect Session Management 1.0をサポートしていません。

IRISが対応しているのはfront channel logoutです。

もし深堀りされたいなら、こちらなどが参考になりそうです。

SPA+BFF

AngularにBFF(Backend For Frontend)を追加することで、Confidential Client化する例です。

                      login/logout
      +-----------------------------------------------------+
      |                                                     | 
ブラウザ(SPA) -----Apache--+--> Webpack                     |
(ユーザセッション,         +--> REST ------------+----> 認可サーバ
sessionID)                      (sessionID,AT等) |      (ユーザセッション)
                                                 +--> リソースサーバ

BFFについては、こちらの説明がわかりやすかったです。

ざっくりと言ってしまえば、SPA単独と比べてよりセキュアで、RPの種類が増えた際にアーキテクチャ上のメリットがある、ということです。

本BFF実装は、下記のIRISのRESTエンドポイント(そこで使用されるRP用のAPI)をAngularから呼び出す事で実現しています。

Angular側の大半の処理はbff.service.tsにあります。IRISのRESTエンドポイントはBFF.REST.clsで実装されています。


AngularIRIS実行されるAPI
接続、ログイン/getauthurl##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint()
userinfo取得/userinfo##class(%SYS.OAuth2.AccessToken).GetUserinfo()
リソースサーバアクセス/call通常のhttpリクエスト+アクセストークン
トークン更新/refresh##class(%ZSYS.OAuth2.Authorization).GetAccessTokenRefreshById() *1
ログアウト/getauthurl##class(%ZSYS.OAuth2.Authorization).GetLogoutEndpoint() *2
トークン破棄/revocation##class(%SYS.OAuth2.AccessToken).RevokeToken()

(*1)トークン更新は独自にメソッドを実装しています。
(*2)現在、独自メソッドを実装していますが、V2023.2以降で標準APIに反映される予定です。

ユーザエージェント-BFF間のセッション維持のために、##class(BFF.REST).GetAuthorizationCodeEndpoint()で、サーバ側で独自にsessionID(httponlyクッキー)発行しています。セッションIDの生成ロジックはセキュリティ強度に直結しますので、適宜見直してください。

Set sessionid=##class(%OAuth2.Utils).Base64UrlEncode($system.Encryption.GenCryptRand(12))  // 要変更
  ・
  ・
Do %response.SetCookie(..#SESSIONCOOKIE,sessionid,,..#COOKIEPATH,,..#SECURECOOKIE,1) ; secure,httponly 

Python

これはアプリケーションとは呼べないです。
認可コードフローを利用する、デバッグ、実験用途のpythonクライアントです。Confidential Clientとして登録しています。 ブラウザで実行すると、リダイレクトの連鎖の中で見落としてしまう内容を捕捉するために使用しました。

python +--> ブラウザ -----Apache--------> 認可サーバ
       |    (ユーザセッション)     (ユーザセッション)
       +---Apache---> リソースサーバ

curl

これはアプリケーションとは呼べないです。
これも、サーバの動作確認のためのデバッグ用途です。Confidential Clientとしています。 リソースオーナー・パスワード・クレデンシャルズのクライアントとして使用します。これもデバッグ、実験用途なので、受信したトークンの有効性チェック等は一切行っていません。

リソースオーナー・パスワード・クレデンシャルズの使用は禁止されるようです。

curl --Apache--+--> 認可サーバ
               +--> リソースサーバ

クライアント認証メソッドの設定

認可サーバへのRP登録時に、RPがトークンエンドポイントにアクセスする際のクライアント認証メソッドを指定します。

SPAだけは「エンコードされたボディから(client_secret_post)」を選択しています。残りは全て「ベーシック(client_secret_basic)」になっています。

下記は、MyApps.Installer.clsからの抜粋。

Set c.Metadata."token_endpoint_auth_method" = "client_secret_basic"

「エンコードされたボディから」はPOSTリクエストのBodyにパラメータとして渡す形式、「ベーシック」は、HTTPヘッダでAuthorization: Basic BASE64(client_id:client_secret)を指定する形式です。

IRISに用意されているRP用のAPI群

IRISベースのRP(CSPベースのWebアプリケーション、SPA+BFFのBFFサーバ)の場合、RP用に用意された以下のAPI群を使用できます。

詳細は、ドキュメントを参照ください。

ログイン時

##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint()は、認可サーバへのログインを実行するためのURLを返却します。ユーザがこのURLをクリックすることで、認可コードフローが開始します。

使用例はMyApp.Login.clsを参照ください。

set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
  $$$APP,
  scope,
  "https://webgw.localdomain/irisclient/csp/"_ns_"/MyApp.AppMain.cls",
  .properties,
  .isAuthorized,
  .sc)

$$$APPはRPを認可サーバに登録した際に使用した登録名(USER_CLIENT_APP等)です。scopeには認可を要求するスコープ("openid profile scope1"等)を指定します。

返却されるURLは次のような内容になります。いわゆる承認リクエストです。

https://webgw.localdomain/irisauth/authserver/oauth2/authorize
  ?response_type=code
  &client_id=01yJ2zW0K9XQqrroIB4b7PLci3NkJBegm0b_kBcFhgw
  &redirect_uri=https%3A//webgw.localdomain/irisclient/csp/sys/oauth2/OAuth2.Response.cls
  &scope=openid%20profile%20scope1
  &state=gWr5ipHAtskDhf0fFbNtxxJWnwY
  &nonce=paEDbHweG2xiJxWxotTfaJPbdB4
  &code_challenge=8X5l4fxCNOdLj-u87Mu3UwRH96dxYpjoGcv6Z-JZmEc
  &code_challenge_method=S256

RPのリダイレクト先を指定するredirectクエリパラメータ値がOAuth2.Response.clsになっていますが、これはIRISベースのRP専用のランディングページになっていて、RPがするべきこと(認可コードの取得、stateのチェック、アクセストークンリクエストの発行、などなど)を行ってくれます。

RPの登録内容(クライアントのリダイレクトURL)も、下記のようにホスト名、ポート番号、プレフィックスのみを指定すれば、このリダイレクト先が自動選択されます。

CSP(非REST)ベースのアプリケーションは、CSPセッションという独自のセッションID(%session.SessionId)と保存領域を作成します。oAuth2クライアントとして一度認証・認可を受けると、認証関連の情報(アクセストークン,有効期限など)を、このCSPセッションIDを主キーとしたテーブルに保存します(さきほどのOAuth2.Response.clsが実行)。

$ docker compose exec irisclient iris session iris -U%SYS "##class(%SYSTEM.SQL).Shell()"
[SQL]%SYS>>select ApplicationName,SessionId,Expires,ResponseType,Scope,CONVERT(VARCHAR(10),AccessToken) from OAuth2.AccessToken

ApplicationName SessionId       Expires ResponseType    Scope   Expression_6
CLIENT_APP      S1YXEXP9SP      1677551976      code    openid profile scope1 scope2 scope99    eyJ0eXAiOi

一方、ブラウザにはCSPセッションID(を拡張したデータ)のみがセッションクッキー(HttpOnly, デフォルト設定ではSameSite=Strict)で保存されます。

この辺りは従来のCSPのメカニズムそのものです。例えば CSPSESSIONID-SP-443-UP-irisclient-csp-user- というクッキー名になります。

認可コードフローの終了時には、GetAuthorizationCodeEndpoint()の第3引数で指定したURL、この場合はMyApp.AppMain.clsにリダイレクトされます。

認証済みか否かの確認

##class(%SYS.OAuth2.AccessToken).IsAuthorized()はRPが認証済みかどうかを確認するAPIです。

認可コードフローのリダイレクト先であるOAuth2.Response.clsはRPが認証済みかどうかを確認するための情報元をRPに格納します

2つの事を行ってくれます。

  • その情報の状態によってRPが既に認証済みかどうかの確認を行います

  • 認証済みである場合、アクセストークン、IDトークンなどを返却します

MyApp.Login.clsのOnPreHTTP()や、MyApp.AppMain.clsで使用されています。

RPが既に認証済みの場合は、MyApp.Login.clsのOnPreHTTP()が実行され、IsAuthorized()がTrueとなり、認可サーバを経由することなく、MyApp.AppMain.clsにリダイレクトされます。

ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
{
  #dim %response as %CSP.Response
  set scope="openid profile "_..#SCOPES
  if ##class(%SYS.OAuth2.AccessToken).IsAuthorized($$$APP,,scope,.accessToken,.idtoken,.responseProperties,.error) {
    set %response.Redirect="MyApp.AppMain.cls"
  }
  Return $$$OK
}

IDトークンの有効性の確認(Validation)

##class(%SYS.OAuth2.Validation).ValidateIDToken()はIDトークンの有効性を確認します。

どのような確認を行うかは、OIDC で定められています。

MyApp.AppMain.clsで使用されています。

set valid=##class(%SYS.OAuth2.Validation).ValidateIDToken(
    $$$APP,
    idtoken,
    accessToken,
    ..#SCOPES,
    ..#RSCURL1,  // RSCURL2の存在チェックはしていないので注意
    .jsonObject,
    .securityParameters,
    .sc)

トークンの受信後には署名の有無の確認を行います。

        Set isSigned=$Data(securityParameters("sigalg"))#2
        if 'isSigned="" { 
            write "署名されていません !<br>",!
        }

Introspection

##class(%SYS.OAuth2.AccessToken).GetIntrospection()は認可サーバにアクセストークンの有効性を問い合わせます。

認可サーバへのトラフィックが発生するため、opaqueのアクセストークンの場合は必須ですが、JWTの場合は任意とされています

MyApp.AppMain.clsで使用されています。

set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection($$$APP,accessToken,.jsonObject)

ログアウト時

##class(%SYS.OAuth2.Authorization).GetLogoutEndpoint()は、SLO(シングルログアウト)を実行するためのURLを返却します。ユーザがこのURLをクリックすることで、RP Initiatedのフロントチャネルログアウトが開始します。

Set ns=$ZCVT($NAMESPACE,"L") Set fclouri="https://webgw.localdomain/irisclient/csp/"_ns_"/MyApp.Login.cls"
Set url=##class(%SYS.OAuth2.Authorization).GetLogoutEndpoint($$$APP,fclouri)

返却されるURLは次のような内容になります。

https://webgw.localdomain/irisclient/csp/sys/oauth2/OAuth2.PostLogoutRedirect.cls
  ?register=irzgE0VENqa51QpmvGcmG-nDWn0

このURLをGETすると、さらに認可サーバのlogoutエンドポイントへのURLにリダイレクトされます。RP-Initiated Logoutのためのエンドポイントです。

 https://webgw.localdomain/irisauth/authserver/oauth2/logout
  ?id_token_hint=eyJ0eXAiO.....
  &post_logout_redirect_uri=https://webgw.localdomain/irisclient/csp/sys/oauth2/OAuth2.PostLogoutRedirect.cls
  &state=Pc73yDQFouYO1Y-a0PioOI3qRtw

RPのリダイレクト先を指定するpost_logout_redirect_uriクエリパラメータ値がOAuth2.PostLogoutRedirect.clsになっていますが、これはIRISベースのRP専用のランディングページになっていて、RPがするべきこと(RPに保存されているトークンの除去等)を行ってくれます。

ログアウト終了時には、GetLogoutEndpoint()の第2引数で指定したURL(この場合は、ログインページであるMyApp.Login.cls)にリダイレクトされます。

トークンの失効処理(Revocation)

##class(%SYS.OAuth2.AccessToken).RevokeToken()は指定したトークンを失効させます。

ユーザセッション使用時に、単独のアクセストークンのみを失効させることで、特定のアプリケーション単独のログアウトを実現します。ログイン時はSSOのメリットを受けつつ、ログアウトは個別に行いたい場合に使用できます。

CSPベースのWebアプリケーションの場合、ログアウト時にはCSPセッションを終了させますが、本メソッドの呼び出しは、CSPセッションの終了時に自動実行されるため、アプリケーションコードで明示的に呼び出す必要はありません。RESTを使用する場合、(USESESSIONパラメータを1にしない限り)CSPセッションは使用しませんので、明示的に呼び出す必要があります。

本例では、MyApp.AppMain.clsのOnPreHTTP()で下記を実行しています。

Do %session.Logout() 
Set %session.EndSession=1 ; will call OAuth2.SessionEvents.OnEndSession()
Set %response.Redirect="MyApp.Login.cls"

リソースサーバへのRESTアクセス

リソースサーバのRESTエンドポイント呼び出し時は、##class(%SYS.OAuth2.AccessToken).AddAccessToken()を使用してアクセストークンを追加します。追加方法は第2引数で指定します。省略時はHTTPヘッダーに"Authorization: Bearer"を追加します。

set httpRequest=##class(%Net.HttpRequest).%New()
// AddAccessToken adds the current access token to the request.
set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
    httpRequest,,
    ..#SSLCONFIG,
    $$$APP)
if $$$ISOK(sc) {
    Set url=..#RSCURL2_"/private" w "URL:"_url_"</BR>",!
    set sc=httpRequest.Get(url)
}

導入手順

複数のgitをcloneしますが、下記のフォルダ構造にしておけば、各種手順やシェルを修正することなく(最もストレスなく...)動作させる事が出来ます。

Linux
(任意)/iris-oauth             <= サーバ編で導入済みのはず
(任意)/angular-oauth2-client  <= git clone https://github.com/IRISMeister/angular-oauth2-client.git

Windows
C:\git\python-oauth2-client   <= git clone https://github.com/IRISMeister/python-oauth2-client.git
C:\git\angular-oauth2-client  <= ngビルドをWindowsで動かすのであれば

以下、(任意)は$HOME/gitとしています。

Webアプリケーション

導入手順

不要です。サーバに同梱されています。

起動方法

こちらはIRISサーバで完結するので、単独の起動方法はありません。サーバ用のコンテナ起動時に自動起動します。

アクセス方法



操作については、サーバ編を参照ください。

SPA(Angular)

F5(リロード)対策としてアクセストークンをsessionStorageに保存しています。XSS脆弱性に対してご注意ください。

導入手順

LinuxあるいはWindowsで実行します。
ビルド・実行にはnode, npm, ngが必要です。私の環境は以下の通りです。Windowsでも同じことが出来ます。

Windowsの場合、./build_and_deploy.ps1, ng-start.ps1内の$env:gitrootの値を環境にあわせて修正してください。

$ ng version

Angular CLI: 15.1.6
Node: 16.16.0
Package Manager: npm 9.3.0
OS: linux x64

Angular: 15.1.5
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1501.6
@angular-devkit/build-angular   15.1.6
@angular-devkit/core            15.1.6
@angular-devkit/schematics      15.1.6
@angular/cli                    15.1.6
@schematics/angular             15.1.6
rxjs                            7.8.0
typescript                      4.9.5

下記の手順でビルドしたangularアプリケーションのディストリビューション(./dist/*)を、iris-oauth2のhtdocs/下にコピーすることで使用可能になります。

  1. Git Clone

    カレントディレクトリが./iris-oauth2であることを確認してください。その1階層上のディレクトリに移動して、cloneします。

    $ pwd
    /home/irismeister/git/iris-oauth2
    $ cd ..
    $ git clone https://github.com/IRISMeister/angular-oauth2-client.git
    $ cd angular-oauth2-client
    
  2. $ npm install
    $ ./build_and_deploy.sh   (Windowsの場合は>powershell ./build_and_deploy.ps1)
    

    このシェルは下記を行います。

    • client_id, client_secretをサーバ配下のファイル(environment*.ts)から取得
    • NGビルド実行。 ターゲットは/myapp/
    • htdocs/myapp/以下にビルド成果物をコピー
    • NGビルド実行。 ターゲットは/myapp2/
    • htdocs/myapp2/以下にビルド成果物をコピー

    下記のように表示されれば成功です。

    $ ./build_and_deploy.sh
    &#x2714; Browser application bundle generation complete.
    &#x2714; Copying assets complete.
    &#x2714; Index html generation complete.
          ・
          ・
          ・
    Build at: 2023-04-05T07:23:58.705Z - Hash: 67aabdbbe8ad0bfa - Time: 6796ms
    deploying to iris-oauth2
    done
    Go to https://webgw.localdomain/myapp/ or https://webgw.localdomain/myapp2/
    $
    

以後、iris-oauth2をリビルドするなどで、client_id,client_secretが変更された場合は、build_and_deploy.shを再実行してください。

起動方法

こちらはApacheを使用しますので単独の起動方法はありません。サーバ用のコンテナ起動時に自動起動します。

アクセス方法


名称エンドポイント
SPAアプリケーション/myapp
SPAアプリケーション/myapp2

両者は、client_idが異なる(つまり別のRPとみなす)だけ内容は同じです。下記のような「最高にクール」な画面が表示されます。

「BFF接続テスト」を押すとBFFとの疎通確認を行い、成功の場合IRISバージョンを画面表示します。

「SPAでログイン」を押すと、SPAのみで認証を行います。下記のような画面が表示されれば成功です。適当にボタンを押して(処理内容はボタンが示す通りです)動作確認してください。

修正・デバッグ

修正・デバッグ目的で、Angular Live Development Serverで起動することができます。

./ng-start.sh (Windowsの場合は>powershell ./ng-start.ps1)

Compiled successfullyという起動メッセージが表示されたら、Angularアプリケーション(live)にアクセスします。

よくあるエラー

Unexpected request - client_idと表示される場合、(恐らく)client_idが正しく反映されていません。build_and_deploy.shで直ります。

今後の修正

今後のドキュメントへの加筆・修正等はこちらで行います。

SPA(Angular)+REST+BFF

Webアプリケーションではセッション管理にCSPセッションを使用していますが、SPA+BFFでは独自のセッションを作成しています。

sessionidというクッキー名です。

SLOが機能します。

導入手順

SPA(Angular)に同梱されています。

起動方法

SPA(Angular)と同時に起動されます。

アクセス方法

SPA(Angular)と同じです。

「SPA+BFFでログイン」を押すと、SPA+BFFで認証を行います。下記のような画面が表示されれば成功です。SPAとほぼ同じですが、こちらにはBFFサーバの情報表示とトークンの取り消し機能が追加されています。

Python

導入手順

ブラウザをポップアップさせる必要があるため、Windowsで稼働させます。
DOS窓から下記を実行します。

C:\git> git clone https://github.com/IRISMeister/python-oauth2-client.git
C:\git> cd python-oauth2-client

Linux上のssl/web/all.crt(Webサーバの証明書チェーン)をローカルのc:\tempにコピーしてください。

$ pwd
/home/irismeister/git/iris-oauth2
$ cp ssl/web/all.crt /mnt/c/temp

クライアント情報を起動時に出力された内容(client/credentials_python.json)で上書きコピー(無ければ新規作成)します。
コピー先は、cloneを実行したパスに合わせてください。

$ cp client/credentials_python.json /mnt/c/git/python-oauth2-client/credentials.json

アクセス方法

pythonコードを起動します。

C:\git\python-oauth2-client>run.bat

ブラウザ上に、認証画面が表示されますので、「引き受ける」を押します。Please close the window.と表示されたら、ブラウザは以後不要なので閉じます。 DOS窓上にサーバから得た情報が出力されているはずです。

C:\git\python-oauth2-client>python request.py

***** auth_url *****
https://webgw.localdomain/irisauth/authserver/oauth2/authorize?response_type=code&client_id=EKj6Bww0yiFgeuYUxtkyzfge84-keMy20uE5xTmlWPk&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2F&scope=openid+profile+scope1+scope2&state=aiq21soYtKWlzomgChYUlzVgjpby1v&code_challenge=AIu6-HIPnl1MKWIxImJ3Earg0ezfcUbNF75hLljXSmM&code_challenge_method=S256

***** headers *****
{'Content-Type': 'application/x-www-form-urlencoded'}

***** body *****

127.0.0.1 - - [05/Apr/2023 16:09:49] "GET /?code=N9Y2DT77QNd_d1mmnsSjwUiqbxOvDB3AfT9QM5mD6XFuYYByhtSD4AfIjjWGF6AFTwdhA8QyOjfX63RO1mYsvw&state=aiq21soYtKWlzomgChYUlzVgjpby1v HTTP/1.1" 200 -

***** decoded access token *****
{'alg': 'RS512', 'kid': '3', 'typ': 'JWT'}
{   'aud': [   'https://webgw.localdomain/irisrsc/csp/myrsc',
               'https://webgw.localdomain/irisrsc2/csp/myrsc',
               'EKj6Bww0yiFgeuYUxtkyzfge84-keMy20uE5xTmlWPk'],
               ・
               ・
               ・

ユーザセッション(CSPOAuth2Sessionクッキー)が有効になっているため、最後にログアウトを実行しています。新しいタブが開き、空白のページが表示されますので、閉じてください。

  # Logout 
  logout_url='https://webgw.localdomain/irisauth/authserver/oauth2/logout?id_token_hint='+id_token+'&post_logout_redirect_uri=http://localhost:8080/fclogout&state='+state
  open_new(logout_url)

ここをコメントアウトすれば、次回以降の認証時にはログイン画面がスキップされます。

よくあるエラー

urllib.error.URLError: <urlopen error [WinError 10061] 対象のコンピューターによって拒否されたため、接続できませんでした 。>

サーバ環境が起動していない、あるいは到達できない場合に発生します。

Unexpected request - client_id

ブラウザに上記の内容が表示される場合、credentials.jsonが正しくコピーされていません。
このエラーが発生すると、DOSがControl-Cを受け付けない状態で止まってしまいます。その場合、ブラウザで別タブを開いて http://localhost:8080/ を開いてください(何も表示されません)。これでpythonコードが完了するはずです。

  File "F:\ISJ\WorkAtHome\git\python-oauth2\httpserverhandler.py", line 32, in get_access_token_from_url
    context.load_verify_locations(os.environ['REQUESTS_CA_BUNDLE'])
FileNotFoundError: [Errno 2] No such file or directory

証明書のチェーン(all.crt)がc:\temp\にコピーされていません。

今後の修正

今後のドキュメントへの加筆・修正等はこちらで行います。

curl

導入手順

不要です。

本来Windowsで実行するべき内容ですが、サーバの修正時に使用することが大半なので、ここではLinuxで実行しています。そのため、Linuxの/etc/hostsにも下記を追加しています。

127.0.0.1       webgw.localdomain

認可サーバには以下の設定が行われています。

  1. 認可サーバの構成で「リソース所有者のパスワード認証情報」がチェックされている

  2. curlがクライアントとして登録されている

クライアントの種別:機密
リダイレクト URL:  http://localhost:8080/  (実際には使用されないので何でも良いです)
サポートする許可タイプ (少なくともひとつをチェック):「リソース所有者のパスワード認証情報」をチェック
サポートされている応答タイプ(少なくとも1つをチェックする): 「コード」をチェック

起動方法

下記は、curl.shの実行結果です。

$ pwd
/home/irismeister/git/iris-oauth2
$ ./curl.sh

{
  "sub": "_SYSTEM"
}

{
  "HostName": "irisrsc",
  "UserName": "UnknownUser",
  "sub": "_SYSTEM",
  "aud": [
    "https://webgw.localdomain/irisrsc/csp/myrsc",
    "https://webgw.localdomain/irisrsc2/csp/myrsc",
    ""
  ],
  "sigalg": "RS512",
  "Status": "OK",
  "TimeStamp": "04/04/2023 17:25:57",
  "exp": "1680600357(2023-04-04 18:25:57)",
  "debug": {
    "jti": "https://webgw.localdomain/irisauth/authserver/oauth2.A20EIIXeenDpM6oK_At6ZEtCcKY",
    "iss": "https://webgw.localdomain/irisauth/authserver/oauth2",
    "sub": "_SYSTEM",
    "exp": 1680600357,
    "aud": [
      "https://webgw.localdomain/irisrsc/csp/myrsc",
      "https://webgw.localdomain/irisrsc2/csp/myrsc",
      ""
    ],
    "scope": "openid profile scope1 scope2",
    "iat": 1680596757,
    "customer_id": "RSC-00001"
  }
}

その他の使い道

アクセストークンはBearerトークンなので、他の方法で取得したアクセストークンを使用してリソースサーバにアクセス出来ます。
どのRPを使っても構わないのですが、下記のように実行すれば、正当なアクセスとみなされます。

access_token="eyJ0eXAiO.....BlohDZw" # 表示されたアクセストークンをコピペして環境変数に設定
curl --insecure -H "Authorization: Bearer ${access_token}" -H 'Content-Type:application/json;charset=utf-8' \
https://webgw.localdomain/irisrsc/csp/myrsc/private -s | jq

このURLはサーバ編でアクセスを拒否('NoAccessToken'エラー)させるために使用したものと同じです。

アクセストークンの秘匿性を保つことがいかに重要であるかを示す例ですが、開発時にはリソースサーバの簡単なデバッグツールとして使用できます。

0
0 436
記事 Tomohiro Iwamoto · 4月 3, 2023 31m read

本記事は、あくまで執筆者の見解であり、インターシステムズの公式なドキュメントではありません。

IRISのoAuth2機能関連の情報発信は既に多数ありますが、本稿では

  • 手順(ほぼ)ゼロでひとまず動作させてみる
  • 設定の見通しを良くするために、役割ごとにサーバを分ける
  • 目に見えない動作を確認する
  • クライアント実装(PythonやAngular,CSPアプリケーション等)と合わせて理解する
  • シングルサインオン/シングルログアウトを実現する

ということを主眼においています。

コミュニティ版で動作しますので、「とりあえず動かす」の手順に従って、どなたでもお試しいただけます。

現状、使用IRISバージョンはIRIS 2023.1のプレビュー版になっていますが、ソースコードは適宜変更します。

手順に沿ってコンテナを起動すると下記の環境が用意されます。この環境を使用して動作を確認します。

ユーザエージェント(ブラウザ)や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については、こちらの説明がわかりやすかったです。

リソースサーバの役割はデータプラットフォームであるIRISは最適な選択肢です。医療系用のサーバ機能ですがFHIRリポジトリはその良い例です。本例では、至極簡単な情報を返すAPIを使用しています。

少しの努力でFHIRリポジトリを組み込むことも可能です。

サーバ編とクライアント編に分けて記載します。今回はサーバ編です。

とはいえ、クライアントとサーバが協調動作する仕組みですので、境界は少しあいまいです


使用環境

  • Windows10
    ブラウザ(Chrome使用)、curl及びpythonサンプルコードを実行する環境です。

  • Liunx (Ubuntu)
    IRIS, WebGateway(Apache)を実行する環境です。Windows10上のwsl2、仮想マシンあるいはクラウドで動作させる事を想定しています。

参考までに私の環境は以下の通りです。


用途O/Sホストタイプ
クライアントPCWindows10 Pro物理ホスト
Linux環境ubuntu 22.04.1 LTS上記Windows10上のwsl2

Linux環境はVMでも動作します。VMのubuntuは、ubuntu-22.04-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で実行します。

    git clone https://github.com/IRISMeister/iris-oauth2.git --recursive
    cd iris-oauth2
    ./first-run.sh
    

    この時点で下記をLinuxで実行し、OpenIDプロバイダーのメタデータを取得できる事を確認してください。こちらのような出力が得られるはずです。

    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指定)していないことに注目してください。

    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トークンによる認証・認可チェック処理を施したエンドポイントにアクセスします。

    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クライアントアプリケーションを開きます。

    プライベート認証局発行のサーバ証明書を使用しているため、初回はブラウザで「この接続ではプライバシーが保護されません」といったセキュリティ警告が出ます。アクセスを許可してください。

    「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で実行します。

    終了させるには下記を実行します。

    ./down.sh
    

主要エンドポイント一覧

下図は、コード認可フローを例にした、各要素の役割になります。用語としてはoAuth2を採用しています。

OIDCはoAuth2の仕組みに認証機能を載せたものなので、各要素は重複しますが異なる名称(Authorization serverはOIDC用語ではOP)で呼ばれています。

CLIENT SERVERという表現は「何どっち?」と思われる方もおられると思いますが、Client's backend serverの事で、サーバサイドに配置されるロジック処理機能を備えたWebサーバの事です。描画を担うJavaScriptなどで記述されたClient's frontendと合わせて単にClientと呼ぶこともあります。


要素サービス名OIDC用語oAuth2用語エンドポイント
ユーザエージェントN/AUser AgentUser AgentN/A
Web GatewaywebgwN/AN/A/csp/bin/Systems/Module.cxw
認可サーバの管理irisauthN/AN/A/irisauth/csp/sys/%25CSP.Portal.Home.zen
リソースサーバ#1の管理irisrscN/AN/Airisrsc/csp/sys/%25CSP.Portal.Home.zen
リソースサーバ#1irisrscN/AResource server/irisrsc/csp/myrsc/private
リソースサーバ#2の管理irisrsc2N/AN/A/irisrsc2/csp/sys/%25CSP.Portal.Home.zen
リソースサーバ#2irisrsc2N/AResource server/irisrsc2/csp/myrsc/private
WebApp 1a,1bの管理irisclientN/AN/A/irisclient/csp/sys/%25CSP.Portal.Home.zen
WebApp 1airisclientRPClient server/irisclient/csp/user/MyApp.Login.cls
WebApp 1birisclientRPClient server/irisclient2/csp/user/MyApp.AppMain.cls
WebApp 2の管理irisclient2N/AN/A/irisclient2/csp/sys/%25CSP.Portal.Home.zen
WebApp 2irisclient2RPClient server/irisclient2/csp/user/MyApp.AppMain.cls

エンドポイントのオリジン(https://webgw.localdomain)は省略しています


組み込みのIRISユーザ(SuperUser,_SYSTEM等)のパスワードは、merge1.cpfのPasswordHashで一括で"SYS"に設定しています。管理ポータルへのログイン時に使用します。

導入手順の解説

first-run.shは、2~5を行っています。

  1. ソースコード入手

    git clone https://github.com/IRISMeister/iris-oauth2.git --recursive
    
  2. SSL証明書を作成

    ./create_cert_keys.sh
    

    apache-sslに同梱のsetup.shを使って、鍵ペアを作成し、出来たsslフォルダの中身を丸ごと、ssl/web下等にコピーしています。コピー先と用途は以下の通りです。

    コピー先使用場所用途
    ssl/web/ApacheのSSL設定およびクライアントアプリ(python)Apacheとのhttps通信用
    irisauth/ssl/auth/認可サーバ認可サーバのクライアント証明書
    irisclient/ssl/client/CSPアプリケーション#1a,1bIRIS(CSP)がクライアントアプリになる際のクライアント証明書
    irisclient2/ssl/client/CSPアプリケーション#2IRIS(CSP)がクライアントアプリになる際のクライアント証明書
    irisrsc/ssl/resserver/リソースサーバリソースサーバのクライアント証明書
    irisrsc2/ssl/resserver/リソースサーバ#2リソースサーバのクライアント証明書
  3. PCにクライアント用の証明書チェーンをコピー

    all.crtには、サーバ証明書、中間認証局、ルート認証局の情報が含まれています。curlやpythonなどを使用する場合、これらを指定しないとSSL/TLSサーバ証明書の検証に失敗します。

    cp ssl/web/all.crt /mnt/c/temp
    

    備忘録

    下記のコマンドで内容を確認できます。

    openssl crl2pkcs7 -nocrl -certfile ssl/web/all.crt | openssl pkcs7 -print_certs -text -noout
    
  4. Web Gatewayの構成ファイルを上書きコピー

    cp webgateway* iris-webgateway-example/
    
  5. コンテナイメージをビルドする

    ./build.sh
    

各種セットアップは、各サービス用のDockerfile以下に全てスクリプト化されています。iris関連のサービスは、原則、##class(MyApps.Installer).setup()で設定を行い、必要に応じてアプリケーションコードをインポートするという動作を踏襲しています。例えば、認可サーバの設定はこちらのDockefileと、インストーラ用のクラスであるMyApps.Installer(内容は後述します)を使用しています。

  1. ブラウザ、つまりクライアントPC(Windows)にホスト名webgw.localdomainを認識させる

    上述の通りです。

起動方法

./up.sh

up時に表示される下記のようなjsonは、後々、pythonなどの非IRISベースのクライアントからのアクセス時に使用する事を想定しています。各々client/下に保存されます。

{
  "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

停止方法

./down.sh  

認可サーバの設定について

カスタマイズ内容

多様なユースケースに対応するために、認可サーバの動作をカスタマイズする機能を提供しています。

特に、%OAuth2.Server.Authenticateはプロダクションには適さない可能性が高いのでなんらかのカスタマイズを行うように注記されていますのでご注意ください。

本例では、認証関連で下記の独自クラスを採用しています。

  • 認証クラス

    %ZOAuth2.Server.MyAuthenticate.cls

    下記を実装しています。
    BeforeAuthenticate() — 必要に応じてこのメソッドを実装し、認証の前にカスタム処理を実行します。

    ドキュメントに下記の記載があります。本例ではscope2が要求された場合には、応答に必ずscope99も含める処理を行っています。

    通常、このメソッドを実装する必要はありません。ただし、このメソッドの使用事例の1つとして、FHIR® で使用される launch と launch/patient のスコープを実装するのに利用するというようなものがあります。この事例では、特定の患者を含めるようにスコープを調整する必要があります。

    AfterAuthenticate() — 必要に応じてこのメソッドを実装し、認証の後にカスタム処理を実行します。

    ドキュメントに下記の記載があります。本例ではトークンエンドポイントからの応答にaccountno=12345というプロパティを付与する処理を行っています。

    通常、このメソッドを実装する必要はありません。ただし、このメソッドの使用事例の1つとして、FHIR® で使用される launch と launch/patient のスコープを実装するのに利用するというようなものがあります。この事例では、特定の患者を含めるようにスコープを調整する必要があります。

    トークンエンドポイントからの応答はリダイレクトの関係でブラウザのDevToolでは確認できません。pythonクライアントで表示出来ます。

    {   '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

    下記を実装しています。
    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をブラウザで開きます。ユーザ名・パスワードを入力し、認証を行います。

  2. 同じブラウザの別タブで、異なるclient_idを持つCSPベースのアプリケーション#1bを開きます。本来であれば、ユーザ名・パスワード入力を求められますが、今回はその工程はスキップされます。

  3. 上記はほぼ同じ表示内容ですが$NAMESPACE(つまり実行されているアプリケーション)が異なります。

アプリケーションが最初に認可されたスコープと異なるスコープを要求した場合、以下のようなスコープ確認画面だけが表示されます。

この時点で認可サーバで下記を実行すると、現在1個のセッションに属する(同じGroupIdを持つ)トークンが2個存在することが確認できます。

$ 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
  1. 両方のタブでF5を何度か押して、%session.Data("COUNTER")の値が増えて行くことを確認します。

セッションを持つアプリケーションの動作という見立てです。

  1. 1個目のタブ(CSPベースのアプリケーション#1a)でログアウト(SSO)をクリックします。ログアウトが実行され、最初のページに戻ります。

  2. 2個目のタブ(CSPベースのアプリケーション#1b)でF5を押します。「認証されていません! 認証を行う」と表示されます。

これで、1度のログアウト操作で、全てのアプリケーションからログアウトするSLOが動作したことがが確認できました。

同様に、サンプルのpythonコードも、一度認証を行うと、それ以降、何度実行してもユーザ名・パスワード入力を求めることはありません。これはpythonが利用するブラウザに"ユーザセッション"が記録されるためです。

          redirect
  <-----------------------------------
                                     |
python +--> ブラウザ           --> 認可サーバ
       |    (ユーザセッション)  
       +--> リソースサーバ

この設定が有効の場合、認可サーバはユーザエージェントに対して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"
}

詳細はこちら の「ユーザ・セッションのサポート」の項目を参照ください。

PKCE

認可コード横取り攻撃への対策である、PKCE(ピクシーと発音するそうです)関連の設定を行っています。そのため、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を含むページが描画されます。

<body onload="check(0)">
<iframe id=frame0 src=RP1のfrontchannel_logout_uri hidden></iframe>
<iframe id=frame1 src=RP2のfrontchannel_logout_uri hidden></iframe>
  ・
  ・
<scr1pt language=javascript type="text/javascript">
    function check(start) {
      個々のiframeの実行完了待ち
      if (完了) doRedirect()
    }
    function doRedirect() {
            post_logout_redirect_uriへのリダイレクト処理
    }
</scr1pt>

表示がおかしくなってしまうので、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関連の設定を行っています。

ドキュメントに従って、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の設定

ドキュメントに従って、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になっていることで確認できます)ので、この設定は不要ですが、備忘目的で設定しています。

  1. "frontchannel_logout_session_required"をTrueに設定しています。

  2. "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'の同等機能の記述はこちらです。IRISLogout=endは、CSPセッション情報の破棄を確実なものとするためと理解しておけば良いでしょう。

一般論として、ログアウト時のRP側での処理は認可サーバ側では制御不可能です。本当にセッションやトークンを破棄しているか知るすべがありません。IRISLogout=endはRPがCSPベースである場合に限り、それら(cspセッションとそれに紐づくセッションデータ)の破棄を強制するものです。非CSPベースのRPにとっては意味を持ちませんので、無視してください。

各サーバの設定について

各サーバの設定内容とサーバ環境を自動作成する際に使用した各種インストールスクリプトに関する内容です。

認可サーバ

認可サーバ上の、oAUth2/OIDC関連の設定は、MyApps.Installerにスクリプト化してあります。

下記の箇所で、「OAuth 2.0 認可サーバ構成」を行っています。

Set cnf=##class(OAuth2.Server.Configuration).%New()
  ・
  ・
Set tSC=cnf.%Save()

これらの設定は、認可サーバで確認できます。

認可サーバ上のクライアントデスクリプション

下記のような箇所が3か所あります。これらは「 OAuth 2.0 サーバ クライアントデスクリプション」で定義されている、python, curl, angularのエントリに相当します。

Set c=##class(OAuth2.Server.Client).%New()
Set c.Name = "python"
  ・
  ・
Set tSC=c.%Save()

これらに続くファイル操作は、利便性のためにclient_idなどをファイル出力しているだけで、本来は不要な処理です。

これらはコンテナイメージのビルド時に実行されます。

これらの設定は、認可サーバで確認できます。

CSPベースのWebアプリケーション

実行内容の説明は、クライアント編で行います。

CSPベースのWebアプリケーションの設定は、MyApps.Installerにスクリプト化してあります。

oAUth2/OIDC関連の設定(クライアントの動的登録)は、irisclient用のRegisterAll.mac、およびirisclient2用のRegisterAll.macにスクリプト化してあります。

これらはregister_oauth2_client.shにより、コンテナ起動後に実行されます。

これらの設定は、クライアント用サーバで確認できます。

動的登録を行った時点で、これらの内容が認可サーバに渡されて、認可サーバ上に保管されます。その内容は、認可サーバで確認できます。

ビルド時に生成されるclient_idがURLに含まれるため、リンクを用意できません。画像イメージのみです。

リソースサーバ

リソースサーバの設定は、MyApps.Installerにスクリプト化してあります。

リソースサーバのRESTサービスは、IRISユーザUnknownUserで動作しています。

リソースサーバは、受信したトークンのバリデーションをするために、REST APIの実装で、下記のAPIを使用しています。

アクセストークンをhttp requestから取得します。

set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.tSC)

アクセストークンのバリデーションを実行します。この際、..#AUDがアクセストークンのaudクレームに含まれていることをチェックしています。

if '(##class(%SYS.OAuth2.Validation).ValidateJWT($$$APP,accessToken,,..#AUD,.jsonObjectJWT,.securityParameters,.tSC)) {

署名の有無の確認をしています。

Set sigalg=$G(securityParameters("sigalg"))
if sigalg="" { 
  set reason=..#HTTP401UNAUTHORIZED
  $$$ThrowOnError(tSC)				
}

(べた書きしていますが)受信したアクセストークンのSCOPEクレーム値がscope1を含まない場合、http 404エラーを返しています。

if '(jsonObjectJWT.scope_" "["scope1 ") { set reason=..#HTTP404NOTFOUND throw }

oAUth2/OIDC関連の設定(クライアントの動的登録)は、Register.macにスクリプト化してあります。

これらはregister_oauth2_client.shにより、コンテナ起動後に実行されます。

これらの設定は、リソースサーバで確認できます。

動的登録を行った時点で、これらの内容が認可サーバに渡されて、認可サーバ上に保管されます。その内容は、認可サーバで確認できます。

ビルド時に生成される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://jwt.io/ で確認してみます。ヘッダにはkidというクレームが含まれます。これはトークンの署名に使用されたキーのIDです。

{
  "typ": "JWT",
  "alg": "RS512",
  "kid": "3"
}

これで、このトークンはkid:3で署名されていることがわかります。 この時点で、Signature Verifiedと表示されていますが、これはkid:3の公開鍵を使用して署名の確認がとれたことを示しています。

公開鍵は公開エンドポイントから取得されています

次に、エンコード処理(データへのJWSの付与)を確認するために、ペーストしたトークンの水色の部分(直前のピリオドも)をカットします。Invalid Signatureに変わります。

さきほどSQLで表示したPrivateJWKSの内容のkid:3の部分だけ(下記のような内容)を抜き出して下のBOXにペーストします。

{
  "kty": "RSA",
  "n": "....",
  "e": "....",
  "d": "....",
    ・
    ・
    ・
  "alg": "RS512",
  "kid": "3"
}

水色部分が復元され、再度、Signature Verifiedと表示されるはずです。また、水色部分は元々ペーストしたアクセストークンのものと一致しているはずです。

本当に大切な秘密鍵はこういう外部サイトには張り付けないほうが無難かも、です

ログ取得方法

各所でのログの取得方法です。

認可サーバ(IRIS)

認可サーバ上の実行ログを取得、参照出来ます。クライアントの要求が失敗した際、多くの場合、クライアントが知りえるのはhttpのステータスコードのみで、その理由は明示されません。認可サーバ(RPがIRISベースの場合は、クライアントサーバでも)でログを取得すれば、予期せぬ動作が発生した際に、原因のヒントを得ることができます。

  • ログ取得開始

    ./log_start.sh
    

    これ以降、発生した操作に対するログが保存されます。ログは^ISCLOGグローバルに保存されます。

  • ログを出力

    ログは非常に多くなるので、いったんファイルに出力してIDE等で参照するのが良いです。

    ./log_display.sh
    

    Webアプリケーション1aをユーザエージェント(ブラウザ)からアクセスした際のログファイルの出力例はこちらです。

  • ログを削除
    ログを削除します。ログ取得は継続します。

    ./log_clear.sh
    
  • ログ取得停止 ログ取得を停止します。ログ(^ISCLOGグローバル)は削除されません。

    ./log_end.sh
    

IRISサーバのログ確認

IRISサーバが稼働しているサービス名(認可サーバならirisauth)を指定します。

IRISコミュニティエディション使用時に、接続数オーバ等を発見できます。

docker compose logs -f irisauth

WebGWのログ確認

WebGWコンテナ内で稼働するapacheのログを確認できます。

全体の流れを追ったり、エラー箇所を発見するのに役立ちます。

docker compose logs -f webgw
0
1 570
記事 Tomohiro Iwamoto · 7月 11, 2022 2m read

オリジナルの「InterSystems IRIS で Python を使って IMAPクライアントを実装する」は、埋め込みPythonを使用してIMAPインバウンドアダプタを実装されていますが、最近メールプロバイダがあいついでoAuth2認証しか受け付けなくなってきているので、その対応をしてみました。

本稿のGitHubはこちらです。

変更点

GMAILに対してメールの送受信を可能とするためにオリジナルに以下の修正を施しています。

  1. IMAP(Python版)インバウンドアダプタにoAuth2認証およびRefreshTokenによるAccessTokenの更新を追加
  2. oAuth2認証およびRefreshTokenによるAccessTokenの更新機能を持つSMTPアウトバウンドアダプタを新規作成
  3. IMAPにバイナリの添付ファイルの処理を追加
  4. メッセージ削除に、推奨APIであるclient.uid("STORE")を使用するように変更
  5. ClientIdなど、センシティブな情報をコンテナ起動時に動的に適用するように変更
  6. 日本語使用時の文字化けに対処

3.添付ファイルが存在する場合、追加設定/ファイル・パスで指定したファイルパス(既定値は/var/tmp/)上に保存します。

5.の実現は、プロダクション(IMAPPyProduction.cls)起動の際に実行されるコールバックOnStart()で、準備したjsonファイルの取り込みを行っています。

zpmパッケージの内容はオリジナルのままです。

事前準備

実行には、以下のパラメータの準備が必要です。

パラメータ取得方法
GMAILアカウント認証対象となるGMAILアカウント。xxxx@gmail.com
ClientIDGCPで発行されるclient_id
ClientSecretGCPで発行されるclient_secret
TokenEndPointGCPで発行されるtoken_uri
RefreshToken下記のoauth2.py等を使用して取得

これらの値をgmail_client_secret.templateを参考に、gmail_client_secret.jsonに設定してください。

ClientID, ClientSecret, TokenEndPointは、GCPのコンソールで、デスクトップクライアント用にoAuth2を発行した際にダウンロードできるJSONファイルから取得すると便利です。

$ cat client_secret_xxxxxx.apps.googleusercontent.com.json | jq

{
  "installed": {
    "client_id": "xxxxx.apps.googleusercontent.com",  <=ココ
    "project_id": "iris-mail-355605",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",       <=ココ
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "yyyyy",    <=ココ
    "redirect_uris": [
      "http://localhost"
    ]
  }
}

Refresh Tokenの取得には、oauth2.pyを使用しました。

$ python2 oauth2.py --user=xxxx@gmail.com \
    --client_id=xxxxxx.apps.googleusercontent.com \
    --client_secret=GOCSPX-yyyyyyy  \
    --generate_oauth2_token

Refresh Token: xxxxxxx    <=ココ
Access Token: yyyyyyyyyyyyyyyy
Access Token Expiration Seconds: 3599

実行

オリジナルと同じで、下記コマンドを実行します。1つのビジネスサービス、1つのビジネスオペレーションを持つ簡単なプロダクションが起動します。

URLはオリジナルのままのhttp://localhost:52785/csp/sys/%25CSP.Portal.Home.zenにしてあります。

git clone https://github.com/IRISMeister/iris-imap-inbound-adapter-demo
cd iris-imap-inbound-adapter-demo
docker-compose build
docker-compose up -d

安全策として、IMAP-GMAIL, SMTP-GMAILはいずれもdisableにしてあります。それぞれのパラメータ設定が適切に適用されていることを確認の上、有効化してください。

image

ビジネスホストの設定値

ビジネスホスト名パラメータ名
IMAP-GMAILRefreshTokengmail_client_secret.json設定値
IMAP-GMAILClientIdgmail_client_secret.json設定値
IMAP-GMAILClientSecretgmail_client_secret.json設定値
IMAP-GMAIL認証情報mail-gmail
SMTP-GMAILRefreshTokengmail_client_secret.json設定値
SMTP-GMAILClientIdgmail_client_secret.json設定値
SMTP-GMAILClientSecretgmail_client_secret.json設定値
SMTP-GMAILTokenEndPointgmail_client_secret.json設定値
SMTP-GMAIL認証情報mail-gmail

以降、IMAP-GMAILサービスが30秒毎に件名に[IMAP test]を含むメールをチェックし、存在した場合、SMTP-GMAILオペレーションが自分自身に送信します。

同件名のメールが存在しないと、何も起こりません。下記で、そのようなメールを1通送信することが出来ます。

もちろん、通常のメールクライアントソフトウェアを使って、送信しても構いません

docker-compose exec iris iris session iris -U IRISAPP "Send"

プロダクションを停止しない限り、メールの送受信を延々と繰り返しますので、適当なタイミングで停止してください。

image

0
0 631
記事 Tomohiro Iwamoto · 7月 3, 2022 18m read

IRISリリース2022.1のご紹介

本稿は、「InterSystems IRIS、IRIS for Health、HealthShare Health Connect 2022.1がリリースされました!」でご案内している内容を、補足解説する内容となります。

紹介ビデオ

米国本社プロダクトマネージャーによる本リリースのハイライトのご紹介ビデオ(英語)です。 https://www.intersystems.com/resources/whats-new-in-intersystems-iris-2022-1

リリース内容

年一度のEMリリースです。

  • メンテナンスアップデート提供:リリース日から2年間

  • セキュリティアップデート提供:リリース日から4年間

  • InterSystems IRIS, InterSystems IRIS for Health

  • 同時期にリリースされるモジュール

    ICM, IKO, InterSyetms Reports, InterSystems Studio

  • 独立してリリースされるモジュール

    IAM, SAM, 各種ドライバ類, VS Code ObjectScript 拡張

プラットフォーム

O/S
Microsoft Windows Server 2012, Server 2016, Server 2019, Server 2022, 10, 11 for x86-64
Oracle Linux 7, 8 for x86–64
Red Hat Enterprise Linux 7.9 for x86-64
Red Hat Enterprise Linux 8.1–8.5 for x86-64 or ARM64
SUSE Linux Enterprise Server 15 SP3 for x86-64
Ubuntu 18.04, 20.04 LTS for x86-64 or ARM64
IBM AIX® 7.2, 7.3 for Power System-64
開発環境用O/S
CentOS-7 x86-64
Apple macOS 10.15 for x86-64 AND M1

以下、2021.2,2022.1で追加された機能についてご紹介しています。

開発者向け

Kafkaメッセージのサポート

Apache Kafka用のインバウンド・アウトバウンドアダプター、ビジネスサービス、ビジネスオペレーションおよび、低レベルなAPIを提供します。

これらを使用して、プロダクション環境、非プロダクション環境を問わず、kafkaの持つ、高速なデータパイプライン、ストリームに対する分析、データ投入などの機能を容易に利用できるようになります。

プロダクション用途

インバウンドアダプタは、Kafkaのコンシューマ機能を提供します。

アウトバウンドアダプタは、Kafkaのプロデューサ機能を提供します。

低レベルなAPI

Kafkaのコンシューマ、プロデューサ機能を持つ、低レベルAPIを提供します。

Set settings = ##class(%External.Messaging.KafkaSettings).%New()
Set settings.username = "amandasmith"
Set settings.password = "234sdsge"
Set settings.servers = "100.0.70.179:9092, 100.0.70.089:7070"
Set settings.clientId = "BazcoApp"
Set client = ##class(%External.Messaging.Client).CreateClient(settings, .tSC)

Set topic = "quickstart-events"
Set value = "MyMessage", key = "OptionalTag"
Set msg = ##class(%External.Messaging.KafkaMessage).%New()
Set msg.topic = topic
Set msg.value = value
Set msg.key = key

Set tSC = client.SendMessage(msg)

Kafkaとの接続に関して、開発者コミュニティに多数の寄稿がありますが、それらとは別にKafkaとの接続性を製品として提供するものになります。

Python関連の強化

埋め込みPython

言語としてObjectScriptが使用できるあらゆる場面でPythonを使用可能になります。

使用例) ePy.Test.cls

実行環境としてのIRISとPythonは類似点が多く(インタプリタ、c言語実装、オブジェクトモデル)、同一プロセス空間で動作させるなど、他言語に比べて踏み込んだ統合を行っています。

Python Gateway(PEX)やPython SDKを置き換えるものではありません。

埋め込みPythonについては、開発者コミュニティに多数の寄稿がありますので、参照ください。

【はじめてのInterSystems IRIS】Embedded Python セルフラーニングビデオシリーズ公開!

6/28 (火)に開催した「Embedded Pythonの新設トレーニングコースのご紹介ウェビナ」のオンデマンド配信もあります。

Python SDK

既存のpyODBC,IRIS Native, Python Gwatewayに加えてDB-APIを追加。

SQLAlchemy(PythonのORマッパー)を使用できるようにするための機能追加の一環です。

プロダクション環境でのPython使用

プロダクションで多様なプログラム言語を使用可能にする機能であるPEXにPythonが追加されました。

これで、サポートされる言語はPython, Java, .NET, ObjectScriptになりました。

IRIS-Python間のAPIはIRIS Nativeを使用しています。埋め込みPythonではありません。

PEXとは、下記のようにJavaや.NETをプロダクションで使用するための機能です。これらが持つ多彩な外部接続ライブラリの使用に加え、ビジネスロジック(ビジネスプロセス)そのものをこれらの言語で記述することも可能です。

image

Pythonの機能を利用して、アダプタの作成、分析や演算を実施、永続メッセージの作成、長時間実行されるビジネスプロセスへの参加が可能となります。

Visual Studio Code ObjectScript 拡張

ドキュメントとの統合

該当箇所にマウスをかざすだけでドキュメントを表示。クラス階層のブラウジング。ユーザ作成のクラスドキュメントのプレビュー表示が可能になりました。

シンタックスに埋め込みPythonサポート

埋め込みPythonのシンタックスカラーリングを正しく行います。

Studioライクな使用方法の拡充

CSPファイルの編集・コンパイルが可能になりました。

サーバ側での検索が可能になりました。

現在サーバサイドの検索機能は、VSCODEのProposed APIを使用しているため、それらAPIが正式リリースになるまではこちらの手順でプレビュー機能を有効化する必要があります。

デバッグ機能

オブジェクトのプロパティ値を参照可能になりました。安定性をより向上しました。

SQLクエリ実行のローコード化

外部データソースに対してSQLを実行するための、汎用のビジネスサービス、ビジネスオペレーションを用意しました。

今まではSQLアダプタを使用するビジネスサービス、ビジネスオペレーションを作成する必要がありました。

受信したレコードの内容を保持するためのメッセージを個別に作成する必要はありません。レコードの内容はJSON形式でEns.StreamContainerに格納されます。

詳細はこちらをご覧ください。

.NET 5

ADO.NET, Native API, Gateway(PEX)において、.NET5をサポートします。

.NET6は時期リリースでの対応に向けて準備中です。

実行速度、スケーラビリティ、セキュリティ関連

アダプティブなSQLオプティマイザ

テーブルの統計情報をより軽量に取得する手段を実現することにより、高精度なランタイム時のクエリプランの選択が可能となりクエリパフォーマンスが向上しました。

下記は、パートナ様の実データと実際に使用されているクエリを使用したベンチマーク測定の結果です。2021.1-2022.1間で、I/O量が25%減、実行時間が半分に改善されている事がわかります。

image

スマートサンプリング & テーブル統計の自動化

ブロックレベルでサンプリングを行うことで、今まで行単位で行っていたサンプリング(もしくはフルスキャン)による統計処理のコストを大幅に短縮しました。

クエリ初回実行時に統計情報が存在しない場合は、自動的に統計取得を行い、より良いクエリプランの選択を試みます。

今までは、統計処理の実行を忘れてしまったり、テーブルサイズが大きいため、プロダクション環境での実行をためらったりという事態が発生し、その結果、あまり効率的ではないクエリプランが使用され続ける可能性がありました。

従来のメカニズムと新しいメカニズムとの実行時間の比較は下記のようになります。

tune

グラフ横軸は、テーブルサイズ0.000008GB(8KB)から22GBまでの自然対数ln(x)の値です。つまり
ln(0.000008)=-11.736069016284 ~ ln(22)=3.0910424533583

デフォルトのストレージマッピングの使用が前提になります。

こちらに解説記事があります。

高度なテーブル統計

テーブル統計情報としてヒストグラムを取得するようになりました。これにより、レンジを指定するクエリに対しても選択性を計算できるようになりました。この結果をRTPCが使用することで、クエリの性能が向上する事が期待できます。

例えば、下記のようなクエリにおいて、LocationCountryに対してはRTPCの外れ値検知のメカニズムを使用可能ですが、EventDateに対しては個別の値の選択性はあまり役に立ちません。ヒストグラムを使用することで、レンジに対して、どの程度の選択性を持ちうるかを導出することが可能になりました。

SELECT * FROM Aviation.Event WHERE EventDate < '2004-05-01' AND LocationCountry = 'California'

histgram

こちらに解説記事があります。

ランタイム時のクエリプランの選択

ランタイム時のクエリプランの選択(RTPC, Run-Time Plan Choice)をデフォルトで有効にしました。これにより、下記のようなクエリが高速化する可能性があります。

  • 外れ値
....FROM log WHERE level='INFO'

特定の値がレコードの大半を占めるようなケース。

  • 範囲指定
....FROM Aviation.Event WHERE EventDate < '2004-05-01'

条件で指定した値(この場合は日付)が、Eventテーブル内で初期のものか、終盤のものかによって、対象レコード数が変動するケース。

  • 常に真、偽になる条件
....FROM log WHERE (1=0 AND ...)

クエリツール、BIツールでよく見られるパターン。

こちらに解説記事があります。

ストレージ使用量の削減

ストリームおよびジャーナルを圧縮することにより、必要なストレージ使用量を大幅に削減しました。

高価なストレージ容量削減(特にクラウドでは重要な要素)のメリットは、圧縮・伸張にかかるCPUコスト上昇というデメリットを上回るとの価値判断が根底にあります。

ストリームの圧縮について

グローバルベースのストリームを使用する際、デフォルトでその内容を圧縮します。既存の未圧縮状態のストリームは、次回の書き込み時に圧縮を行います。アプリケーションの修正は必要ありません。

弊社で計測したところ、モダンなH/W環境において、CPU消費は3%程度の上昇で抑えられていました。

実際の顧客データを使用した計測では、小~中サイズのテキストについては30~50%、XML/JSON形式のデータについては60~80%程度の削減効果が観測されました。

image

ジャーナルの圧縮について

今までは、オプトインの機能でしたが、デフォルトで有効になりました(無効化することもできます)。
ジャーナルが切り替わった際に、非アクティブなジャーナルファイルを圧縮します。ロールバック、ロールフォワードは、圧縮済みのジャーナルファイルを使用します。

irisowner@637fa733ef04:/usr/irissys/mgr/journal$ ll -h
total 359M
drwxrwxr-x 1 irisowner irisowner 4.0K Jun 17 01:09 ./
drwx------ 1 irisowner irisowner 4.0K Jun 17 01:03 ../
-rw-rw---- 1 irisowner irisowner 7.0M Jun 17 01:08 20220617.001z
-rw-rw---- 1 irisowner irisowner 6.8M Jun 17 01:09 20220617.002z
-rw-rw---- 1 irisowner irisowner 6.8M Jun 17 01:09 20220617.003z
-rw-rw---- 1 irisowner irisowner 338M Jun 17 01:09 20220617.004
-rw-rw---- 1 irisowner irisowner   35 Jun 17 01:09 iris.lck

ミラージャーナルファイルもプライマリ、バックアップ共に同様に圧縮されます。

-rw-rw---- 1 irisowner irisowner 216K Jun 17 01:07 20220617.001z
-rw-rw---- 1 irisowner irisowner 1.3M Jun 17 01:16 MIRROR-MIRRORSET-20220617.001z
-rw-rw---- 1 irisowner irisowner  80K Jun 17 01:16 MIRROR-MIRRORSET-20220617.002z
-rw-rw---- 1 irisowner irisowner 1.0M Jun 17 01:19 MIRROR-MIRRORSET-20220617.003

実際の顧客のケースでは、ジャーナルサイズが85%縮小され、信頼性を損なうことなく、EBSストレージ代金を2,000 USドル/月削減出来ました。 image

ジャーナルファイルを(AWS S3のような)安価なストレージにアーカイブする機能は今後のリリースで提供予定です。

オンラインでのShardのリバランス

Shardのリバランスをオンライン状態(ユーザがクエリ実行やデータの追加を継続)で実行できるように強化しました。その結果、Shardクラスタのデータノード追加をオンラインのまま実施出来るようになりました。今までは、一時的にクエリ、データ追加を保留する必要がありました。

データノードの削除のオンライン化は今後のリリースにて対応予定です。

TLS 1.3 Support

TLS1.3(OpenSSL 1.1.1)をサポートします。

TLS1.3は、接続確立時のハンドシェークのやり取りを減らせるなど、より高速であることが知られています。

分析と機械学習関連

SQLローダ

SQLテーブルへの新しいデータローディングの仕組みを提供するために、SQLにLOADコマンドを追加しました。

データソースとしてCSVもしくはJDBCを選択できます。 INSERT文によく似た構文でソースカラム、ターゲットカラムの調整を、IntegrgatedMLのUsingとよく似た構文で、動作の調整(デリミタの指定など)を行うことが出来ます。

LOAD DATA FROM FILE 'C://mydata/countries.csv'
COLUMNS (
    src_name VARCHAR(50),
    src_continent VARCHAR(30),
    src_region VARCHAR(30),
    src_surface_area INTEGER)
INTO Sample.Countries (Name,SurfaceArea,Region)
VALUES (src_name,src_surface_area,src_region)
USING {"from":{"file":{"columnseparator":";"}}}
LOAD DATA FROM JDBC CONNECTION MyJDBCConnection
TABLE countries
INTO Sample.Countries

紹介記事がありますのでご覧ください。

アダプティブアナリティクス

アダプティブアナリティクス(IRIS+AtScale)に以下の機能が追加されました。

  1. アダプティブアナリティクスのクライアントとして、InterSystems Reportsが使用できるようになりました。これにより、PowerBIやTableauと同じデータモデルを使用してInterSystems Reportsを使用できるようになります。

  2. InterSystems BI(DeepSee)のデータモデルを、アダプティブアナリティクスの定義としてインポートできるようになりました。

キューブ間のRelationshipsやデータコネクタのような移行できない機能があります

InterSystems Reportsとは帳票作成エンジンであるZEN Report(既にオブソリート)の後継製品です。下記の紹介記事をご覧ください。

クラウド、運用関連

クラウドコネクタ

クラウド上でのIRISの管理を容易にするために、新たに下記のコネクタ(アダプタ)を提供いたします。

オブジェクトストレージ

プロダクション用途

S3,Azure Blob, Google Cloud Storageの読み書きのためにインバウンド、アウトバウンドアダプタを提供します。

インバウンドアダプタは、指定したパターンに合致するオブジェクト(群)を取得し、ビジネスサービスのコールバック関数に、その内容をストリームとして提供します。
アウトバウンドアダプタはUpload, Deleteを実行可能です。

アダプタはJava PEXを使用して実装されています。

低レベルなAPI

同等な機能を持つ、低レベルAPIを提供します。

Set bucketName = "my-bucket"
Set blobName = "test.txt"
// Cloud Storage Client作成 (クラウドプロバイダの種類は第3引数の内容で判断。この場合はAWS S3用)
Set myClient = ##class(%Net.Cloud.Storage.Client).CreateClient(,0,"C:\Users\irisowner\.aws\config", "ap-northeast-1", .tSC)

If myClient.BucketExists(bucketName){
	// S3にファイルをアップロード	
	Do myClient.UploadBlobFromFile(bucketName, blobName, "c:\temp\test.txt")

	// 指定バケット内のオブジェクトを列挙
	Set blobs=myClient.ListBlobs(bucketName)
	For i=1:1:blobs.Size {
		Set blob=blobs.GetAt(i)
		w blob.name," ",blob.size," ",blob.updateTime,!
	}
}

// clientをclose
Do myClient.Close()

詳細はこちらをご覧ください。

Cloudwatch

Cloudwatchへの出力のための、アウトバウンドアダプタおよびビジネスオペレーションを提供します。下記のaws cliコマンドに相当します。

>aws cloudwatch put-metric-data --namespace MyNameSpace --metric-name TestMetric \ 
 --dimensions TestKey=TestValue --value 100

CloudwatchアウトバウンドアダプタEnsLib.AmazonCloudWatch.OutboundAdapterはCloudwatchが公開しているPutMetricData APIを呼び出すために、下記のメソッドを提供しています。

Method PutMetricData(namespace As %String, metricName As %String,
					metricValue As %Numeric, metricUnit As %String,
					dims As %String = "") As %Status

ビジネスオペレーションはEnsLib.AmazonCloudWatch.MetricDataOperationです。

詳細はこちらをご覧ください。

2022.1ドキュメントより「現在使用できるのはPutMetricDataのみです。PutMetricAlarmは将来インターフェースが変更される可能性があります。」

SNS(Amazon Simple Notification Service)

SNSへの出力(publish)のための、アウトバウンドアダプタおよびビジネスオペレーションを提供します。下記のaws cliコマンドに相当します。

aws sns publish --topic-arn arn:aws:sns:ap-northeast-1:[AWSアカウントID]:my_topic \
 --subject "Test mail" --message "Hello World"

SNSアウトバウンドアダプタEnsLib.AmazonSNS.OutboundAdapterはSNSが公開しているpublish APIを呼び出すために、下記のメソッドを提供しています

Set tSC = ..Adapter.Publish(..ARNTopic, request.Message, ..Subject)

ビジネスオペレーションはEnsLib.AmazonSNS.BusinessOperationです。

詳細はこちらをご覧ください。

IKO (InterSystems Kubernetes Operator)

IRISのKubernetesへのデプロイを容易にするために、新たに下記の機能を提供します。

IKOについてはこちらをご覧ください。

SAMおよびIAM

SAM(InterSystems System Alert and Monitoring)およびIAM(InterSystems API Manager)をデプロイ、管理できます。IRISクラスタをスケールさせる(そしてそれらの監視を行う)ことが用意になります。

SAMに関しては、こちらの記事を、IAMに関しては、こちらの記事をごらんください。

ロックダウンバージョンのIRISおよびWeb Gateway

ロックダウンバージョンのIRISおよびWeb Gatewayをデプロイできます。Web GatewayはNginxもしくはApache版を選択可能です。

ロックダウンバージョンのIRISは、通常版のirisコンテナと下記の点で異なります。

  • インストール時のセキュリティレベルでロックダウンを選択してあります
  • 組み込みのApacheサーバを起動しません

ロックダウン、通常版を問わず、2021.2以降のirisコンテナは、安全性強化のために下記変更を行っています。
インストールユーザ: root → irisowner
所有、実行ユーザ: root, irisowner, irisuser → irisowner

非永続ボリュームおよび永続ボリューム

非永続ボリューム、永続ボリュームいずれへのデプロイが可能です。

その他

新しくなったTRACEユーティリティを提供します。

実行時のパフォーマンス分析ツールであるMONLBLやPERFMONは、データ取得時に出力(利用)目的を決めておく必要がありました。 TRACEは、ジェネリックなフォーマットでデータを蓄積することで、トレースしたイベントのナビゲーションやサマライズ等の分析操作を、より対話的に行えるようになりました。

サポートされるイベントタイプには、グローバルのSET/KILL、物理書き込み、ネットワーク超えの要求、キャッシュ、ジャーナルイベントなどが含まれます。

イベントの収集ツールです。収集・蓄積にはオーバヘッドがかかります。蓄積されたデータをExcelなどで加工する必要があります。内容の解釈には専門知識と(公開されていない)ソースコードへのアクセスが必要になる事があるため、弊社のサポートの指導の元ご利用ください。

Reading TRACE file C:\InterSystems\IRIS20221\trace\iristrace_16816.txt
0.000000:      START
               STACK [ 0] X - @ +0
0.000185:      MemAlloc
0.000189:      CALL
               ARG [ 0] STR:app
0.000204:        MemAlloc
0.000225:        GloRef, ^rOBJ("app")
0.000243:        GloRef, ^rOBJ("app")
0.000260:        DirBlkBuf, ^rOBJ("app")
0.000273:        BpntBlkBuf, ^rOBJ("app")
0.000280:        DataBlkBuf, ^rOBJ("app")
0.001557:        RtnFetch
0.001569:        MemAlloc at ^app
0.001571:        RtnLoad ^app
0.001576:        RtnLine at +2^app
                 src:  Kill ^a
0.001582:        GloRef at +2^app, ^a

APIを使用して独自出力を作成することも可能です。

0
0 593
記事 Tomohiro Iwamoto · 5月 18, 2021 18m read

目的

Japan Virtual Summit 2021で、Kubernetesに関するセッションを実施させていただいたのですが、AzureのアカウントやIRIS評価用ライセンスキーをお持ちの方が対象になっていました。もう少し手軽に試してみたいとお考えの開発者の方もおられると思いますので、本記事では仮想環境でも利用可能なk8sの軽量実装であるmirok8sで、IRIS Community Editionを稼働させる手順をご紹介いたします。

2022/1/7 若干の加筆・修正しました

マルチノード化する手順はこちらに記載しています。

参考までに私の環境は以下の通りです。

用途O/SホストタイプIP
クライアントPCWindows10 Pro物理ホスト172.X.X.30/24, (vmware NAT)192.168.11.1/24
mirok8s環境ubuntu 20.04.1 LTS上記Windows10上の仮想ホスト(vmware)192.168.11.49/24

ubuntuは、ubuntu-20.04.1-live-server-amd64.isoを使用して、最低限のサーバ機能のみをインストールしました。

概要

IRIS Community EditionをKubernetesのStatefulSetとしてデプロイする手順を記します。 IRISのシステムファイルやユーザデータベースを外部保存するための永続化ストレージには、microk8s_hostpathもしくはLonghornを使用します。
使用するコードはこちらにあります。

インストレーション

microk8sをインストール・起動します。

$ sudo snap install microk8s --classic --channel=1.20
$ sudo usermod -a -G microk8s $USER
$ microk8s start
$ microk8s enable dns registry storage metallb
  ・
  ・
Enabling MetalLB
Enter each IP address range delimited by comma (e.g. '10.64.140.43-10.64.140.49,192.168.0.105-192.168.0.111'):192.168.11.110-192.168.11.130

ロードバランサに割り当てるIPのレンジを聞かれますので、適切な範囲を設定します。私の環境はk8sが稼働しているホストのCIDRは192.168.11.49/24ですので適当な空いているIPのレンジとして、[192.168.11.110-192.168.11.130]と指定しました。

この時点で、シングルノードのk8s環境が準備されます。

$ microk8s kubectl get pods -A
NAMESPACE            NAME                                      READY   STATUS    RESTARTS   AGE
metallb-system       speaker-gnljw                             1/1     Running   0          45s
metallb-system       controller-559b68bfd8-bkrdz               1/1     Running   0          45s
kube-system          hostpath-provisioner-5c65fbdb4f-2z9j8     1/1     Running   0          48s
kube-system          calico-node-bwp2z                         1/1     Running   0          65s
kube-system          coredns-86f78bb79c-gnd2n                  1/1     Running   0          57s
kube-system          calico-kube-controllers-847c8c99d-pzvnb   1/1     Running   0          65s
container-registry   registry-9b57d9df8-bt9tf                  1/1     Running   0          48s
$ microk8s kubectl get node
NAME     STATUS   ROLES    AGE   VERSION
ubuntu   Ready    <none>   10d   v1.20.7-34+df7df22a741dbc

kubectl実行時に毎回microk8sをつけるのは手間なので、下記コマンドでエリアスを設定しました。以降の例ではmicrok8sを省略しています。

注意 すでに"普通の"kubectlがインストールされていると、そちらが優先されてしまいますので、alias名をkubectl2にするなど衝突しないようにしてください。

$ sudo snap alias microk8s.kubectl kubectl
$ kubectl get node
NAME     STATUS   ROLES    AGE   VERSION
ubuntu   Ready    <none>   10d   v1.20.7-34+df7df22a741dbc

元の状態に戻すには sudo snap unalias kubectl

環境が正しく動作することを確認するためにIRISを起動してみます。下記コマンドの実行で、USERプロンプトが表示されるはずです。

$ kubectl run iris --image=intersystemsdc/iris-community:2022.1.0.209.0-zpm
$ watch kubectl get pod
$ kubectl exec -ti iris -- iris session iris
USER>

今後の作業に備えて、作成したPODを削除しておきます。

$ kubectl delete pod iris

起動

$ kubectl apply -f mk8s-iris.yml

IRIS Community版なので、ライセンスキーもコンテナレジストリにログインするためのimagePullSecretsも指定していません。

しばらくするとポッドが2個作成されます。これでIRISが起動しました。

$ kubectl get pod
NAME     READY   STATUS    RESTARTS   AGE
data-0   1/1     Running   0          107s
data-1   1/1     Running   0          86s
$ kubectl get statefulset
NAME   READY   AGE
data   2/2     3m32s
$ kubectl get service
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)           AGE
kubernetes   ClusterIP      10.152.183.1     <none>           443/TCP           30m
iris         ClusterIP      None             <none>           52773/TCP         8m55s
iris-ext     LoadBalancer   10.152.183.137   192.168.11.110   52773:31707/TCP   8m55s

この時点で、下記コマンドでirisのREST/APIで提供されているメトリックスを取得できるはずです。

$ curl http://192.168.11.110:52773/api/monitor/metrics

ポッドのSTATUSがrunningにならない場合、下記コマンドでイベントを確認できます。イメージ名を間違って指定していてPullが失敗したり、なんらかのリソースが不足していることが考えられます。

$ kubectl describe pod data-0

Events:
  Type     Reason            Age                From               Message
  ----     ------            ----               ----               -------
  Warning  FailedScheduling  4m (x3 over 4m3s)  default-scheduler  0/1 nodes are available: 1 pod has unbound immediate PersistentVolumeClaims.
  Normal   Scheduled         3m56s              default-scheduler  Successfully assigned default/data-0 to ubuntu
  Normal   Pulling           3m56s              kubelet            Pulling image "containers.intersystems.com/intersystems/iris-community:2021.1.0.215.3"
  Normal   Pulled            69s                kubelet            Successfully pulled image "containers.intersystems.com/intersystems/iris-community:2021.1.0.215.3" in 2m46.607639152s
  Normal   Created           69s                kubelet            Created container iris
  Normal   Started           68s                kubelet            Started container iris

下記コマンドでirisにO/S認証でログインできます。

$ kubectl exec -it data-0 -- iris session iris
Node: data-0, Instance: IRIS
USER>

下記で各IRISインスタンスが使用するPVCが確保されていることが確認できます。

$ kubectl get pvc
NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS        AGE
dbv-mgr-data-0    Bound    pvc-fbfdd797-f90d-4eac-83a8-f81bc608d4bc   5Gi        RWO            microk8s-hostpath   12m
dbv-data-data-0   Bound    pvc-b906a687-c24c-44fc-acd9-7443a2e6fec3   5Gi        RWO            microk8s-hostpath   12m
dbv-mgr-data-1    Bound    pvc-137b0ccf-406b-40ac-b8c5-6eed8534a6fb   5Gi        RWO            microk8s-hostpath   9m3s
dbv-data-data-1   Bound    pvc-4f2be4f1-3691-4f7e-ba14-1f0461d59c76   5Gi        RWO            microk8s-hostpath   9m3s

dfを実行すると、データべースファイルを配置するための/vol-dataがマウント対象に表示されていなくて、一瞬、?となりますが、--all指定すると表示されます。

irisowner@data-0:~$ df --all
Filesystem     1K-blocks     Used Available Use% Mounted on
  ・
  ・
/dev/sda2      205310952 26925908 167883056  14% /iris-mgr
/dev/sda2      205310952 26925908 167883056  14% /vol-data
/dev/sda2      205310952 26925908 167883056  14% /irissys/cpf
/dev/sda2      205310952 26925908 167883056  14% /etc/hosts
/dev/sda2      205310952 26925908 167883056  14% /dev/termination-log
/dev/sda2      205310952 26925908 167883056  14% /etc/hostname
/dev/sda2      205310952 26925908 167883056  14% /etc/resolv.conf
  ・
  ・

/dev/sda2はコンテナ内のデバイスではなく、ホスト上のデバイスなので、microk8s-hostpathの仕組み上、そのような表示になるのでしょう。

個別のポッド上のIRISの管理ポータルにアクセスする

下記コマンドで各ポッドの内部IPアドレスを確認します。

$ kubectl get pod -o wide
NAME     READY   STATUS    RESTARTS   AGE   IP             NODE     NOMINATED NODE   READINESS GATES
data-0   1/1     Running   0          46m   10.1.243.202   ubuntu   <none>           <none>
data-1   1/1     Running   0          45m   10.1.243.203   ubuntu   <none>           <none>
$

私の仮想環境のLinuxはGUIがありませんので、下記のコマンドを実行することで、Windowsのブラウザから管理ポータルにアクセスできるようにしました。

$ kubectl port-forward data-0 --address 0.0.0.0 9092:52773
$ kubectl port-forward data-1 --address 0.0.0.0 9093:52773
対象URLユーザパスワード
ポッドdata-0上のIRIShttp://192.168.11.49:9092/csp/sys/%25CSP.Portal.Home.zenSuperUserSYS
ポッドdata-1上のIRIShttp://192.168.11.49:9093/csp/sys/%25CSP.Portal.Home.zenSuperUserSYS

パスワードはCPFのPasswordHashで指定しています

データベースの構成を確認してください。下記のデータベースがPV上に作成されていることを確認できます。

データベース名path
IRISSYS/iris-mgr/IRIS_conf.d/mgr/
TEST-DATA/vol-data/TEST-DATA/

停止

作成したリソースを削除します。

$ kubectl delete -f mk8s-iris.yml --wait

これで、IRISのポッドも削除されますが、PVCは保存されたままになっていることに留意ください。これにより、次回に同じ名前のポッドが起動した際には、以前と同じボリュームが提供されます。つまり、ポッドのライフサイクルと、データベースのライフサイクルの分離が可能となります。次のコマンドでPVCも削除出来ます(データベースの内容も永久に失われます)。

$ kubectl delete pvc -l app=iris

O/Sをシャットダウンする際には下記を実行すると、k8s環境を綺麗に停止します。

$ microk8s stop

O/S再起動後には下記コマンドでk8s環境を起動できます。

$ microk8s start

microk8s環境を完全に消去したい場合は、microk8s stopを「実行する前」に下記を実行します。(やたらと時間がかかりました。日頃は実行しなくて良いと思います)

$ microk8s reset --destroy-storage

観察

ストレージの場所

興味本位の観察ではありますが、/iris-mgr/はどこに存在するのでしょう?microk8sはスタンドアロンで起動するk8s環境ですので、storageClassNameがmicrok8s-hostpathの場合、ファイルの実体は同ホスト上にあります。まずはkubectl get pvで、作成されたPVを確認します。

$ kubectl apply -f mk8s-iris.yml
$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                               STORAGECLASS        REASON   AGE
pvc-ee660281-1de4-4115-a874-9e9c4cf68083   20Gi       RWX            Delete           Bound    container-registry/registry-claim   microk8s-hostpath            37m
pvc-772484b1-9199-4e23-9152-d74d6addd5ff   5Gi        RWO            Delete           Bound    default/dbv-data-data-0             microk8s-hostpath            10m
pvc-112aa77e-2f2f-4632-9eca-4801c4b3c6bb   5Gi        RWO            Delete           Bound    default/dbv-mgr-data-0              microk8s-hostpath            10m
pvc-e360ef36-627c-49a4-a975-26b7e83c6012   5Gi        RWO            Delete           Bound    default/dbv-mgr-data-1              microk8s-hostpath            9m55s
pvc-48ea60e8-338e-4e28-9580-b03c9988aad8   5Gi        RWO            Delete           Bound    default/dbv-data-data-1             microk8s-hostpath            9m55s

ここで、data-0ポッドのISC_DATA_DIRECTORYに使用されている、default/dbv-mgr-data-0 をdescribeします。

$ kubectl describe pv pvc-112aa77e-2f2f-4632-9eca-4801c4b3c6bb
  ・
  ・
Source:
    Type:          HostPath (bare host directory volume)
    Path:          /var/snap/microk8s/common/default-storage/default-dbv-mgr-data-0-pvc-112aa77e-2f2f-4632-9eca-4801c4b3c6bb

このpathが実体ファイルのありかです。

$ ls /var/snap/microk8s/common/default-storage/default-dbv-mgr-data-0-pvc-112aa77e-2f2f-4632-9eca-4801c4b3c6bb/IRIS_conf.d/
ContainerCheck  csp  dist  httpd  iris.cpf  iris.cpf_20210517  _LastGood_.cpf  mgr
$

storageClassNameにhostpathは使用しないでください。microk8s_hostpathとは異なり、同じフォルダに複数IRISが同居するような状態(破壊された状態)になってしまいます。

ホスト名の解決

StatefulSetでは、各ポットにはmetadata.nameの値に従い、data-0, data-1などのユニークなホスト名が割り当てられます。 ポッド間の通信に、このホスト名を利用するために、Headless Serviceを使用しています。

kind: StatefulSet
metadata:
  name: data

kind: Service
spec:
  clusterIP: None # Headless Service

この特徴は、ノード間で通信をするShardingのような機能を使用する際に有益です。本例では直接の便益はありません。

nslookupを使いたいのですが、kubectlやk8sで使用されているコンテナランタイム(ctr)にはdockerのようにrootでログインする機能がありません。また、IRISのコンテナイメージはセキュリティ上の理由でsudoをインストールしていませんので、イメージのビルド時以外のタイミングで追加でソフトウェアをapt install出来ません。ここではbusyboxを追加で起動して、そこでnslookupを使ってホスト名を確認します。

$ kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
/ # nslookup data-0.iris
Server:    10.152.183.10
Address 1: 10.152.183.10 kube-dns.kube-system.svc.cluster.local

Name:      data-0.iris
Address 1: 10.1.243.202 data-0.iris.default.svc.cluster.local
/ #

10.152.183.10はk8sが用意したDNSサーバです。data-0.irisには10.1.243.202というIPアドレスが割り当てられていることがわかります。FQDNはdata-0.iris.default.svc.cluster.localです。同様にdata-1.irisもDNSに登録されています。

独自イメージを使用する場合

現在のk8sはDockerを使用していません。ですので、イメージのビルドを行うためには別途Dockerのセットアップが必要です。

k8sはあくまで運用環境のためのものです

サンプルイメージを使用する場合

イメージはどんな内容でも構いませんが、ここでは例としてsimpleを使用します。このイメージはMYAPPネームスペース上で、ごく簡単なRESTサービスを提供します。データの保存場所をコンテナ内のデータベース(MYAPP-DATA)から外部データベース(MYAPP-DATA-EXT)に切り替えるために、cpfのactionにModifyNamespaceを使用しています。
mk8s-simple.ymlとしてご用意しました(mk8s-iris.ymlとほとんど同じです)。これを使用して起動します。

自分でイメージをビルドする場合

ご自身でビルドを行いたい場合は、下記の手順でmicrok8sが用意した組み込みのコンテナレジストリに、イメージをpushします。

内容のわからない非公式コンテナイメージって...ちょっと気持ち悪いかも、ですよね。

(Docker及びdocker-composeのセットアップが済んでいること)

$ git clone https://github.com/IRISMeister/simple.git
$ cd simple
$ ./build.sh
$ docker tag dpmeister/simple:latest localhost:32000/simple:latest
$ docker push localhost:32000/simple:latest

このイメージを使用するように、ymlを書き換えます。

mk8s-simple.ymlを編集
前)        image: dpmeister/simple:latest
後)        image: localhost:32000/simple

起動方法

既にポッドを起動しているのであれば、削除します。

$ kubectl delete -f mk8s-iris.yml
$ kubectl delete pvc -l app=iris
$ kubectl apply -f mk8s-simple.yml
$ kubectl get svc
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)           AGE
kubernetes   ClusterIP      10.152.183.1     <none>           443/TCP           3h36m
iris         ClusterIP      None             <none>           52773/TCP         20m
iris-ext     LoadBalancer   10.152.183.224   192.168.11.110   52773:30308/TCP   20m
$
$ curl -s -H "Content-Type: application/json; charset=UTF-8" -H "Accept:application/json" "http://192.168.11.110:52773/csp/myapp/get" --user "appuser:SYS" | python3 -mjson.tool
{
    "HostName": "data-1",
    "UserName": "appuser",
    "Status": "OK",
    "TimeStamp": "05/17/2021 19:34:00",
    "ImageBuilt": "05/17/2021 10:06:27"
}

curlの実行を繰り返すと、HostName(RESTサービスが動作したホスト名)がdata-0だったりdata-1だったりしますが、これは(期待通りに)ロードバランスされているためです。

まれにログインに失敗したり、待たされることがあります。Community EditionはMAX 5セッションまでですが、以前の何らかの操作によりその上限を超えてしまっている可能性があります。その場合、ライセンス上限を超えた旨のメッセージがログされます。

$ kubectl logs data-0
  ・
  ・
05/17/21-19:21:17:417 (2334) 2 [Generic.Event] License limit exceeded 1 times since instance start.

Longhornを使用する場合

分散KubernetesストレージLonghornについては、こちらを参照ください。

IRISのようなデータベース製品にとってのメリットは、クラウド環境でアベーラビリティゾーンをまたいだデータベースの冗長構成を組めることにあります。アベーラビリティゾーン全体の停止への備えは、ミラー構成を組むことで実現しますが、Kubernetes環境であれば、分散Kubernetesストレージの採用という選択子が増えます。

ミラー構成とは異なり、データベース以外のファイルも保全できるというメリットもあります。ただしパフォーマンスへの負のインパクトには要注意です。

起動方法

longhornを起動し、すべてのポッドがREADYになるまで待ちます。

$ kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.2.3/deploy/longhorn.yaml
$ kubectl -n longhorn-system get pods
NAME                                       READY   STATUS    RESTARTS   AGE
longhorn-ui-5b864949c4-72qkz               1/1     Running   0          4m3s
longhorn-manager-wfpnl                     1/1     Running   0          4m3s
longhorn-driver-deployer-ccb9974d5-w5mnz   1/1     Running   0          4m3s
instance-manager-e-5f14d35b                1/1     Running   0          3m28s
instance-manager-r-a8323182                1/1     Running   0          3m28s
engine-image-ei-611d1496-qscbp             1/1     Running   0          3m28s
csi-attacher-5df5c79d4b-gfncr              1/1     Running   0          3m21s
csi-attacher-5df5c79d4b-ndwjn              1/1     Running   0          3m21s
csi-provisioner-547dfff5dd-pj46m           1/1     Running   0          3m20s
csi-resizer-5d6f844cd8-22dpp               1/1     Running   0          3m20s
csi-provisioner-547dfff5dd-86w9h           1/1     Running   0          3m20s
csi-resizer-5d6f844cd8-zn97g               1/1     Running   0          3m20s
csi-resizer-5d6f844cd8-8nmfw               1/1     Running   0          3m20s
csi-provisioner-547dfff5dd-pmwsk           1/1     Running   0          3m20s
longhorn-csi-plugin-xsnj9                  2/2     Running   0          3m19s
csi-snapshotter-76c6f569f9-wt8sh           1/1     Running   0          3m19s
csi-snapshotter-76c6f569f9-w65xp           1/1     Running   0          3m19s
csi-attacher-5df5c79d4b-gcf4l              1/1     Running   0          3m21s
csi-snapshotter-76c6f569f9-fjx2h           1/1     Running   0          3m19s

mk8s-iris.ymlの全て(2箇所あります)のstorageClassNameをlonghornに変更してください。 もし、microk8s_hostpathで既に起動しているのであれば、ポッド、PVCともに全て削除したうえで、上述の手順を実行してください。つまり...

$ kubectl delete -f mk8s-iris.yml --wait
$ kubectl delete pvc -l app=iris

mk8s-iris.yml編集
前)storageClassName: microk8s-hostpath
後)storageClassName: longhorn

$ kubectl apply -f mk8s-iris.yml

マウントしたLonghorn由来のボリュームのオーナがrootになっていたのでsecurityContext:fsGroupを指定しています。これ無しでは、データベース作成時にプロテクションエラーが発生します。
fsGroup指定なしの場合

$ kubectl exec -it data-0 -- ls / -l
drwxr-xr-x   3 root      root         4096 May 18 15:40 vol-data

fsGroup指定ありの場合

$ kubectl exec -it data-0 -- ls / -l
drwxrwsr-x   4 root      irisowner     4096 Jan  5 17:09 vol-data

2021.1まではfsGroup:52773を指定すると動きましたが、2022.1以後はfsGroup:51773を指定すると動きました。

下記を実行すれば、Windowsのブラウザから、Longhorn UIを参照できます。

$ microk8s enable ingress
$ kubectl apply -f longhorn-ui-ingress.yml

ポート80を他の用途に使ってる場合、下記のようにport-forwardを使う方法もあります。この場合ポートは8080なので、URLはこちらになります。

$ kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80 --address 0.0.0.0

UIで、VolumeのStateが"Degraded"になっていますが、これはReplicaの数がnumberOfReplicasの既定値3を満たしていないためです。

以降の操作は、同様です。不要になれば削除します。

$ kubectl delete -f mk8s-iris.yml
$ kubectl delete pvc --all

削除方法

Longhorn環境が不要になった場合は、下記のコマンドで削除しておくと良いようです(いきなりdeleteしてはダメ)。

$ kubectl create -f https://raw.githubusercontent.com/longhorn/longhorn/v1.2.3/uninstall/uninstall.yaml
$ kubectl get job/longhorn-uninstall -n default -w
NAME                 COMPLETIONS   DURATION   AGE
longhorn-uninstall   1/1           79s        97s
^C
$ kubectl delete -f https://raw.githubusercontent.com/longhorn/longhorn/v1.2.3/deploy/longhorn.yaml
$ kubectl delete -f https://raw.githubusercontent.com/longhorn/longhorn/v1.2.3/uninstall/uninstall.yaml

apply時のエラー

Longhornの前回の使用時に綺麗に削除されなかった場合に、apply時に下記のようなエラーが出ることがあります。

$ kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml
  ・
  ・
Error from server (Forbidden): error when creating "https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml": serviceaccounts "longhorn-service-account" is forbidden: unable to create new content in namespace longhorn-system because it is being terminated
Error from server (Forbid

上記のuninstall.yamlを使った削除手順をもう一度実行したら回復しました。

その他気づいた事

storageClassにmicrok8s_hostpathを指定した場合、マルチノード環境ではsecurityContext:fsGroupが正しく機能しないようです。その結果、下記のようなエラーが発生して、データベースの作成に失敗します(Error=-13はPermission denieです)。longhornは問題なく動作しました。

01/07/22-23:11:32:729 (1205) 1 [Utility.Event] ERROR #503: Error executing [Actions] section in file /iris-mgr/IRIS_conf.d/merge_actions.cpf
01/07/22-23:11:32:729 (1205) 1 [Utility.Event] ERROR #507: Action 'CreateDatabase' failed at line 2, Method Config.CPF:CreateOneDatabase(), Error=ERROR #5032: Cannot create directory '/vol-data/db/TEST-DATA/, Error=-13'

InterSystems Kubernetes Operator

IKOもmicrok8sで動作しますが、Community向けの機能ではないので、今回のご紹介は見送りました。

0
0 4769
記事 Tomohiro Iwamoto · 2月 16, 2021 19m read

目的

CloudFormationの記事は、Linux系のものが多いですが、Windowsでも自動化したいという需要がありそうですので、オリジナル記事を元に、CloudFormationを使用してミラークラスターをWindowsサーバにデプロイする例を実装してみました。また、簡単な実行例も追加しました。
ソースコード一式はこちらのGitレポジトリにあります。

更新: 2021年3月1日 ワンライナーで踏み台ホスト経由でWindowsに公開鍵認証する方法を追記しました

更新: 2022年11月29日 QuickStartの形式に合わせて大幅に変更しました。以前の内容はこちらに保存してあります。

更新: 2022年12月21日 踏み台ホストの使用を止め、代わりにAWS System Manager(SSM)を有効化しました。プライベートサブネット上のインスタンスへのアクセスが簡素化されます。以前の内容はこちらに保存してあります。

事前準備

QuickStartのデプロイ方法に従います。

非公開のS3バケットにIRISのキット及びライセンスキーをアップロードします。

  • IRISをWindowsにデプロイする場合
S3BucketName=<my bucket>
aws s3 mb s3://$S3BucketName
aws s3 cp iris.key s3://$S3BucketName
aws s3 cp ISCAgent-2022.1.0.209.0-lnxrh7x64.tar.gz s3://$S3BucketName
aws s3 cp IRIS-2022.1.0.209.0-win_x64.exe s3://$S3BucketName
  • IRISをLinux(AmazonLinuxあるいはRedHat7/x64)にデプロイする場合
S3BucketName=<my bucket>
aws s3 mb s3://$S3BucketName
aws s3 cp iris.key s3://$S3BucketName
aws s3 cp ISCAgent-2022.1.0.209.0-lnxrh7x64.tar.gz s3://$S3BucketName
aws s3 cp IRIS-2022.1.0.209.0-lnxrh7x64.tar.gz s3://$S3BucketName
  • IRISをLinux(Ubuntu/x64)にデプロイする場合
S3BucketName=<my bucket>
aws s3 mb s3://$S3BucketName
aws s3 cp iris.key s3://$S3BucketName
aws s3 cp ISCAgent-2022.1.0.209.0-lnxrh7x64.tar.gz s3://$S3BucketName
aws s3 cp IRIS-2022.1.0.209.0-lnxubuntu2004x64.tar.gz s3://$S3BucketName

このテンプレートは、AmazonLinuxを使用しています。RedHat7もしくはUbuntuを利用する場合は、別途「カスタマイズ候補」のチャプターの作業が必要です

また、事前にAWS上に用意されているテンプレートではなく、カスタマイズを行ったテンプレートを使用するので、下記を実行します。

QSS3BucketNameはS3BucketNameと同じでも、異なっていても構いません。

$ git clone https://github.com/IRISMeister/AWSIRISDeployment.git --recursive
$ cd AWSIRISDeployment

Windowsにデプロイする場合

QSS3BucketName=<my bucket>
aws s3 cp quickstart-intersystems-iris-windows s3://$QSS3BucketName/quickstart-intersystems-iris-windows --recursive
aws s3 cp submodules/quickstart-aws-vpc/templates s3://$QSS3BucketName/quickstart-intersystems-iris-windows/submodules/quickstart-aws-vpc --recursive
aws s3 cp submodules/quickstart-linux-bastion/templates s3://$QSS3BucketName/quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion --recursive
aws s3 cp submodules/quickstart-linux-bastion/scripts s3://$QSS3BucketName/quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion --recursive

Linuxにデプロイする場合

QSS3BucketName=<my bucket>
aws s3 cp quickstart-intersystems-iris s3://$QSS3BucketName/quickstart-intersystems-iris --recursive
aws s3 cp submodules/quickstart-aws-vpc/templates s3://$QSS3BucketName/quickstart-intersystems-iris/submodules/quickstart-aws-vpc --recursive
aws s3 cp submodules/quickstart-linux-bastion/templates s3://$QSS3BucketName/quickstart-intersystems-iris/submodules/quickstart-linux-bastion --recursive
aws s3 cp submodules/quickstart-linux-bastion/scripts s3://$QSS3BucketName/quickstart-intersystems-iris/submodules/quickstart-linux-bastion --recursive

実行例

以下、既存VPCにWindows版IRISのミラー構成をデプロイする際の実行例です。

VPCを新規作成する場合は、下記のVPC関連の事前準備は不要です。

事前準備

VPC関連

既存VPC上に、以下のリソースを事前に用意しました。

デプロイ実行時にNATゲートウエイが無いと、スタックの作成に失敗します。必ず用意してください。

  • パブリックサブネット
    image

  • プライベートサブネット
    image

  • ルートテーブル/ルート
    image

  • ルートテーブル/サブネットの関連付け
    image

  • NATゲートウエイ
    image

S3への各種ファイルのアップロード

S3BucketName=iwamoto-cf-templates
aws s3 mb s3://$S3BucketName
aws s3 cp ISCAgent-2022.1.0.209.0-lnxrh7x64.tar.gz s3://$S3BucketName
aws s3 cp iris.key s3://$S3BucketName
aws s3 cp IRIS-2022.1.0.209.0-win_x64.exe s3://$S3BucketName

git clone https://github.com/IRISMeister/AWSIRISDeployment.git
cd AWSIRISDeployment
# キットファイル格納先と同じbucketを使用しています
QSS3BucketName=iwamoto-cf-templates
aws s3 cp quickstart-intersystems-iris-windows s3://$QSS3BucketName/quickstart-intersystems-iris-windows --recursive
aws s3 cp submodules/quickstart-aws-vpc/templates s3://$QSS3BucketName/quickstart-intersystems-iris-windows/submodules/quickstart-aws-vpc --recursive
aws s3 cp submodules/quickstart-linux-bastion/templates s3://$QSS3BucketName/quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion --recursive
aws s3 cp submodules/quickstart-linux-bastion/scripts s3://$QSS3BucketName/quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion --recursive

このような内容になります。

aws s3 ls s3://$QSS3BucketName --recursive
2022-06-15 07:48:57  675737136 IRIS-2022.1.0.209.0-win_x64.exe
2022-10-12 01:09:57   16139988 ISCAgent-2022.1.0.209.0-lnxrh7x64.tar.gz
2022-03-29 07:11:24        546 iris.key
2022-11-25 09:15:50          0 quickstart-intersystems-iris-windows/
2022-11-25 09:16:05          0 quickstart-intersystems-iris-windows/scripts/
2022-11-28 05:34:49       6008 quickstart-intersystems-iris-windows/scripts/MirrorInstaller.xml
2022-11-28 13:34:52          0 quickstart-intersystems-iris-windows/submodules/
2022-11-28 13:34:52          0 quickstart-intersystems-iris-windows/submodules/quickstart-aws-vpc/
2022-11-28 13:34:52          0 quickstart-intersystems-iris-windows/submodules/quickstart-aws-vpc/templates/
2022-11-28 13:34:52      59966 quickstart-intersystems-iris-windows/submodules/quickstart-aws-vpc/templates/aws-vpc.template.yaml
2022-11-28 13:34:52          0 quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion/
2022-11-28 13:34:52          0 quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion/scripts/
2022-11-28 13:34:52        531 quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion/scripts/auditing_configure.sh
2022-11-28 13:34:52        881 quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion/scripts/banner_message.txt
2022-11-28 13:34:52      11763 quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion/scripts/bastion_bootstrap.sh
2022-11-28 13:34:52          0 quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion/templates/
2022-11-28 13:34:52      24068 quickstart-intersystems-iris-windows/submodules/quickstart-linux-bastion/templates/linux-bastion.template
2022-11-25 09:16:16          0 quickstart-intersystems-iris-windows/templates/
2022-11-28 09:35:42       6059 quickstart-intersystems-iris-windows/templates/iris-cluster-arbiter-node.template.yaml
2022-11-28 09:35:42      21529 quickstart-intersystems-iris-windows/templates/iris-cluster-iris-node.template.yaml
2022-11-28 09:35:42      13736 quickstart-intersystems-iris-windows/templates/iris-cluster-main.template.yaml
2022-11-28 09:35:43      10403 quickstart-intersystems-iris-windows/templates/iris-entrypoint-new-vpc.template.yaml

(オプション)全パラメータ設定済みyamlの編集

繰り返し実行することを考慮して、パラメータを全て指定した状態のテンプレートを作成しておくと便利です。

vi test-iris-cluster-main-windows.yaml

編集後のファイルをうっかり公開してしまわないよう注意してください

編集時の注意

  • BastionSubnetIdParameterには、異なるAZに属する2つのpublic subnetを指定してください。
  • InstanceSubnetIdParameterには、異なるAZに属する3つのsubnetを指定してください。はじめの2つがIRISホスト、最後に指定したサブネットがArbiterホストが稼働するサブネットになります。
  • IRISをインストールするホストは、インストール作業の際にインターネットへのアクセスを行います。具体的にはs3アクセスのためにaws cliが必要(amazon linuxと違ってUbuntuにはプリインストールされていません)なのですが、その他にもchocolateyを使用してaws cliをインストールしています。NATゲートウェイ等を構成済みのプライベートサブネットを構築済みで無い場合は、こちらを参考にして一時的に追加してください。

NATゲートウェイは存在するだけでコストが発生します。(VM起動直後のS/Wインストール時など)必要な時に作成し、不要になったらすぐ削除したかったので、別のCFテンプレートにしています。

コンソールからCloudFormationの実行

  1. スタックを「新しいリソースを使用」して作成します。

テンプレートソースには「テンプレートファイルのアップロード」を選択し、先ほど編集したファイル(test-iris-cluster-main-windows.yaml)を指定します。

「スタックの詳細を指定」画面でパラメータを設定します。

パラメータ設定値例
BastionSubnetIdParametersubnet-0f7c4xxxxxxxxxxxx,subnet-05b42xxxxxxxxxxxx
IRISPasswordParameterSYS1
InstanceSubnetIdParametersubnet-0180bxxxxxxxxxxxx,subnet-03272xxxxxxxxxxxx,subnet-08e8fxxxxxxxxxxxx
QSS3BucketNameiwamoto-cf-templates
QSS3BucketRegionap-northeast-1
QSS3KeyPrefixquickstart-intersystems-iris-windows/
S3BucketNameParameteriwamoto-cf-templates
SshKeyParameteraws
VpcIdParametervpc-0e538xxxxxxxxxxxx

「スタックオプションの設定」画面に特に設定はありません。実行がうまくいかない場合は、スタックの作成オプションの「失敗時のロールバック」を無効にしておくと、作成された環境がロールバックされずに残りますので、問題の解析がしやすくなります(不要になったら、忘れずに削除すること)。

「レビュー」画面で、「The following resource(s) require capabilities:」で、下記にチェックをいれる必要があります。

  • AWS CloudFormation によって IAM リソースがカスタム名で作成される場合があることを承認します。
  • AWS CloudFormation によって、次の機能が要求される場合があることを承認します: CAPABILITY_AUTO_EXPAND

「スタックの作成」ボタンを押します。複数のネストされたスタックの作成が開始されます。

  1. 出力内容を確認します。 スタックのステータスがCREATE_COMPLETEになるまで待機します(15分ほどかかりました)。
    スタック(アップロードしたスタックではなく、ネストされたxxx-IRISStack-xxxx)の出力を表示します。ギアアイコンで行の折り返し指定が出来ます。
Stackキー説明
IRIStackJDBCEndpointjdbc:IRIS://xxxx-NLB-xxxxx.elb.ap-northeast-1.amazonaws.com:51773/DATAJDBC Connection String
IRIStackNode01InstanceIdi-xxxxxxxxxxNode 01 Instance ID
IRIStackNode01PrivateIP10.0.12.85Node 01 Private IP
IRIStackNode02InstanceIdi-xxxxxxxxxxNode 02 Instance ID
IRIStackNode02PrivateIP10.0.10.159Node 02 Private IP

これらの出力値を記録しておいてください。

IRIS管理ポータルへのアクセス

Node01ホスト(ミラープライマリサーバ)の管理ポータルに接続します。下記のコマンドを実行します。

  • 踏み台ホストを使用しない場合

セッションマネージャのポート転送を利用します。事前にAWS CLIのインストール・構成SSMプラグインのインストールが必要です。私はWindows10+WSL2(Ubuntu)を使用していますので、下記を実行しました。

$ curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
$ sudo dpkg -i session-manager-plugin.deb
$ export instanceid=i-xxxxxxxxxx
$ aws ssm start-session --target $instanceid \
                        --document-name AWS-StartPortForwardingSession \
                        --parameters '{"portNumber":["52773"],"localPortNumber":["52773"]}'

SSHトンネルを作成する方法もあります。

  • 踏み台ホストを使用する場合
BastionPublicIP=54.168.xxx.xxx
Node01PrivateIP=10.0.12.85
ssh -i aws.pem -L 52773:$Node01PrivateIP:52773 ec2-user@$BastionPublicIP

この時点で、http://localhost:52773/csp/sys/UtilHome.csp で管理ポータルにアクセスできます。アカウントはSuperUser、パスワードはスタックのパラメータで指定したものを使用します。ミラーの状態を確認するために、管理ポータルのホーム画面の右端に「ミラー・モニターを表示」というリンクがありますので、クリックします。以下の画面のように表示されていれば正常です。

image

WindowsへのRDP接続

RDP接続する場合は、(推奨に従って)Node01をプライベートサブネットに作成している場合、ssh同様、RDPも直接接続できません。フリートマネージャーをRDPの代替えとして利用可能です。

image

フリートマネージャーを使しない場合は、下記のようなコマンドをローカルで実行し、localhostから転送する必要があります。

  • 踏み台ホストを使用しない場合
$ export instanceid=i-xxxxxxxxxx
$ aws ssm start-session --target $instanceid \
                       --document-name AWS-StartPortForwardingSession \
                       --parameters '{"portNumber":["3389"],"localPortNumber":["33389"]}'
  • 踏み台ホストを使用する場合
ssh -i aws.pem -L 33389:$Node01PrivateIP:3389 ec2-user@$BastionPublicIP

Windowsのパスワードは、AWSコンソールからのRDP接続方法で取得します。 その上で、RDPでlocalhost:33389に接続し、Administrator/取得したパスワード、でログインします。

Linuxへの接続

  • 踏み台ホストを使用しない場合

SSMのセッションマネージャを使用します。

image

あるいは、~/.ssh/configを設定することで、SSM経由でSSHアクセスすることも可能です。

$ export instanceid=i-xxxxxxxxxx
$ ssh -i aws.pem ec2-user@$instanceid
  • 踏み台ホストを使用する場合
BastionPublicIP=54.168.xxx.xx
Node01PrivateIP=10.0.12.85
ssh -i aws.pem -L 52773:$Node01PrivateIP:52773 ec2-user@$BastionPublicIP
ssh -i aws.pem -L 33389:$Node01PrivateIP:3389 ec2-user@$BastionPublicIP
ssh -i aws.pem -oProxyCommand="ssh -i aws.pem -W %h:%p ec2-user@$BastionPublicIP" ec2-user@$Node01PrivateIP
or
ssh -i aws.pem -oProxyCommand="ssh -i aws.pem -W %h:%p ec2-user@$BastionPublicIP" ubuntu@$Node01PrivateIP

BastionPublicIPは、事前に用意した踏み台ホストの公開IPです。

動作確認

  • 外部ロードバランサ経由のアクセス
    この時点で、JDBC経由でアクセス可能になっているはずです。JDBCEndpointを使用してアクセスします。

手順は省略します。実行にはJDBCドライバが必要です。

カスタマイズ候補

Windows

iris-cluster-iris-node.template.yamlでPSファイル等を作成しています。下記の箇所は、環境・目的に応じて変更してください。

  • Windows環境のローカライズ(timezone, firewall設定)

    • c:\cfn\scripts\Setup-config.ps1 注意) firewallを無効に設定しています
  • ドライブの作成、割り当て

    • Resourcesセクション
    Resources:
    NodeInstance:
        Properties:
        BlockDeviceMappings:
    
    • c:\cfn\scripts\drives.diskpart.txt
  • IRISインストール先など

    • c:\cfn\scripts\Install-IRIS.ps1
        $irisdir="h:\InterSystems\IRIS"
        $irissvcname="IRIS_h-_intersystems_iris"
        $irisdbdir="I:\iris\db\"
        $irisjrndir="J:\iris\jrnl\pri"
        $irisjrnaltdir="K:\iris\jrnl\alt"
    

    このpsファイルは、実行時に作成される/temp/envs.ps1と組み合わせれば、IRISの無人インストール用のスクリプトとして機能します。

  • プリインストールするソフトウェア

    • c:\cfn\scripts\Install-choco-packages.ps1
      s3からファイルを入手する場合、awscliは必須です。利便性のためnotepadplusplus, google chromeを追加インストールしています。

Linux

  • O/Sの指定

    現在、O/Sは利便性によりAmazon Linuxを指定してあります。

O/SをRedHatに変更するには下記ファイルの内容を変更してください。

quickstart-intersystems-iris\templates\iris-cluster-iris-node.template.yaml

  LatestAmiIdParameter:
    Type: AWS::EC2::Image::Id
    Default: ami-0be4c0b05bbeb2afd

O/SをUbuntuに変更するには下記ファイルの内容を変更してください。

quickstart-intersystems-iris\templates\iris-cluster-iris-node.template.yaml

  LatestAmiIdParameter:
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: /aws/service/canonical/ubuntu/server/focal/stable/current/amd64/hvm/ebs-gp2/ami-id
  IRISKitNameParameter:
    Type: String
    Default: IRIS-2022.1.0.209.0-lnxubuntu2004x64

オリジナル(quickstart)との差異

デプロイ先をWindowsに変更する際に、Linux版との互換性を維持したままで、いくつかの修正を加えました。

  • Windowsの起動に時間がかかるため、デフォルトのInstance typeをm5.largeからm5.xlargeに変更。gp2をgp3に変更。IOPSを指定。

  • LatestAmiIdForIRISParameterパラメータを新設。Windowsのキット(日本語版、英語版等)の指定。

  • IRISKitNameParameterパラメータを新設。キット名の指定。

  • RDPアクセス用にSecurityGroupIngress(ポート:3389)を追加

  • SE.ShardInstallerクラス修正

    • CreateMirrorSet(), JoinAsFailover()を変更
      ##class(SYS.Mirror).CreateMirrorSet(),JoinAsFailover()実行時のECPAddressのデフォルト($system.INetInfo.LocalHostName())が、Windowsでは"EC2AMAZ-F1UF3QM"のようなWindowsホスト名になる。このホスト名では他のホストからDNSで名前解決できないので、JoinMirrorAsFailoverMember()がエラーになる。そのため、下記を追加。

    CreateMirrorSet()

    set mirror("ECPAddress") = hostName  // Windows on AWS need this
    

    JoinAsFailover()

    set MirrorInfo("ECPAddress") = hostName  // Windows on AWS need this
    
  • Roleを追加

キット用S3バケットに対するs3:GetObject
ec2:Describe*
ec2:DeleteRoute
ec2:CreateRoute
ec2:ReplaceRoute

セッションマネージャ有効化のため
AmazonSSMManagedInstanceCore

  • Linux用のcloud-initのUserData(初回起動時に実行されるshell)にO/SがUbuntu,RedHat7の場合分けを追加
  • 踏み台ホストをデプロイしない代わりにAWS System Managerを有効化

その他

1. LBのヘルスチェック値

LBのヘルスチェックのデフォルト値を使用しています。quickstart-intersystems-iris-windows\templates\iris-cluster-main.template.yamlのコメントを解除して適切な値に調整してください。

      #HealthCheckTimeoutSeconds: 10
      #HealthCheckIntervalSeconds: 10
      #UnhealthyThresholdCount: 3

2. テンプレートとAWSリソースの関係

以下、テンプレートと作成されるAWSリソースの関係です。

テンプレート名作成されるリソース
iris-cluster-iris-node.template.yamlスタンドアロン
iris-cluster-iris-main.template.yamlミラー環境
iris-entrypoint-new-vpc.template.yamlVPC+ミラー環境

AWS System Managerを有効化しているため、いずれも踏み台ホストは作成しません。必要であれば、このテンプレートを参考にして作成しください。

3. テンプレートのデフォルト値

環境の明滅を繰り返し実行する可能性がある場合、パラメータを全て指定した状態のテンプレートを別途作成しておくと便利です。

テンプレート名O/S用途
test-iris-cluster-iris-node-linux.yamlLinuxスタンドアロン
test-iris-cluster-iris-node-windows.yamlWindowsスタンドアロン
test-iris-cluster-iris-main-linux.yamlLinuxミラー環境
test-iris-cluster-iris-main-windows.yamlWindowsミラー環境
test-iris-entrypoint-new-vpc-linux.yamlLinuxVPC+ミラー環境
test-iris-entrypoint-new-vpc-windows.yamlWindowsVPC+ミラー環境

例えば、スタンドアローン構成でIRISを起動したい場合は、下記のファイルを編集します。

$ #デプロイするサブネット(InstanceSubnetIdParameterの値)には既存のパブリックサブネットを指定してください
$ vi test-iris-cluster-iris-node-linux.yaml

スタック作成時に、test-iris-cluster-iris-node-windows.yamlを指定すれば、スタンドアローン構成でIRISを起動することが出来ます。

3. WindowsへのSSH

SSMのセッションマネージャを使用します。

踏み台ホストの使用を止めたため、下記内容は無効になりました。下記の方法を試す場合は、別途、こちらのようなテンプレートを利用して踏み台ホストを作成してください。

IRIS稼働ホストにOpenSSHをインストールすれば、踏み台ホスト経由で、IRISホストにSSHする事が可能です。ただし、Linux版に比べて、Windows版のIRISではCLIで実行できることが限られているため、効果は限定的です。
IRIS稼働ホストで実行。

PS C:\Users\Administrator> Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
PS C:\Users\Administrator> Start-Service sshd

Windowクライアントからアクセスする際には、Windows版OpenSSHクライアント固有の問題「posix_spawn: No such file or directory」を回避するために、Git同梱のBashを使用しました。

user@DESKTOP-XXXX MINGW64 ~ ssh -oProxyCommand="ssh -i aws.pem -W %h:%p ec2-user@54.95.xxx.xxx"  Administrator@10.0.0.62
Administrator@10.0.0.62's password: RDP接続の際に取得したパスワード

(load pubkey "aws.pem": invalid format が出ますが無視)

また、ひと手間いりますが、踏み台ホストの.ssh/authorized_keys(パブリックキー)を、デプロイ先のWindowsにコピーすれば、ワンライナーで公開鍵認証できます。

user@DESKTOP-XXXX MINGW64 ~ ssh -i aws.pem -oProxyCommand="ssh -i aws.pem -W %h:%p ec2-user@54.95.xxx.xxx" Administrator@10.0.0.62

Adminグループには特別の設定が必要でした。こちらのConfiguring the Serverを参考にしました。

4. cfn-init.logにPythonのエラーが出る場合

cfn-init.logに下記のようなエラーが出ることがあるようです。

2021-02-12 02:50:32,957 [ERROR] -----------------------BUILD FAILED!------------------------
2021-02-12 02:50:32,957 [ERROR] Unhandled exception during build: 'utf8' codec can't decode byte 0x83 in position 8: invalid start byte

回避されることを期待して、Install-IRIS.ps1に、以下の命令を追加しています。

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

参考にしたサイト

下記のサイトを参考にしています。

0
0 767
記事 Tomohiro Iwamoto · 12月 10, 2020 17m read

この記事は、GitHub Actions を使って GKE に InterSystems IRIS Solution をデプロイするの継続記事で、そこではGitHub Actions パイプラインを使って、 Terraform で作成された Google Kubernetes クラスタにzpm-registry をデプロイしています。 繰り返しにならないよう、次の項目を満たしたものを開始点とします。

訳者注) 上記の記事を読まれてから、本記事に進まれることをお勧めしますが、GKE上のサービスにドメイン名を紐づける方法を解説した単独記事としてもお読みいただけます。

0
0 389
記事 Tomohiro Iwamoto · 11月 23, 2020 21m read

この記事では、OData API 標準に基づいて開発された RESTful API サービスを利用するための IRIS クライアントの開発について説明します。

HTTP リクエストを作成し、JSON ペイロードの読み取りと書き込みを行い、それらを組み合わせて OData 用の汎用クライアントアダプタを構築する方法を確認するため、多数の組み込み IRIS ライブラリを説明します。 また、JSON を永続オブジェクトに逆シリアル化するための新しい JSON アダプタについても説明します。

RESTful API の操作

REST は World Wide Web の標準化に関する作業から作成された一連の設計原則です。 これらの原則はあらゆるクライアントサーバー通信に適用でき、HTTP API が RESTful であることを説明するためによく使用されます。

REST はステートレスなリクエスト、キャッシュ処理、統一した API 設計など、さまざまな原則を網羅しています。 ただし、詳細な実装については網羅していません。また、これらのギャップを埋めるための一般的な API 仕様は存在しません。

この曖昧さは、RESTful API に幾分かの理解、ツール、より厳密なエコシステムを中心によく構築されるライブラリが不足している原因となっています。 特に、開発者は RESTful API の検出と文書化のために独自のソリューションを構築する必要があります。

OData

OData は、一貫性のある RESTful API を構築するための OASIS の仕様です。 OASIS コミュニティには、Microsoft / Citrix / IBM / Red Hat / SAP などの有名なソフトウェア会社が参加しています。 2007 年に OData 1.0 が最初に導入され、最新バージョンの 4.1 が今年リリースされました。

OData の仕様は、メタデータ、一貫性のある操作の実装、クエリ、例外処理などを対象としています。 また、アクションや関数などの追加機能も対象としています。

TripPinWS OData API の説明

この記事では、Odata.org が例示している TripPinWS API を使用します。

他の RESTful API と同様に、一般的にはサービスのベース URL が必要です。 OData でこのベース URL にアクセスすると、API エンティティのリストも返されます。

https://services.odata.org:443/V4/(S(jndgbgy2tbu1vjtzyoei2w3e))/TripPinServiceRW

この API には Photos、People、Airlines、Airports、Me のエンティティと、GetNearestAirport という関数が含まれていることがわかります。

応答には、TripPinWS メタデータドキュメントへのリンクも含まれています。

https://services.odata.org/V4/(S(djd3m5kuh00oyluof2chahw0))/TripPinServiceRW/$metadata

このメタデータは XML ドキュメントとして実装されており、独自の XSD ドキュメントが含まれています。 これにより、IRIS XML スキーマウィザードから生成されるコードを使用してメタデータドキュメントを消費する可能性が広がります。

メタデータドキュメントは一見かなり複雑に見えるかもしれませんが、エンティティのスキーマ定義を構成するために使用されるタイプのプロパティを表しているだけです。

次の URL を使用すると、API から People のリストを取得できます。

https://services.odata.org/V4/(S(4hkhufsw5kohujphemn45ahu))/TripPinServiceRW/People

この URL は 8 人のリストを返します。この 8 という数値は、厳密な結果ごとのエンティティ数の上限値です。 実際には、これよりもはるかに大きな上限値を使用することになるでしょう。 ただし、OData が @odata.nextLink などの追加のハイパーテキストリンクを含んでいる例も示されています。そのリンクを使用すると、People を検索した結果の次のページを取得できます。

また、次のようにして上位 1 件の結果のみを選択するなど、クエリ文字列値を使用して結果リストを絞り込むこともできます。

https://services.odata.org/V4/(S(4hkhufsw5kohujphemn45ahu))/TripPinServiceRW/People?$top=1

FirstName でリクエストを絞り込むこともできます。

https://services.odata.org/V4/(S(4hkhufsw5kohujphemn45ahu))/TripPinServiceRW/People?$filter=FirstName eq 'Russell'

この例では eq 演算子を使用し、「Russell」に等しいすべての FirstName を抽出しました。 ここでは対象の文字列を一重引用符で囲むことが重要です。 OData では、さまざまな演算子を組み合わせて表現力の高い検索クエリを作成することができます。

IRIS %Net パッケージ

IRIS には、包括的な標準ライブラリが含まれています。 私たちは FTP / メール / LDAP / HTTP などのプロトコルをサポートする %Net パッケージを使用することになります。

TripPinWS サービスを使用するには、HTTPS を使用する必要があります。そのためには、IRIS 管理ポータルに HTTPS 構成を登録する必要があります。 複雑な証明書をインストールする必要はないため、次のように数ステップで作業を完了することができます。

  • IRIS 管理ポータルを開きます。
  • [ システム 管理] > [セキュリティ] > [SSL/TLS 構成] をクリックします。
  • [新規構成の作成] ボタンをクリックします。
  • 「odata_org」という構成名を入力し、[保存] をクリックします。
  • ここでは任意の名前を選択できますが、記事の残りの部分では odata_org を使用します。

これで、HttpRequest クラスを使用して全員のリストを取得できるようになりました。 Get() が動作すると、OK の場合に 1 が返ってきます。 その後、次のように応答オブジェクトにアクセスして結果を端末に出力できます。

DC>set req=##class(%Net.HttpRequest).%New()
DC>set req.SSLConfiguration="odata_org"
DC>set sc=req.Get("https://services.odata.org:443/V4/(S(jndgbgy2tbu1vjtzyoei2w3e))/TripPinServiceRW/People")
DC>w sc
1
DC>do req.HttpResponse.OutputToDevice()

先に進む前に、基本的な HttpRequest を自由に試してみてください。 Airlines や Airports を取得してみたり、不正な URL を入力した場合にどのようなエラーが返ってくるかを調べたりしてください。

汎用 OData クライアントの開発

HttpRequest クラスを抽象化し、さまざまな OData クエリオプションの実装を簡単にする汎用 OData クライアントを作成しましょう。

このクライアントを DcLib.OData.Client と名付け、%RegisteredObject を拡張して作成します。 また、特定の OData サービスの名前を定義するために使用できるいくつかのサブクラスと、HttpRequest オブジェクトなどのランタイムオブジェクトと値をカプセル化するいくつかのプロパティを定義します。

OData クライアントのインスタンス化を簡単にするため、%OnNew() メソッド(クラスのコンストラクタメソッド)もオーバーライドし、それを使用して実行時のプロパティを設定します。

Class DcLib.OData.Client Extends %RegisteredObject
{
Parameter BaseURL;
Parameter SSLConfiguration;
Parameter EntityName;
Property HttpRequest As %Net.HttpRequest;
Property BaseURL As %String;
Property EntityName As %String;
Property Debug As %Boolean [ InitialExpression = 0 ];
Method %OnNew(pBaseURL As %String = "", pSSLConfiguration As %String = "") As %Status [ Private, ServerOnly = 1 ]
{
   set ..HttpRequest=##class(%Net.HttpRequest).%New()
   set ..BaseURL=$select(pBaseURL'="":pBaseURL,1:..#BaseURL)
   set ..EntityName=..#EntityName
   set sslConfiguration=$select(pSSLConfiguration'="":pSSLConfiguration,1:..#SSLConfiguration)
   if sslConfiguration'="" set ..HttpRequest.SSLConfiguration=sslConfiguration
   quit $$$OK
}
}

このように DcLib.OData.Client を拡張し、BaseURL と SSL 構成パラメーターを一箇所で設定することにより、TripPinWS サービスに固有のクライアントクラスを定義できるようになります。

Class TripPinWS.Client Extends DcLib.OData.Client
{
Parameter BaseURL = "https://services.odata.org:443/V4/(S(jndgbgy2tbu1vjtzyoei2w3e))/TripPinServiceRW";
Parameter SSLConfiguration = "odata_org";
}

この基本クライアントを使用すれば、サービスで使用したいエンティティタイプごとにクラスを作成できます。 この新しいクライアントクラスを拡張すれば、EntityName パラメーターでエンティティ名を定義するだけで済みます。

Class TripPinWS.People Extends TripPinWS.Client
{
Parameter EntityName = "People";
}

次に、エンティティのクエリを簡単にするため、基本の DcLib.OData.Client クラスにさらにいくつかのメソッドを追加する必要があります。

Method Select(pSelect As %String) As DcLib.OData.Client
{
   do ..HttpRequest.SetParam("$select",pSelect)
   return $this
}
Method Filter(pFilter As %String) As DcLib.OData.Client
{
   do ..HttpRequest.SetParam("$filter",pFilter)
   return $this
}
Method Search(pSearch As %String) As DcLib.OData.Client
{
   do ..HttpRequest.SetParam("$search",pSearch)
   return $this
}
Method OrderBy(pOrderBy As %String) As DcLib.OData.Client
{
   do ..HttpRequest.SetParam("$orderby",pOrderBy)
   return $this
}
Method Top(pTop As %String) As DcLib.OData.Client
{
   do ..HttpRequest.SetParam("$top",pTop)
   return $this
}
Method Skip(pSkip As %String) As DcLib.OData.Client
{
   do ..HttpRequest.SetParam("$skip",pSkip)
   return $this
}
Method Fetch(pEntityId As %String = "") As DcLib.OData.ClientResponse
{
   if pEntityId="" return ##class(DcLib.OData.ClientResponse).%New($$$ERROR($$$GeneralError,"Entity ID must be provided"),"")
   set pEntityId="('"_pEntityId_"')"
   if $extract(..BaseURL,*)'="/" set ..BaseURL=..BaseURL_"/"
   set sc=..HttpRequest.Get(..BaseURL_..EntityName_pEntityId,..Debug)
   set response=##class(DcLib.OData.ClientResponse).%New(sc,..HttpRequest.HttpResponse,"one")
   quit response
}
Method FetchCount() As DcLib.OData.ClientResponse
{
   if $extract(..BaseURL,*)'="/" set ..BaseURL=..BaseURL_"/"
   set sc=..HttpRequest.Get(..BaseURL_..EntityName_"/$count")
   set response=##class(DcLib.OData.ClientResponse).%New(sc,..HttpRequest.HttpResponse,"count")
   quit response
}
Method FetchAll() As DcLib.OData.ClientResponse
{
   #dim response As DcLib.OData.ClientResponse
   if $extract(..BaseURL,*)'="/" set ..BaseURL=..BaseURL_"/"
   set sc=..HttpRequest.Get(..BaseURL_..EntityName,..Debug)
   set response=##class(DcLib.OData.ClientResponse).%New(sc,..HttpRequest.HttpResponse,"many")
   if response.IsError() return response
   //応答に nextLink が含まれる場合は、さらにデータを取得し続ける必要があります。
   while response.Payload.%IsDefined("@odata.nextLink") {
       //前の値を配列に退避し、新しい値をその配列にプッシュしてから
       //新しい応答にそれを設定し直し、新しい値のイテレータを作成します。
       set previousValueArray=response.Payload.value
       set sc=..HttpRequest.Get(response.Payload."@odata.nextLink",..Debug)
       set response=##class(DcLib.OData.ClientResponse).%New(sc,..HttpRequest.HttpResponse)
       if response.IsError() return response
       while response.Value.%GetNext(.key,.value) {
           do previousValueArray.%Push(value)    
       }
       set response.Payload.value=previousValueArray
       set response.Value=response.Payload.value.%GetIterator()
   }
   return response
}

ここでは 9 つの新しいメソッドを追加しました。 最初の 6 つはクエリオプションを定義するためのインスタンスメソッドであり、最後の 3 つは 1 つのエンティティ、すべてのエンティティ、またはすべてのエンティティのカウントを取得するためのメソッドです。

最初の 6 つのメソッドは、基本的に HTTP リクエストオブジェクトにパラメーターを設定するためのラッパーです。 コードを実装しやすくするため、これらの各メソッドはこのオブジェクトのインスタンスを返し、メソッドをチェーン化できるようにしています。

メインの Fetch() メソッドについて説明する前に、Filter() メソッドの動作を見てみましょう。

set people=##class(TripPinWS.People).%New().Filter("UserName eq 'ronaldmundy'").FetchAll()
while people.Value.%GetNext(.key,.person) {
 write !,person.FirstName," ",person.LastName    
}

このメソッドを使用すると、次のような結果が返ってきます。

Ronald Mundy

このサンプルコードでは、TripPinWS Peopleオブジェクトのインスタンスを作成しています。 これにより、ベース URL と基本クラスの証明書の構成が設定されます。 その後、その Filter メソッドを呼び出してフィルタークエリを定義してから FetchAll() を呼び出すと HTTP リクエストを呼び出すことができます。

ここで直接アクセスできるのは生の JSON データではなく、動的オブジェクトとしての People の結果であることに注意してください。 これは、例外処理を簡単にする ClientResponse オブジェクトも実装するためです。 また、返される結果のタイプに応じて動的オブジェクトを生成します。

まず、FetchAll() メソッドについて説明します。 この段階で、実装クラスが基本クラス構成で OData URL を定義しており、ヘルパーメソッドが追加のパラメーターを設定しています。FetchAll() メソッドは URL を組み立てて GET リクエストを発行する必要があります。 元のコマンドラインの例と同様に HttpRequest クラスで Get() メソッドを呼び出し、その結果から ClientResponse を作成します。

API が一度に 8 件しか結果を返さないため、このメソッドは複雑になっています。 コード内でこの制限に対処し、前の結果の nextLink 値を使用して最後のページに到達するまで次の結果ページを取得し続ける必要があります。 追加の各ページを取得する際に前の結果配列を保管してから、新しい結果をそれぞれその配列にプッシュしています。

Fetch() / FetchAll() / FetchCount() メソッドは、DcLib.OData.ClientResponse というクラスのインスタンスを返します。 例外を処理し、有効な JSON 応答を自動的に逆シリアル化するためにこのクラスを作成しましょう。

Class DcLib.OData.ClientResponse Extends %RegisteredObject
{
Property InternalStatus As %Status [ Private ];
Property HttpResponse As %Net.HttpResponse;
Property Payload As %Library.DynamicObject;
Property Value;
Method %OnNew(pRequestStatus As %Status, pHttpResponse As %Net.HttpResponse, pValueMode As %String = "") As %Status [ Private, ServerOnly = 1 ]
{
   //直接の HTTP エラーをチェック
   set ..InternalStatus = pRequestStatus
   set ..HttpResponse = pHttpResponse
   if $$$ISERR(pRequestStatus) {
       if $SYSTEM.Status.GetOneErrorText(pRequestStatus)["" set ..InternalStatus=$$$ERROR($$$GeneralError,"Could not get a response from HTTP server, server could be uncontactable or server details are incorrect")
       return $$$OK
   }
   
   //モードが count の場合、応答は JSON ではなく単なる数値になります。
   //数値であることを確認し、true ならばすべて ok を返しますが、それ以外の場合は
   //JSON で表現されるエラーを検出するためにフォールスルーします。
   if pValueMode="count" {
       set value=pHttpResponse.Data.Read(32000)
       if value?1.N {
           set ..Value=value
           return $$$OK
       }
   }
   
   //JSON ペイロードをシリアル化し、シリアル化エラーをキャッチします。
   try {
       set ..Payload={}.%FromJSON(pHttpResponse.Data)    
   } catch err {
       //先に HTTP ステータスコードのエラーをチェックします。
       if $e(pHttpResponse.StatusCode,1)'="2" {
           set ..InternalStatus = $$$ERROR($$$GeneralError,"Unexpected HTTP Status Code "_pHttpResponse.StatusCode)
           if pHttpResponse.Data.Size>0 return $$$OK
       }
       set ..InternalStatus=err.AsStatus()
       return $$$OK
   }
   
   //OData エラーのペイロードをチェックします。
   if ..Payload.%IsDefined("error") {
       do ..HttpResponse.Data.Rewind()
       set error=..HttpResponse.Data.Read(32000)
       set ..InternalStatus=$$$ERROR($$$GeneralError,..Payload.error.message)    
       return $$$OK
   }
   
   //すべて ok なら、必要なモード(many, one, count)に一致するように応答値を設定します。
   if pValueMode="one" {
       set ..Value=..Payload
   } else {
       set iterator=..Payload.value.%GetIterator()
       set ..Value=iterator
   }
   
   return $$$OK
}
Method IsOK()
{
   return $$$ISOK(..InternalStatus)
}
Method IsError()
{
   return $$$ISERR(..InternalStatus)
}
Method GetStatus()
{
   return ..InternalStatus
}
Method GetStatusText()
{
   return $SYSTEM.Status.GetOneStatusText(..InternalStatus)
}
Method ThrowException()
{
   Throw ##class(%Exception.General).%New("OData Fetch Exception","999",,$SYSTEM.Status.GetOneStatusText(..InternalStatus))
}
Method OutputToDevice()
{
   do ..HttpResponse.OutputToDevice()
}
}

ClientResponse オブジェクトのインスタンスが与えられた場合、最初にテストを実行してエラーがあったかどうかを確認することができます。 エラーは複数のレベルで発生する可能性があるため、単一の使いやすいソリューションでエラーを返すのが望ましいです。

set response=##class(TripPinWS.People).%New().Filter("UserName eq 'ronaldmundy'").FetchAll()
if response.IsError() write !,response.GetStatusText() quit

IsOK() メソッドと IsError() メソッドはオブジェクトのエラーをチェックします。 エラーが発生した場合は GetStatus() または GetStatusText() を呼び出してエラーにアクセスするか、ThrowException() を使用してエラーを例外ハンドラに渡すことができます。

エラーが発生していない場合、ClientResponse は生のペイロードオブジェクトを応答ペイロードのプロパティに代入します。

set ..Payload={}.%FromJSON(pHttpResponse.Data)

次に、応答の Value プロパティを単一のインスタンスとして、または多数の結果を探索するための配列イテレータとして、ペイロード内のメインデータ配列に設定します。

私はこれらすべてのコードを GitHub https://github.com/SeanConnelly/IrisOData/blob/master/README.md 上の単一のプロジェクトに格納しています。そこで全体を見直せば、より深く理解することができるでしょう。 次の例はすべて、ソースの GitHub プロジェクトに含まれています。

OData クライアントの使用

基本 Client クラスに関しては、With() メソッドも理解しておく必要があります。 すべてのエンティティのインスタンスを作成する代わりに、単一のクライアントクラスだけで With() メソッドを使用することができます。 With() メソッドは、指定されたエンティティ名で新しいクライアントを定義します。

ClassMethod With(pEntityName As %String) As DcLib.OData.Client
{
   set client=..%New()
   set client.EntityName=pEntityName
   return client
}

このメソッドを使用すれば、次のように基本 Client クラスですべての People を取得できます。

/// 基本クライアントクラスと .With("People") を使用してすべての "People" を取得します
ClassMethod TestGenericFetchAllUsingWithPeople()
{
   #dim response As DcLib.OData.ClientResponse
   set response=##class(TripPinWS.Client).With("People").FetchAll()
   
   if response.IsError() write !,response.GetStatusText() quit
   
   while response.Value.%GetNext(.key,.person) {
       write !,person.FirstName," ",person.LastName    
   }
}

または、次のようにクラスごとのエンティティを使用します。

/// People クラスを使用してすべての "People" を取得します
ClassMethod TestFetchAllPeople()
{
   #dim people As DcLib.OData.ClientResponse
   set people=##class(TripPinWS.People).%New().FetchAll()
   
   if people.IsError() write !,people.GetStatusText() quit
   
   while people.Value.%GetNext(.key,.person) {
       write !,person.FirstName," ",person.LastName    
   }
}

ご覧のとおり、これらの方法は非常に似通っています。 どちらの方法を選択すべきかは、具体的なエンティティについて自動補完がどれほど重要であるか、および具体的なエンティティクラスにエンティティ固有のメソッドを追加するかどうかによって異なります。

DC>do ##class(TripPinWS.Tests).TestFetchAllPeople()
Russell Whyte
Scott Ketchum
Ronald Mundy
… およびその他の人

次に、Airlines についても同じ処理を実装しましょう。

/// すべての "Airlines" を取得します
ClassMethod TestFetchAllAirlines()
{
   #dim airlines As DcLib.OData.ClientResponse
   set airlines=##class(TripPinWS.Airlines).%New().FetchAll()
   
   if airlines.IsError() write !,airlines.GetStatusText() quit
   
   while airlines.Value.%GetNext(.key,.airline) {
       write !,airline.AirlineCode," ",airline.Name    
   }
}

そして、コマンドラインから次の結果を得ることができます。

DC>do ##class(TripPinWS.Tests).TestFetchAllAirlines()
AA American Airlines
FM Shanghai Airline
… およびその他の航空会社

次は Airports の実装です。

/// すべての "Airports" を取得します
ClassMethod TestFetchAllAirports()
{
   #dim airports As DcLib.OData.ClientResponse
   set airports=##class(TripPinWS.Airports).%New().FetchAll()
   
   if airports.IsError() write !,airports.GetStatusText() quit
   
   while airports.Value.%GetNext(.key,.airport) {
       write !,airport.IataCode," ",airport.Name    
   }
}

そして、コマンドラインから次の結果を得ることができます。

DC>do ##class(TripPinWS.Tests).TestFetchAllAirports()
SFO San Francisco International Airport
LAX Los Angeles International Airport
SHA Shanghai Hongqiao International Airport
… およびその他の空港

これまでは FetchAll() メソッドを使用してきました。 次のように Fetch() メソッドを使用し、エンティティの主キーを使用して単一のエンティティを取得することもできます。

/// 人の識別子を使用して単一の "People" エンティティを取得します
ClassMethod TestFetchPersonWithID()
{
   #dim response As DcLib.OData.ClientResponse
   set response=##class(TripPinWS.People).%New().Fetch("russellwhyte")
   
   if response.IsError() write !,response.GetStatusText() quit
   
   //新しいフォーマッターを使用して出力を美しく整形してみましょう(最新バージョンの IRIS のみ)
   set jsonFormatter = ##class(%JSON.Formatter).%New()
   do jsonFormatter.Format(response.Value)
}

この例では動的配列またはオブジェクトを取得し、整形したJSONに出力できる新しい JSON フォーマッタークラスを使用しています。

DC>do ##class(TripPinWS.Tests).TestFetchPersonWithID()
{
 "@odata.context":"http://services.odata.org/V4/(S(jndgbgy2tbu1vjtzyoei2w3e))/TripPinServiceRW/$metadata#People/$entity",
 "@odata.id":"http://services.odata.org/V4/(S(jndgbgy2tbu1vjtzyoei2w3e))/TripPinServiceRW/People('russellwhyte')",
 "@odata.etag":"W/\"08D720E1BB3333CF\"",
 "@odata.editLink":"http://services.odata.org/V4/(S(jndgbgy2tbu1vjtzyoei2w3e))/TripPinServiceRW/People('russellwhyte')",
 "UserName":"russellwhyte",
 "FirstName":"Russell",
 "LastName":"Whyte",
 "Emails":[
   "Russell@example.com",
   "Russell@contoso.com"
 ],
 "AddressInfo":[
   {
     "Address":"187 Suffolk Ln.",
     "City":{
       "CountryRegion":"United States",
       "Name":"Boise",
       "Region":"ID"
     }
   }
 ],
 "Gender":"Male",
 "Concurrency":637014026176639951
}

OData の永続化

最後のいくつかの例では、新しい JSON アダプタクラスを使用して OData JSON を永続オブジェクトに逆シリアル化する方法を説明します。 ここでは Person、Address、City の 3 つのクラスを作成しますが、いずれも Person のデータ構造を OData メタデータに反映します。 また、@odata.context のような追加の OData プロパティが逆シリアル化エラーをスローしないよう、1 に設定された %JSONIGNOREINVALIDFIELD を使用します。

Class TripPinWS.Model.Person Extends (%Persistent, %JSON.Adaptor)
{
Parameter %JSONIGNOREINVALIDFIELD = 1;
Property UserName As %String;
Property FirstName As %String;
Property LastName As %String;
Property Emails As list Of %String;
Property Gender As %String;
Property Concurrency As %Integer;
Relationship AddressInfo As Address [ Cardinality = many, Inverse = Person ];
Index UserNameIndex On UserName [ IdKey, PrimaryKey, Unique ];
}
Class TripPinWS.Model.Address Extends (%Persistent, %JSON.Adaptor)
{
Property Address As %String;
Property City As TripPinWS.Model.City;
Relationship Person As Person [ Cardinality = one, Inverse = AddressInfo ];
}
Class TripPinWS.Model.City Extends (%Persistent, %JSON.Adaptor)
{
Property CountryRegion As %String;
Property Name As %String;
Property Region As %String;
}

次に、OData サービスから Russel Whyte を取得し、Person モデルの新しいインスタンスを作成した後に応答値を使用して %JSONImport() メソッドを呼び出します。 これにより、Address と City の詳細とともに Person オブジェクトにデータが入力されます。

ClassMethod TestPersonModel()
{
   #dim response As DcLib.OData.ClientResponse
   set response=##class(TripPinWS.People).%New().Fetch("russellwhyte")
   
   if response.IsError() write !,response.GetStatusText() quit
   
   set person=##class(TripPinWS.Model.Person).%New()
   
   set sc=person.%JSONImport(response.Value)
   if $$$ISERR(sc) write !!,$SYSTEM.Status.GetOneErrorText(sc) return
   
   set sc=person.%Save()
   if $$$ISERR(sc) write !!,$SYSTEM.Status.GetOneErrorText(sc) return
}

次に、次のように SQL コマンドを実行してデータが永続化されていることを確認できます。

SELECT ID, Concurrency, Emails, FirstName, Gender, LastName, UserName
FROM TripPinWS_Model.Person
ID                          Concurrency                      Emails                                                                                    FirstName    Gender    LastName    UserName
russellwhyte    637012191599722031    Russell@example.com Russell@contoso.com    Russell            Male         Whyte            russellwhyte

最終的な考え

上記のように、組み込みの %NET クラスを使用して RESTful な OData サービスを利用するのは簡単です。 少しばかりの追加ヘルパーコードを使用すれば、OData クエリの構築を単純化し、エラーレポートを統合し、JSON を動的オブジェクトに自動的に逆シリアル化できます。

そして、必要に応じてベース URL と HTTPS 構成を指定するだけで、新しい OData クライアントを作成できます。 さらに、この単一のクラスと .With('エンティティ') メソッドを使用してサービス上の任意のエンティティを利用するか、関心のあるエンティティの名前付きサブクラスを作成することができます。

また、新しい JSON アダプタを使用して JSON 応答を永続クラスに直接逆シリアル化できることも説明しました。 現実的には最初にこのデータを非正規化することを検討し、JSON アダプタクラスがカスタムマッピングで機能することを確認する必要があります。

最後になりますが、OData の操作は非常に簡単です。 私が特注の実装でよく経験する場合よりもはるかに少ないコード量でサービス実装の一貫性を維持することができました。 私は RESTful 設計の自由さを楽しんでいますが、次のサーバーサイドソリューションでは標準を実装することを検討したいと思います。

0
0 301
記事 Tomohiro Iwamoto · 10月 22, 2020 12m read

リモートや在宅での勤務が一般化しつつあります。

そのため、今までの集中型、オンサイトの開発体制を見直し、分散型の開発体制への移行を進めておられるユーザさんも多いのではないかと思います。

VSCodeを使用したIRISアプリケーションの開発が、コミュニティーを中心に広まり始めて久しいですが、Gitとの相性が良いこの開発ツールが今後さらに浸透していくことは間違いありません。あちらこちらで、その使いまわし方が語られていますが、ここでは、ソースコントロールとの関連を中心にご紹介したいと思います。

ObjectScript Extensionの使い方の基本については、こちらこちらをご覧ください。

 VSCode InterSystems ObjectScript Extensionのプロダクションリリース(V1.0.x)の配布が始まりました。  

これに合わせて、今までのコミュニティーサポートに加え、InterSystemsによる公式サポートもアナウンスされています。よりいっそう安心してご利用いただけるようになりました。

目的

0
1 3198
記事 Tomohiro Iwamoto · 8月 28, 2020 3m read

本記事について

InterSystems IRISをモニタリングする方法はいくつかあります。

  • SNMP
  • システムモニターとemail通知機能
  • 管理ポータルのシステムダッシュボード
  • Prometheus+Grafanaを使用
  • InterSystems SAM (System Alerting and Monitoring)

本記事では上記に加えてAWSにIRISをデプロイする場合に自然な選択子となりうる方法として、CloudWatchを使用する方法をご紹介します。

AWSネイティブの各種サービスとIRISを連携させる方法の典型のご紹介を兼ねています。

内容は、Open Exchangeで公開されています。日本語のREADMEがありますのでそちらをご覧ください。
README.MDからの引用

InterSystems IRISの各種メトリクスとログをAWS CloudWatchに簡単に公開することができます。
これらメトリクスとログがあれば、IRISのデータをダッシュボードやアラートなどに統合することができます。

0
0 246
記事 Tomohiro Iwamoto · 8月 25, 2020 6m read

本稿について

ICM(InterSystems Cloud Manager)のセットアップは難しいものではありませんが、様々な理由でそもそもDockerが使いづらいという状況があり得ます。
また、セキュリティ的に堅固な環境を得るために、既存VPC内のプライベートサブネット上にIRISクラスタをデプロイする方法のひとつに、同VPC内でICM実行する方法があります。
本稿では、ICMをAWSにデプロイする作業を、CloudFormationで自動化する方法をご紹介します。ICMに関しては、こちらの記事をご覧ください。

更新: 2020年11月24日  デフォルトVPC以外でも動作するよう変更しました。

0
0 1024
記事 Tomohiro Iwamoto · 8月 17, 2020 9m read

本稿について

本稿では、InterSystems IRISを使用してSQLベースのベンチマークを行う際に、実施していただきたい項目をご紹介します。 Linuxを念頭においていますが、Windowsでも考慮すべき点は同じです。

メモリ自動設定をやめる

パフォーマンスに直結する、データベースバッファサイズの自動設定はデフォルトで有効になっています。自動設定は、実メモリの搭載量にかかわらず、データベースバッファを最大で1GBしか確保しません。

更新: 2020年11月20日   バージョン2020.3から、確保を試みるデータベースバッファが実メモリの25%に変更されました。

搭載実メモリ64GB未満の場合は実メモリの50%程度、搭載実メモリ64GB以上の場合は実メモリの70%を目途に、明示的に設定を行ってください。

設定するにはiris停止状態で、iris.cpfファイル(IRISインストール先\mgr\iris.cpf)を変更します。下記はブロックサイズ8KB用(既定値です)のデータベースバッファサイズの自動構成を4096(MB)に変更する例です。

修正前

[config]
globals=0,0,0,0,0,0

修正後

[config]
globals=0,0,4096,0,0,0

詳細はこちらです。

また、Linuxの場合、ヒュージ・ページ有効化の設定を行ってください。設定値の目安ですが、IRIS起動時のメッセージから、確保される共有メモリサイズ(下記だと749MB)を読み取り、その値めがけて設定するのが簡単です。

コンテナバージョンは非rootで動作するため、ヒュージ・ページは利用できません

$ iris start iris
Starting IRIS
Using 'iris.cpf' configuration file
Starting Control Process
Allocated 749MB shared memory: 512MB global buffers, 64MB routine buffer
$ cat /proc/meminfo
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

ページサイズが2048kBですので、750MB(丸めました)を確保するには750/2=375ページ必要になります。構成を変えると必要な共有メモリサイズも変わりますので、再調整してください。

$ echo 375 > /proc/sys/vm/nr_hugepages
$ iris start iris
Starting IRIS
Using 'iris.cpf' configuration file
Starting Control Process
Allocated 750MB shared memory using Huge Pages: 512MB global buffers, 64MB routine buffer

メモリ関連の設定値は、パフォーマンスに大きく影響しますので、比較対象のDBMSが存在するのであればその設定値に見合う値を設定するようにしてください。

データベース関連ファイルの配置

特にクラウド上でのベンチマークの場合、ストレージのレイアウトがパフォーマンスに大きな影響を与えます。クラウドではストレージごとのIOPSが厳格に制御されているためです。

こちらに細かな推奨事項が掲載されていますが、まずは下記のように、アクセス特性の異なる要素を、それぞれ別のストレージに配置することをお勧めします。

Filesystem      Size  Used Avail Use% Mounted on
/dev/sdc        850G  130G  720G  16% /irissys/data      (IRIS本体のインストール先、ユーザデータべース)
/dev/sdd        800G  949M  799G   1% /irissys/wij       (ライトイメージジャーナルファイル)
/dev/sde        200G  242M  200G   1% /irissys/journal1  (ジャーナルファイル)
/dev/sdf        100G  135M  100G   1% /irissys/journal2  (予備のジャーナルファイル保存エリア)

サイズは用途次第です。WIJに割り当てているサイズが大きめなのは、IOPS性能がファイルシステムの容量に比例するクラウドを使用するケースを意識したためで、IOPSとサイズが連動しないオンプレミス環境では、これほどのサイズは必要ありません。

テーブルへのインデックス追加 

IRISのテーブルへのインデックス追加は一部のDWH製品のように自動ではありません。多くのRDBMS製品と同様に、CREATE TABLE命令の実行時に、プライマリキー制約やユニーク制約が指定されている場合、インデックが追加されますが、それ以外のインデックスはCREATE INDEX命令で明示的に追加する必要があります。

どのようなインデックスが有用なのかは、実行するクエリに依存します。 一般的に、ジョインの結合条件(ON句)となるフィールド、クエリやUPDATE文の選択条件(WHERE句)となるフィールド、グルーピングされる(GROUP BY)フィールドが対象となります。

インデックスの対象を参照ください。

データベースファイルのサイズ

IRISのデータベースファイルの初期値は1MBです。既定では、ディスク容量が許容する限り自動拡張を行いますので、エラーにはなりませんが、パフォーマンスは劣化します。ベンチマークプログラムでそのことに気づくことはありません。お勧めは、一度ベンチマークを実行して、必要な容量まで自動拡張を行い、データを削除(DROP TABLEやTRUNCATE TABLEを実行)した後に、再度ベンチマークを実行することです。

統計情報の取得 

IRISはクエリの実行プランを最適化するために、テーブルデータに関する統計情報を使用します。 統計情報の取得処理(TuneTable/TuneSchema)は、初期データロード後に明示的に実行する必要があります。

IRISバージョン2022.1以降、統計情報の取得処理が一度も実行されていない場合、初回のクエリ実行時に自動実行されるようになりました。そのため、初回のクエリに若干のオーバヘッドが発生します。

TuneTable(テーブル対象)/TuneSchema(スキーマ対象)を実行すると、既存のクエリプランがパージされるので、次回の初回クエリ実行時はクエリ解析・プラン生成にかかる分だけ時間が多めにかかります。 また、TuneTable/TuneSchema実行後にインデックスを追加した場合には、そのテーブルに対してTuneTableを再実行してください。

これらを考慮すると、ベンチマークで安定した計測結果を得るには、テストデータのロード後に、明示的に下記を実施することが重要になります。

  • TuneTable/TuneSchemaを実行
  • 最低でも一度は全クエリを実行する

もちろん、これらの性能を計測したい場合は、その限りではありません。

TuneTable/TuneSchemaは、管理ポータル及びCLIで、手動で実行可能です。ベンチマーク測定時は、データの再投入やインデックスの追加や削除といった、統計情報の更新を繰り返し実行する必要性が高くなることを考慮すると、手動での更新は避けて、下記のObjectScriptのCLIやSQL文で実施するのが効果的です。

下記コマンドはスキーマMySchemaで始まる全てのテーブルの統計情報を取得します。

MYDB>d $SYSTEM.SQL.Stats.Table.GatherSchemaStats("MySchema")

下記コマンドは指定したテーブルのテーブルの統計情報を取得します。

MYDB>d $SYSTEM.SQL.Stats.Table.GatherTableStats("MySchema.MyTable")

あるいはSQL文として、データロード実行後にベンチマークプログラム内で実行する事も可能です。

TUNE TABLE MySchema.MyTable

タイムスタンプ型に%TimeStampではなく%PosixTimeを使用する

更新: 2020年11月20日   使用方法を、SQL文の修正が不要な方法に変更しました

IRISバージョン2022.1以降、SQLのTimeStamp型の既定値が%PosixTimeに変更されました。この変更によりテーブルを新規作成した際のTimeStamp型は%PosixTimeになります。

create table TEST (ts TIMESTAMP)

これを、下記のように変更してください。

create table TEST (ts POSIXTIME)

デフォルトではSQLのTIMESTAMP型は、IRIS内部では%TimeStamp型で保持されます。 %TimeStamp型は、データを文字列として保存するのに対して、%PosixTimeは64bitの整数で保持します。その分、ディスクやメモリ上のデータサイズや比較演算処理などで有利になります。両者はxDBC上は、共にTIMESTAMP型となり、互換性がありますので、DML文の修正は不要です(敢えて指摘するとすれば、ミリ秒以下の精度が異なります)。以下は、実行例です。%PosixTimeに対するクエリのほうが、程度の差こそあれ、高速になっています。

create table TEST (ts TIMESTAMP, pt POSIXTIME)
create index idxts on TABLE TEST (ts)
create index idxpt on TABLE TEST (pt)
insert into TEST ... 100万レコードほどINSERT

select count(*),DATEPART(year,pt) yyyy from TEST  group by DATEPART(year,pt) 
平均 669ミリ秒
select count(*),DATEPART(year,ts) yyyy from TEST  group by DATEPART(year,ts) 
平均 998ミリ秒

select count(*) from TEST  where DATEPART(year,pt)='1990' 
平均 533ミリ秒
select count(*) from TEST  where DATEPART(year,ts)='1990' 
平均 823ミリ秒

select count(*) from TEST where pt>='1980-01-01 00:00:00' and pt<'1991-01-01 00:00:00'
平均 350ミリ秒
select count(*) from TEST where ts>='1980-01-01 00:00:00' and ts<'1991-01-01 00:00:00'
平均 381ミリ秒

TIMESTAMP型を%PosixTimeに変更するには、テーブルの作成を行う前、かつIRIS停止中に構成ファイル(IRISインストール先\mgr\iris.cpf)の下記を変更してください。以後、TIMESTAMP型で定義したカラムは%PosixTimeになります。

IRISバージョン2022.1以降、既定値がPosixTimeに変更されましたので、構成ファイルの変更は不要です。

[SQL]
TimePrecision=0  (修正前)
TimePrecision=6  (修正後)
[SqlSysDatatypes]
TIMESTAMP=%Library.TimeStamp (修正前)
TIMESTAMP=%Library.PosixTime  (修正後)

バイナリデータの保存には可能であればVARBINARYを使用する。

バイナリデータを保存する場合、サイズの上限が指定できる場合はVARBINARYを使用してください。LONGVARBINARYはサイズ上限が無い代わりに、内部でストリーム形式で保存する機構を伴うためパフォーマンスが低下します。

CREATE TABLE TestTable (ts TIMESTAMP, binaryA VARBINARY(512), binaryB VARBINARY(256))

VARBINARY使用時には、行全体のサイズに注意が必要です。SQLの行は、IRIS内部では、既定では下記のフォーマットで、1つのノードのデータ部(IRISの内部形式の用語でKV形式のバリューに相当します)に格納されます。このノードのサイズ上限は3,641,144バイトです。

長さ|データタイプ|データ|長さ|データタイプ|データ|...

この長さを超えると、<MAXSTRING>という内部エラーが発生し、INSERTが失敗します。

Query ソース保存有効化

実行プランがコード化されたものがソースコードとして保存されます。デフォルトでは無効です。本稿では扱っていませんが、後の解析で有用になることもありますので、念のため有効化しておきます。

[SQL]
SaveMAC=1   <== 既定値は0です

一通りのベンチマークを実行

この時点での実行結果は思った結果が得られないかもしれません。 ベンチマークプログラムによるレコードのINSERTにより、ユーザデータベースの自動拡張、一時データべースの自動拡張、WIJの自動拡張が、クエリにより、クエリプランの作成、インデックスの不足によるテーブルフルスキャンなどが発生している可能性があるためです。

(ベンチマーク実施により蓄積したデータの消去後の)2回目以降は、インデックスの不足以外の問題は解消されているので、パフォーマンスが向上するかもしれません。 また、これとは逆にジャーナルは、随時蓄積していきますので、ベンチマーク実施を繰り返すうちに、いずれはディスクの空き容量を圧迫します。 適宜ジャーナルファイルを削除してください。運用環境では絶対禁止の方法ですが、データベースミラーリングを使用しておらず、データの保全が必要のないベンチマーク環境でしたら、最新のジャーナルファイル以外をO/Sシェルでrmしてしまって構いません。

これ以後の、細かな確認作業はさておき、まずは上記の項目の実施をスタートラインとすることをお勧めします。

0
0 587
記事 Tomohiro Iwamoto · 8月 13, 2020 7m read

本記事について

InterSystems IRISは、管理ポータルへのアクセス方法がデフォルトではhttpとなっており、クライアントが社内、サーバがクラウドという配置の場合、なんらかの方法でトラフィックを暗号化したいと考える方も多いかと思います。
そこで、AWS上にて稼働中のIRISの管理ポータル(あるいは各種RESTサービス)との通信を暗号化する方法をいくつかご紹介したいと思います。

本記事では、アクセスにIRIS組み込みのapacheサーバを使用しています。ベンチマーク目的や本番環境のアプリケーションからのアクセス方法としては使用しないでください。
短期間・少人数での開発・動作検証・管理目的でのアクセスを暗号化する事を想定しています。

ドメイン名とメジャーな認証局発行のSSLサーバ証明書を用意できればベストなのですが、上記のような用途の場合、コスト面でなかなか難しいと思います。
ですので、下記の証明書の使用を想定しています。
- 自己署名(いわゆるオレオレ証明書)
- 自分で建てた認証局で署名した証明書(いわゆるオレオレ認証局)

また、下記のような実行環境を想定しています。

0
0 802
記事 Tomohiro Iwamoto · 7月 16, 2020 25m read

この記事ではVMware ESXi 5.5以降の環境にCaché 2015以降を導入する場合の構成、システムのサイジング、およびキャパシティ計画のガイドラインを示します。 

ここでは、皆さんがVMware vSphere仮想化プラットフォームについてすでに理解していることを前提としています。 このガイドの推奨事項は特定のハードウェアやサイト固有の実装に特化したものではなく、vSphereの導入を計画して構成するための完全なガイドとして意図されたものでもありません。これは、皆さんが選択可能なベストプラクティス構成をチェックリストにしたものです。 これらの推奨事項は、皆さんの熟練したVMware実装チームが特定のサイトのために評価することを想定しています。 

InterSystems データプラットフォームとパフォーマンスに関する他の連載記事のリストはこちらにあります。 

注意: 本番データベースインスタンス用のVMメモリを予約し、Cachéに確実にメモリを使用させてデータベースのパフォーマンスに悪影響を与えるスワップやバルーニングの発生を防ぐ必要があることを強調するため、この記事を2017年1月3日に更新しています。 詳細については、以下のメモリセクションを参照してください。 

参考情報 

0
0 636
記事 Tomohiro Iwamoto · 6月 29, 2020 32m read

ここ数年の間、ハイパーコンバージドインフラストラクチャ(HCI)ソリューションが勢いを増しており、導入件数が急速に増加しています。 IT部門の意思決定者は、VMware上ですでに仮想化されているアプリケーションなどに対し、新規導入やハードウェアの更新を検討する際にHCIを考慮に入れています。 HCIを選択する理由は、単一ベンダーと取引できること、すべてのハードウェアおよびソフトウェアコンポーネント間の相互運用性が検証済みであること、IO面を中心とした高いパフォーマンス、単純にホストを追加するだけで拡張できること、導入や管理の手順が単純であることが挙げられます。 

この記事はHCIソリューションの一般的な機能を取り上げ、HCIを初めて使用する読者に紹介するために執筆しました。 その後はデータベースアプリケーションの具体的な例を使用し、InterSystems データプラットフォーム上に構築されたアプリケーションを配置する際の、キャパシティプランニングとパフォーマンスに関する構成の選択肢と推奨事項を確認します。 HCIソリューションはパフォーマンスを向上させるためにフラッシュストレージを利用しているため、選択されたフラッシュストレージオプションの特性と使用例に関するセクションも含めています。 

0
0 422
記事 Tomohiro Iwamoto · 6月 8, 2020 24m read

VMware vSphereで実行する大規模な本番データベースのCPUキャパシティプランニングについて、お客様やベンダー、または社内のチームから説明するように頼まれることが良くあります。 

要約すると、大規模な本番データベースのCPUのサイジングには、いくつかの単純なベストプラクティスがあります。 

  • 物理CPUコア当たり1つのvCPUを計画する。 
  • NUMAを考慮し、CPUとメモリをNUMAノードに対してローカルに維持できるようVMの理想的なサイズを決定する。 
  • 仮想マシンを適正化する。 vCPUは必要な場合にのみ追加する。 

このことから、通常いくつかの一般的な疑問が生まれます。 

  • ハイパースレッディングにより、VMwareでは物理CPUの2倍の数でVMを作成できます。 これはキャパシティが2倍になるということか? できるだけ多くのCPUを使ってVMを作成すべきではないのか? 
  • NUMAノードとは? NUMAに配慮する必要があるのか? 
  • VMを適正化する必要があるが、どうすれば適正化されたことがわかるのか? 
0
0 12195
記事 Tomohiro Iwamoto · 6月 5, 2020 14m read

前回の記事では、pButtonsを使って履歴パフォーマンスメトリックを収集する方法を説明しました。 すべてのデータプラットフォームインスタンス(Ensemble、Cachéなど)にはpButtonsがインストールされていることがわかっているため、私はpButtonsを使用する傾向にありますが、 Cachéパフォーマンスメトリックをリアルタイムで収集、処理、表示する方法はほかにもあり、単純な監視や、それよりもさらに重要な、より高度な運用分析とキャパシティプランニングに使用することができます。 データ収集の最も一般的な方法の1つは、SNMP(簡易ネットワーク管理プロトコル)を使用することです。 

SNMPは、Cachéが管理・監視情報をさまざまな管理ツールに提供するための標準的な方法です。 Cachéのオンラインドキュメンテーションには、CachéとSNMP間のインターフェースに関する詳細が説明されています。 SNMPはCachéと「単純に連携」するはずですが、構成にはいくつかの技と罠があります。 私自身、はじめに何度も過ちを繰り返し、InterSystemsの同僚から助けを得ながらCachéをオペレーティングシステムのSNMPマスターエージェントにやっと接続できた経験から、皆さんが同じような困難を避けられるようにこの記事を書くことにしました。 

0
0 352
記事 Tomohiro Iwamoto · 6月 5, 2020 27m read

この記事では、InterSystemsデータプラットフォームで実行するデータベースアプリケーションにおけるグローバルバッファ、ルーチンバッファ、gmheap、locksizeなどの共有メモリ要件のサイジングアプローチを説明し、サーバー構成時およびCachéアプリケーションの仮想化時に検討すべきパフォーマンスのヒントをいくつか紹介します。 これまでと同じように、Cachéについて話すときは、すべてのデータプラットフォーム(Ensemble、HealthShare、iKnow、Caché)を指しています。 

このシリーズの他の記事のリストはこちら 

0
0 641
記事 Tomohiro Iwamoto · 6月 5, 2020 18m read

今週は、ハードウェアの主な”食品群” (^_^)  の1つであるCPUに注目します。お客様から、次のようなシナリオについてのアドバイスを求められました。お客様の本番サーバーはサポート終了に近づいており、ハードウェアを交換する時期が来ています。 また、仮想化によってサーバーを一元的に管理できるようにし、ベアメタルか仮想化によりキャパシティを適正化したいとも考えています。 今日はCPUに焦点を当てますが、後日の記事では、メモリやIOといったほかの主要食品群を適正化するアプローチについて説明したいと思います。 

では、質問を整理しましょう。 

  • 5年前のプロセッサをもとに作られたアプリケーション要件を今日のプロセッサに変換するには? 
  • 現在ではどのプロセッサが適しているのか? 
  • 仮想化はCPUキャパシティプランニングにどのような影響を及ぼすのか? 

2017年6月追記: 
VMware CPUに関する考慮事項と計画の詳細、および一般的な質問と問題の詳細については、「大規模データベースの仮想化 - VMware CPUのキャパシティ計画」の記事も参照してください。 

このシリーズの他の記事のリストはこちら 

spec.orgのベンチマークを使ったCPUパフォーマンスの比較 

0
0 741
記事 Tomohiro Iwamoto · 6月 4, 2020 11m read

前回の投稿では、pButtonsを使用してパフォーマンスメトリックを24時間収集する処理をスケジュールしました。 この投稿では、収集対象の主なメトリックのいくつかと、それらの土台となるシステムハードウェアがどのように関連しているかを見ていきます。 また、Caché(または任意のInterSystemsデータプラットフォーム)メトリックとシステムメトリックの関係についても調べます。 さらに、これらのメトリックを使用してシステムの毎日の健全性を把握し、パフォーマンスの問題を診断する方法もご紹介します。 

このシリーズの他の記事のリストはこちら 

2016年10月に編集... 

pButtonsデータを .csv ファイルに抽出するスクリプトの例はこちらで確認できます。 

2018年3月に編集... 

画像が消えていましたが、元に戻しました。 

ハードウェア食品グループ 

この連載を読み進めていくと、パフォーマンスに影響を与えるサーバーコンポーネントを次のように分類できることが分かってきます。 

- CPU 

- メモリ 

- ストレージIO 

- ネットワークIO 

0
0 278
記事 Tomohiro Iwamoto · 6月 3, 2020 12m read

アプリケーションがデプロイされ、すべてが問題なく動作しています。 素晴らしいですね! しかし、その後突然電話が鳴り止まなくなりました。アプリケーションが時々「遅くなる」というユーザーからの苦情の電話です。 これは一体どういうことなのでしょうか? なぜ時々遅くなるのでしょうか? このような速度低下を検出し、解決するにはどのようなツールと統計情報に注目すべきなのでしょうか? お使いのシステムのインフラはユーザーの負荷に対応できていますか? 本番環境を調べる前に、どのようなインフラ設計上の問題を問うべきなのでしょうか? 新しいハードウェアのキャパシティプランニングを、必要以上の設備投資を行うことなく自信を持って行うにはどうすればよいのでしょうか? どうすれば電話がかかってこなくなるのでしょうか? そもそも電話がかかってこないようにするには、どうすればよかったのでしょうか? 

このシリーズの他の記事のリストはこちら 

長い道のりの始まり 

0
0 290
記事 Tomohiro Iwamoto · 6月 3, 2020 2m read

「データプラットフォームのキャパシティプランニングとパフォーマンス」シリーズの全記事をリストしました。 その下には私の一般的なその他の記事も記載しています。 このシリーズに新しい記事が追加されるたびに、このリストを更新する予定です。 

「キャパシティプランニングとパフォーマンス」シリーズ 

通常、各記事はその前の記事の続きとして書かれていますが、ほかの記事を飛び越して気になるものを読むこともできます。 

その他の記事 

コミュニティに掲載中のアーキテクチャ全般の記事を集めたリストです。 

0
0 332
記事 Tomohiro Iwamoto · 5月 7, 2020 15m read

Cachéの優れた可用性とスケーリング機能の1つは、エンタープライズキャッシュプロトコル(ECP)です。 アプリケーション開発中に考慮することにより、ECPを使用した分散処理は、Cachéアプリケーションのスケールアウトアーキテクチャを可能にします。 アプリケーション処理は、アプリケーションを変更することなく、単一のアプリケーションサーバーから最大255台といった非常に高いレートにまで、アプリケーションサーバー処理能力を拡張できます。 

ECPは、私が関与していたTrakCareのデプロイメントで長年広く使用されていました。 10年前は、主要ベンダーの1つが提供する「大きな」x86サーバーは、合計で8つのコアしか備えていなかったかもしれません。 大規模なデプロイメントの場合、ECPは、高価な大型コンピュータを使う単一のエンタープライズサーバーではなく、コモディティサーバーでの処理をスケールアウトする方法でした。 コア数の多いエンタープライズサーバーでさえ制限があったため、ECPはそれらのサーバーへのデプロイメントのスケーリングにも使用されました。 

0
0 458
記事 Tomohiro Iwamoto · 5月 1, 2020 6m read

私や他のテクノロジアーキテクトは、Caché IOの要件と、Cachéアプリケーションがストレージシステムを使用する方法を顧客とベンダーに説明しなければならないことがよくあります。以下の表は、一般的なCaché IOプロファイルと、トランザクションデータベースアプリケーションの要件を顧客やベンダーに説明するときに役立ちます。 オリジナルの表は、マーク・ボリンスキーが作成しました。 

今後の投稿ではストレージIOについて詳しく説明する予定なので、今後の記事の参考資料として、今回これらの表も公開します。 

このシリーズの他の記事のリストはこちら 

予測可能なディスクIOパフォーマンスを提供し、高可用性機能をサポートし、アプリケーションにストレージの冗長性、スケーラビリティ、および信頼性を提供するには、ストレージアレイなど、ストレージを適切に設定することが不可欠です。 

CachéストレージIOプロファイル 

0
0 318