Azure Key VaultにマネージドIDを登録しAzure Media ServicesのAPIを実施してみる

nazology.net

日本の岡山大学で行われた研究によれば、1秒間に発音できる「タ」の数が、2年後の認知機能や身体機能の状態に関連している、とのこと。

「あーたたたた!」1秒間に「タ」を6回言えないと2年後にボケる可能性 - ナゾロジー

「タ」をたくさん言えた人(6回以上)は、2年後も健康である可能性が高かった一方で、あまり言えなかった人(6回以下)は、2年後に要介護の前段階にある「フレイル(虚弱)」状態になる可能性が高くなっていました。

「あーたたたた!」1秒間に「タ」を6回言えないと2年後にボケる可能性 - ナゾロジー

どうやら発音能力と未来の健康状態は深く関連しているようです。

「あーたたたた!」1秒間に「タ」を6回言えないと2年後にボケる可能性 - ナゾロジー

⇧ う~ん...海外の場合で検証した場合、どうなるか気になりますね、英語だと「Ta」になるのかとか。

というわけで、例の如く冒頭から関係ない話でしたが、今回もAzureにまつわる話についてです。

レッツトライ~。

Azure Key Vaultって?

Microsoftさんのドキュメントによると、

docs.microsoft.com

Azure Key Vault は、シークレットを安全に保管し、それにアクセスするためのクラウド サービスです。 シークレットは、API キー、パスワード、証明書、暗号化キーなど、アクセスを厳密に制御する必要がある任意のものです。

https://docs.microsoft.com/ja-jp/azure/key-vault/general/basic-concepts

Key Vault サービスでは、ボールトおよびマネージド ハードウェア セキュリティ モジュール (HSM) プールという 2 種類のコンテナーがサポートされています。 ボールトでは、ソフトウェアと HSM でバックアップされるキー、シークレット、証明書を保存できます。 Managed HSM プールは、HSM でバックアップされるキーにのみ対応しています。

https://docs.microsoft.com/ja-jp/azure/key-vault/general/basic-concepts

⇧ とあるのですが、シークレットの定義が曖昧ですと...。

で、さらにややこしいのが、

認証

Key Vault で操作を行うには、まず、それを認証する必要があります。 次の 3 つの方法で Key Vault を認証します。

  • Azure リソースのマネージド ID:Azure の仮想マシンにアプリをデプロイするときに、Key Vault にアクセスできる仮想マシンに ID を割り当てることができます。 他の Azure リソースにも ID を割り当てることができます。 この手法の利点は、最初のシークレットのローテーションがアプリやサービスで管理されないことにあります。 Azure では、ID が自動的にローテーションされます。 ベスト プラクティスとして、この手法をお勧めします。
  • サービス プリンシパルと証明書:サービス プリンシパルと、Key Vault にアクセスできる関連証明書を使用することができます。 アプリケーションの所有者または開発者が証明書をローテーションする必要があるため、この手法はお勧めできません。
  • サービス プリンシパルとシークレット: サービス プリンシパルとシークレットを使用して Key Vault に対する認証を行うことはできますが、これはお勧めできません。 Key Vault に対する認証で使用されるブートストラップ シークレットを自動的にローテーションするのは困難です。

https://docs.microsoft.com/ja-jp/azure/key-vault/general/basic-concepts

⇧ Key Vaultを操作するために、Key Vaultを認証するとあるのですが、『Azure Key Vault は、シークレットを安全に保管し、それにアクセスするためのクラウド サービスです。』とあって、Key Vaultって「シークレット」を安全に保管し、それにアクセスするサービスって言ってるのに、その「シークレット」で認証が必要ってことらしい。

「シークレット」の初回登録については、認証しようがないんじゃないのかという疑念が拭えませんが...

Azure Key Vaultに対する認証

ここで言ってる認証というのは、「Key Vaultの操作」を利用できるようにするための認証が済んだ後の認証ということになるのではないかと、Microsoftさんのドキュメントが解釈し辛いので推測になってしまいますが...。

docs.microsoft.com

Key Vault による認証は、特定のセキュリティ プリンシパルの ID の認証を行う Azure Active Directory (Azure AD) と連携して機能します。

https://docs.microsoft.com/ja-jp/azure/key-vault/general/authentication

セキュリティ プリンシパルは、Azure リソースへのアクセスを要求するユーザー、グループ、サービス、またはアプリケーションを表すオブジェクトです。 

https://docs.microsoft.com/ja-jp/azure/key-vault/general/authentication

すべてのセキュリティ プリンシパルには、Azure によって一意のオブジェクト ID が割り当てられます。

https://docs.microsoft.com/ja-jp/azure/key-vault/general/authentication

⇧ とあるのですが、

  • ユーザー セキュリティ プリンシパルでは、Azure Active Directory 内にプロファイルを持つ個人が示されます。

  • グループ セキュリティ プリンシパルでは、Azure Active Directory 内に作成されたユーザーのセットが示されます。 グループに割り当てられたすべてのロールまたはアクセス許可は、グループ内のすべてのユーザーに付与されます。

  • サービス プリンシパルは、アプリケーションまたはサービス、つまりユーザーまたはグループではなくコードを示すセキュリティ プリンシパルの一種です。 サービス プリンシパルのオブジェクト ID はクライアント ID と呼ばれ、ユーザー名のように機能します。 サービス プリンシパルクライアント シークレットは、パスワードのように機能します。

