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

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

# 構成と検証ステップ

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

![figure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1950110/176ca430-8671-bba1-00d4-559e3a15b392.png)

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

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


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

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

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

## バックの実装

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

```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サーバーと同じです。

https://github.com/hsmtkk/qiita-cloud-run-grpc-acl/blob/noauth/locationprovider/locationprovider.go

## フロントの実装

私は[echoフレームワーク](https://github.com/labstack/echo)を使用しましたが、HTTPサーバーとして動作すれば実装は問いません。
gRPCでバックを呼び出し、緯度と経度を取得します。
Google Maps地図をiframeで埋め込む方法は[こちら](https://developers.google.com/maps/documentation/embed/embedding-map?hl=ja#view_mode)を参考にしました。

https://github.com/hsmtkk/qiita-cloud-run-grpc-acl/blob/noauth/maprender/maprender.go

## デプロイ

ステップ1では認証不要でCloud Runを構成します。
[TerraformでCloud Runを認証不要とする設定例](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_service#example-usage---cloud-run-service-noauth)です。

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

```typescript
    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` オプションを付与します。

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

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

![cloud-run-noauth.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1950110/43d1d5e9-793a-b1d0-218f-644da0471b02.png)

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

![map-example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1950110/2322e418-e720-3a82-cd75-ca9d38af0d52.png)

# ステップ2 バック要認証

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

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

![cloud-run-auth.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1950110/0541b50a-f92e-f6cd-26a6-e7c90d2d8837.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のコードは以下のとおりでした。

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

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

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

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

[認証付き gRPC リクエストの送信](https://cloud.google.com/run/docs/triggering/grpc?hl=ja#request-auth)を参考に、gRPCリクエストにトークンを付与します。
`NewTokenSource()` の引数にはアクセス先Cloud RunのURLを指定します。

```go
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)
}
```

# ソースコード全体

https://github.com/hsmtkk/qiita-cloud-run-grpc-acl