.NET を使用したバックグラウンド処理

多くのアプリでは、ウェブ リクエストのコンテキストの外部でバックグラウンド処理を行う必要があります。このチュートリアルでは、ユーザーが翻訳するテキストを入力した後、以前の翻訳のリストを表示するウェブアプリを作成します。翻訳は、ユーザーのリクエストをブロックしないようにバックグラウンド プロセスで行われます。

次の図は、翻訳リクエストのプロセスを示しています。

アーキテクチャの図

チュートリアル アプリが動作する際のイベントの順序は次のとおりです。

  1. ウェブページにアクセスすると、Firestore に保存されている以前の翻訳のリストが表示されます。
  2. HTML フォームに入力してテキストの翻訳をリクエストします。
  3. 翻訳リクエストは Pub/Sub にパブリッシュされます。
  4. その Pub/Sub トピックに登録されている Cloud Run サービスが起動されます。
  5. Cloud Run サービスが Cloud Translation を使用してテキストを翻訳します。
  6. Cloud Run サービスは、その結果を Firestore に保存します。

このチュートリアルは、 でのバックグラウンド処理の詳細に関心をお持ちの方を対象としています。 Google CloudPub/Sub、Firestore、App Engine、Cloud Run 関数についての経験は必須要件ではありません。ただし、すべてのコードを理解するには、.NET、JavaScript、HTML の経験があると役立ちます。

目標

  • Cloud Run サービスを理解し、デプロイします。
  • アプリを試してみます。

費用

このドキュメントでは、課金対象である次のコンポーネントを使用します。 Google Cloud

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。

新規の Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

このドキュメントに記載されているタスクの完了後、作成したリソースを削除すると、それ以上の請求は発生しません。詳細については、クリーンアップをご覧ください。

始める前に

  1. アカウントにログインします。 Google Cloud を初めて使用する場合は、 アカウントを作成して、実際のシナリオで Google プロダクトのパフォーマンスを評価してください。 Google Cloud新規のお客様には、ワークロードの実行、テスト、デプロイができる無料クレジット $300 分を差し上げます。
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  5. Google Cloud CLI をインストールします。

  6. 外部 ID プロバイダ(IdP)を使用している場合は、まず連携 ID を使用して gcloud CLI にログインする必要があります。

  7. gcloud CLI を初期化するには、次のコマンドを実行します:

    gcloud init
  8. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  9. Verify that billing is enabled for your Google Cloud project.

  10. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  11. Google Cloud CLI をインストールします。

  12. 外部 ID プロバイダ(IdP)を使用している場合は、まず連携 ID を使用して gcloud CLI にログインする必要があります。

  13. gcloud CLI を初期化するには、次のコマンドを実行します:

    gcloud init
  14. gcloud コンポーネントを更新します。
    gcloud components update
  15. 開発環境を準備します。

    .NET 開発環境の設定

アプリの準備

  1. ターミナル ウィンドウで、サンプルアプリ リポジトリのクローンをローカル マシンに作成します。

    git clone https://github.com/GoogleCloudPlatform/getting-started-dotnet.git

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

  2. バックグラウンド タスクのサンプルコードを含むディレクトリに移動します。

    cd getting-started-dotnet/BackgroundProcessing

