Cloud Run サービスのセキュリティ保護のチュートリアル


このチュートリアルでは、Cloud Run で実行されるセキュアな 2 つのサービス アプリケーションを作成する方法について説明します。このアプリケーションは、誰でもマークダウン テキストを作成できる一般公開の「フロントエンド」サービス、およびマークダウン テキストを HTML にレンダリングする限定公開の「バックエンド」サービスを含む、Markdown エディタです。

フロントエンドの「Editor」からバックエンドの「Renderer」へのリクエスト フローを示す図。
「Renderer」バックエンドは限定公開のサービスです。これにより、複数言語のライブラリ間の変更をトラッキングすることなく、組織全体に対してテキスト変換の標準を保証できます。

このバックエンド サービスは、Cloud Run 組み込みの IAM ベースのサービス間認証機能を使用した限定公開です。これにより、サービスを呼び出すことができるユーザーが限定されます。どちらのサービスも、最小権限の原則に基づいて構築されており、必要な場合を除いて Google Cloud のその他のサービスにはアクセスできません。

このチュートリアルの制限事項または目標でないこと

  • このチュートリアルでは、Identity Platform または Firebase Authentication を使用してユーザー ID を生成し、手動でユーザー認証を行うエンドユーザー認証については説明しません。エンドユーザー認証の詳細については、Cloud Run チュートリアルのエンドユーザー認証をご覧ください。

  • IAM ベースの認証と ID トークンのメソッドを組み合わせることについては、サポートされていないためこのチュートリアルでは説明しません。

目標

  • サービス間の認証用および Google Cloud のその他のサービスへのアクセス用に最小権限を持つ、専用のサービス アカウントを作成する。
  • 対話する 2 つのサービスを作成およびビルドし、Cloud Run にデプロイする。
  • 一般公開と限定公開の Cloud Run サービス間でリクエストを送信する。

費用

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

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

始める前に

  1. Google Cloud アカウントにログインします。Google Cloud を初めて使用する場合は、アカウントを作成して、実際のシナリオでの Google プロダクトのパフォーマンスを評価してください。新規のお客様には、ワークロードの実行、テスト、デプロイができる無料クレジット $300 分を差し上げます。
  2. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

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

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

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

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

  5. Google Cloud プロジェクトで課金が有効になっていることを確認します

  6. Cloud Run API を有効にします。

    API を有効にする

  7. gcloud CLI をインストールして初期化します
  8. サービスを試用するために、curl をインストールします。

必要なロール

チュートリアルを完了するために必要な権限を取得するには、プロジェクトに対して次の IAM ロールを付与するよう管理者に依頼してください。

ロールの付与の詳細については、アクセスの管理をご覧ください。

必要な権限は、カスタムロールや他の事前定義ロールから取得することもできます。

gcloud のデフォルトを設定する

Cloud Run サービスを gcloud のデフォルトに構成するには:

  1. デフォルト プロジェクトを設定します。

    gcloud config set project PROJECT_ID

    PROJECT_ID は、このチュートリアルで作成したプロジェクトの名前に置き換えます。

  2. 選択したリージョン向けに gcloud を構成します。

    gcloud config set run/region REGION

    REGION は、任意のサポートされている Cloud Run のリージョンに置き換えます。

Cloud Run のロケーション

Cloud Run はリージョナルです。つまり、Cloud Run サービスを実行するインフラストラクチャは特定のリージョンに配置され、そのリージョン内のすべてのゾーンで冗長的に利用できるように Google によって管理されます。

レイテンシ、可用性、耐久性の要件を満たしていることが、Cloud Run サービスを実行するリージョンを選択する際の主な判断材料になります。一般的には、ユーザーに最も近いリージョンを選択できますが、Cloud Run サービスで使用されている他の Google Cloud サービスのロケーションも考慮する必要があります。使用する Google Cloud サービスが複数のロケーションにまたがっていると、サービスの料金だけでなくレイテンシにも影響することがあります。

Cloud Run は、次のリージョンで利用できます。

