Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Get started developing with the Azure IoT Operations SDKs. Follow these steps to set up your development environment for building and running the samples, as well as creating and testing your own highly available edge applications.
GitHub repository | .NET SDK | Go SDK | Rust SDK
Prerequisites
Before you begin, prepare the following prerequisites:
- An Azure subscription. If you don't have an Azure subscription, create one for free before you begin.
A GitHub account.
Azure access permissions. For more information, see Deployment overview > Required permissions.
The Azure CLI examples in this article use environment variables so that you can set each value once and then copy and paste the commands as-is. If you're using the Azure IoT Operations Codespaces environment from the quickstart, these variables are already set for you and you can skip this step. Otherwise, set the following environment variables in your shell before you run the commands.
The following scripts set the most commonly used environment variables:
| Environment variable | Description |
|---|---|
SUBSCRIPTION_ID |
The ID of the subscription that contains your Azure IoT Operations instance. |
RESOURCE_GROUP |
The name of the resource group that contains your Azure IoT Operations instance. |
AIO_INSTANCE_NAME |
The name of your Azure IoT Operations instance. To list your instances, run az iot ops list -o table. |
CLUSTER_NAME |
The name of the Azure Arc-enabled Kubernetes cluster that hosts your instance. |
LOCATION |
The Azure region to use for new resources, for example eastus. |
SUBSCRIPTION_ID=<subscription-id>
RESOURCE_GROUP=<resource-group-name>
AIO_INSTANCE_NAME=<instance-name>
CLUSTER_NAME=<cluster-name>
LOCATION=<region>
You only need to set the variables that this article uses. This article might use additional environment variables for resource names that you choose. The article explains how to set them where they're introduced.
This article also uses the following environment variables for resource names that you choose: SCHEMA_REGISTRY (the name of the schema registry), SCHEMA_REGISTRY_NAMESPACE (the name of the schema registry namespace), STORAGE_ACCOUNT (the name of the storage account). Set each one to a value that you want before you run the related commands.
Setting up
Developing with the Azure IoT Operations SDKs requires a Kubernetes cluster with Azure IoT Operations deployed. Further configuration allows you to access the MQTT broker directly from the developer environment.
Important
The following development environment setup options use K3s running in K3d for a lightweight Kubernetes cluster. They deploy Azure IoT Operations with test settings. For production deployments, choose secure settings.
To use secure settings, follow the instructions in Prepare your Azure Arc-enabled Kubernetes cluster to create a K3s cluster on Ubuntu and Deploy Azure IoT Operations to a production cluster to deploy with secure settings. Then proceed to configure Azure IoT Operations for development.
GitHub Codespaces provides the most streamlined experience and can get the development environment up and running in a couple of minutes.
Deploy Azure IoT Operations
You'll arc-enable the development cluster created in the previous step and deploy Azure IoT Operations with test settings.
Open a new Bash terminal and complete the following steps:
Go to the repository root directory:
cd <REPOSITORY ROOT>Run the
install-aio-arc.shscript to arc-enable your cluster and deploy Azure IoT Operations. Replace the placeholders with your values:Parameter Value LOCATIONAn Azure region close to you. For the list of currently supported regions, see Supported regions. RESOURCE_GROUPA name for a new Azure resource group where your cluster will be created. CLUSTER_NAMEA name for your Kubernetes cluster. STORAGE_ACCOUNT_NAMEA name for your storage account. Storage account names must be between 3 and 24 characters in length and only contain numbers and lowercase letters. SCHEMA_REGISTRY_NAMEA name for your schema registry. Schema registry names can only contain numbers, lowercase letters, and hyphens. SCHEMA_REGISTRY_NAMESPACEA name for your schema registry namespace. The namespace uniquely identifies a schema registry within a tenant. Schema registry namespace names can only contain numbers, lowercase letters, and hyphens. DEVICE_REGISTRY_NAMESPACEA name for your device registry namespace. Must be unique within your tenant, and between 3 and 24 characters. Can only contain numbers, letters, hyphens, and underscores. ./tools/deployment/install-aio-arc.sh -l <LOCATION> -g <RESOURCE_GROUP> -c <CLUSTER_NAME> -s <STORAGE_ACCOUNT_NAME> -r <SCHEMA_REGISTRY_NAME> -n <SCHEMA_REGISTRY_NAMESPACE> -d <DEVICE_REGISTRY_NAMESPACE>This script runs the following commands:
- Log in to Azure CLI
- Create a resource group
- Register required Azure providers
- Connect Kubernetes cluster to Azure Arc
- Enable Azure Arc features
- Create Azure Storage account
- Create Azure IoT Operations schema registry
- Create Azure device registry namespace
- Initialize Azure IoT Operations
- Create Azure IoT Operations instance
After the deployment finishes, use az iot ops check to evaluate Azure IoT Operations service deployment for health, configuration, and usability. The check command can help you find problems in your deployment and configuration.
az iot ops check
Configure Azure IoT Operations for development
After you deploy Azure IoT Operations, configure it for development. Set up the MQTT broker and authentication methods. Make sure you set the necessary environment variables for your development environment:
Go to the repository root directory:
cd <REPOSITORY ROOT>Run the
configure-aio.shscript to configure Azure IoT Operations for development:./tools/deployment/configure-aio.shThis script runs the following commands:
- Sets up certificate services, if missing
- Creates root and intermediate CAs for x509 authentication
- Creates the trust bundle ConfigMap for the Broker to authentication x509 clients
- Configures
BrokerListenerandBrokerAuthenticationresources for SAT and x509 auth
Testing the installation
To test the setup, use mosquitto_pub to connect to the MQTT broker and validate the x509 certs, SAT, and trust bundle.
Export the
.sessiondirectory:export SESSION=$(git rev-parse --show-toplevel)/.sessionTest no TLS, no auth:
mosquitto_pub -L mqtt://localhost:1883/hello -m world --debugTest TLS with x509 auth:
mosquitto_pub -L mqtts://localhost:8883/hello -m world --cafile $SESSION/broker-ca.crt --cert $SESSION/client.crt --key $SESSION/client.key --debugTest TLS with SAT auth:
mosquitto_pub -L mqtts://localhost:8884/hello -m world --cafile $SESSION/broker-ca.crt -D CONNECT authentication-method K8S-SAT -D CONNECT authentication-data $(cat $SESSION/token.txt) --debug
Run a sample
This sample demonstrates a simple communication between a client and a server using Telemetry and remote procedure call (RPC). The server tracks the value of a counter and accepts RPC requests from the client to either read or increment that counter.
The sample uses the v2 protocol compiler with WoT Thing model files, see the TestThing folder.
Install the .NET 9.0 SDK.
The samples within Azure IoT Operations SDKs GitHub repository read configuration from environment variables. The repository root provides an
.envfile that exports the variables used by the samples to connect to the MQTT broker. Edit the.envfile to set the values for your environment, or use the default values provided in the file.Navigate to the
CounterServersample directory:cd <REPOSITORY ROOT>/dotnet/samples/Protocol/RPC/CounterServer/Build the sample:
dotnet buildRun the sample:
source `git rev-parse --show-toplevel`/.env; export AIO_MQTT_CLIENT_ID=counter-server; dotnet runOpen a new shell and navigate to the
CounterClientsample directory:cd <REPOSITORY ROOT>/dotnet/samples/Protocol/RPC/CounterClient/Build the sample:
dotnet buildRun the sample:
source `git rev-parse --show-toplevel`/.env; export AIO_MQTT_CLIENT_ID=counter-client; export COUNTER_SERVER_ID=counter-server; dotnet runYou see the client and server communicating, with the client sending requests to read and increment the counter value, and the server sending telemetry. For details, see Examine client and server output.
The
CounterClientsample automatically exits when it completes. You can also stop theCounterServersample by pressingCtrl+Cin its terminal.
Examine client and server output
This section shows examples of the output for the client and the server. The client makes calls to read and increment the counter value, and the server responds. The server also outputs telemetry, so the client can track the counter value. In these examples, messages are grouped logically, but in your output the order of messages will vary depending on the timing of the client and server. These examples show output for MQTT messages, but you can suppress this output by setting the mqttDiag environment variable to false in the appsettings.json files for the client and the server.
Read counter
The following example shows the output messages for read counter. Requests and responses across the client and server are matched by correlation ID, which, in this example, is 5b282690-8c59-4bf2-9926-9236582ce4b3.
Client output:
# Subscribe once on startup.
CounterClient Information: 0 : >> [2026-07-29T17:35:53.4663363Z] [11]: TX (51 bytes) >>> Subscribe: [PacketIdentifier=2] [TopicFilters=clients/+/rpc/command-samples/+/readCounter@AtLeastOnce]
CounterClient Information: 0 : >> [2026-07-29T17:35:53.4689212Z] [6]: RX (6 bytes) <<< SubAck: [PacketIdentifier=2] [ReasonCode=GrantedQoS1]
CounterClient Information: 0 : Subscribed to topic filter 'clients/+/rpc/command-samples/+/readCounter' for command invoker 'readCounter'
# Invoke readCounter once on startup and once at end -- only one shown.
info: CounterClient.RpcCommandRunner[0] Calling ReadCounter with 5b282690-8c59-4bf2-9926-9236582ce4b3
CounterClient Information: 0 : >> [2026-07-29T17:35:53.4777793Z] [11]: TX (335 bytes) >>> Publish: [Topic=rpc/command-samples/counter-server/readCounter] [PayloadLength=0] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=3]
CounterClient Information: 0 : >> [2026-07-29T17:35:53.4818287Z] [10]: RX (4 bytes) <<< PubAck: [PacketIdentifier=3] [ReasonCode=Success]
CounterClient Information: 0 : Invoked command 'readCounter' with correlation ID 5b282690-8c59-4bf2-9926-9236582ce4b3 to topic 'rpc/command-samples/counter-server/readCounter'
CounterClient Information: 0 : >> [2026-07-29T17:35:53.5861515Z] [10]: RX (260 bytes) <<< Publish: [Topic=clients/counter-client/rpc/command-samples/counter-server/readCounter] [PayloadLength=21] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=1]
info: CounterClient.RpcCommandRunner[0] called read 0 with id 5b282690-8c59-4bf2-9926-9236582ce4b3
CounterClient Information: 0 : >> [2026-07-29T17:35:53.6572000Z] [10]: TX (4 bytes) >>> PubAck: [PacketIdentifier=1] [ReasonCode=Success]
Server output:
# Subscribe to readCounter topic on startup.
CounterServer Information: 0 : >> [2026-07-29T17:35:12.3456281Z] [7]: TX (54 bytes) >>> Subscribe: [PacketIdentifier=1] [TopicFilters=rpc/command-samples/counter-server/readCounter@AtLeastOnce]
CounterServer Information: 0 : >> [2026-07-29T17:35:12.3553742Z] [11]: RX (6 bytes) <<< SubAck: [PacketIdentifier=1] [ReasonCode=GrantedQoS1]
CounterServer Information: 0 : Command executor for 'readCounter' started.
# Multiple (2) invocations occur - only one shown.
CounterServer Information: 0 : >> [2026-07-29T17:35:53.4842146Z] [11]: RX (334 bytes) <<< Publish: [Topic=rpc/command-samples/counter-server/readCounter] [PayloadLength=0] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=1]
<6>17:35:53CounterServer.CounterService[0] --> Executing Counter.ReadCounter with id 5b282690-8c59-4bf2-9926-9236582ce4b3 for counter-client
<6>17:35:53CounterServer.CounterService[0] --> Executed Counter.ReadCounter with id 5b282690-8c59-4bf2-9926-9236582ce4b3 for counter-client
CounterServer Information: 0 : >> [2026-07-29T17:35:53.5815820Z] [7]: TX (261 bytes) >>> Publish: [Topic=clients/counter-client/rpc/command-samples/counter-server/readCounter] [PayloadLength=21] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=4]
CounterServer Information: 0 : >> [2026-07-29T17:35:53.5854665Z] [11]: RX (4 bytes) <<< PubAck: [PacketIdentifier=4] [ReasonCode=Success]
CounterServer Information: 0 : >> [2026-07-29T17:35:53.5881430Z] [12]: TX (4 bytes) >>> PubAck: [PacketIdentifier=1] [ReasonCode=Success]
Increment counter
The following example shows the output messages for increment counter. Requests and responses are matched across the client and server by correlation ID, which, in this example, is 5a4da7c4-3922-4049-8065-77f344c3478f.
Client output:
# Invoke increment multiple times -- only one shown.
info: CounterClient.RpcCommandRunner[0] calling counter.incr with id 5a4da7c4-3922-4049-8065-77f344c3478f
CounterClient Information: 0 : >> [2026-07-29T17:35:53.6375578Z] [11]: TX (49 bytes) >>> Subscribe: [PacketIdentifier=4] [TopicFilters=clients/+/rpc/command-samples/+/increment@AtLeastOnce]
CounterClient Information: 0 : >> [2026-07-29T17:35:53.6399406Z] [10]: RX (6 bytes) <<< SubAck: [PacketIdentifier=4] [ReasonCode=GrantedQoS1]
CounterClient Information: 0 : Subscribed to topic filter 'clients/+/rpc/command-samples/+/increment' for command invoker 'increment'
CounterClient Information: 0 : >> [2026-07-29T17:35:53.6461370Z] [11]: TX (372 bytes) >>> Publish: [Topic=rpc/command-samples/counter-server/increment] [PayloadLength=20] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=21]
CounterClient Information: 0 : >> [2026-07-29T17:35:53.6552803Z] [10]: RX (4 bytes) <<< PubAck: [PacketIdentifier=21] [ReasonCode=Success]
CounterClient Information: 0 : Invoked command 'increment' with correlation ID 5a4da7c4-3922-4049-8065-77f344c3478f to topic 'rpc/command-samples/counter-server/increment'
CounterClient Information: 0 : >> [2026-07-29T17:35:53.6960747Z] [13]: RX (259 bytes) <<< Publish: [Topic=clients/counter-client/rpc/command-samples/counter-server/increment] [PayloadLength=22] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=12]
info: CounterClient.RpcCommandRunner[0] called counter.incr 10 with id 5a4da7c4-3922-4049-8065-77f344c3478f
CounterClient Information: 0 : >> [2026-07-29T17:35:53.7138884Z] [10]: TX (4 bytes) >>> PubAck: [PacketIdentifier=12] [ReasonCode=Success]
Server output:
# Subscribe to increment once on startup.
CounterServer Information: 0 : >> [2026-07-29T17:35:12.3548320Z] [7]: TX (52 bytes) >>> Subscribe: [PacketIdentifier=2] [TopicFilters=rpc/command-samples/counter-server/increment@AtLeastOnce]
CounterServer Information: 0 : >> [2026-07-29T17:35:12.3613847Z] [11]: RX (6 bytes) <<< SubAck: [PacketIdentifier=2] [ReasonCode=GrantedQoS1]
CounterServer Information: 0 : Command executor for 'increment' started.
# Multiple invocations are received -- only one shown.
CounterServer Information: 0 : >> [2026-07-29T17:35:53.6502886Z] [13]: RX (371 bytes) <<< Publish: [Topic=rpc/command-samples/counter-server/increment] [PayloadLength=20] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=2]
<6>17:35:53CounterServer.CounterService[0] --> Executing Counter.Increment with id 5a4da7c4-3922-4049-8065-77f344c3478f for counter-client
<6>17:35:53CounterServer.CounterService[0] --> Executed Counter.Increment with id 5a4da7c4-3922-4049-8065-77f344c3478f for counter-client
CounterServer Information: 0 : >> [2026-07-29T17:35:53.6924040Z] [7]: TX (260 bytes) >>> Publish: [Topic=clients/counter-client/rpc/command-samples/counter-server/increment] [PayloadLength=22] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=15]
CounterServer Information: 0 : >> [2026-07-29T17:35:53.6983722Z] [18]: RX (4 bytes) <<< PubAck: [PacketIdentifier=15] [ReasonCode=Success]
CounterServer Information: 0 : >> [2026-07-29T17:35:53.6992783Z] [12]: TX (4 bytes) >>> PubAck: [PacketIdentifier=2] [ReasonCode=Success]
CounterValue telemetry
The following example shows the output messages for CounterValue telemetry. Server output is shown first because it sends the telemetry.
Server output:
# Telemetry data is published multiple times.
CounterServer Information: 0 : >> [2026-07-29T17:35:53.6805980Z] [7]: TX (203 bytes) >>> Publish: [Topic=telemetry/telemetry-samples/counterValue] [PayloadLength=18] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=5]
CounterServer Information: 0 : >> [2026-07-29T17:35:53.6843445Z] [11]: RX (4 bytes) <<< PubAck: [PacketIdentifier=5] [ReasonCode=Success]
CounterServer Information: 0 : Telemetry sent successfully to the topic 'telemetry/telemetry-samples/counterValue'
Client output:
# Subscribe performed once on startup.
CounterClient Information: 0 : >> [2026-07-29T17:35:53.4283758Z] [11]: TX (48 bytes) >>> Subscribe: [PacketIdentifier=1] [TopicFilters=telemetry/telemetry-samples/counterValue@AtLeastOnce]
CounterClient Information: 0 : >> [2026-07-29T17:35:53.4355484Z] [10]: RX (6 bytes) <<< SubAck: [PacketIdentifier=1] [ReasonCode=GrantedQoS1]
CounterClient Information: 0 : Telemetry receiver subscribed for topic telemetry/telemetry-samples/counterValue.
# Multiple telemetry values received - only one shown.
CounterClient Information: 0 : >> [2026-07-29T17:35:53.6820692Z] [6]: RX (202 bytes) <<< Publish: [Topic=telemetry/telemetry-samples/counterValue] [PayloadLength=18] [QoSLevel=AtLeastOnce] [Dup=False] [Retain=False] [PacketIdentifier=2]
CounterClient Information: 0 : Telemetry received from telemetry/telemetry-samples/counterValue
info: CounterClient.CounterClient[0] Telemetry received from counter-server: CounterValue=4
CounterClient Information: 0 : >> [2026-07-29T17:35:53.7131604Z] [10]: TX (4 bytes) >>> PubAck: [PacketIdentifier=2] [ReasonCode=Success]
Configuration summary
MQTT broker configuration
After you install the software, the cluster contains the following MQTT broker definitions:
| Component Type | Name | Description |
|---|---|---|
Broker |
default | The MQTT broker |
BrokerListener |
default | Provides cluster access to the MQTT broker |
BrokerListener |
default-external | Provides off-cluster access to the MQTT broker |
BrokerAuthentication |
default | SAT authentication definition |
BrokerAuthentication |
default-x509 | An x509 authentication definition |
MQTT broker access
You can access the MQTT broker both on-cluster and off-cluster by using the connection information described in the following table. For information on which environment variables to use when configuring your application, see Connection Settings.
Note
The hostname when accessing the MQTT broker off-cluster might differ from localhost depending on your setup.
| Hostname | Authentication | TLS | On cluster port | Off cluster port |
|---|---|---|---|---|
aio-broker |
SAT | ✅ | 18883 |
- |
localhost |
None | ❌ | 1883 |
1883 |
localhost |
x509 | ✅ | 8883 |
8883 |
localhost |
SAT | ✅ | 8884 |
8884 |
Development artifacts
As part of the deployment script, the local environment creates the following files to facilitate connection and authentication to the MQTT broker. You can find these files in the .session directory at the repository root.
| File | Description |
|---|---|
broker-ca.crt |
The MQTT broker trust bundle required to validate the MQTT broker on ports 8883 and 8884 |
token.txt |
A service authentication token (SAT) for authenticating with the MQTT broker on 8884 |
client.crt |
An x509 client certificate for authenticating with the MQTT broker on port 8883 |
client.key |
An x509 client private key for authenticating with the MQTT broker on port 8883 |
Troubleshooting
Check the troubleshooting guide for common issues in the Azure IoT Operations SDKs GitHub repository.
Next steps
In this quickstart, you set up the Azure IoT Operations SDKs and ran a sample application. To learn more about developing with the SDKs, check out the following resources: