Tutorial: Add OPC UA assets to your Azure IoT Operations Preview cluster

Important

Azure IoT Operations Preview – enabled by Azure Arc is currently in preview. You shouldn't use this preview software in production environments.

You'll need to deploy a new Azure IoT Operations installation when a generally available release becomes available. You won't be able to upgrade a preview installation.

For legal terms that apply to Azure features that are in beta, in preview, or otherwise not yet released into general availability, see the Supplemental Terms of Use for Microsoft Azure Previews.

In this tutorial, you manually add OPC UA assets to your Azure IoT Operations Preview cluster. These assets publish messages to the MQTT broker in your Azure IoT Operations cluster. Typically, an OT user completes these steps.

An asset is a physical device or logical entity that represents a device, a machine, a system, or a process. For example, a physical asset could be a pump, a motor, a tank, or a production line. A logical asset that you define can have properties, stream telemetry, or generate events.

OPC UA servers are software applications that communicate with assets. OPC UA tags are data points that OPC UA servers expose. OPC UA tags can provide real-time or historical data about the status, performance, quality, or condition of assets.

In this tutorial, you use the operations experience web UI to create your assets. You can also use the Azure CLI to complete some of these tasks.

Prerequisites

An instance of Azure IoT Operations Preview deployed in a Kubernetes cluster. To create an instance, use one of the following to deploy Azure IoT Operations:

To sign in to the operations experience web UI, you need a Microsoft Entra ID account with at least contributor permissions for the resource group that contains your Kubernetes - Azure Arc instance. To learn more, see Operations experience web UI.

Unless otherwise noted, you can run the console commands in this tutorial in either a Bash or PowerShell environment.

What problem will we solve?

The data that OPC UA servers expose can have a complex structure and can be difficult to understand. Azure IoT Operations provides a way to model OPC UA assets as tags, events, and properties. This modeling makes it easier to understand the data and to use it in downstream processes such as the MQTT broker and dataflows.

Deploy the OPC PLC simulator

This tutorial uses the OPC PLC simulator to generate sample data. To deploy the OPC PLC simulator, run the following command:

kubectl apply -f https://raw.githubusercontent.com/Azure-Samples/explore-iot-operations/main/samples/quickstarts/opc-plc-deployment.yaml

The following snippet shows the YAML file that you applied:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: opc-plc-000000
  namespace: azure-iot-operations
  labels:
    app.kubernetes.io/component: opcplc-000000
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/component: opcplc-000000
  template:
    metadata:
      labels:
        app.kubernetes.io/component: opcplc-000000
    spec:
      containers:
      - name: opc-plc
        image: mcr.microsoft.com/iotedge/opc-plc:latest
        args:
          - "--ph=opcplc-000000"
          - "--cdn=opcplc-000000"
          - "--ut"
          - "--sn=25"
          - "--sr=10"
          - "--fn=2000"
          - "--veryfastrate=1000"
          - "--gn=5"
          - "--pn=50000"
          - "--maxsessioncount=100"
          - "--maxsubscriptioncount=100"
          - "--maxqueuedrequestcount=2000"
          - "--ses"
          - "--alm"
          - "--at=FlatDirectory"
          - "--drurs"
          - "--ll-debug"
          - "--nodesfile"
          - "/app/config/nodesfile.json"
        ports:
        - containerPort: 50000
        volumeMounts:
          - name: opc-plc-default-application-cert
            mountPath: /app/pki/own
          - name: opc-plc-trust-list
            mountPath: /app/pki/trusted
          - name: config-volume
            mountPath: /app/config
      volumes:
        - name: opc-plc-default-application-cert
          secret:
            secretName: opc-plc-default-application-cert
        - name: opc-plc-trust-list
          secret:
            secretName: opc-plc-trust-list
        - name: config-volume
          configMap:
            name: opc-plc-config
      serviceAccountName: opcplc-000000-service-account
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: opc-plc-config
  namespace: azure-iot-operations
  labels:
    app.kubernetes.io/component: opcplc-000000
data:
  nodesfile.json: |
    {
      "Folder": "MyTelemetry",
      "NodeList": [
        {
          "NodeId": "ns=3;s=FastUInt100",
          "Name": "Fryer Temperature",
          "DataType": "Double",
          "ValueRank": -1,
          "AccessLevel": "CurrentReadOrWrite",
          "Description": "Fryer Temperature with spikes",
          "Anomaly": "Spike",
          "MinValue": 150.0,
          "MaxValue": 200.0          
        }
      ]
    }