TranslateWorker サービスについて

  • このサービスは、Firestore や Translation のようないくつかの依存関係をインポートすることから始まります。

  • Firestore クライアントと Translation クライアントは初期化されているため、ハンドラの呼び出し間で再利用できます。これにより、実行速度の低下の要因となる、呼び出しごとに新しいクライアントを初期化する必要がなくなります。

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<FirestoreDb>(provider =>
            FirestoreDb.Create(GetFirestoreProjectId()));
        services.AddSingleton<TranslationClient>(
            TranslationClient.Create());
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    
  • 翻訳 API は、選択した言語に文字列を翻訳します。

    var result = await _translator.TranslateTextAsync(sourceText, "es");
    
  • コントローラのコンストラクタは、Firestore クライアントと Pub/Sub クライアントを受け取ります。

    Post メソッドは Pub/Sub メッセージを解析して、翻訳するテキストを取得します。メッセージ ID を翻訳リクエストの一意の名前として使用して、重複した翻訳が保存されないようにします。

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace TranslateWorker.Controllers
    {
        /// <summary>
        /// The message Pubsub posts to our controller.
        /// </summary>
        public class PostMessage
        {
            public PubsubMessage message { get; set; }
            public string subscription { get; set; }
        }
    
        /// <summary>
        /// Pubsub's inner message.
        /// </summary>
        public class PubsubMessage
        {
            public string data { get; set; }
            public string messageId { get; set; }
            public Dictionary<string, string> attributes { get; set; }
        }
    
    
        [Route("api/[controller]")]
        [ApiController]
        public class TranslateController : ControllerBase
        {
            private readonly ILogger<TranslateController> _logger;
            private readonly FirestoreDb _firestore;
            private readonly TranslationClient _translator;
            // The Firestore collection where we store translations.
            private readonly CollectionReference _translations;
    
            public TranslateController(ILogger<TranslateController> logger,
                FirestoreDb firestore,
                TranslationClient translator)
            {
                _logger = logger ?? throw new ArgumentNullException(nameof(logger));
                _firestore = firestore ?? throw new ArgumentNullException(
                    nameof(firestore));
                _translator = translator ?? throw new ArgumentNullException(
                    nameof(translator));
                _translations = _firestore.Collection("Translations");
            }
    
            /// <summary>
            /// Handle a posted message from Pubsub.
            /// </summary>
            /// <param name="request">The message Pubsub posts to this process.</param>
            /// <returns>NoContent on success.</returns>
            [HttpPost]
            public async Task<IActionResult> Post([FromBody] PostMessage request)
            {
                // Unpack the message from Pubsub.
                string sourceText;
                try
                {
                    byte[] data = Convert.FromBase64String(request.message.data);
                    sourceText = Encoding.UTF8.GetString(data);
                }
                catch (Exception e)
                {
                    _logger.LogError(1, e, "Bad request");
                    return BadRequest();
                }
                // Translate the source text.
                _logger.LogDebug(2, "Translating {0} to Spanish.", sourceText);
                var result = await _translator.TranslateTextAsync(sourceText, "es");
                // Store the result in Firestore.
                Translation translation = new Translation()
                {
                    TimeStamp = DateTime.UtcNow,
                    SourceText = sourceText,
                    TranslatedText = result.TranslatedText
                };
                _logger.LogDebug(3, "Saving translation {0} to {1}.",
                    translation.TranslatedText, _translations.Path);
                await _translations.Document(request.message.messageId)
                    .SetAsync(translation);
                // Return a success code.
                return NoContent();
            }
    
            /// <summary>
            /// Serve a root page so Cloud Run knows this process is healthy.
            /// </summary>
            [Route("/")]
            public IActionResult Index()
            {
                return Content("Serving translate requests...");
            }
        }
    }
    

TranslateWorker サービスのデプロイ

  • BackgroundProcessing ディレクトリで、PowerShell スクリプトを実行してサービスをビルドし、Cloud Run にデプロイします。

    PublishTo-CloudRun.ps1

PublishTo-CloudRun.ps1 スクリプトについて

PublishTo-CloudRun.ps1 スクリプトはサービスを Cloud Run に公開し、TranslateWorker サービスが誤用されるのを防ぎます。サービスがすべての着信接続を許可した場合、誰でもコントローラに翻訳リクエストを送信することができ、それによりコストが発生します。したがって、Pub/Sub からの POST リクエストのみを受け入れるようにサービスを設定します。