ティア 1 料金を適用

  • asia-east1(台湾)
  • asia-northeast1(東京)
  • asia-northeast2(大阪)
  • europe-north1(フィンランド) リーフアイコン 低 CO2
  • europe-southwest1(マドリッド)
  • europe-west1(ベルギー) リーフアイコン 低 CO2
  • europe-west4(オランダ)
  • europe-west8(ミラノ)
  • europe-west9(パリ) リーフアイコン 低 CO2
  • me-west1(テルアビブ)
  • us-central1(アイオワ) リーフアイコン 低 CO2
  • us-east1(サウスカロライナ)
  • us-east4(北バージニア)
  • us-east5(コロンバス)
  • us-south1(ダラス)
  • us-west1(オレゴン) リーフアイコン 低 CO2

ティア 2 料金を適用

  • africa-south1(ヨハネスブルグ)
  • asia-east2(香港)
  • asia-northeast3(ソウル、韓国)
  • asia-southeast1(シンガポール)
  • asia-southeast2 (ジャカルタ)
  • asia-south1(ムンバイ、インド)
  • asia-south2(デリー、インド)
  • australia-southeast1(シドニー)
  • australia-southeast2(メルボルン)
  • europe-central2(ワルシャワ、ポーランド)
  • europe-west10(ベルリン)
  • europe-west12(トリノ)
  • europe-west2(ロンドン、イギリス) リーフアイコン 低 CO2
  • europe-west3(フランクフルト、ドイツ) リーフアイコン 低 CO2
  • europe-west6(チューリッヒ、スイス) リーフアイコン 低 CO2
  • me-central1(ドーハ)
  • me-central2(ダンマーム)
  • northamerica-northeast1(モントリオール) リーフアイコン 低 CO2
  • northamerica-northeast2(トロント) リーフアイコン 低 CO2
  • southamerica-east1(サンパウロ、ブラジル) リーフアイコン 低 CO2
  • southamerica-west1(サンティアゴ、チリ) リーフアイコン 低 CO2
  • us-west2(ロサンゼルス)
  • us-west3(ソルトレイクシティ)
  • us-west4(ラスベガス)

Cloud Run サービスをすでに作成している場合は、Google Cloud コンソールの Cloud Run ダッシュボードにリージョンが表示されます。

コードサンプルを取得する

使用するコードサンプルを取得するには:

  1. Cloud Shell またはローカルマシンにサンプルアプリ リポジトリのクローンを作成します。

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

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

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

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

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

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

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

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

    C#

    git clone https://github.com/GoogleCloudPlatform/dotnet-docs-samples.git

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

  2. Cloud Run のサンプルコードが含まれているディレクトリに移動します。

    Node.js

    cd nodejs-docs-samples/run/markdown-preview/

    Python

    cd python-docs-samples/run/markdown-preview/

    Go

    cd golang-samples/run/markdown-preview/

    Java

    cd java-docs-samples/run/markdown-preview/

    C#

    cd dotnet-docs-samples/run/markdown-preview/

限定公開の Markdown レンダリング サービスを確認する

フロントエンドの観点から、Markdown サービスの簡単な API 仕様があります。

  • / に 1 つのエンドポイントがある
  • POST リクエストの受信を想定する
  • POST リクエストの本文が、Markdown テキストである

セキュリティ上の問題については、すべてのコードを確認するか、または ./renderer/ ディレクトリで詳細を確認してください。このチュートリアルでは、Markdown 変換コードについては説明しません。

限定公開の Markdown レンダリング サービスを配布する

