IoT Hub デバイス ID の一括でのインポートおよびエクスポート

各 IoT ハブには、サービス内にデバイス リソースを作成するために使用できる ID レジストリがあります。 この ID レジストリを使って、デバイス向けエンドポイントへのアクセスを制御することもできます。 この記事では、Microsoft Azure IoT SDK for .NET に含まれている ImportExportDeviceSample サンプルを使用して、ID レジストリとの間でデバイス ID を一括でインポートおよびエクスポートする方法について説明します。 IoT ハブを別のリージョンに移行するときにこの機能を使用する方法の詳細については、「Azure Resource Manager テンプレートを使用して Azure IoT Hub を手動で移行する方法」を参照してください。

Note

IoT Hub では最近、いくつかのリージョンにおいて仮想ネットワークのサポートが追加されました。 この機能により、インポート操作とエクスポート操作がセキュリティで保護され、認証のためのキーを渡す必要がなくなります。 現在のところ、仮想ネットワークのサポートが利用できるのは、次のリージョンのみです: WestUS2EastUSSouthCentralUS。 仮想ネットワークのサポートおよび実装のための API 呼び出しの詳細については、「仮想ネットワークのための IoT Hub サポート」を参照してください。

インポートおよびエクスポート操作は、IoT ハブに対する一括サービス操作を実行するのを可能にする "ジョブ" のコンテキストで行われます。

SDK の RegistryManager クラスには、ジョブ フレームワークを使用する ExportDevicesAsync および ImportDevicesAsync メソッドが含まれています。 これらのメソッドを使用すると、IoT Hub ID レジストリ全体のエクスポート、インポート、および同期化を行うことができます。

この記事では、IoT ハブの ID レジストリとの間でデバイスの一括インポートおよびエクスポートを実行するために、RegistryManager クラスとジョブ システムを使用する方法を説明します。 また、Azure IoT Hub Device Provisioning Service を使用して 1 つまたは複数の IoT ハブに対してゼロタッチの Just-In-Time プロビジョニングを実現できます。 詳しくは、Provisioning Service のドキュメントをご覧ください。

Note

この記事のコード スニペットの一部は、Microsoft Azure IoT SDK for .NET で提供されている ImportExportDevicesSample サービスのサンプルからのものが含まれています。 サンプルは SDK の /iothub/service/samples/how to guides/ImportExportDevicesSample フォルダーにあり、指定されている場合は、その SDK サンプルの ImportExportDevicesSample.cs ファイルのコード スニペットが含まれています。 ImportExportDevicesSample サンプルと、Azure IoT SDK for.NET に含まれるその他のサービス サンプルの詳細については、C# の Azure IoT Hub サービスのサンプルに関するページを参照してください。

ジョブとは

ID レジストリの操作は、以下の場合にジョブ システムを使用します。

  • 操作の実行時間が、標準のランタイム操作と比べて長くなる可能性がある。

  • 操作で大量のデータがユーザーに返される。

単一の API 呼び出しが操作の結果が得られるまで待機したりブロックしたりするのでなく、操作によって該当の IoT ハブ用のジョブが非同期で作成されます。 その後すぐに JobProperties オブジェクトを返します。

次の C# コード スニペットでは、エクスポート ジョブの作成方法を示します。

// Call an export job on the IoT hub to retrieve all devices
JobProperties exportJob = await 
  registryManager.ExportDevicesAsync(containerSasUri, false);

注意

C# コードで RegistryManager クラスを使用するには、プロジェクトに Microsoft.Azure.Devices NuGet パッケージを追加します。 RegistryManager クラスは、Microsoft.Azure.Devices 名前空間にあります。

RegistryManager クラスを使用すると、返された JobProperties メタデータを基にジョブの状態を照会することができます。 RegistryManager クラスのインスタンスを作成するには、CreateFromConnectionString メソッドを使用します。

