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

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

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

アーキテクチャの図

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

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

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

目標

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

料金

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

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。新しい Google Cloud ユーザーは無料トライアルをご利用いただけます。

このチュートリアルを終了した後、作成したリソースを削除すると、それ以上の請求は発生しません。詳しくは、クリーンアップをご覧ください。

始める前に

  1. Google アカウントにログインします。

    Google アカウントをまだお持ちでない場合は、新しいアカウントを登録します。

  2. Cloud Console のプロジェクト セレクタページで、Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタのページに移動

  3. Google Cloud プロジェクトに対して課金が有効になっていることを確認します。プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  4. Firestore, Cloud Run, Pub/Sub, and Cloud Translation API を有効にします。

    API を有効にする

  5. Cloud SDK をインストールして初期化します。
  6. gcloud コンポーネントを更新します。
    gcloud components update
  7. 開発環境を準備します。

    .NET 開発環境の設定

アプリの準備

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

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

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

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

    cd getting-started-dotnet/BackgroundProcessing

TranslateWorker サービスについて

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

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    
  • 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. Google Cloud Console で、[Cloud Run] ページに移動します。

      [Cloud Run] ページに移動

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

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

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud Platform アカウントに課金されないようにする手順は次のとおりです。

Cloud プロジェクトの削除

  1. Cloud Console で [リソースの管理] ページに移動します。

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

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

Cloud Run サービスの削除

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

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

次のステップ