コードを配布するには、Cloud Build でビルドし、Container Registry にアップロードしてから、Cloud Run にデプロイします。

  1. renderer ディレクトリに移動します。

    Node.js

    cd renderer/

    Python

    cd renderer/

    Go

    cd renderer/

    Java

    cd renderer/

    C#

    cd Samples.Run.MarkdownPreview.Renderer/

  2. 次のコマンドを実行してコンテナをビルドし、Container Registry に公開します。

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/renderer

    PROJECT_ID は Google Cloud プロジェクト ID、renderer はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/renderer

    PROJECT_ID は Google Cloud プロジェクト ID、renderer はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/renderer

    PROJECT_ID は Google Cloud プロジェクト ID、renderer はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Java

    このサンプルでは、Jib を使用して一般的な Java ツールにより Docker イメージをビルドします。Jib は、Dockerfile や Docker をインストールせずにコンテナのビルドを最適化します。Jib を使用して Java コンテナを構築する方法の詳細を確認します。

    1. Docker を承認して Container Registry に push するには、gcloud 認証ヘルパーを使用します。

      gcloud auth configure-docker

    2. Jib Maven プラグインを使用して、コンテナをビルドして Container Registry に push します。

      mvn compile jib:build -Dimage=gcr.io/PROJECT_ID/renderer

    PROJECT_ID は Google Cloud プロジェクト ID、renderer はサービスに付ける名前です。

    ビルドが成功すると、BUILD SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    C#

    gcloud builds submit --tag gcr.io/PROJECT_ID/renderer

    PROJECT_ID は Google Cloud プロジェクト ID、renderer はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

  3. アクセスが制限された限定公開のサービスとしてデプロイします。

    Cloud Run では、すぐに使用できる機能としてアクセス制御サービス ID があります。アクセス制御では、ユーザーやその他のサービスによるサービスの呼び出しを制限する認証レイヤが提供されます。サービス ID によって、権限が制限された専用のサービス アカウントを作成することにより、その他の Google Cloud リソースへのアクセスが制限できます。

    1. レンダリング サービスの「コンピューティング ID」として機能する、サービス アカウントを作成します。このアカウントには、デフォルトでは、プロジェクト メンバーシップ以外の権限は付与されません。

      コマンドライン

      gcloud iam service-accounts create renderer-identity

      Terraform

      Terraform 構成を適用または削除する方法については、基本的な Terraform コマンドをご覧ください。

      resource "google_service_account" "renderer" {
        provider     = google-beta
        account_id   = "renderer-identity"
        display_name = "Service identity of the Renderer (Backend) service."
      }

      Markdown レンダリング サービスは、Google Cloud 内のその他のサービスとは直接には統合されません。それ以上の権限は必要ありません。

    2. renderer-identity サービス アカウントを使用してデプロイします。未認証のアクセスは拒否します。

      コマンドライン

      gcloud run deploy renderer \
      --image gcr.io/PROJECT_ID/renderer \
      --service-account renderer-identity \
      --no-allow-unauthenticated

      このサービス アカウントが同じプロジェクトに含まれている場合は、Cloud Run において、短い形式のサービス アカウント名を完全なメールアドレスの代わりに使用できます。

      Terraform

      Terraform 構成を適用または削除する方法については、基本的な Terraform コマンドをご覧ください。

      resource "google_cloud_run_v2_service" "renderer" {
        provider = google-beta
        name     = "renderer"
        location = "us-central1"
        template {
          containers {
            # Replace with the URL of your Secure Services > Renderer image.
            #   gcr.io/<PROJECT_ID>/renderer
            image = "us-docker.pkg.dev/cloudrun/container/hello"
          }
          service_account = google_service_account.renderer.email
        }
      }

限定公開の Markdown レンダリング サービスを試用する

限定公開のサービスは、ウェブブラウザから直接読み込むことはできません。その代わりに、curl を使用するか、または Authorization ヘッダーを挿入できる同様の HTTP リクエスト CLI ツールを使用します。

このサービスに太字のテキストを送信し、それによりマークダウン アスタリスクが HTML の <strong> タグに変換されるのを確認するには、次の手順を行います。

  1. デプロイ出力から URL を取得します。

  2. gcloud を使用して、特別な開発専用の ID トークンを取得します。

    TOKEN=$(gcloud auth print-identity-token)
  3. 未加工の Markdown テキストを、URL の生成から除外されたクエリ文字列のパラメータとして渡す、curl リクエストを作成します。

    curl -H "Authorization: Bearer $TOKEN" \
       -H 'Content-Type: text/plain' \
       -d '**Hello Bold Text**' \
       SERVICE_URL
  4. レスポンスは HTML スニペットである必要があります。

     <strong>Hello Bold Text</strong>
    

エディタ サービスとレンダリング サービスの統合を確認する

エディタ サービスは、単純なテキスト入力 UI と HTML プレビューを表示する空間を提供します。続行する前に、./editor/ ディレクトリを開いて、先ほど取得したコードを確認してください。

次に、以下のセクションで 2 つのサービスをセキュアに統合するコードを確認します。

Node.js

render.js モジュールは、限定公開の Renderer サービスへの認証済みリクエストを作成します。Cloud Run 環境の Google Cloud メタデータ サーバーを使用して、ID トークンを作成し、その ID トークンを Authorization ヘッダーの一部として HTTP リクエストに追加します。

他の環境では、render.jsアプリケーションのデフォルト認証情報を使用して Google のサーバーからトークンをリクエストします。