RegistryManager registryManager =
  RegistryManager.CreateFromConnectionString("{your IoT Hub connection string}");

IoT ハブ の接続文字列を取得するには、Azure Portal で次の操作を行います。

  1. IoT Hub に移動します。

  2. [共有アクセス ポリシー] を選択します。

  3. 必要なアクセス許可を考慮して、ポリシーを選択します。

  4. そのポリシーの接続文字列をコピーします。

次の C# コード スニペットは、SDK サンプルの WaitForJobAsync メソッドからのもので、5 秒ごとにポーリングして、ジョブの実行が完了したかどうかを確認する方法を示しています。

// Wait until job is finished
while (true)
{
    job = await registryManager.GetJobAsync(job.JobId);
    if (job.Status == JobStatus.Completed
        || job.Status == JobStatus.Failed
        || job.Status == JobStatus.Cancelled)
    {
        // Job has finished executing
        break;
    }
    Console.WriteLine($"\tJob status is {job.Status}...");

    await Task.Delay(TimeSpan.FromSeconds(5));
}

注意

ストレージアカウントに IoT Hub の接続性を制限するファイアウォール構成がある場合には、Microsoft が信頼を置くファーストパーティーのエクセプション の使用を検討してください (限られたリージョンで、管理サービス ID を持つ IoT hub を対象に利用可能)。

デバイスのインポート ジョブまたはエクスポート ジョブの制限

すべての IoT Hub のレベルにわたり、一度に 1 つのアクティブなデバイスのインポート ジョブまたはエクスポート ジョブしか実行できません。 また、IoT Hub では、ジョブ操作の速度に対する制限もあります。 詳細については、「参照 - IoT Hub のクォータと調整」をご覧ください。

デバイスのエクスポート

ExportDevicesAsync メソッドでは、Shared Access Signature (SAS) を使用して IoT ハブの ID レジストリ全体を Azure Storage BLOB コンテナーにエクスポートすることができます。 このメソッドでは、制御対象の BLOB コンテナーにデバイス情報のバックアップを確実に作成することができます。

ExportDevicesAsync メソッドには、次の 2 つのパラメーターが必要です。

  • BLOB コンテナーの URI が格納される文字列。 この URI には、コンテナーに対する書き込みアクセスを付与する SAS トークンを含める必要があります。 ジョブでは、デバイスのシリアル化されたエクスポート データを格納するために、このコンテナー内にブロック BLOB を作成します。 SAS トークンには、次のアクセス許可を含める必要があります。

    SharedAccessBlobPermissions.Write | SharedAccessBlobPermissions.Read 
       | SharedAccessBlobPermissions.Delete
    
  • エクスポート データから認証キーを除外するかどうかを示すブール値false の場合、認証キーはエクスポート出力に含められます。 それ以外の場合、キーは null としてエクスポートされます。

次の C# コード スニペットは、エクスポート データにデバイスの認証キーを含むエクスポート ジョブを開始し、ポーリングを実行して完了する方法を示します。

// Call an export job on the IoT Hub to retrieve all devices
JobProperties exportJob = 
  await registryManager.ExportDevicesAsync(containerSasUri, false);

// Wait until job is finished
while(true)
{
    exportJob = await registryManager.GetJobAsync(exportJob.JobId);
    if (exportJob.Status == JobStatus.Completed || 
        exportJob.Status == JobStatus.Failed ||
        exportJob.Status == JobStatus.Cancelled)
    {
    // Job has finished executing
    break;
    }

    await Task.Delay(TimeSpan.FromSeconds(5));
}

同様のコードは、SDK サンプルの ExportDevicesAsync メソッドで確認できます。 ジョブは、その出力を、指定された BLOB コンテナー内に devices.txtという名前のブロック BLOB として格納します。 出力データは、JSON のシリアル化されたデバイス データで構成され、1 行につき 1 つのデバイスが配置されます。

次の例は、出力データを示します。

