はじめに
Cloud RunでgPRCのサービスを動作させます。
かつ、サービスアカウントによるアクセス制御をほどこします。
構成と検証ステップ
下図のようなフロント/バックともにCloud Runの構成とします。
フロントはHTTPS、バックはgRPCでリクエストを受け付けます。
フロント、バックの機能は以下のとおりです。
コンポーネント | 機能 |
---|---|
バック | ランダムな緯度、経度の組み合わせを返す |
フロント | 上の緯度、経度を中心とした地図を表示する (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
の表示になります。
この時点ではフロントからバックにアクセスでき、地図を表示できます。
ステップ2 バック要認証
バックを要認証とします。
ステップ1で location-provider
サービスにおいて、 allUsers
に roles/run.invokder
を付与していました。
これを取り消します。
GUIでは Require authentication
の表示になります。
フロントからバックへのアクセスは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)
}
ソースコード全体