const {GoogleAuth} = require('google-auth-library');
const got = require('got');
const auth = new GoogleAuth();

let client, serviceUrl;

// renderRequest creates a new HTTP request with IAM ID Token credential.
// This token is automatically handled by private Cloud Run (fully managed) and Cloud Functions.
const renderRequest = async markdown => {
  if (!process.env.EDITOR_UPSTREAM_RENDER_URL)
    throw Error('EDITOR_UPSTREAM_RENDER_URL needs to be set.');
  serviceUrl = process.env.EDITOR_UPSTREAM_RENDER_URL;

  // Build the request to the Renderer receiving service.
  const serviceRequestOptions = {
    method: 'POST',
    headers: {
      'Content-Type': 'text/plain',
    },
    body: markdown,
    timeout: 3000,
  };

  try {
    // Create a Google Auth client with the Renderer service url as the target audience.
    if (!client) client = await auth.getIdTokenClient(serviceUrl);
    // Fetch the client request headers and add them to the service request headers.
    // The client request headers include an ID token that authenticates the request.
    const clientHeaders = await client.getRequestHeaders();
    serviceRequestOptions.headers['Authorization'] =
      clientHeaders['Authorization'];
  } catch (err) {
    throw Error('could not create an identity token: ' + err.message);
  }

  try {
    // serviceResponse converts the Markdown plaintext to HTML.
    const serviceResponse = await got(serviceUrl, serviceRequestOptions);
    return serviceResponse.body;
  } catch (err) {
    throw Error('request to rendering service failed: ' + err.message);
  }
};

JSON のマークダウンを解析し、HTML に変換する Renderer サービスに送信します。

app.post('/render', async (req, res) => {
  try {
    const markdown = req.body.data;
    const response = await renderRequest(markdown);
    res.status(200).send(response);
  } catch (err) {
    console.error('Error rendering markdown:', err);
    res.status(500).send(err);
  }
});

Python

new_request メソッドは、限定公開サービスに対する認証済みリクエストを作成します。Cloud Run 環境の Google Cloud メタデータ サーバーを使用して、ID トークンを作成し、その ID トークンを Authorization ヘッダーの一部として HTTP リクエストに追加します。

他の環境では、new_request は、アプリケーションのデフォルト認証情報で認証することで Google のサーバーから ID トークンをリクエストします。

import os
import urllib

import google.auth.transport.requests
import google.oauth2.id_token

def new_request(data):
    """Creates a new HTTP request with IAM ID Token credential.

    This token is automatically handled by private Cloud Run and Cloud Functions.

    Args:
        data: data for the authenticated request

    Returns:
        The response from the HTTP request
    """
    url = os.environ.get("EDITOR_UPSTREAM_RENDER_URL")
    if not url:
        raise Exception("EDITOR_UPSTREAM_RENDER_URL missing")

    req = urllib.request.Request(url, data=data.encode())
    auth_req = google.auth.transport.requests.Request()
    target_audience = url

    id_token = google.oauth2.id_token.fetch_id_token(auth_req, target_audience)
    req.add_header("Authorization", f"Bearer {id_token}")

    response = urllib.request.urlopen(req)
    return response.read()

JSON のマークダウンを解析し、HTML に変換する Renderer サービスに送信します。

@app.route("/render", methods=["POST"])
def render_handler():
    """Parse the markdown from JSON and send it to the Renderer service to be
    transformed into HTML.
    """
    body = request.get_json(silent=True)
    if not body:
        return "Error rendering markdown: Invalid JSON", 400

    data = body["data"]
    try:
        parsed_markdown = render.new_request(data)
        return parsed_markdown, 200
    except Exception as err:
        return f"Error rendering markdown: {err}", 500

Go

RenderService は、限定公開サービスに対する認証済みリクエストを作成します。Cloud Run 環境の Google Cloud メタデータ サーバーを使用して、ID トークンを作成し、その ID トークンを Authorization ヘッダーの一部として HTTP リクエストに追加します。

他の環境では、RenderService は、アプリケーションのデフォルト認証情報で認証することで Google のサーバーから ID トークンをリクエストします。

import (
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"net/http"
	"time"

	"golang.org/x/oauth2"
	"google.golang.org/api/idtoken"
)

