Blogical

AWS/Salesforceを中心に様々な情報を配信していきます(/・ω・)/

ユーザ認証を設定した OPC UA サーバから IoT SiteWise でデータを取り込む

こんにちは、ロジカル・アーツの井川です。

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 サーバを構築しておくことを推奨します。

blog.logical.co.jp

おおよその手順は以下の通りです。

手順がやや複雑ですが、まず、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 はそのまま「次へ」をクリックし、ステップ 2aws.greengrass.SecretManager を追加します。

aws.greengrass.SecretManager を選択し、「コンポーネントを設定」をクリックします。

表示された画面右側の「マージする設定」の箇所を以下の内容で置き換え、画面右下の「確認」をクリックします。
<secret arn> の箇所は上で作成したシークレットの ARN を指定してください。

{
    "cloudSecrets": [
        {
            "arn": "<secret arn>"
        }
    ]
}

他は「次へ」をクリックし、最後に「デプロイ」をクリックします。

データソースの認証情報の追加

SiteWise のコンソール画面からゲートウェイの詳細画面へ行き、データソースを選択し「編集」をクリックします。

アドバンスド設定」をクリックし、以下のように設定して「保存」をクリックします。

項目 設定値 備考
認証設定 sitewise-demo-auth 上で作成したシークレットを選択

動作確認

保存後、認証設定が自動的にゲートウェイに同期されます。データが取れているか確認しましょう。

SiteWise のコンソール画面左ペインの「データストリーム」をクリックし、取得対象のデータストリームを選択します。以下の画像のようにデータポイント数が存在していることが確認できれば成功です!

おわりに

実際のプロジェクトでは、SiteWise を使用してデータを取り込む際にユーザ認証の設定をすることは多いと思います。その時に SiteWise 側で必要になる手順を紹介しました。単に SiteWise のデータソースの設定だけでなく、シークレットマネージャーの作成や権限設定が必要だったりで、やや手順が煩雑ですが、一度落ち着いてやってみると思ったほど難しくはないと思います。

参考リンク

*1:データが取得できないことを確認するには、例えば動作確認の手順を参考にしてください。

*2:例えば検索ボックスに 「iot」と入力し、IoT Core サービスをクリックして遷移できます。

*3:デフォルトセットアップでゲートウェイを作成すると、ロールエイリアス名のプレフィックスに(ゲートウェイに関連付けられた)Greengrass コアデバイス名がついていると思います。