https://docs.microsoft.com/ja-jp/azure/key-vault/general/authentication

⇧「セキュリティ プリンシパル」の全量が見えてこない...

上記で説明してる「ユーザー」「グループ」「サービス プリンシパル」の3つが「セキュリティ プリンシパル」の全量と考えて良いのか...

アプリケーションの場合、サービス プリンシパルを取得するには次の 2 つの方法があります。

  • 推奨: アプリケーションに対してシステム割り当てのマネージド ID を有効にします。

    Azure では、マネージ ID を使用することにより、アプリケーションのサービス プリンシパルが内部的に管理され、他の Azure サービスでのアプリケーションの認証が自動的に行われます。 マネージド ID は、さまざまなサービスにデプロイされたアプリケーションで使用できます。

    詳細については、マネージド ID の概要に関する記事を参照してください。 また、マネージド ID がサポートされている Azure サービスに関する記事も参照してください。それには、特定のサービス (App Service、Azure Functions、Virtual Machines など) でマネージド ID を有効にする方法について説明されている記事へのリンクが含まれます。

  • マネージド ID を使用できない場合は、Azure AD テナントにアプリケーションを登録します。詳細については、クイックスタート: Azure ID プラットフォームでのアプリケーションの登録に関する記事を参照してください。 また、登録によって、すべてのテナントでそのアプリを示す 2 つ目のアプリケーション オブジェクトも作成されます。

https://docs.microsoft.com/ja-jp/azure/key-vault/general/authentication

⇧ やんごとなき「マネージド ID」推し。

マネージド IDって?

Microsoftさんのドキュメントによると、

docs.microsoft.com

マネージド ID は、Azure Active Directory (Azure AD) 認証をサポートするリソースに接続するときに使用する ID をアプリケーションに提供します。 アプリケーションは、マネージド ID を使用して Azure AD トークンを取得できます。 Azure Key Vault を使用すると、開発者はマネージド ID を使用してリソースにアクセスできます。 Key Vault を使用すれば、資格情報を安全な方法で格納し、ストレージ アカウントにアクセスすることができます。

https://docs.microsoft.com/ja-JP/azure/active-directory/managed-identities-azure-resources/overview

⇧ う~ん...よく分からん...

というのも、すべてのAzureリソースって、

docs.microsoft.com

⇧「Azure Resource Manager」経由でアクセスされるんじゃなかったでしたっけ?

tech-lab.sios.jp

⇧ 上記サイト様によりますと、「マネージド ID」を利用することの利点は、ソースコードから認証に関わる情報を外部に委譲できるということみたい。「Azure Resource Manager」経由でアクセスしていないわけではないってことですかね。

2022年5月21日(土)追記:↓ ここから

YouTubeMicrosoftの社員の方が解説していて、


www.youtube.com

⇧ Azure Active Directoryで認証後に、Azure Resource Management API(Azure Resource Managerのことだと思うけど)で、各リソースにアクセスできるとのこと。

で、マネージドIDの仕組みは、

⇧ Azureのリソースを識別するClient IDを自動生成、管理するサービスで、Client IDによってAzure Active DirectoryでAzureリソースを認証するということらしい。

で、そのClient IDを保持する方法の1つとしてAzure Key Vaultが利用できるということらしい。

2022年5月21日(土)追記:↑ ここまで

Microsoftさんのドキュメントが要領を得ないので、

www.purin-it.com

⇧ 上記サイト様を参考にさせていただきました。

2022年5月18日(水)追記:↓ ここから

公式のチュートリアルによると、

docs.microsoft.com

 重要

この記事の手順を完了するには、Spring Boot バージョン 2.5 または 2.6 が必要です。

https://docs.microsoft.com/ja-jp/azure/developer/java/spring-framework/configure-spring-boot-starter-java-app-with-azure-key-vault

⇧ とのことらしいのですが、知りたいのは、Azure Key VaultのversionとSpring Bootのversionの対応表だと思うんだけど、情報が見つからない...

ちなみに、自分の環境では、余計な依存関係も入ってないので、Spring Bootのversion 2.6.6で動作しました。

2022年5月18日(水)追記:↑ ここから

Azure Key Vaultのリソースを作成

Azure Portalで「すべてのサービス」を選択し、

「Key Vault」で検索し、「キー コンテナー」を選択。

「キー コンテナーの作成」ボタン押下。

「Key Vault 名」など入力し、

「アクセス ポリシー」タブはデフォルトの設定のままで特に変更してません。現場の環境に合わせましょう。

「ネットワーク」タブも同様にデフォルトの設定で変更してません。現場の環境に合わせましょう。

「タグ」タブもデフォルトの設定で変更してません。「確認および作成」を選択。

「作成」ボタン押下で、リソースの作成が始まります。

Azureリソースとして「キー コンテナー」が作成されています。

Azure Key Vaultにマネージド IDを追加

マネージド IDについては、

ts0818.hatenablog.com

⇧ 上記の記事で作成したものを追加していきます。

Azure Portalで「キー コンテナー」のリソースを選択してる状態で、「アクセス ポリシー」の「アクセス ポリシーの追加」を選択。

プリンシパルの選択」を選択。

ここでは、「Azure Media Services」に作成していた「マネージド ID」を選択してます。

「選択」ボタン押下。