// RenderService represents our upstream render service.
type RenderService struct {
	// URL is the render service address.
	URL string
	// tokenSource provides an identity token for requests to the Render Service.
	tokenSource oauth2.TokenSource
}

// NewRequest creates a new HTTP request to the Render service.
// If authentication is enabled, an Identity Token is created and added.
func (s *RenderService) NewRequest(method string) (*http.Request, error) {
	req, err := http.NewRequest(method, s.URL, nil)
	if err != nil {
		return nil, fmt.Errorf("http.NewRequest: %w", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Create a TokenSource if none exists.
	if s.tokenSource == nil {
		s.tokenSource, err = idtoken.NewTokenSource(ctx, s.URL)
		if err != nil {
			return nil, fmt.Errorf("idtoken.NewTokenSource: %w", err)
		}
	}

	// Retrieve an identity token. Will reuse tokens until refresh needed.
	token, err := s.tokenSource.Token()
	if err != nil {
		return nil, fmt.Errorf("TokenSource.Token: %w", err)
	}
	token.SetAuthHeader(req)

	return req, nil
}

変換するマークダウン テキストを HTML に追加した後、リクエストが Renderer サービスに送信されます。レンダリング機能と通信の問題を切り分けるため、レスポンス エラーが処理されます。


var renderClient = &http.Client{Timeout: 30 * time.Second}

// Render converts the Markdown plaintext to HTML.
func (s *RenderService) Render(in []byte) ([]byte, error) {
	req, err := s.NewRequest(http.MethodPost)
	if err != nil {
		return nil, fmt.Errorf("RenderService.NewRequest: %w", err)
	}
	req.Body = ioutil.NopCloser(bytes.NewReader(in))
	defer req.Body.Close()

	resp, err := renderClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("http.Client.Do: %w", err)
	}

	out, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("ioutil.ReadAll: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return out, fmt.Errorf("http.Client.Do: %s (%d): request not OK", http.StatusText(resp.StatusCode), resp.StatusCode)
	}

	return out, nil
}

Java

makeAuthenticatedRequest は、限定公開サービスに対する認証済みリクエストを作成します。Cloud Run 環境の Google Cloud メタデータ サーバーを使用して、ID トークンを作成し、その ID トークンを Authorization ヘッダーの一部として HTTP リクエストに追加します。

他の環境では、makeAuthenticatedRequest は、アプリケーションのデフォルト認証情報で認証することで Google のサーバーから ID トークンをリクエストします。

// makeAuthenticatedRequest creates a new HTTP request authenticated by a JSON Web Tokens (JWT)
// retrievd from Application Default Credentials.
public String makeAuthenticatedRequest(String url, String markdown) {
  String html = "";
  try {
    // Retrieve Application Default Credentials
    GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
    IdTokenCredentials tokenCredentials =
        IdTokenCredentials.newBuilder()
            .setIdTokenProvider((IdTokenProvider) credentials)
            .setTargetAudience(url)
            .build();

    // Create an ID token
    String token = tokenCredentials.refreshAccessToken().getTokenValue();
    // Instantiate HTTP request
    MediaType contentType = MediaType.get("text/plain; charset=utf-8");
    okhttp3.RequestBody body = okhttp3.RequestBody.create(markdown, contentType);
    Request request =
        new Request.Builder()
            .url(url)
            .addHeader("Authorization", "Bearer " + token)
            .post(body)
            .build();

    Response response = ok.newCall(request).execute();
    html = response.body().string();
  } catch (IOException e) {
    logger.error("Unable to get rendered data", e);
  }
  return html;
}

JSON のマークダウンを解析し、HTML に変換する Renderer サービスに送信します。

// '/render' expects a JSON body payload with a 'data' property holding plain text
// for rendering.
@PostMapping(value = "/render", consumes = "application/json")
public String render(@RequestBody Data data) {
  String markdown = data.getData();

  String url = System.getenv("EDITOR_UPSTREAM_RENDER_URL");
  if (url == null) {
    String msg =
        "No configuration for upstream render service: "
            + "add EDITOR_UPSTREAM_RENDER_URL environment variable";
    logger.error(msg);
    throw new IllegalStateException(msg);
  }

  String html = makeAuthenticatedRequest(url, markdown);
  return html;
}

C#

GetAuthenticatedPostResponse は、限定公開サービスに対する認証済みリクエストを作成します。Cloud Run 環境の Google Cloud メタデータ サーバーを使用して、ID トークンを作成し、その ID トークンを Authorization ヘッダーの一部として HTTP リクエストに追加します。

他の環境では、GetAuthenticatedPostResponse は、アプリケーションのデフォルト認証情報で認証することで Google のサーバーから ID トークンをリクエストします。

private async Task<string> GetAuthenticatedPostResponse(string url, string postBody)
{
    // Get the OIDC access token from the service account via Application Default Credentials
    GoogleCredential credential = await GoogleCredential.GetApplicationDefaultAsync();
    OidcToken token = await credential.GetOidcTokenAsync(OidcTokenOptions.FromTargetAudience(url));
    string accessToken = await token.GetAccessTokenAsync();

    // Create request to the upstream service with the generated OAuth access token in the Authorization header
    var upstreamRequest = new HttpRequestMessage(HttpMethod.Post, url);
    upstreamRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    upstreamRequest.Content = new StringContent(postBody);

    var upstreamResponse = await _httpClient.SendAsync(upstreamRequest);
    upstreamResponse.EnsureSuccessStatusCode();

    return await upstreamResponse.Content.ReadAsStringAsync();
}

JSON のマークダウンを解析し、HTML に変換する Renderer サービスに送信します。

public async Task<IActionResult> Index([FromBody] RenderModel model)
{
    var markdown = model.Data ?? string.Empty;
    var renderedHtml = await GetAuthenticatedPostResponse(_editorUpstreamRenderUrl, markdown);
    return Content(renderedHtml);
}

一般公開のエディタ サービスを配布する

コードをビルドしてデプロイするには、次の手順を行います。

  1. editor ディレクトリに移動します。

    Node.js

    cd ../editor

    Python

    cd ../editor

    Go

    cd ../editor

    Java

    cd ../editor

    C#

    cd ../Samples.Run.MarkdownPreview.Editor/

  2. 次のコマンドを実行してコンテナをビルドし、Container Registry に公開します。

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/editor

    PROJECT_ID は Google Cloud プロジェクト ID、editor はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/editor

    PROJECT_ID は Google Cloud プロジェクト ID、editor はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/editor

    PROJECT_ID は Google Cloud プロジェクト ID、editor はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Java

    このサンプルでは、Jib を使用して一般的な Java ツールにより Docker イメージをビルドします。Jib は、Dockerfile や Docker をインストールせずにコンテナのビルドを最適化します。Jib を使用して Java コンテナを構築する方法の詳細を確認します。

    mvn compile jib:build -Dimage=gcr.io/PROJECT_ID/editor

    PROJECT_ID は Google Cloud プロジェクト ID、editor はサービスに付ける名前です。

    ビルドが成功すると、BUILD SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    C#

    gcloud builds submit --tag gcr.io/PROJECT_ID/editor

    PROJECT_ID は Google Cloud プロジェクト ID、editor はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

  3. レンダリング サービスへの特別なアクセス権を持つ、限定公開のサービスとしてデプロイします。

    1. レンダリング サービスの「コンピューティング ID」として機能する、サービス アカウントを作成します。このアカウントには、デフォルトでは、プロジェクト メンバーシップ以外の権限は付与されません。

      コマンドライン

      gcloud iam service-accounts create editor-identity

      Terraform

      Terraform 構成を適用または削除する方法については、基本的な Terraform コマンドをご覧ください。

      resource "google_service_account" "editor" {
        provider     = google-beta
        account_id   = "editor-identity"
        display_name = "Service identity of the Editor (Frontend) service."
      }

      エディタ サービスは、Markdown レンダリング サービス以外の Google Cloud 内の要素とやり取りする必要はありません。

    2. editor-identity コンピューティング ID へのアクセス権を付与して、Markdown レンダリング サービスを呼び出します。このコンピューティング ID を使用するすべてのサービスに、この権限が付与されています。

      コマンドライン

      gcloud run services add-iam-policy-binding renderer \
      --member serviceAccount:editor-identity@PROJECT_ID.iam.gserviceaccount.com \
      --role roles/run.invoker

      Terraform

      Terraform 構成を適用または削除する方法については、基本的な Terraform コマンドをご覧ください。

      resource "google_cloud_run_service_iam_member" "editor_invokes_renderer" {
        provider = google-beta
        location = google_cloud_run_v2_service.renderer.location
        service  = google_cloud_run_v2_service.renderer.name
        role     = "roles/run.invoker"
        member   = "serviceAccount:${google_service_account.editor.email}"
      }

      レンダリング サービスのコンテキスト内で起動元ロールが付与されるため、このレンダリング サービスは、エディタから呼び出せる唯一の限定公開 Cloud Run サービスになります。

    3. editor-identity サービス アカウントでデプロイし、一般公開の未認証アクセスを許可します。

      コマンドライン

      gcloud run deploy editor --image gcr.io/PROJECT_ID/editor \
      --service-account editor-identity \
      --set-env-vars EDITOR_UPSTREAM_RENDER_URL=RENDERER_SERVICE_URL \
      --allow-unauthenticated

      次のように置き換えます。

      • PROJECT_ID はプロジェクト ID に置き換えます。
      • RENDERER_SERVICE_URL は、Markdown レンダリング サービスのデプロイ後に提供された URL に置き換えます。

      Terraform

      Terraform 構成を適用または削除する方法については、基本的な Terraform コマンドをご覧ください。

      エディタ サービスをデプロイします。

      resource "google_cloud_run_v2_service" "editor" {
        provider = google-beta
        name     = "editor"
        location = "us-central1"
        template {
          containers {
            # Replace with the URL of your Secure Services > Editor image.
            #   gcr.io/<PROJECT_ID>/editor
            image = "us-docker.pkg.dev/cloudrun/container/hello"
            env {
              name  = "EDITOR_UPSTREAM_RENDER_URL"
              value = google_cloud_run_v2_service.renderer.uri
            }
          }
          service_account = google_service_account.editor.email
      
        }
      }

      サービスを呼び出すための allUsers 権限を付与します。

      data "google_iam_policy" "noauth" {
        provider = google-beta
        binding {
          role = "roles/run.invoker"
          members = [
            "allUsers",
          ]
        }
      }
      
      resource "google_cloud_run_service_iam_policy" "noauth" {
        provider = google-beta
        location = google_cloud_run_v2_service.editor.location
        project  = google_cloud_run_v2_service.editor.project
        service  = google_cloud_run_v2_service.editor.name
      
        policy_data = data.google_iam_policy.noauth.policy_data
      }

HTTPS トラフィックについて

これらのサービスを使用したマークダウンのレンダリングに関する HTTP リクエストは 3 つあります。

ユーザーからエディタへのリクエスト フロー、メタデータ サーバーからトークンを取得するエディタ、レンダリング サービスへのリクエストを行うエディタ、HTML をエディタに返すレンダリング サービスを示す図。
editor-identity を含むフロントエンド サービスがレンダリング サービスを呼び出します。editor-identityrenderer-identity の両方に権限の制限が適用されているため、セキュリティ悪用やコード挿入を目的とするその他の Google Cloud リソースへのアクセスは制限されます。

試してみる

完全な 2 つのサービス アプリケーションを試すには、次の手順を行います。

  1. ブラウザで、前述のデプロイの手順により提供された URL に移動します。

  2. 左側のマークダウン テキストを編集してからボタンをクリックすると、右側にプレビューが表示されます。

    次のようになります。

    マークダウン エディタのユーザー インターフェースのスクリーン ショット

これらのサービスを開発し続けることにした場合、Google Cloud の他のサービスへの Identity and Access Management(IAM)アクセスが制限されます。他の多くのサービスにアクセスするには、追加の IAM ロールをこれらのサービスに与える必要があることにご注意ください。

クリーンアップ

このチュートリアル用に新規プロジェクトを作成した場合は、そのプロジェクトを削除します。既存のプロジェクトを使用し、このチュートリアルで変更を加えずに残す場合は、チュートリアル用に作成したリソースを削除します。

プロジェクトを削除する

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。

プロジェクトを削除するには:

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

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

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

チュートリアル リソースを削除する

  1. このチュートリアルでデプロイした Cloud Run サービスを削除します。

    コマンドライン

    gcloud run services delete editor
    gcloud run services delete renderer

    Cloud Run サービスは Google Cloud コンソールから削除することもできます。

  2. チュートリアルを設定したときに追加した gcloud のデフォルト構成を削除します。

     gcloud config unset run/region
    
  3. プロジェクト構成を削除します。

     gcloud config unset project
    
  4. このチュートリアルで作成した他の Google Cloud リソースを削除します。

次のステップ