자습서: Azure Web PubSub 및 Azure Functions를 사용하여 IoT Hub에서 IoT 디바이스 데이터 시각화
이 자습서에서는 Azure Web PubSub 서비스 및 Azure Functions를 사용하여 IoT Hub에서 실시간 데이터 시각화로 서버리스 애플리케이션을 빌드하는 방법을 알아봅니다.
이 자습서에서는 다음을 하는 방법을 알아볼 수 있습니다.
- 서버리스 데이터 시각화 앱 빌드
- Web PubSub 함수 입력 및 출력 바인딩과 Azure IoT Hub를 함께 사용
- 로컬로 샘플 함수 실행
Important
원시 연결 문자열 데모용으로만 이 문서에 표시됩니다.
연결 문자열에는 애플리케이션이 Azure Web PubSub 서비스에 액세스하는 데 필요한 권한 부여 정보가 포함됩니다. 연결 문자열 내의 액세스 키는 서비스의 루트 암호와 비슷합니다. 프로덕션 환경에서는 항상 액세스 키를 보호합니다. Azure Key Vault를 사용하여 키를 안전하게 관리 및 회전하고 연결을 WebPubSubServiceClient
보호합니다.
액세스 키를 다른 사용자에게 배포하거나 하드 코딩하거나 다른 사용자가 액세스할 수 있는 일반 텍스트로 저장하지 않도록 합니다. 키가 손상되었다고 생각되면 키를 교체하세요.
필수 조건
Visual Studio Code와 같은 코드 편집기
Node.js, 버전 18.x 이상
참고 항목
지원되는 Node.js 버전에 대한 자세한 내용은 Azure Functions 런타임 버전 설명서를 참조하세요.
Azure Functions Core Tools(v3 이상 추천) - Azure Function 앱을 로컬로 실행하고 Azure에 배포합니다.
Azure 리소스를 관리하기 위한 Azure CLI.
Azure를 구독하고 있지 않다면 시작하기 전에 Azure 체험 계정을 만듭니다.
IoT Hub 만들기
이 섹션에서는 Azure CLI를 사용하여 IoT 허브 및 리소스 그룹을 만듭니다. Azure 리소스 그룹은 Azure 리소스가 배포 및 관리되는 논리적 컨테이너입니다. IoT 허브는 IoT 애플리케이션과 디바이스 간의 양방향 통신을 위한 중앙 메시지 허브 역할을 합니다.
Azure 구독에 IoT Hub가 이미 있는 경우 이 섹션을 건너뛸 수 있습니다.
IoT 허브 및 리소스 그룹을 만들려면 다음을 수행합니다.
CLI 앱을 시작합니다. 이 문서의 나머지 부분에서 CLI 명령을 실행하려면 명령 구문을 복사하여 CLI 앱에 붙여넣고 변수 값을 편집한 다음,
Enter
를 누릅니다.- Cloud Shell을 사용하려면 CLI 명령에서 사용해 보기 단추를 선택하여 분할 브라우저 창에서 Cloud Shell을 시작합니다. 또는 별도의 브라우저 탭에서 Cloud Shell을 열 수 있습니다.
- Azure CLI를 로컬에서 사용하고 있는 경우 CLI 콘솔 앱을 시작하고 Azure CLI에 로그인합니다.
az extension add를 실행하여 azure-iot 확장을 현재 버전으로 설치하거나 업그레이드합니다.
az extension add --upgrade --name azure-iot
CLI 앱에서 az group create를 실행하여 리소스 그룹을 만듭니다. 다음 명령은 eastus 위치에 MyResourceGroup이라는 리소스 그룹을 만듭니다.
참고 항목
필요에 따라 다른 위치를 설정할 수 있습니다. 사용 가능한 위치를 보려면
az account list-locations
를 실행합니다. 이 빠른 시작에서는 예제 명령에 표시된 대로 eastus를 사용합니다.az group create --name MyResourceGroup --location eastus
az iot hub create 명령을 사용하여 IoT Hub를 만듭니다. IoT Hub를 만드는 데 몇 분 정도 걸릴 수 있습니다.
YourIotHubName. IoT 허브에 대해 선택한 이름을 사용하여 다음 명령에서 이 자리 표시자와 주변 중괄호를 바꿉니다. IoT Hub 이름은 Azure에서 전역적으로 고유해야 합니다. 자리 표시자가 표시될 때마다 이 빠른 시작의 나머지 부분에서 IoT 허브 이름을 사용합니다.
az iot hub create --resource-group MyResourceGroup --name {your_iot_hub_name}
Web PubSub 인스턴스 만들기
Azure 구독에 Web PubSub 인스턴스가 이미 있는 경우 이 섹션을 건너뛸 수 있습니다.
az extension add를 실행하여 webpubsub 확장을 현재 버전으로 설치하거나 업그레이드합니다.
az extension add --upgrade --name webpubsub
Azure CLI az webpubsub create 명령을 사용하여 만든 리소스 그룹에 Web PubSub를 만듭니다. 다음 명령은 EastUS의 리소스 그룹 myResourceGroup 아래에 무료 Web PubSub 리소스를 만듭니다.
Important
각 Web PubSub 리소스에는 고유한 이름이 있어야 합니다. 다음 예제에서 <your-unique-resource-name>을 Web PubSub의 이름으로 바꿉니다.
az webpubsub create --name "<your-unique-resource-name>" --resource-group "myResourceGroup" --location "EastUS" --sku Free_F1
이 명령의 출력에는 새로 만든 리소스의 속성이 표시됩니다. 아래에 나열된 두 개의 속성을 기록합니다.
- 리소스 이름: 위의
--name
매개 변수에 제공한 이름입니다. - hostName: 이 예제에서 호스트 이름은
<your-unique-resource-name>.webpubsub.azure.com/
입니다.
이때 Azure 계정은 이 새 리소스에서 모든 작업을 수행할 권한이 있는 유일한 계정입니다.
로컬로 함수 만들기 및 실행
프로젝트에 대한 빈 폴더를 만든 다음, 새 폴더에서 다음 명령을 실행합니다.
func init --worker-runtime javascript --model V4
클라이언트에 대한 정적 웹 페이지를 읽고 호스팅하는
index
함수를 만듭니다.func new -n index -t HttpTrigger
HTML 콘텐츠를 정적 사이트로 제공하는 다음 코드로
src/functions/index.js
를 업데이트합니다.const { app } = require('@azure/functions'); const { readFile } = require('fs/promises'); app.http('index', { methods: ['GET', 'POST'], authLevel: 'anonymous', handler: async (context) => { const content = await readFile('index.html', 'utf8', (err, data) => { if (err) { context.err(err) return } }); return { status: 200, headers: { 'Content-Type': 'text/html' }, body: content, }; } });
루트 폴더 아래에
index.html
파일을 만듭니다.<!doctype html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0/dist/Chart.min.js" type="text/javascript" charset="utf-8"></script> <script> document.addEventListener("DOMContentLoaded", async function (event) { const res = await fetch(`/api/negotiate?id=${1}`); const data = await res.json(); const webSocket = new WebSocket(data.url); class TrackedDevices { constructor() { // key as the deviceId, value as the temperature array this.devices = new Map(); this.maxLen = 50; this.timeData = new Array(this.maxLen); } // Find a device temperature based on its Id findDevice(deviceId) { return this.devices.get(deviceId); } addData(time, temperature, deviceId, dataSet, options) { let containsDeviceId = false; this.timeData.push(time); for (const [key, value] of this.devices) { if (key === deviceId) { containsDeviceId = true; value.push(temperature); } else { value.push(null); } } if (!containsDeviceId) { const data = getRandomDataSet(deviceId, 0); let temperatures = new Array(this.maxLen); temperatures.push(temperature); this.devices.set(deviceId, temperatures); data.data = temperatures; dataSet.push(data); } if (this.timeData.length > this.maxLen) { this.timeData.shift(); this.devices.forEach((value, key) => { value.shift(); }) } } getDevicesCount() { return this.devices.size; } } const trackedDevices = new TrackedDevices(); function getRandom(max) { return Math.floor((Math.random() * max) + 1) } function getRandomDataSet(id, axisId) { return getDataSet(id, axisId, getRandom(255), getRandom(255), getRandom(255)); } function getDataSet(id, axisId, r, g, b) { return { fill: false, label: id, yAxisID: axisId, borderColor: `rgba(${r}, ${g}, ${b}, 1)`, pointBoarderColor: `rgba(${r}, ${g}, ${b}, 1)`, backgroundColor: `rgba(${r}, ${g}, ${b}, 0.4)`, pointHoverBackgroundColor: `rgba(${r}, ${g}, ${b}, 1)`, pointHoverBorderColor: `rgba(${r}, ${g}, ${b}, 1)`, spanGaps: true, }; } function getYAxy(id, display) { return { id: id, type: "linear", scaleLabel: { labelString: display || id, display: true, }, position: "left", }; } // Define the chart axes const chartData = { datasets: [], }; // Temperature (ºC), id as 0 const chartOptions = { responsive: true, animation: { duration: 250 * 1.5, easing: 'linear' }, scales: { yAxes: [ getYAxy(0, "Temperature (ºC)"), ], }, }; // Get the context of the canvas element we want to select const ctx = document.getElementById("chart").getContext("2d"); chartData.labels = trackedDevices.timeData; const chart = new Chart(ctx, { type: "line", data: chartData, options: chartOptions, }); webSocket.onmessage = function onMessage(message) { try { const messageData = JSON.parse(message.data); console.log(messageData); // time and either temperature or humidity are required if (!messageData.MessageDate || !messageData.IotData.temperature) { return; } trackedDevices.addData(messageData.MessageDate, messageData.IotData.temperature, messageData.DeviceId, chartData.datasets, chartOptions.scales); const numDevices = trackedDevices.getDevicesCount(); document.getElementById("deviceCount").innerText = numDevices === 1 ? `${numDevices} device` : `${numDevices} devices`; chart.update(); } catch (err) { console.error(err); } }; }); </script> <style> body { font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; padding: 50px; margin: 0; text-align: center; } .flexHeader { display: flex; flex-direction: row; flex-wrap: nowrap; justify-content: space-between; } #charts { display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-around; align-content: stretch; } .chartContainer { flex: 1; flex-basis: 40%; min-width: 30%; max-width: 100%; } a { color: #00B7FF; } </style> <title>Temperature Real-time Data</title> </head> <body> <h1 class="flexHeader"> <span>Temperature Real-time Data</span> <span id="deviceCount">0 devices</span> </h1> <div id="charts"> <canvas id="chart"></canvas> </div> </body> </html>
클라이언트가 서비스 연결 URL 및 액세스 토큰을 가져오는 데 사용하는
negotiate
함수를 만듭니다.func new -n negotiate -t HttpTrigger
생성된 토큰이 포함된
WebPubSubConnection
을 사용하도록src/functions/negotiate.js
를 업데이트합니다.const { app, input } = require('@azure/functions'); const connection = input.generic({ type: 'webPubSubConnection', name: 'connection', hub: '%hubName%' }); app.http('negotiate', { methods: ['GET', 'POST'], authLevel: 'anonymous', extraInputs: [connection], handler: async (request, context) => { return { body: JSON.stringify(context.extraInputs.get('connection')) }; }, });
"IoT Hub (Event Hub)"
템플릿을 사용하여 알림을 생성하는messagehandler
함수를 만듭니다.원시 연결 문자열 데모용으로만 이 문서에 표시됩니다. 프로덕션 환경에서는 항상 액세스 키를 보호합니다. Azure Key Vault를 사용하여 키를 안전하게 관리 및 회전하고 연결을
WebPubSubServiceClient
보호합니다.func new --template "Azure Event Hub trigger" --name messagehandler
다음 json 코드를 사용하여 Web PubSub 출력 바인딩을 추가하도록
src/functions/messagehandler.js
를 업데이트합니다. 변수%hubName%
을 IoT eventHubName 및 Web PubSub 허브의 허브 이름으로 사용합니다.const { app, output } = require('@azure/functions'); const wpsAction = output.generic({ type: 'webPubSub', name: 'action', hub: '%hubName%' }); app.eventHub('messagehandler', { connection: 'IOTHUBConnectionString', eventHubName: '%hubName%', cardinality: 'many', extraOutputs: [wpsAction], handler: (messages, context) => { var actions = []; if (Array.isArray(messages)) { context.log(`Event hub function processed ${messages.length} messages`); for (const message of messages) { context.log('Event hub message:', message); actions.push({ actionName: "sendToAll", data: JSON.stringify({ IotData: message, MessageDate: message.date || new Date().toISOString(), DeviceId: message.deviceId, })}); } } else { context.log('Event hub function processed message:', messages); actions.push({ actionName: "sendToAll", data: JSON.stringify({ IotData: message, MessageDate: message.date || new Date().toISOString(), DeviceId: message.deviceId, })}); } context.extraOutputs.set(wpsAction, actions); } });
함수 설정을 업데이트합니다.
hubName
설정을 추가하고{YourIoTHubName}
을 IoT Hub를 만들 때 사용한 허브 이름으로 바꿉니다.func settings add hubName "{YourIoTHubName}"
IoT Hub에 대한 서비스 연결 문자열을 가져옵니다.
az iot hub connection-string show --policy-name service --hub-name {YourIoTHubName} --output table --default-eventhub
<iot-connection-string>
를 값으로 바꾸어IOTHubConnectionString
을 설정합니다.func settings add IOTHubConnectionString "<iot-connection-string>"
- Web PubSub에 대한 연결 문자열을 가져옵니다.
az webpubsub key show --name "<your-unique-resource-name>" --resource-group "<your-resource-group>" --query primaryConnectionString
<webpubsub-connection-string>
를 값으로 바꾸어WebPubSubConnectionString
을 설정합니다.func settings add WebPubSubConnectionString "<webpubsub-connection-string>"
참고 항목
샘플에 사용된
Azure Event Hub trigger
함수 트리거는 Azure Storage에 대한 종속성이 있지만 함수가 로컬로 실행될 때 로컬 스토리지 에뮬레이터를 사용할 수 있습니다.There was an error performing a read operation on the Blob Storage Secret Repository. Please ensure the 'AzureWebJobsStorage' connection string is valid.
와 같은 오류가 발생하면 스토리지 에뮬레이터를 다운로드하여 사용하도록 설정해야 합니다.로컬에서 함수를 실행합니다.
이제 아래 명령으로 로컬 함수를 실행할 수 있습니다.
func start
https://localhost:7071/api/index
를 방문하여 로컬 호스트 정적 페이지를 방문할 수 있습니다.
디바이스를 실행하여 데이터 보내기
디바이스 등록
연결하기 전에 디바이스를 IoT Hub에 등록해야 합니다. IoT Hub에 디바이스가 이미 등록된 경우 이 섹션을 건너뛸 수 있습니다.
Azure Cloud Shell에서 az iot hub device-identity create 명령을 실행하여 디바이스 ID를 만듭니다.
YourIoTHubName: 이 자리 표시자를 IoT 허브용으로 선택한 이름으로 바꿉니다.
az iot hub device-identity create --hub-name {YourIoTHubName} --device-id simDevice
Azure Cloud Shell에서 Az PowerShell module iot hub device-identity connection-string show 명령을 실행하여 방금 등록한 디바이스에 대한 디바이스 연결 문자열을 가져옵니다.
YourIoTHubName: 이 자리 표시자를 IoT Hub용으로 선택한 이름으로 바꿉니다.
az iot hub device-identity connection-string show --hub-name {YourIoTHubName} --device-id simDevice --output table
다음과 같은 디바이스 연결 문자열을 기록해 둡니다.
HostName={YourIoTHubName}.azure-devices.net;DeviceId=simDevice;SharedAccessKey={YourSharedAccessKey}
가장 빠른 결과를 위해 Raspberry Pi Azure IoT Online 시뮬레이터를 사용하여 온도 데이터를 시뮬레이트합니다. 디바이스 연결 문자열에 붙여넣고 실행 단추를 선택합니다.
물리적 Raspberry Pi 및 BME280 센서가 있는 경우 Raspberry Pi를 Azure IoT Hub(Node.js)에 연결 자습서에 따라 실제 온도 및 습도 값을 측정하고 보고할 수 있습니다.
시각화 웹 사이트 실행
함수 호스트 인덱스 페이지 http://localhost:7071/api/index
를 열어 실시간 대시보드를 확인합니다. 여러 디바이스를 등록하면 대시보드에서 여러 디바이스가 실시간으로 업데이트되는 것을 볼 수 있습니다. 여러 브라우저를 열면 모든 페이지가 실시간으로 업데이트되는 것을 볼 수 있습니다.
리소스 정리
이후의 빠른 시작 및 자습서를 계속 진행하려는 경우 이러한 리소스를 유지하는 것이 좋습니다.
더 이상 필요하지 않은 경우 Azure CLI az group delete 명령을 사용하여 리소스 그룹 및 모든 관련 리소스를 제거할 수 있습니다.
az group delete --name "myResourceGroup"