---
apiVersion: v1
kind: Service
metadata:
  name: opcplc-000000
  namespace: azure-iot-operations
  labels:
    app.kubernetes.io/component: opcplc-000000
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/component: opcplc-000000
  ports:
    - port: 50000
      protocol: TCP
      targetPort: 50000
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: opc-plc-self-signed-issuer
  namespace: azure-iot-operations
  labels:
    app.kubernetes.io/component: opcplc-000000
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: opc-plc-default-application-cert
  namespace: azure-iot-operations
  labels:
    app.kubernetes.io/component: opcplc-000000
spec:
  secretName: opc-plc-default-application-cert
  duration: 2160h # 90d
  renewBefore: 360h # 15d
  issuerRef:
    name: opc-plc-self-signed-issuer
    kind: Issuer
  commonName: OpcPlc
  dnsNames:
    - opcplc-000000
    - opcplc-000000.azure-iot-operations.svc.cluster.local
    - opcplc-000000.azure-iot-operations
  uris:
    - urn:OpcPlc:opcplc-000000
  usages:
    - digital signature
    - key encipherment
    - data encipherment
    - server auth
    - client auth
  privateKey:
    algorithm: RSA
    size: 2048
  encodeUsagesInRequest: true
  isCA: false
---
apiVersion: v1
kind: Secret
metadata:
  name: opc-plc-trust-list
  namespace: azure-iot-operations
  labels:
    app.kubernetes.io/component: opcplc-000000
data: {}
---
apiVersion: batch/v1
kind: Job
metadata:
  name: opcplc-000000-execute-mutual-trust
  namespace: azure-iot-operations
  labels:
    app.kubernetes.io/component: opcplc-000000
spec:
  backoffLimit: 1
  template:
    spec:
      containers:
      - name: kubectl
        image: mcr.microsoft.com/oss/kubernetes/kubectl:v1.27.1
        imagePullPolicy: Always
        command: ["/bin/sh"]
        args: ["/scripts/execute-commands.sh"]
        volumeMounts:
        - name: scripts
          mountPath: /scripts
          readOnly: true
      restartPolicy: Never
      serviceAccountName: opcplc-000000-service-account
      volumes:
      - name: scripts
        configMap:
          name: opcplc-000000-execute-commands-script
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: opcplc-000000-execute-commands-script
  namespace: azure-iot-operations
  labels:
    app.kubernetes.io/component: opcplc-000000
data:
  execute-commands.sh: |
    #!/bin/sh

    # wait 20 seconds for the resources to be created
    sleep 20

    # Extract the OPC UA connector application instance certificate and add it to the OPC PLC trust list
    cert=$(kubectl -n azure-iot-operations get secret aio-opc-opcuabroker-default-application-cert -o jsonpath='{.data.tls\.crt}' | base64 -d)
    data=$(kubectl create secret generic temp --from-literal=opcuabroker.crt="$cert" --dry-run=client -o jsonpath='{.data}')
    kubectl patch secret opc-plc-trust-list -n azure-iot-operations -p "{\"data\": $data}"

    # Extract the OPC PLC application instance certificate and add it to the OPC UA connector trust list
    cert=$(kubectl -n azure-iot-operations get secret opc-plc-default-application-cert -o jsonpath='{.data.tls\.crt}' | base64 -d)
    data=$(kubectl create secret generic temp --from-literal=opcplc-000000.crt="$cert" --dry-run=client -o jsonpath='{.data}')
    kubectl patch secret aio-opc-ua-broker-trust-list -n azure-iot-operations -p "{\"data\": $data}"
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: opcplc-000000-service-account
  namespace: azure-iot-operations
  labels:
    app.kubernetes.io/component: opcplc-000000
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: opc-plc-000000-secret-access-role
  namespace: azure-iot-operations
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: opc-plc-000000-secret-access-rolebinding
  namespace: azure-iot-operations
subjects:
- kind: ServiceAccount
  name: opcplc-000000-service-account
  namespace: azure-iot-operations
roleRef:
  kind: Role
  name: opc-plc-000000-secret-access-role
  apiGroup: rbac.authorization.k8s.io

Caution

This configuration isn't secure. Don't use this configuration in a production environment.

Sign into the operations experience

To create asset endpoints, assets and subscribe to OPC UA tags and events, use the operations experience.

Browse to the operations experience in your browser and sign in with your Microsoft Entra ID credentials.

Select your site

A site is a collection of Azure IoT Operations instances. Sites typically group instances by physical location and make it easier for OT users to locate and manage assets. Your IT administrator creates sites and assigns Azure IoT Operations instances to them. Because you're working with a new deployment, there are no sites yet. You can find the cluster you created in the previously by selecting View unassigned instances. In the operations experience, an instance represents a cluster where you deployed Azure IoT Operations.

Screenshot that shows the unassigned instances node in the operations experience.

Select your instance

Select the instance where you deployed Azure IoT Operations in the previous tutorial:

Screenshot of Azure IoT Operations instance list.

Tip

If you don't see any instances, you might not be in the right Microsoft Entra ID tenant. You can change the tenant from the top right menu in the operations experience.

Add an asset endpoint

When you deployed Azure IoT Operations in the previous article, you included a built-in OPC PLC simulator. In this step, you add an asset endpoint that enables you to connect to the OPC PLC simulator.

To add an asset endpoint:

  1. Select Asset endpoints and then Create asset endpoint:

    Screenshot that shows the asset endpoints page in the operations experience.

  2. Enter the following endpoint information:

    Field Value
    Asset endpoint name opc-ua-connector-0
    OPC UA server URL opc.tcp://opcplc-000000:50000
    User authentication mode Anonymous
  3. To save the definition, select Create.

    This configuration deploys a new asset endpoint called opc-ua-connector-0 to the cluster. You can use kubectl to view the asset endpoints:

    kubectl get assetendpointprofile -n azure-iot-operations
    

Manage your assets

After you select your instance in operations experience, you see the available list of assets on the Assets page. If there are no assets yet, this list is empty:

Screenshot of Azure IoT Operations empty asset list.

Create an asset

To create an asset, select Create asset. Then enter the following asset information:

Field Value
Asset Endpoint opc-ua-connector-0
Asset name thermostat
Description A simulated thermostat asset

Remove the existing Custom properties and add the following custom properties. Be careful to use the exact property names, as the Power BI template in a later tutorial queries for them:

Property name Property detail
batch 102
customer Contoso
equipment Boiler
isSpare true
location Seattle

Screenshot of Azure IoT Operations asset details page.

Select Next to go to the Add tags page.

Create OPC UA tags

Add two OPC UA tags on the Add tags page. To add each tag, select Add tag or CSV and then select Add tag. Enter the tag details shown in the following table:

Node ID Tag name Observability mode
ns=3;s=FastUInt10 temperature None
ns=3;s=FastUInt100 Tag 10 None

The Observability mode is one of the following values: None, Gauge, Counter, Histogram, or Log.

You can select Manage default settings to change the default sampling interval and queue size for each tag.

Screenshot of Azure IoT Operations add tag page.

Select Next to go to the Add events page and then Next to go to the Review page.

Review

Review your asset and tag details and make any adjustments you need before you select Create:

Screenshot of Azure IoT Operations create asset review page.

This configuration deploys a new asset called thermostat to the cluster. You can use kubectl to view the assets:

kubectl get assets -n azure-iot-operations

Verify data is flowing

Verify data is flowing to the MQTT broker by using the mosquitto_sub tool. In this example, you run the mosquitto_sub tool inside your Kubernetes cluster:

  1. Run the following command to deploy a pod that includes the mosquitto_pub and mosquitto_sub tools that are useful for interacting with the MQTT broker in the cluster:

    kubectl apply -f https://raw.githubusercontent.com/Azure-Samples/explore-iot-operations/main/samples/quickstarts/mqtt-client.yaml
    

    The following snippet shows the YAML file that you applied:

    # Important: do not use in production environments
    # Creates a pod with mosquitto-clients and mqttui utilities in your cluster
    apiVersion: v1
    kind: Pod
    metadata:
      name: mqtt-client
      # The namespace must match the IoT MQ BrokerListener's namespace
      # Otherwise use the long hostname: aio-broker.azure-iot-operations.svc.cluster.local
      namespace: azure-iot-operations
    spec:
      # Use the "mqtt-client" service account which comes with default deployment
      # Otherwise create it with `kubectl create serviceaccount mqtt-client -n azure-iot-operations`
      serviceAccountName: mqtt-client
      containers:
        # Install mosquitto and mqttui utilities on Alpine linux
      - image: alpine
        name: mqtt-client
        command: ["sh", "-c"]
        args: ["apk add mosquitto-clients mqttui && sleep infinity"]
        resources:
          limits:
            cpu: 500m
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 100Mi
        volumeMounts:
        - name: broker-sat
          mountPath: /var/run/secrets/tokens
        - name: trust-bundle
          mountPath: /var/run/certs
      volumes:
      - name: broker-sat
        projected:
          sources:
          - serviceAccountToken:
              path: broker-sat
              audience: aio-internal # Must match audience in BrokerAuthentication
              expirationSeconds: 86400
      - name: trust-bundle
        configMap:
          name: azure-iot-operations-aio-ca-trust-bundle # Default root CA cert
    

    Caution

    This configuration isn't secure. Don't use this configuration in a production environment.

  2. When the mqtt-client pod is running, run the following command to create a shell environment in the pod you created:

    kubectl exec --stdin --tty mqtt-client -n azure-iot-operations -- sh
    
  3. At the Bash shell in the mqtt-client pod, run the following command to connect to the MQTT broker using the mosquitto_sub tool subscribed to the data/thermostat topic:

    mosquitto_sub --host aio-broker --port 18883 --topic "azure-iot-operations/data/#" -v --debug --cafile /var/run/certs/ca.crt -D CONNECT authentication-method 'K8S-SAT' -D CONNECT authentication-data $(cat /var/run/secrets/tokens/broker-sat)
    

    This command continues to run and displays messages as they arrive on the data/thermostat topic until you press Ctrl+C to stop it. To exit the shell environment, type exit.