「シークレットのアクセス許可」で、「取得」「一覧」にチェックを入れます。

「追加」ボタン押下。

「保存」を押下。

Azure Key Vaultに登録したマネージドIDでAzure Media ServicesのAPIを実施してみる

では、実際に「Azure Key Vault」に登録した「マネージド ID」で「Azure Media Services」のAPIが利用できるようになったのか、確認。

ts0818.hatenablog.com

ts0818.hatenablog.com

⇧ 上記の記事のソースコードを利用するので、変更した部分のみ掲載で。

■test-uml/build.gradle

plugins {
	id 'org.springframework.boot' version '2.6.6'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'org.postgresql:postgresql'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'com.azure.resourcemanager:azure-resourcemanager-mediaservices:2.0.0'
    implementation 'com.googlecode.json-simple:json-simple:1.1.1'
    implementation 'com.azure:azure-storage-blob:12.16.0'
    implementation 'com.azure:azure-identity:1.5.0'
    
    implementation 'com.azure:azure-security-keyvault-secrets:4.4.2'
}

tasks.named('test') {
	useJUnitPlatform()
}

⇧「Azure Key Vault」のAPIを使えるようにするための依存関係を追加してます。

application.propertiesについては、

⇧ azure.media.client.idとazure.media.client.secretをコメントアウト

azure.key.vault.urlについては、

⇧「キー コンテナー」の「概要」で確認できる「コンテナーのURI」の値を設定。

azure.media.managed.idについては、

「マネージド ID」の「概要」で確認できる「クライアント ID」の値を設定。

Javaのほうでは、「Azure Key Vault」に登録した「マネージド ID」によるAPI認証を行うように修正しました。

■test-uml/src/main/java/com/example/demo/service/azure/media/EncodingWithMESPredefinedPreset.java

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.example.demo.service.azure.media;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import javax.naming.AuthenticationException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.azure.core.http.policy.HttpLogDetailLevel;
import com.azure.core.http.policy.HttpLogOptions;
import com.azure.core.management.exception.ManagementException;
import com.azure.core.management.profile.AzureProfile;
import com.azure.identity.DefaultAzureCredential;
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.resourcemanager.mediaservices.MediaServicesManager;
import com.azure.resourcemanager.mediaservices.models.Asset;
import com.azure.resourcemanager.mediaservices.models.AssetContainerPermission;
import com.azure.resourcemanager.mediaservices.models.AssetContainerSas;
import com.azure.resourcemanager.mediaservices.models.BuiltInStandardEncoderPreset;
import com.azure.resourcemanager.mediaservices.models.EncoderNamedPreset;
import com.azure.resourcemanager.mediaservices.models.Job;
import com.azure.resourcemanager.mediaservices.models.JobInput;
import com.azure.resourcemanager.mediaservices.models.JobInputAsset;
import com.azure.resourcemanager.mediaservices.models.JobOutput;
import com.azure.resourcemanager.mediaservices.models.JobOutputAsset;
import com.azure.resourcemanager.mediaservices.models.JobState;
import com.azure.resourcemanager.mediaservices.models.ListContainerSasInput;
import com.azure.resourcemanager.mediaservices.models.ListPathsResponse;
import com.azure.resourcemanager.mediaservices.models.StreamingEndpoint;
import com.azure.resourcemanager.mediaservices.models.StreamingEndpointResourceState;
import com.azure.resourcemanager.mediaservices.models.StreamingLocator;
import com.azure.resourcemanager.mediaservices.models.StreamingPath;
import com.azure.resourcemanager.mediaservices.models.Transform;
import com.azure.resourcemanager.mediaservices.models.TransformOutput;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobContainerClientBuilder;

@Service
public class EncodingWithMESPredefinedPreset {
	
	@Autowired
	private ConfigWrapper configWrapper;
	
    private static final String TRANSFORM_NAME = "AdaptiveBitrate";
    private static final String OUTPUT_FOLDER = "Output";
    //private static final String BASE_URI = "https://nimbuscdn-nimbuspm.streaming.mediaservices.windows.net/2b533311-b215-4409-80af-529c3e853622/";
    //private static final String INPUT_LABEL = "input1";

    // Please change this to your endpoint name
    private static final String STREAMING_ENDPOINT_NAME = "default";

    @Value("${azure.key.vault.url}")
    private String azureKeyVaultUrl;
    
    @Value("${azure.media.managed.id}")
    private String azureMediaManagedId;
    
    private MediaServicesManager createMediaServiceManagerByKeyVault () {
        DefaultAzureCredential defaultCredential = new DefaultAzureCredentialBuilder()
                .managedIdentityClientId(azureMediaManagedId)
                .build();

            // Azure SDK client builders accept the credential as a parameter
            SecretClient client = new SecretClientBuilder()
                .vaultUrl(azureKeyVaultUrl)
                .credential(defaultCredential)
                .buildClient();
            
            AzureProfile profile = new AzureProfile(configWrapper.getAadTenantId(), configWrapper.getAzureSubscriptionId(),
                    com.azure.core.management.AzureEnvironment.AZURE);

            MediaServicesManager manager = MediaServicesManager.configure()
                    .withLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS))
                    .authenticate(defaultCredential, profile);
            return manager; 
    }
    