このスクリプトは次のことを行います。

  1. dotnet publish を使用してローカルでアプリをビルドします。
  2. Cloud Build を使用して、アプリを実行するコンテナをビルドします。
  3. アプリを Cloud Run にデプロイします。
  4. プロジェクトで Pub/Sub 認証トークンを作成できるようにします。
  5. Pub/Sub サブスクリプション ID を表すサービス アカウントを作成します。
  6. サービス アカウントに、TranslateWorker サービスを呼び出す権限を付与します。
  7. Pub/Sub トピックとサブスクリプションを作成します。

    # 1. Build the application locally.
    dotnet publish -c Release
    
    # Collect some details about the project that we'll need later.
    $projectId = gcloud config get-value project
    $projectNumber = gcloud projects describe $projectId --format="get(projectNumber)"
    $region = "us-central1"
    
    # 2. Use Google Cloud Build to build the worker's container and publish to Google
    # Container Registry.
    gcloud builds submit --tag gcr.io/$projectId/translate-worker `
        TranslateWorker/bin/Release/netcoreapp2.1/publish
    
    # 3. Run the container with Google Cloud Run.
    gcloud beta run deploy translate-worker --region $region --platform managed `
        --image gcr.io/$projectId/translate-worker --no-allow-unauthenticated
    $url = gcloud beta run services describe translate-worker --platform managed `
        --region $region --format="get(status.address.hostname)"
    
    # 4. Enable the project to create pubsub authentication tokens.
    gcloud projects add-iam-policy-binding $projectId `
         --member=serviceAccount:service-$projectNumber@gcp-sa-pubsub.iam.gserviceaccount.com `
         --role=roles/iam.serviceAccountTokenCreator
    
    # 5. Create a service account to represent the Cloud Pub/Sub subscription identity.
    $serviceAccountExists = gcloud iam service-accounts describe `
        cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com 2> $null
    if (-not $serviceAccountExists) {
        gcloud iam service-accounts create cloud-run-pubsub-invoker `
            --display-name "Cloud Run Pub/Sub Invoker"
    }
    
    # 6. For Cloud Run, give this service account permission to invoke 
    # translate-worker service.
    gcloud beta run services add-iam-policy-binding translate-worker `
         --member=serviceAccount:cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com `
         --role=roles/run.invoker --region=$region
    
    # 7. Create a pubsub topic and subscription, if they don't already exist.
    $topicExists = gcloud pubsub topics describe translate-requests 2> $null 
    if (-not $topicExists) {
        gcloud pubsub topics create translate-requests
    }
    $subscriptionExists = gcloud pubsub subscriptions describe translate-requests 2> $null
    if ($subscriptionExists) {
        gcloud beta pubsub subscriptions modify-push-config translate-requests `
            --push-endpoint $url/api/translate `
            --push-auth-service-account cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com
    } else {
        gcloud beta pubsub subscriptions create translate-requests `
            --topic translate-requests --push-endpoint $url/api/translate `
            --push-auth-service-account cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com
    }
    
    

TranslateUI サービスについて

TranslateUI サービスは、最近の翻訳を表示するウェブページをレンダリングし、新しい翻訳のリクエストを受け入れます。

  • StartUp クラスは ASP.NET アプリを構成し、Pub/Sub および Firestore クライアントを作成します。

    using Google.Apis.Auth.OAuth2;
    using Google.Cloud.Firestore;
    using Google.Cloud.PubSub.V1;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Net.Http;
    
    namespace TranslateUI
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddSingleton<FirestoreDb>(
                    provider => FirestoreDb.Create(GetFirestoreProjectId()));
                services.AddSingleton<PublisherClient>(
                    provider => PublisherClient.CreateAsync(new TopicName(
                        GetProjectId(), GetTopicName())).Result);
                services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
    
                app.UseHttpsRedirection();
                app.UseStaticFiles();
                app.UseCookiePolicy();
    
                app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");
                });
            }
    
        }
    }
    
  • インデックス ハンドラ Index は、Firestore からすべての既存の翻訳を取得し、リストを使用して ViewModel に入力します。

    using Google.Cloud.Firestore;
    using Google.Cloud.PubSub.V1;
    using Google.Protobuf;
    using Microsoft.AspNetCore.Mvc;
    using System.Diagnostics;
    using System.Linq;
    using System.Threading.Tasks;
    using TranslateUI.Models;
    
    namespace TranslateUI.Controllers
    {
        public class HomeController : Controller
        {
            private readonly FirestoreDb _firestore;
            private readonly PublisherClient _publisher;
            private CollectionReference _translations;
    
            public HomeController(FirestoreDb firestore, PublisherClient publisher)
            {
                _firestore = firestore;
                _publisher = publisher;
                _translations = _firestore.Collection("Translations");
            }
    
            [HttpPost]
            [HttpGet]
            public async Task<IActionResult> Index(string SourceText)
            {
                // Look up the most recent 20 translations.
                var query = _translations.OrderByDescending("TimeStamp")
                    .Limit(20);
                var snapshotTask = query.GetSnapshotAsync();
    
                if (!string.IsNullOrWhiteSpace(SourceText))
                {
                    // Submit a new translation request.
                    await _publisher.PublishAsync(new PubsubMessage()
                    {
                        Data = ByteString.CopyFromUtf8(SourceText)
                    });
                }
    
                // Render the page.
                var model = new HomeViewModel()
                {
                    Translations = (await snapshotTask).Documents.Select(
                        doc => doc.ConvertTo<Translation>()).ToList(),
                    SourceText = SourceText
                };
                return View(model);
            }
    
            [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
            public IActionResult Error()
            {
                return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
            }
        }
    }
  • 新しい翻訳をリクエストするには、HTML フォームを送信します。リクエスト翻訳ハンドラはリクエストを検証し、メッセージを Pub/Sub にパブリッシュします。

    // Submit a new translation request.
    await _publisher.PublishAsync(new PubsubMessage()
    {
        Data = ByteString.CopyFromUtf8(SourceText)
    });
    

TranslateUI サービスのデプロイ

  • BackgroundProcessing ディレクトリで、PowerShell スクリプトを実行してサービスをビルドし、Cloud Run にデプロイします。

    ./PublishTo-CloudRun.ps1

PublishTo-CloudRun.ps1 スクリプトについて

PublishTo-CloudRun.ps1 スクリプトは、アプリを Cloud Run に公開します。

このスクリプトは次のことを行います。

  1. dotnet publish を使用してローカルでアプリをビルドします。
  2. Cloud Build を使用して、アプリを実行するコンテナをビルドします。
  3. アプリを Cloud Run にデプロイします。

    # 1. Build the application locally.
    dotnet publish -c Release
    # 2. Use Google Cloud Build to build the UI's container and publish to Google
    # Container Registry. 
    gcloud builds submit --tag gcr.io/$projectId/translate-ui `
        TranslateUI/bin/Release/netcoreapp2.1/publish
    
    # 3. Run the container with Google Cloud Run.
    gcloud beta run deploy translate-ui --region $region --platform managed `
        --image gcr.io/$projectId/translate-ui --allow-unauthenticated
    
    

アプリをテストする

PublishTo-CloudRun.ps1 スクリプトが正常に実行されたら、翻訳のリクエストを試行します。

  1. PublishTo-CloudRun.ps1 スクリプトの最後のコマンドは、UI サービスの URL を通知してくれます。ターミナル ウィンドウで、TranslateUI サービスの URL を見つけます。

    gcloud beta run services describe translate-ui --region $region --format="get(status.address.hostname)"
  2. ブラウザで、前のステップで見つけた URL に移動します。

    翻訳に関する空のリストと新しい翻訳をリクエストするためのフォームを掲載したページがあります。

  3. [Text to translate] フィールドに、翻訳するテキスト(Hello, World. など)を入力します。

  4. [送信] をクリック

  5. ページを更新するには、[更新] をクリックします。翻訳リストに新しい行が追加されます。翻訳が表示されない場合は、数秒待ってから再度試してください。それでも翻訳が表示されない場合は、アプリのデバッグに関する次のセクションをご覧ください。

アプリのデバッグ

Cloud Run サービスに接続できない場合、または新しい翻訳が表示されない場合は、次の点を確認します。

  • PublishTo-CloudRun.ps1 スクリプトが正常に完了し、エラーが出力されていないことを確認してください。エラーがあった場合(たとえば、message=Build failed)、エラーを修正して、もう一度実行してください。

  • ログのエラーを確認します。

    1. コンソールで、[Cloud Run] ページに移動します。 Google Cloud

      [Cloud Run] ページに移動

    2. サービス名 translate-ui をクリックします。

    3. [ログ] をクリックします。

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするには、リソースを含むプロジェクトを削除するか、プロジェクトを維持して個々のリソースを削除します。

プロジェクトを削除する Google Cloud

  1. コンソールで [**リソースの管理**] ページに移動します。 Google Cloud

    [リソースの管理] に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、 [Shut down] をクリックしてプロジェクトを削除します。

Cloud Run サービスの削除

  • このチュートリアルで作成した Cloud Run サービスを削除します。

    gcloud beta run services delete --region=$region translate-ui
    gcloud beta run services delete --region=$region translate-worker

次のステップ