LoginSignup
3
0

More than 1 year has passed since last update.

GCP, Cloud RunでgRPCのサービスを動かす & サービスアカウントによりアクセス制御する

Posted at

はじめに

Cloud RunでgPRCのサービスを動作させます。
かつ、サービスアカウントによるアクセス制御をほどこします。

構成と検証ステップ

下図のようなフロント/バックともにCloud Runの構成とします。
フロントはHTTPS、バックはgRPCでリクエストを受け付けます。

figure.png

フロント、バックの機能は以下のとおりです。

コンポーネント 機能
バック ランダムな緯度、経度の組み合わせを返す
フロント 上の緯度、経度を中心とした地図を表示する (Google Maps使用)

以下のステップで検証を進めていきます。

ステップ 構成 フロントからバックへのアクセス可否
1 フロント、バックともに認証不要 OK
2 バックを要認証とする NG
3 サービスアカウントにロール付与、フロントからバックへのアクセスにトークン付与 OK

ステップ1 サービス構築~アクセス確認

バックの実装

以下のようなprotoファイルを用いました。

syntax = "proto3";

option go_package = "github.com/hsmtkk/qiita-cloud-run-grpc-acl/proto";

package proto;

message LocationRequest {}

message LocationResponse {
    int32 longitude = 1;
    int32 latitude = 2;
}

service LocationService {
    rpc GetLocation(LocationRequest) returns (LocationResponse){}
}

PORT環境変数で指定のポートにバインドすることを除けば、実装は普通のgRPCサーバーと同じです。

フロントの実装

私はechoフレームワークを使用しましたが、HTTPサーバーとして動作すれば実装は問いません。
gRPCでバックを呼び出し、緯度と経度を取得します。
Google Maps地図をiframeで埋め込む方法はこちらを参考にしました。

デプロイ

ステップ1では認証不要でCloud Runを構成します。
TerraformでCloud Runを認証不要とする設定例です。

私はCDK for Terraformを使用し、以下のようになりました。

    const cloudRunNoAuth = new google.dataGoogleIamPolicy.DataGoogleIamPolicy(this, 'cloudRunNoAuth', {
      binding: [{
        role: 'roles/run.invoker',
        members: ['allUsers'],
      }],
    });

    const locationProvider = new google.cloudRunV2Service.CloudRunV2Service(this, 'locationProvider', {
      ingress: 'INGRESS_TRAFFIC_ALL',
      location: region,
      name: 'location-provider',
      template: {
        containers: [{
          image: 'us-central1-docker.pkg.dev/qiita-cloud-run-grpc-acl/docker-registry/location-provider:latest',
        }],        
        scaling: {
          minInstanceCount: 0,
          maxInstanceCount: 1,
        },
        serviceAccount: locationProviderSA.email,
      },
    });

    new google.cloudRunServiceIamPolicy.CloudRunServiceIamPolicy(this, 'locationProviderNoAuth', {
      location: region,
      policyData: cloudRunNoAuth.policyData,
      service: locationProvider.name,
    });

gcloud CLIでデプロイする場合には --allow-unauthenticated オプションを付与します。

gcloud run deploy location-provider --allow-unauthenticated (以下略)

GUIでは Allow unauthenticated の表示になります。

cloud-run-noauth.png

この時点ではフロントからバックにアクセスでき、地図を表示できます。

map-example.png

ステップ2 バック要認証

バックを要認証とします。
ステップ1で location-provider サービスにおいて、 allUsersroles/run.invokder を付与していました。
これを取り消します。

GUIでは Require authentication の表示になります。

cloud-run-auth.png

フロントからバックへのアクセスはHTTP 403のエラーコードで失敗します。

rpc error: code = PermissionDenied desc = unexpected HTTP status code received from server: 403 (Forbidden); transport: received unexpected content-type "text/html; charset=UTF-8"

ステップ3 サービスアカウントにロール付与、フロントからバックへのアクセスにトークン付与

サービスアカウントにロール付与

フロントのCloud Runを実行するサービスアカウントに、バックのCloud Runに対する roles/run.invoker ロールを付与します。

CDK for Terraformのコードは以下のとおりでした。

    new google.cloudRunServiceIamMember.CloudRunServiceIamMember(this, 'mapRenderSACanInvokeLocationProvider', {
      location: region,
      member: `serviceAccount:${mapRenderSA.email}`,
      role: 'roles/run.invoker',
      service: locationProvider.name,
    });

gcloud CLIで設定する場合には、以下のようなコマンドになるかと思います。

gcloud run services add-iam-policy-binding SERVICE_NAME --member=seviceAccount:foo@bar.com --role=roles/run.invoker

フロントからバックへのアクセスにトークン付与

認証付き gRPC リクエストの送信を参考に、gRPCリクエストにトークンを付与します。
NewTokenSource() の引数にはアクセス先Cloud RunのURLを指定します。

func (h *handler) Handle(ectx echo.Context) error {
	ctx := ectx.Request().Context()

    // 以下、認証トークン付与の処理
	tokenSource, err := idtoken.NewTokenSource(ctx, h.locationProviderURI)
	if err != nil {
		return fmt.Errorf("failed to init token source; %w", err)
	}
	token, err := tokenSource.Token()
	if err != nil {
		return fmt.Errorf("failed to get token; %w", err)
	}
	ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token.AccessToken)
    // 以上

	resp, err := h.locationClient.GetLocation(ctx, &proto.LocationRequest{})
	if err != nil {
		return fmt.Errorf("gRPC request failed; %w", err)
	}
	longitude := resp.GetLongitude()
	latitude := resp.GetLatitude()
	html := fmt.Sprintf(htmlTemplate, h.googleMAPAPIKey, longitude, latitude)
	return ectx.HTML(http.StatusOK, html)
}

ソースコード全体

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0