こんにちは、ロジカル・アーツの井川です。
OPC UA サーバではユーザ名とパスワードによるユーザ認証を設定することができます。今回は、ユーザ認証が設定されたサーバから SiteWise でデータを取り込んでみたいと思います。
はじめに
以前の記事で、SiteWise を使用して OPC UA サーバからデータを取得する方法を紹介しました。
この記事の状況だと、サーバに接続できる状態であれば誰でもデータを取得できてしまうので、ユーザ認証を設定し、匿名ユーザがデータを取得できないようにします。
なお、今回は扱いませんが OPU UA サーバで定義したユーザにはデータに対する細かいアクセス制御(読み込みはできるが書き込みはできないなど)が可能です。
シナリオ
以前の記事の設定を流用します。EC2 インスタンスの CPU 使用率(%)とメモリ使用量(MB)を取り込む OPC UA サーバがあり、さらにユーザ認証設定がされているとします。ユーザ名とパスワードはそれぞれ
- ユーザ名:Demouser
- パスワード:demouserpwd
とします。
この認証情報で OPC UA サーバに接続し、CPU 使用率とメモリ使用量を SiteWise に取り込めるようになることが今回の目標です。
なお、ゲートウェイの作成手順等はこの記事では扱いません。必要に応じて以下の記事を参考にしてください。
※この記事の手順を試すには、Node.js のオープンソースライブラリ node-opcua
で OPC UA サーバを構築しておくことを推奨します。
おおよその手順は以下の通りです。
手順がやや複雑ですが、まず、SiteWise ゲートウェイではユーザ認証情報(ユーザ名とパスワード)は、Secrets Manager のシークレットを介して取得されます。このとき、ゲートウェイデバイスからこのシークレットの値を取得できるよう、シークレットマネージャーのコンポーネントと当該シークレットへのアクセス権が必要です。そのため、上記の手順を実施します。
最後に、全体の構成図は次のようになります。
サンプルコード
以下の操作はすべて EC2 インスタンス内で行います。
まず、以下のコマンドで必要なライブラリをインストールします。
npm install node-opcua
次に、以下の内容のコードを sample_server.mjs
という名前で作成します。
import { DataType, OPCUAServer, Variant, makeRoles, WellKnownRoles, allPermissions, } from 'node-opcua'; import * as os from 'os'; // ユーザ情報定義 const USERS = [ { username: 'Demouser', password: 'demouserpwd', roles: makeRoles([ WellKnownRoles.AuthenticatedUser, WellKnownRoles.ConfigureAdmin, ]), }, ]; let cpuUsage = { previousAvgIdel: 0.0, previousAvgTick: 0.0, get: function () { const currentCPUAvg = _getCPUAverage(); const idelDiff = currentCPUAvg.avgIdle - this.previousAvgIdel; const tickDiff = currentCPUAvg.avgTick - this.previousAvgTick; const cpuUsage = 100 - (idelDiff / tickDiff) * 100; console.log(`CPU Usage: ${cpuUsage}%`); this.previousAvgIdel = currentCPUAvg.avgIdle; this.previousAvgTick = currentCPUAvg.avgTick; return cpuUsage; }, }; const { avgIdle, avgTick } = _getCPUAverage(); cpuUsage.previousAvgIdel = avgIdle; cpuUsage.previousAvgTick = avgTick; (async () => { try { // ユーザマネージャーの定義 const userManager = { // ユーザ名とパスワードが正しければ `true` isValidUser: (username, password) => { const index = USERS.findIndex( (x) => x.username == username && x.password == password ); return index > -1 ? true : false; }, // ユーザに応じたロール(権限)を取得 getUserRoles: (user) => { const index = USERS.findIndex((x) => x.username == user); return index > -1 ? USERS[index].roles : []; }, }; // endpoint is opc.tcp://<hostname>:4334/UA/Server const server = new OPCUAServer({ port: 4334, resourcePath: '/UA/Server', allowAnonymous: false, // 匿名ユーザのアクセスを禁止 userManager: userManager, // 上で定義したユーザマネージャー }); await server.initialize(); const addressSpace = server.engine.addressSpace; const namespace = addressSpace.getOwnNamespace(); namespace.setDefaultRolePermissions([ { roleId: WellKnownRoles.AuthenticatedUser, permissions: allPermissions, }, ]); const device = namespace.addObject({ organizedBy: addressSpace.rootFolder.objects, browseName: 'SampleObject', }); namespace.addVariable({ componentOf: device, browseName: 'CPU', dataType: DataType.Double, minimumSamplingInterval: 1000, value: { get: () => new Variant({ dataType: DataType.Double, value: cpuUsage.get(), }), }, }); namespace.addVariable({ componentOf: device, browseName: 'Memory', dataType: DataType.Double, minimumSamplingInterval: 1000, value: { get: () => new Variant({ dataType: DataType.Double, value: getMemoryUsageMB(), }), }, }); await server.start(); console.log('OPC Server is now starting ...'); console.log( 'endpoint: ', server.endpoints[0].endpointDescriptions()[0].endpointUrl ); process.on('SIGINT', async () => { await server.shutdown(); console.log('Stopping OPC UA Server ...'); }); } catch (err) { console.log(err); process.exit(1); } })(); function _getCPUAverage() { const cpus = os.cpus(); let totalIdel = 0; let totalTick = 0; for (const cpu of cpus) { for (const type in cpu.times) { totalTick += cpu.times[type]; } totalIdel += cpu.times.idle; } return { avgIdle: totalIdel / cpus.length, avgTick: totalTick / cpus.length, }; } function getMemoryUsageMB() { const memoryUsage = (os.totalmem() - os.freemem()) / 1024 ** 2; console.log(`Memory Used: ${memoryUsage}MB`); return memoryUsage; }
作成したら node sample_server.mjs
で OPC UA サーバを起動しておきましょう。この時点ではクライアント(SiteWise)側の設定がまだなので、データは取得できません*1。
シークレットの作成
Secrets Manager コンソール画面へ移動し、「新しいシークレットを保存する」をクリックします。
「その他のシークレットタイプ」を選択し、「キー/値」に以下を入力します。
キー | 値 |
---|---|
username | Demouser |
password | demouserpwd |
「次」をクリックします。
「シークレットの名前」に「sitewise-demo-auth」と入力し、「次」をクリックします。
次の画面ではそのまま「次」をクリックし、最後の画面で「保存」をクリックします。
作成したシークレットの ARN が後で必要になるので控えておきましょう。
ゲートウェイロールの編集
SiteWise が上で設定したシークレットを使用するためにゲートウェイのロールに適切な権限を付与する必要があります。
ゲートウェイのロールは、例えば次のようにして見つけられます。
IoT コンソール画面*2で左ペインの「ロールエイリアス」をクリックし、ゲートウェイに紐づくロールエイリアスをクリックします*3。以下の画像の「ロール」のリンクから、ゲートウェイロールの詳細画面に遷移できます。
遷移したら、ゲートウェイロールに以下のポリシーを作成してアタッチします。
<secret arn>
の箇所は上で作成したシークレットの ARN を指定します。
{ "Version":"2012-10-17", "Statement":[ { "Action":[ "secretsmanager:GetSecretValue" ], "Effect":"Allow", "Resource":[ "<secret arn>" ] } ] }
シークレットマネージャーコンポーネントのデプロイ
ゲートウェイからシークレットにアクセスするために、シークレットマネージャーコンポーネントをデプロイする必要があります。
IoT コンソール画面で左ペインの「デプロイ」をクリックします。デプロイ一覧の中から該当するデプロイを選択し、「変更」をクリックします。
モーダルウィンドウが出てくるので、「デプロイの変更」をクリックします。
ステップ 1 はそのまま「次へ」をクリックし、ステップ 2 で aws.greengrass.SecretManager
を追加します。
aws.greengrass.SecretManager
を選択し、「コンポーネントを設定」をクリックします。
表示された画面右側の「マージする設定」の箇所を以下の内容で置き換え、画面右下の「確認」をクリックします。
※<secret arn>
の箇所は上で作成したシークレットの ARN を指定してください。
{ "cloudSecrets": [ { "arn": "<secret arn>" } ] }
他は「次へ」をクリックし、最後に「デプロイ」をクリックします。
データソースの認証情報の追加
SiteWise のコンソール画面からゲートウェイの詳細画面へ行き、データソースを選択し「編集」をクリックします。
「アドバンスド設定」をクリックし、以下のように設定して「保存」をクリックします。
項目 | 設定値 | 備考 |
---|---|---|
認証設定 | sitewise-demo-auth | 上で作成したシークレットを選択 |
動作確認
保存後、認証設定が自動的にゲートウェイに同期されます。データが取れているか確認しましょう。
SiteWise のコンソール画面左ペインの「データストリーム」をクリックし、取得対象のデータストリームを選択します。以下の画像のようにデータポイント数が存在していることが確認できれば成功です!
おわりに
実際のプロジェクトでは、SiteWise を使用してデータを取り込む際にユーザ認証の設定をすることは多いと思います。その時に SiteWise 側で必要になる手順を紹介しました。単に SiteWise のデータソースの設定だけでなく、シークレットマネージャーの作成や権限設定が必要だったりで、やや手順が煩雑ですが、一度落ち着いてやってみると思ったほど難しくはないと思います。