To verify that the thermostat asset you added is publishing data, view the telemetry in the azure-iot-operations/data topic:

Client $server-generated/05a22b94-c5a2-4666-9c62-837431ca6f7e received PUBLISH (d0, q0, r0, m0, 'azure-iot-operations/data/thermostat', ... (152 bytes))
{"temperature":{"SourceTimestamp":"2024-07-29T15:02:17.1858435Z","Value":4558},"Tag 10":{"SourceTimestamp":"2024-07-29T15:02:17.1858869Z","Value":4558}}
Client $server-generated/05a22b94-c5a2-4666-9c62-837431ca6f7e received PUBLISH (d0, q0, r0, m0, 'azure-iot-operations/data/thermostat', ... (152 bytes))
{"temperature":{"SourceTimestamp":"2024-07-29T15:02:18.1838125Z","Value":4559},"Tag 10":{"SourceTimestamp":"2024-07-29T15:02:18.1838523Z","Value":4559}}
Client $server-generated/05a22b94-c5a2-4666-9c62-837431ca6f7e received PUBLISH (d0, q0, r0, m0, 'azure-iot-operations/data/thermostat', ... (152 bytes))
{"temperature":{"SourceTimestamp":"2024-07-29T15:02:19.1834363Z","Value":4560},"Tag 10":{"SourceTimestamp":"2024-07-29T15:02:19.1834879Z","Value":4560}}
Client $server-generated/05a22b94-c5a2-4666-9c62-837431ca6f7e received PUBLISH (d0, q0, r0, m0, 'azure-iot-operations/data/thermostat', ... (152 bytes))
{"temperature":{"SourceTimestamp":"2024-07-29T15:02:20.1861251Z","Value":4561},"Tag 10":{"SourceTimestamp":"2024-07-29T15:02:20.1861709Z","Value":4561}}
Client $server-generated/05a22b94-c5a2-4666-9c62-837431ca6f7e received PUBLISH (d0, q0, r0, m0, 'azure-iot-operations/data/thermostat', ... (152 bytes))
{"temperature":{"SourceTimestamp":"2024-07-29T15:02:21.1856798Z","Value":4562},"Tag 10":{"SourceTimestamp":"2024-07-29T15:02:21.1857211Z","Value":4562}}

If there's no data flowing, restart the aio-opc-opc.tcp-1 pod:

  1. Find the name of your aio-opc-opc.tcp-1 pod by using the following command:

    kubectl get pods -n azure-iot-operations
    

    The name of your pod looks like aio-opc-opc.tcp-1-849dd78866-vhmz6.

  2. Restart the aio-opc-opc.tcp-1 pod by using a command that looks like the following example. Use the aio-opc-opc.tcp-1 pod name from the previous step:

    kubectl delete pod aio-opc-opc.tcp-1-849dd78866-vhmz6 -n azure-iot-operations
    

The sample tags you added in the previous tutorial generate messages from your asset that look like the following example:

{
    "temperature": {
        "SourceTimestamp": "2024-08-02T13:52:15.1969959Z",
        "Value": 2696
    },
    "Tag 10": {
        "SourceTimestamp": "2024-08-02T13:52:15.1970198Z",
        "Value": 2696
    }
}

How did we solve the problem?

In this tutorial, you added an asset endpoint and then defined an asset and tags. The assets and tags model data from the OPC UA server to make the data easier to use in an MQTT broker and other downstream processes. You use the thermostat asset you defined in the next tutorial.

Clean up resources

If you're continuing on to the next tutorial, keep all of your resources.

If you want to remove the Azure IoT Operations deployment but keep your cluster, use the az iot ops delete command:

az iot ops delete --cluster $CLUSTER_NAME --resource-group $RESOURCE_GROUP

If you want to delete all the resources you created for this quickstart, delete the Kubernetes cluster where you deployed Azure IoT Operations and then remove the Azure resource group that contained the cluster.

If you used Codespaces for these quickstarts, delete your Codespace from GitHub.

Next step

Tutorial: Send asset telemetry to the cloud using the data lake connector for the MQTT broker.