//    private MediaServicesManager createMediaServicesManager () {
//        // Connect to media services, please see https://docs.microsoft.com/en-us/azure/media-services/latest/configure-connect-java-howto
//        // for details.
//        TokenCredential credential = new ClientSecretCredentialBuilder()
//                .clientId(configWrapper.getAzureClientId())
//                .clientSecret(configWrapper.getAzureClientSecret())
//                .tenantId(configWrapper.getAadTenantId())
//                .build();
//        AzureProfile profile = new AzureProfile(configWrapper.getAadTenantId(), configWrapper.getAzureSubscriptionId(),
//                com.azure.core.management.AzureEnvironment.AZURE);
//
//        // MediaServiceManager is the entry point to Azure Media resource management.
//        MediaServicesManager manager = MediaServicesManager.configure()
//                .withLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS))
//                .authenticate(credential, profile);
//        return manager;
//    }

    /**
     * Run the sample.
     *
     * @param config This param is of type ConfigWrapper. This class reads values from local configuration file.
     */
    public void runEncodingWithMESPredefinedPreset(MultipartFile uploadFile) {
        // MediaServiceManager is the entry point to Azure Media resource management.
        //MediaServicesManager manager = createMediaServicesManager();
        MediaServicesManager manager = createMediaServiceManagerByKeyVault();
        // Creating a unique suffix so that we don't have name collisions if you run the
        // sample
        UUID uuid = UUID.randomUUID();
        String uniqueness = uuid.toString();
        String jobName = "job-" + uniqueness.substring(0, 13);
        String locatorName = "locator-" + uniqueness;
        String outputAssetName = "output-" + uniqueness;
        String inputAssetName = "input-" + uniqueness;
        boolean stopEndpoint = false;

        try {
            List<TransformOutput> outputs = new ArrayList<>();
            outputs.add(new TransformOutput().withPreset(
                    new BuiltInStandardEncoderPreset().withPresetName(EncoderNamedPreset.CONTENT_AWARE_ENCODING)));

            // Create the transform.
            System.out.println("Creating a transform...");
            Transform transform = manager.transforms()
                    .define(TRANSFORM_NAME)
                    .withExistingMediaService(configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName())
                    .withOutputs(outputs)
                    .create();
            System.out.println("Transform created");

            // Create a JobInputHttp. The input to the Job is a HTTPS URL pointing to an MP4 file.
//            List<String> files = new ArrayList<>();
//            files.add(uploadFile.getOriginalFilename());
//            JobInputHttp input = new JobInputHttp().withBaseUri(BASE_URI);
//            input.withFiles(files);
//            input.withLabel(INPUT_LABEL);

            Asset inputAsset = createInputAsset(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), inputAssetName,
            		uploadFile);
            
            // Output from the encoding Job must be written to an Asset, so let's create one. Note that we
            // are using a unique asset name, there should not be a name collision.
            System.out.println("Creating an output asset...");
            Asset outputAsset = manager.assets()
                    .define(outputAssetName)
                    .withExistingMediaService(configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName())
                    .create();

            Job job = submitJob(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), transform.name(), jobName,
            		inputAsset.name(), outputAsset.name());

            long startedTime = System.currentTimeMillis();

            // In this demo code, we will poll for Job status. Polling is not a recommended best practice for production
            // applications because of the latency it introduces. Overuse of this API may trigger throttling. Developers
            // should instead use Event Grid. To see how to implement the event grid, see the sample
            // https://github.com/Azure-Samples/media-services-v3-java/tree/master/ContentProtection/BasicAESClearKey.
            job = waitForJobToFinish(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), transform.name(),
                    jobName);

            long elapsed = (System.currentTimeMillis() - startedTime) / 1000; // Elapsed time in seconds
            System.out.println("Job elapsed time: " + elapsed + " second(s).");            

            if (job.state() == JobState.FINISHED) {
                System.out.println("Job finished.");
                System.out.println();

                // Now that the content has been encoded, publish it for Streaming by creating
                // a StreamingLocator. 
                StreamingLocator locator = getStreamingLocator(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(),
                        outputAsset.name(), locatorName);

                StreamingEndpoint streamingEndpoint = manager.streamingEndpoints()
                        .get(configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), STREAMING_ENDPOINT_NAME);

                if (streamingEndpoint != null) {
                    // Start The Streaming Endpoint if it is not running.
                    if (streamingEndpoint.resourceState() != StreamingEndpointResourceState.RUNNING) {
                        System.out.println("Streaming endpoint was stopped, restarting it...");
                        manager.streamingEndpoints().start(configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), STREAMING_ENDPOINT_NAME);

                        // We started the endpoint, we should stop it in cleanup.
                        stopEndpoint = true;
                    }

                    System.out.println();
                    System.out.println("Streaming urls:");
                    List<String> urls = getStreamingUrls(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), locator.name(), streamingEndpoint);

                    for (String url : urls) {
                        System.out.println("\t" + url);
                    }

                    System.out.println();
                    System.out.println("To stream, copy and paste the Streaming URL into the Azure Media Player at 'http://aka.ms/azuremediaplayer'.");
                    System.out.println("When finished, press ENTER to continue.");
                    System.out.println();
                    System.out.flush();
                    

                    // Download output asset for verification.
                    System.out.println("Downloading output asset...");
                    System.out.println();
                    File outputFolder = new File(OUTPUT_FOLDER);
                    if (outputFolder.exists() && !outputFolder.isDirectory()) {
                        outputFolder = new File(OUTPUT_FOLDER + uniqueness);
                    }
                    if (!outputFolder.exists()) {
                        outputFolder.mkdir();
                    }

                    downloadResults(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), outputAsset.name(),
                            outputFolder);

                    System.out.println("Done downloading. Please check the files at " + outputFolder.getAbsolutePath());
                } else {
                    System.out.println("Could not find streaming endpoint: " + STREAMING_ENDPOINT_NAME);
                }

                System.out.println("When finished, press ENTER to cleanup.");
                System.out.println();
                System.out.flush();
               
            } else if (job.state() == JobState.ERROR) {
                System.out.println("ERROR: Job finished with error message: " + job.outputs().get(0).error().message());
                System.out.println("ERROR:                   error details: "
                        + job.outputs().get(0).error().details().get(0).message());
            }
        } catch (Exception e) {
            Throwable cause = e;
            while (cause != null) {
                if (cause instanceof AuthenticationException) {
                    System.out.println("ERROR: Authentication error, please check your account settings in appsettings.json.");
                    break;
                } else if (cause instanceof ManagementException) {
                    ManagementException apiException = (ManagementException) cause;
                    System.out.println("ERROR: " + apiException.getValue().getMessage());
                    break;
                }
                cause = cause.getCause();
            }
            System.out.println();
            e.printStackTrace();
            System.out.println();
        } finally {
            System.out.println("Cleaning up...");
            cleanup(manager, configWrapper.getAzureResourceGroup(), configWrapper.getAzureMediaServicesAccountName(), TRANSFORM_NAME, jobName,
                    outputAssetName, locatorName, stopEndpoint, STREAMING_ENDPOINT_NAME);
            System.out.println("Done.");
        }
    }

    /**
     * Create and submit a job.
     *
     * @param manager         The entry point of Azure Media resource management.
     * @param resourceGroup   The name of the resource group within the Azure subscription.
     * @param accountName     The Media Services account name.
     * @param transformName   The name of the transform.
     * @param jobName         The name of the job.
     * @param jobInput        The input to the job.
     * @param outputAssetName The name of the asset that the job writes to.
     * @return The job created.
     */
    private static Job submitJob(MediaServicesManager manager, String resourceGroup, String accountName, String transformName,
                                 String jobName, String inputAssetName,  String outputAssetName) {
        System.out.println("Creating a job...");
        // Use the name of the created input asset to create the job input.
        JobInput jobInput = new JobInputAsset().withAssetName(inputAssetName);

        // First specify where the output(s) of the Job need to be written to
        List<JobOutput> jobOutputs = new ArrayList<>();
        jobOutputs.add(new JobOutputAsset().withAssetName(outputAssetName));

        Job job = manager.jobs().define(jobName)
                .withExistingTransform(resourceGroup, accountName, transformName)
                .withInput(jobInput)
                .withOutputs(jobOutputs)
                .create();

        return job;
    }

    /**
     * Polls Media Services for the status of the Job.
     *
     * @param manager       This is the entry point of Azure Media resource
     *                      management
     * @param resourceGroup The name of the resource group within the Azure
     *                      subscription
     * @param accountName   The Media Services account name
     * @param transformName The name of the transform
     * @param jobName       The name of the job submitted
     * @return The job
     */
    private static Job waitForJobToFinish(MediaServicesManager manager, String resourceGroup, String accountName,
                                          String transformName, String jobName) {
        final int SLEEP_INTERVAL = 10 * 1000;

        Job job = null;
        boolean exit = false;

        do {
            job = manager.jobs().get(resourceGroup, accountName, transformName, jobName);

            if (job.state() == JobState.FINISHED || job.state() == JobState.ERROR || job.state() == JobState.CANCELED) {
                exit = true;
            } else {
                System.out.println("Job is " + job.state());

                int i = 0;
                for (JobOutput output : job.outputs()) {
                    System.out.print("\tJobOutput[" + i++ + "] is " + output.state() + ".");
                    if (output.state() == JobState.PROCESSING) {
                        System.out.print("  Progress: " + output.progress());
                    }
                    System.out.println();
                }

                try {
                    Thread.sleep(SLEEP_INTERVAL);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } while (!exit);

        return job;
    }

    /**
     * Use Media Service and Storage APIs to download the output files to a local folder
     *
     * @param manager       The entry point of Azure Media resource management
     * @param resourceGroup The name of the resource group within the Azure subscription
     * @param accountName   The Media Services account name
     * @param assetName     The asset name
     * @param outputFolder  The output folder for downloaded files.
     * @throws Exception
     * @throws URISyntaxException
     * @throws IOException
     */
    private static void downloadResults(MediaServicesManager manager, String resourceGroup, String accountName,
                                        String assetName, File outputFolder) throws URISyntaxException, IOException {
        ListContainerSasInput parameters = new ListContainerSasInput()
                .withPermissions(AssetContainerPermission.READ)
                .withExpiryTime(OffsetDateTime.now().plusHours(1));
        AssetContainerSas assetContainerSas = manager.assets()
                .listContainerSas(resourceGroup, accountName, assetName, parameters);

        BlobContainerClient container =
                new BlobContainerClientBuilder()
                        .endpoint(assetContainerSas.assetContainerSasUrls().get(0))
                        .buildClient();

        File directory = new File(outputFolder, assetName);
        directory.mkdir();

        container.listBlobs().forEach(blobItem -> {
            BlobClient blob = container.getBlobClient(blobItem.getName());
            File downloadTo = new File(directory, blobItem.getName());
            blob.downloadToFile(downloadTo.getAbsolutePath());
        });

        System.out.println("Download complete.");
    }

    /**
     * Creates a StreamingLocator for the specified asset and with the specified streaming policy name.
     * Once the StreamingLocator is created the output asset is available to clients for playback.
     *
     * @param manager       The entry point of Azure Media resource management
     * @param resourceGroup The name of the resource group within the Azure subscription
     * @param accountName   The Media Services account name
     * @param assetName     The name of the output asset
     * @param locatorName   The StreamingLocator name (unique in this case)
     * @return The locator created
     */
    private static StreamingLocator getStreamingLocator(MediaServicesManager manager, String resourceGroup, String accountName,
                                                        String assetName, String locatorName) {
        // Note that we are using one of the PredefinedStreamingPolicies which tell the Origin component
        // of Azure Media Services how to publish the content for streaming.
        System.out.println("Creating a streaming locator...");
        StreamingLocator locator = manager
                .streamingLocators().define(locatorName)
                .withExistingMediaService(resourceGroup, accountName)
                .withAssetName(assetName)
                .withStreamingPolicyName("Predefined_ClearStreamingOnly")
                .create();

        return locator;
    }

    /**
     * Checks if the streaming endpoint is in the running state, if not, starts it.
     *
     * @param manager           The entry point of Azure Media resource management
     * @param resourceGroup     The name of the resource group within the Azure subscription
     * @param accountName       The Media Services account name
     * @param locatorName       The name of the StreamingLocator that was created
     * @param streamingEndpoint The streaming endpoint.
     * @return List of streaming urls
     */
    private static List<String> getStreamingUrls(MediaServicesManager manager, String resourceGroup, String accountName,
                                                 String locatorName, StreamingEndpoint streamingEndpoint) {
        List<String> streamingUrls = new ArrayList<>();

        ListPathsResponse paths = manager.streamingLocators().listPaths(resourceGroup, accountName, locatorName);

        for (StreamingPath path : paths.streamingPaths()) {
            StringBuilder uriBuilder = new StringBuilder();
            uriBuilder.append("https://")
                    .append(streamingEndpoint.hostname())
                    .append("/")
                    .append(path.paths().get(0));

            streamingUrls.add(uriBuilder.toString());
        }
        return streamingUrls;
    }

    /**
     * Cleanup
     *
     * @param manager               The entry point of Azure Media resource management.
     * @param resourceGroupName     The name of the resource group within the Azure subscription.
     * @param accountName           The Media Services account name.
     * @param transformName         The transform name.
     * @param jobName               The job name.
     * @param assetName             The asset name.
     * @param streamingLocatorName  The streaming locator name.
     * @param stopEndpoint          Stop endpoint if true, otherwise keep endpoint running.
     * @param streamingEndpointName The endpoint name.
     */
    private static void cleanup(MediaServicesManager manager, String resourceGroupName, String accountName, String transformName, String jobName,
                                String assetName, String streamingLocatorName, boolean stopEndpoint, String streamingEndpointName) {
        if (manager == null) {
            return;
        }

        manager.jobs().delete(resourceGroupName, accountName, transformName, jobName);
        manager.assets().delete(resourceGroupName, accountName, assetName);

        manager.streamingLocators().delete(resourceGroupName, accountName, streamingLocatorName);

        if (stopEndpoint) {
            // Because we started the endpoint, we'll stop it.
            manager.streamingEndpoints().stop(resourceGroupName, accountName, streamingEndpointName);
        } else {
            // We will keep the endpoint running because it was not started by this sample. Please note, There are costs to keep it running.
            // Please refer https://azure.microsoft.com/en-us/pricing/details/media-services/ for pricing.
            System.out.println("The endpoint '" + streamingEndpointName + "' is running. To halt further billing on the endpoint, please stop it in azure portal or AMS Explorer.");
        }
    }
    /**
     * Creates a new input Asset and uploads the specified local video file into it.
     *
     * @param manager           This is the entry point of Azure Media resource
     *                          management.
     * @param resourceGroupName The name of the resource group within the Azure
     *                          subscription.
     * @param accountName       The Media Services account name.
     * @param assetName         The name of the asset where the media file to
     *                          uploaded to.
     * @param mediaFile         The path of a media file to be uploaded into the
     *                          asset.
     * @return The asset.
     */
    private static Asset createInputAsset(MediaServicesManager manager, String resourceGroupName, String accountName,
            String assetName, MultipartFile mediaFile) throws Exception {

        System.out.println("Creating an input asset...");
        // Call Media Services API to create an Asset.
        // This method creates a container in storage for the Asset.
        // The files (blobs) associated with the asset will be stored in this container.
        Asset asset = manager.assets().define(assetName).withExistingMediaService(resourceGroupName, accountName)
                .create();

        // Use Media Services API to get back a response that contains
        // SAS URL for the Asset container into which to upload blobs.
        // That is where you would specify read-write permissions
        // and the expiration time for the SAS URL.
        ListContainerSasInput parameters = new ListContainerSasInput()
                .withPermissions(AssetContainerPermission.READ_WRITE).withExpiryTime(OffsetDateTime.now().plusHours(4));
        AssetContainerSas response = manager.assets()
                .listContainerSas(resourceGroupName, accountName, assetName, parameters);

        // Use Storage API to get a reference to the Asset container
        // that was created by calling Asset's create method.
        BlobContainerClient container = new BlobContainerClientBuilder()
                .endpoint(response.assetContainerSasUrls().get(0))
                .buildClient();

        // Uploading from a local file:
       // URI fileToUpload = StreamHLSAndDASH.class.getClassLoader().getResource(mediaFile).toURI(); // The file is a
        // resource in
        // CLASSPATH.
        //File file = new File(fileToUpload);
        BlobClient blob = container.getBlobClient(mediaFile.getOriginalFilename());

        // Use Storage API to upload the file into the container in storage.
        System.out.println("Uploading a media file to the asset...");
        blob.upload(mediaFile.getInputStream(), mediaFile.getBytes().length);
        //blob.uploadFromFile(mediaFile.getOriginalFilename());

        return asset;
    }
}

で、実行して、

動画ファイルをアップロードしてみる。

Azure Portalで「メディア サービス」のリソースで「資産」を確認。

一応、「Azure Media Services」のAPI利用できたようです。

エンコードした結果のAssetについては、ローカル環境にダウンロードされてました。ローカル環境じゃなくて、Azure環境にアップロードされて欲しいんだけどな...

Input用のAssetの時みたいにAzure Blob Storageにアップロードすれば良いということですかね?

ただ、「Azure Media Services v3 API」のJavaの「downloadResults」メソッドの説明を見た限りだと、ローカル環境にダウンロードって説明はあるのですが、ダウンロード元のファイルがどこに作られてるのか謎です...

実際に、該当箇所を確認すると、

■test-uml/src/main/java/com/example/demo/service/azure/media/EncodingWithMESPredefinedPreset.java

    private static void downloadResults(MediaServicesManager manager, String resourceGroup, String accountName,
                                        String assetName, File outputFolder) throws URISyntaxException, IOException {
        ListContainerSasInput parameters = new ListContainerSasInput()
                .withPermissions(AssetContainerPermission.READ)
                .withExpiryTime(OffsetDateTime.now().plusHours(1));
        AssetContainerSas assetContainerSas = manager.assets()
                .listContainerSas(resourceGroup, accountName, assetName, parameters);

        BlobContainerClient container =
                new BlobContainerClientBuilder()
                        .endpoint(assetContainerSas.assetContainerSasUrls().get(0))
                        .buildClient();

        File directory = new File(outputFolder, assetName);
        directory.mkdir();

        container.listBlobs().forEach(blobItem -> {
            BlobClient blob = container.getBlobClient(blobItem.getName());
            File downloadTo = new File(directory, blobItem.getName());
            blob.downloadToFile(downloadTo.getAbsolutePath());
        });

        System.out.println("Download complete.");
    }

⇧ Azure Blob StorageのBLOBコンテナーから取得してるんだけども、Azure Portalで確認してもエンコード済みのファイルが見当たらないんですよね...

Microsoftのドキュメントによると、「Azure Media Services」には「Asset」という概念があり、

docs.microsoft.com

アセット には、デジタル ファイル (ビデオ、オーディオ、画像、サムネイルのコレクション、テキスト トラック、クローズド キャプション ファイルなど) と、それらのファイルに関するメタデータが含まれます。 デジタル ファイルがアセットにアップロードされた後は、Media Services エンコードおよびストリーミング ワークフローで使用できます。

https://docs.microsoft.com/ja-jp/azure/media-services/previous/media-services-concepts#assets

資産は Azure ストレージ アカウント内の BLOB コンテナーにマップされ、資産内のファイルはブロック BLOB としてそのコンテナーに格納されます。 ページ BLOB は Azure Media Services ではサポートされていません。

https://docs.microsoft.com/ja-jp/azure/media-services/previous/media-services-concepts#assets

⇧「Asset」が「Azure Blob Storage」の「BLOBコンテナー」にマップされるらしく、「Azure Blob Storage」の「BLOBコンテナー」の「BLOB」に、「Asset」内のファイルなどが保存されるとあるのですが、

⇧ 明示的にAzure Blob Storageにアップロードしたファイルのみが、「Azure Blob Storage」で永続的に保存されるということでしょうか?

2022年5月15日(日)追記:↓ ここから

以下の部分をコメントアウトすると、エンコード後のファイルもAzure Blob Storageに保持されるようになりました。

    /**
     * Cleanup
     *
     * @param manager               The entry point of Azure Media resource management.
     * @param resourceGroupName     The name of the resource group within the Azure subscription.
     * @param accountName           The Media Services account name.
     * @param transformName         The transform name.
     * @param jobName               The job name.
     * @param assetName             The asset name.
     * @param streamingLocatorName  The streaming locator name.
     * @param stopEndpoint          Stop endpoint if true, otherwise keep endpoint running.
     * @param streamingEndpointName The endpoint name.
     */
    private static void cleanup(MediaServicesManager manager, String resourceGroupName, String accountName, String transformName, String jobName,
                                String assetName, String streamingLocatorName, boolean stopEndpoint, String streamingEndpointName) {
        if (manager == null) {
            return;
        }

        manager.jobs().delete(resourceGroupName, accountName, transformName, jobName);
        //manager.assets().delete(resourceGroupName, accountName, assetName);

        manager.streamingLocators().delete(resourceGroupName, accountName, streamingLocatorName);

        if (stopEndpoint) {
            // Because we started the endpoint, we'll stop it.
            manager.streamingEndpoints().stop(resourceGroupName, accountName, streamingEndpointName);
        } else {
            // We will keep the endpoint running because it was not started by this sample. Please note, There are costs to keep it running.
            // Please refer https://azure.microsoft.com/en-us/pricing/details/media-services/ for pricing.
            System.out.println("The endpoint '" + streamingEndpointName + "' is running. To halt further billing on the endpoint, please stop it in azure portal or AMS Explorer.");
        }
    }    

「Asset」が「Input」「Output」の2つのフォルダが残っていくことになるけども、エンコードのJobの実施の際に「Input」「Output」の2つの「Asset」を指定しないといけないっぽいから、仕方ないんですかね?

「Input」の「Asset」フォルダのファイルを「Output」の「Asset」フォルダにアップロードして、「Input」の「Asset」フォルダを削除するようにすれば良いってことなのかな?

どうするのが良いのかが分からんです...教えて偉い人。

2022年5月15日(日)追記:↑ ここまで

2022年5月19日(木)追記:↓ ここから

以下の部分をコメントアウトすると、Azure Portalでストリーミング再生が確認できるようです。

    /**
     * Cleanup
     *
     * @param manager               The entry point of Azure Media resource management.
     * @param resourceGroupName     The name of the resource group within the Azure subscription.
     * @param accountName           The Media Services account name.
     * @param transformName         The transform name.
     * @param jobName               The job name.
     * @param assetName             The asset name.
     * @param streamingLocatorName  The streaming locator name.
     * @param stopEndpoint          Stop endpoint if true, otherwise keep endpoint running.
     * @param streamingEndpointName The endpoint name.
     */
    private static void cleanup(MediaServicesManager manager, String resourceGroupName, String accountName, String transformName, String jobName,
                                String assetName, String streamingLocatorName, boolean stopEndpoint, String streamingEndpointName) {
        if (manager == null) {
            return;
        }

        manager.jobs().delete(resourceGroupName, accountName, transformName, jobName);
        //manager.assets().delete(resourceGroupName, accountName, assetName);

        //manager.streamingLocators().delete(resourceGroupName, accountName, streamingLocatorName);

        if (stopEndpoint) {
            // Because we started the endpoint, we'll stop it.
            manager.streamingEndpoints().stop(resourceGroupName, accountName, streamingEndpointName);
        } else {
            // We will keep the endpoint running because it was not started by this sample. Please note, There are costs to keep it running.
            // Please refer https://azure.microsoft.com/en-us/pricing/details/media-services/ for pricing.
            System.out.println("The endpoint '" + streamingEndpointName + "' is running. To halt further billing on the endpoint, please stop it in azure portal or AMS Explorer.");
        }
    }    

で、Streaming endpointを起動しておいて、動画ファイルのアップロードを試したところ、

ストリーミング用の動画データが確認できました。

で、どうやら、ストリーミングURLのフォーマットは

https://{streaming endpoint name-media services account name}/{locator ID}/{file name}.ism/Manifest(format=xxx)    

⇧ みたいな感じらしい。

旧い方のドキュメントでしか説明が見当たらんという...

docs.microsoft.com

HLS ストリーミング URL を作成するには、次のように、 (format=m3u8-aapl) を URL に追加します。

{streaming endpoint name-media services account name}/{locator ID}/{file name}.ism/Manifest(format=m3u8-aapl)

https://docs.microsoft.com/ja-jp/azure/media-services/previous/media-services-portal-publish#overview

⇧ そもそも、「コンテンツを発行する」の説明が、新しいAzure Media Services v3 APIのドキュメントで見当たらんのだよね...

そして、Microsoftさんの公式のJavaサンプルを使ってるのだけど、コード間違ってるっぽい気がする...「/(スラッシュ)」が二重になってるし...

試しに、

    private static List getStreamingUrls(MediaServicesManager manager, String resourceGroup, String accountName,
                                                 String locatorName, StreamingEndpoint streamingEndpoint) {
        List streamingUrls = new ArrayList<>();

        ListPathsResponse paths = manager.streamingLocators().listPaths(resourceGroup, accountName, locatorName);

        for (StreamingPath path : paths.streamingPaths()) {
            StringBuilder uriBuilder = new StringBuilder();
            uriBuilder.append("https://")
                    .append(streamingEndpoint.hostname())
                    //.append("/")
                    .append(path.paths().get(0));

            streamingUrls.add(uriBuilder.toString());
        }
        return streamingUrls;
    }    

⇧ をコメントアウトしたところ、「/(スラッシュ)」が1個になりました。

一応、issueとして上げてみました。

github.com

まぁ、相変わらず、Azure Media Services がよく分からんですけど...

2022年5月19日(木)追記:↑ ここまで

Microsoftのドキュメント相変わらず、情報が読み取り辛い。

そもそもとして、

docs.microsoft.com

docs.microsoft.com

⇧ 何故、「マネージド ID」の話を出さないのか...

これ、「Azure Key Vault」のドキュメントを探っていて「サービス プリンシパル」に「マネージド ID」が使えるかもしれないって推測したけども、何の前提知識も持ち合わせていない状態で上記の情報から読み取れるわけない気がするんだけど...

今回はこのへんで。