{"id":"Device1","eTag":"MA==","status":"enabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}
{"id":"Device2","eTag":"MA==","status":"enabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}
{"id":"Device3","eTag":"MA==","status":"disabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}
{"id":"Device4","eTag":"MA==","status":"disabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}
{"id":"Device5","eTag":"MA==","status":"enabled","authentication":{"symmetricKey":{"primaryKey":"abc=","secondaryKey":"def="}}}

デバイスにツイン データがある場合は、デバイスのデータと共にツイン データもエクスポートされます。 次の例は、この形式を示しています。 "twinETag" 行から最後の行までがツイン データです。

{
   "id":"export-6d84f075-0",
   "eTag":"MQ==",
   "status":"enabled",
   "authentication":null,
   "twinETag":"AAAAAAAAAAI=",
   "tags":{
      "Location":"LivingRoom"
   },
   "properties":{
      "desired":{
         "Thermostat":{
            "Temperature":75.1,
            "Unit":"F"
         },
      },
      "reported":{}
   }
}

コード内でこのデータにアクセスする必要がある場合、ExportImportDevice クラスを使用してこのデータを簡単に逆シリアル化できます。 次の C# コード スニペットは、SDK サンプルの ReadFromBlobAsync メソッドからのもので、以前に ExportImportDevice から BlobClient インスタンスにエクスポートされたデバイス情報を読み取る方法を示しています。

private static async Task<List<string>> ReadFromBlobAsync(BlobClient blobClient)
{
    // Read the blob file of devices, import each row into a list.
    var contents = new List<string>();

    using Stream blobStream = await blobClient.OpenReadAsync();
    using var streamReader = new StreamReader(blobStream, Encoding.UTF8);
    while (streamReader.Peek() != -1)
    {
        string line = await streamReader.ReadLineAsync();
        contents.Add(line);
    }

    return contents;
}

デバイスのインポート

RegistryManager クラスの ImportDevicesAsync メソッドを使用すると、IoT Hub ID レジストリの一括インポートおよび同期化操作を実行することができます。 ExportDevicesAsync メソッドと同様に、ImportDevicesAsync メソッドでもジョブ フレームワークを使用します。

ImportDevicesAsync メソッドを使用する場合は注意が必要です。このメソッドでは、ID レジストリ内に新しいデバイスをプロビジョニングするほか、既存のデバイスを更新および削除する可能性もあるためです。

警告

インポート操作は元に戻すことができません。 ID レジストリに対して一括変更を加える場合は、必ず事前に ExportDevicesAsync メソッドを使用して既存のデータを別の BLOB コンテナーにバックアップしておく必要があります。

ImportDevicesAsync メソッドには、次の 2 つのパラメーターが必要です。

  • ジョブへの "入力" として使用する Azure Storage BLOB コンテナーの URI を含む "文字列"。 この URI には、コンテナーに対する読み取りアクセスを付与する SAS トークンを含める必要があります。 このコンテナーには、ID レジストリにインポートするシリアル化されたデバイス データが入っている devices.txt という名前の BLOB を含める必要があります。 インポート データには、ExportImportDevice ジョブが devices.txt BLOB を作成する際に使用するのと同じ JSON 形式でデバイス情報を含める必要があります。 SAS トークンには、次のアクセス許可を含める必要があります。

    SharedAccessBlobPermissions.Read
    
  • ジョブからの "出力" として使用する Azure Storage BLOB コンテナーの URI を含む "文字列"。 ジョブは、このコンテナー内にブロック BLOB を作成して、完了したインポート ジョブからのすべてのエラー情報を保存します。 SAS トークンには、次のアクセス許可を含める必要があります。

    SharedAccessBlobPermissions.Write | SharedAccessBlobPermissions.Read 
       | SharedAccessBlobPermissions.Delete
    

注意

この 2 つのパラメーターは、同じ BLOB コンテナーを指すことができます。 出力コンテナーで追加のアクセス許可が必要な場合は、個々のパラメーターでデータのより細かな制御を簡単に実現できます。

次の C# コード スニペットでは、インポート ジョブの開始方法を示します。

JobProperties importJob = 
   await registryManager.ImportDevicesAsync(containerSasUri, containerSasUri);

この方法を使用して、デバイス ツインのデータをインポートすることもできます。 データ入力の形式は、ExportDevicesAsync セクションに示されている形式と同じです。 この方法で、エクスポートされたデータを再インポートすることができます。

インポートの動作

ImportDevicesAsync メソッドを使用して、ID レジストリで次の一括操作を実行することができます。

  • 新しいデバイスの一括登録
  • 既存のデバイスの一括削除
  • 状態の一括変更 (デバイスの有効化または無効化)
  • 新しいデバイス認証キーの一括割り当て
  • デバイス認証キーの一括自動再生成
  • ツイン データの一括更新

上記の操作の任意の組み合わせを 1 回の ImportDevicesAsync 呼び出しで実行できます。 たとえば、新しいデバイスの登録と、既存のデバイスの削除または更新とを同時に行うことができます。 ExportDevicesAsync メソッドと一緒に使用すると、すべてのデバイスを一つの IoT Hub から別の IoT Hub へ完全に移行できます。

デバイスごとにインポート プロセスを制御するには、デバイスごとのインポート シリアル化データにオプションの importMode プロパティを使用します。 importMode プロパティには、次のオプションが用意されています。

  • 作成
  • CreateOrUpdate (既定値)
  • CreateOrUpdateIfMatchETag
  • 削除
  • DeleteIfMatchETag
  • 更新プログラム
  • UpdateIfMatchETag
  • UpdateTwin
  • UpdateTwinIfMatchETag

これらのインポート モード オプションのそれぞれの詳細については、「ImportMode」を参照してください

インポート ジョブのトラブルシューティングを行う

IoT ハブのデバイス数の上限に近い場合、インポート ジョブを使用したデバイスの作成は、クォータの問題で失敗する可能性があります。 この失敗は、デバイスの合計数がまだクォータ制限を下回っている場合でも発生する可能性があります。 IotHubQuotaExceeded (403002) エラーが返され、[Total number of devices on IotHub exceeded the allocated quota](IotHub 上のデバイスの合計数が割り当てられたクォータを超えました) というエラー メッセージが表示されます。

このエラーが発生した場合は、次のクエリを使用して、IoT ハブに登録されているデバイスの合計数を返すことができます。

SELECT COUNT() as totalNumberOfDevices FROM devices

IoT ハブに登録できるデバイスの合計数については、IoT Hub 制限に関する記事を参照してください。

使用可能なクォータがまだある場合は、IotHubQuotaExceeded (403002) エラーで失敗したデバイスのジョブ出力 BLOB を調べることができます。 その後、これらのデバイスを IoT ハブに個別に追加できます。 たとえば、AddDeviceAsync または AddDeviceWithTwinAsync メソッドを使用できます。 同じエラーが発生する可能性が高いため、別のジョブを使用したデバイスの追加は試さないでください。

デバイスのインポートの例 – デバイスの一括プロビジョニング

次の C# コード スニペットは、SDK サンプルの GenerateDevicesAsync メソッドからのもので、複数のデバイス ID を生成して以下の操作を行う方法を示しています。

  • 認証キーを含める。
  • ブロック BLOB にデバイスの情報を書き込む。
  • ID レジストリにデバイスをインポートする。
private async Task GenerateDevicesAsync(RegistryManager registryManager, int numToAdd)
{
    var stopwatch = Stopwatch.StartNew();

    Console.WriteLine($"Creating {numToAdd} devices for the source IoT hub.");
    int interimProgressCount = 0;
    int displayProgressCount = 1000;
    int totalProgressCount = 0;

    // generate reference for list of new devices we're going to add, will write list to this blob
    BlobClient generateDevicesBlob = _blobContainerClient.GetBlobClient(_generateDevicesBlobName);

    // define serializedDevices as a generic list<string>
    var serializedDevices = new List<string>(numToAdd);

    for (int i = 1; i <= numToAdd; i++)
    {
        // Create device name with this format: Hub_00000000 + a new guid.
        // This should be large enough to display the largest number (1 million).
        string deviceName = $"Hub_{i:D8}_{Guid.NewGuid()}";
        Debug.Print($"Adding device '{deviceName}'");

        // Create a new ExportImportDevice.
        var deviceToAdd = new ExportImportDevice
        {
            Id = deviceName,
            Status = DeviceStatus.Enabled,
            Authentication = new AuthenticationMechanism
            {
                SymmetricKey = new SymmetricKey
                {
                    PrimaryKey = GenerateKey(32),
                    SecondaryKey = GenerateKey(32),
                }
            },
            // This indicates that the entry should be added as a new device.
            ImportMode = ImportMode.Create,
        };

        // Add device to the list as a serialized object.
        serializedDevices.Add(JsonConvert.SerializeObject(deviceToAdd));

        // Not real progress as you write the new devices, but will at least show *some* progress.
        interimProgressCount++;
        totalProgressCount++;
        if (interimProgressCount >= displayProgressCount)
        {
            Console.WriteLine($"Added {totalProgressCount}/{numToAdd} devices.");
            interimProgressCount = 0;
        }
    }

    // Now have a list of devices to be added, each one has been serialized.
    // Write the list to the blob.
    var sb = new StringBuilder();
    serializedDevices.ForEach(serializedDevice => sb.AppendLine(serializedDevice));

    // Write list of serialized objects to the blob.
    using Stream stream = await generateDevicesBlob.OpenWriteAsync(overwrite: true);
    byte[] bytes = Encoding.UTF8.GetBytes(sb.ToString());
    for (int i = 0; i < bytes.Length; i += BlobWriteBytes)
    {
        int length = Math.Min(bytes.Length - i, BlobWriteBytes);
        await stream.WriteAsync(bytes.AsMemory(i, length));
    }
    await stream.FlushAsync();

    Console.WriteLine("Running a registry manager job to add the devices.");

    // Should now have a file with all the new devices in it as serialized objects in blob storage.
    // generatedListBlob has the list of devices to be added as serialized objects.
    // Call import using the blob to add the new devices.
    // Log information related to the job is written to the same container.
    // This normally takes 1 minute per 100 devices (according to the docs).

    // First, initiate an import job.
    // This reads in the rows from the text file and writes them to IoT Devices.
    // If you want to add devices from a file, you can create a file and use this to import it.
    //   They have to be in the exact right format.
    try
    {
        // The first URI is the container to import from; the file defaults to devices.txt, but may be specified.
        // The second URI points to the container to write errors to as a blob.
        // This lets you import the devices from any file name. Since we wrote the new
        // devices to [devicesToAdd], need to read the list from there as well.
        var importGeneratedDevicesJob = JobProperties.CreateForImportJob(
            _containerUri,
            _containerUri,
            _generateDevicesBlobName);
        importGeneratedDevicesJob = await registryManager.ImportDevicesAsync(importGeneratedDevicesJob);
        await WaitForJobAsync(registryManager, importGeneratedDevicesJob);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Adding devices failed due to {ex.Message}");
    }

    stopwatch.Stop();
    Console.WriteLine($"GenerateDevices, time elapsed = {stopwatch.Elapsed}.");
}

デバイスのインポートの例 – 一括削除

次の C# コード スニペットは、SDK サンプルの DeleteFromHubAsync メソッドからのもので、IoT ハブからすべてのデバイスを削除する方法を示しています。

private async Task DeleteFromHubAsync(RegistryManager registryManager, bool includeConfigurations)
{
    var stopwatch = Stopwatch.StartNew();

    Console.WriteLine("Deleting all devices from an IoT hub.");

    Console.WriteLine("Exporting a list of devices from IoT hub to blob storage.");

    // Read from storage, which contains serialized objects.
    // Write each line to the serializedDevices list.
    BlobClient devicesBlobClient = _blobContainerClient.GetBlobClient(_destHubDevicesImportBlobName);

    Console.WriteLine("Reading the list of devices in from blob storage.");
    List<string> serializedDevices = await ReadFromBlobAsync(devicesBlobClient);

    // Step 1: Update each device's ImportMode to be Delete
    Console.WriteLine("Updating ImportMode to be 'Delete' for each device and writing back to the blob.");
    var sb = new StringBuilder();
    serializedDevices.ForEach(serializedEntity =>
    {
        // Deserialize back to an ExportImportDevice and change import mode.
        ExportImportDevice device = JsonConvert.DeserializeObject<ExportImportDevice>(serializedEntity);
        device.ImportMode = ImportMode.Delete;

        // Reserialize the object now that we've updated the property.
        sb.AppendLine(JsonConvert.SerializeObject(device));
    });

    // Step 2: Write the list in memory to the blob.
    BlobClient deleteDevicesBlobClient = _blobContainerClient.GetBlobClient(_hubDevicesCleanupBlobName);
    await WriteToBlobAsync(deleteDevicesBlobClient, sb.ToString());

    // Step 3: Call import using the same blob to delete all devices.
    Console.WriteLine("Running a registry manager job to delete the devices from the IoT hub.");
    var importJob = JobProperties.CreateForImportJob(
        _containerUri,
        _containerUri,
        _hubDevicesCleanupBlobName);
    importJob = await registryManager.ImportDevicesAsync(importJob);
    await WaitForJobAsync(registryManager, importJob);

    // Step 4: delete configurations
    if (includeConfigurations)
    {
        BlobClient configsBlobClient = _blobContainerClient.GetBlobClient(_srcHubConfigsExportBlobName);
        List<string> serializedConfigs = await ReadFromBlobAsync(configsBlobClient);
        foreach (string serializedConfig in serializedConfigs)
        {
            try
            {
                Configuration config = JsonConvert.DeserializeObject<Configuration>(serializedConfig);
                await registryManager.RemoveConfigurationAsync(config.Id);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to deserialize or remove a config.\n\t{serializedConfig}\n\n{ex.Message}");
            }
        }
    }

    stopwatch.Stop();
    Console.WriteLine($"Deleted IoT hub devices and configs: time elapsed = {stopwatch.Elapsed}");
}

コンテナーの SAS URI の取得

次のコード サンプルでは、BLOB コンテナーに対する読み取り、書き込み、および削除アクセス許可を使用して SAS URI を生成する方法を示します。

static string GetContainerSasUri(CloudBlobContainer container)
{
  // Set the expiry time and permissions for the container.
  // In this case no start time is specified, so the
  // shared access signature becomes valid immediately.
  var sasConstraints = new SharedAccessBlobPolicy();
  sasConstraints.SharedAccessExpiryTime = DateTime.UtcNow.AddHours(24);
  sasConstraints.Permissions = 
    SharedAccessBlobPermissions.Write | 
    SharedAccessBlobPermissions.Read | 
    SharedAccessBlobPermissions.Delete;

  // Generate the shared access signature on the container,
  // setting the constraints directly on the signature.
  string sasContainerToken = container.GetSharedAccessSignature(sasConstraints);

  // Return the URI string for the container,
  // including the SAS token.
  return container.Uri + sasContainerToken;
}

次のステップ

この記事では、IoT Hub の ID レジストリに対して一括操作を実行する方法について説明しました。 ハブ間でデバイスを移動する方法など、こうした操作の多くは、「Azure Resource Manager テンプレートを使用して Azure IoT Hub を手動で移行する方法」の「IoT ハブに登録されたデバイスを管理する」セクションで使用されています。