Cloud Pub/Subのpushリクエストを認証したい

複数のアプリケーションを開発していると、色んなサービス間で連携をしたいことは良くあります。

方法は色々あるかと思いますが、Pub/Subを使うと比較的疎結合性を保ちながら連携ができるため、自分はPub/Subを用いた連携の仕組みを組むことが多いです。

ただし、Pub/Subが送りつけるリクエストは特に認証情報を持っていないため、api(Subscriber)側は来たリクエストが本当にPub/Subから来てるのかチェックするにはメッセージに認証情報を含めるしかありませんでした。

最近Pub/Subで認証を設定できるようになっていたようなので、設定方法について共有したいと思います。

なお、Subscriberには

- Google App Engine(Standard/Flexible Environment)
- Cloud Functions
- Cloud Run

等が設定できますが、今回はCloud Runを対象としています。

Cloud Runは認証周りの設定が少ない上、Cloud Run自体がデプロイが早くて簡単なためオススメです。
残りについては時間があれば調査して追記したいと思います。

# Cloud Run

まずはSubscriber側です。

## 準備するもの

- 指定ポートでweb serverが立ち上がるDockerイメージ

だけ。お手軽ですね。

## 設定

まずはコンソール上で作成するときの設定を見てみます。

<img width="1280" alt="スクリーンショット 2019-08-15 14.55.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/c138008b-6a74-16a9-622d-7175a2a3db53.png">
<img width="1275" alt="スクリーンショット 2019-08-15 14.55.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/5e79501f-ddb3-c985-6155-119b68a0a9e8.png">

- ソース
    - 準備したイメージをどこかGCPから見れる場所に置きます。
    - DockerHubならそのパス、privateなものはGCPのContainer Registry上に置いてそのパス(`gcr.io/$PROJECT_ID/$IMAGE_NAME`)を指定します。
- デプロイメント プラットフォーム
    - 簡単にいくなら`フルマネージド`で
    - ロケーションは2019年8月現在4箇所ほど選べるようになっています。気づいたら`asia-northeast1`が設定できるようになっていたので、日本向けサービスならこちらで。
- サービスの設定
    - 名前は一意なものに
- 認証
    - 有効にするとリクエストが全部認証を必要とします。
    - 通常の公開サービスであれば未認証を許可にしておきます。
    - 今回の用途ではPub/Subかどうか認証したいので有効にします。
- オプション設定
    - ここは好きな値で

あとは作成すればCloud Runが立ち上がります。

通常コンソールでは気にしないと思いますが、作成にはrun.adminのIAM権限が必要です。
Cloud BuildなどCIツールでデプロイする際には、IAMの確認をしておくとよいです。

イメージのサイズにも依るかもしれませんが、同じことができるGAE(FE)に比べても3,4倍くらい早い印象です。

<img width="1123" alt="スクリーンショット 2019-08-15 15.13.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/34e7067a-26bd-120a-13d4-7ecaa9ab8bb0.png">

作成するとhttpsのURLが発行されます。
ドメインのところは`service名-{ランダムっぽい文字列}-{region短縮}.a.run.app`のような感じになるみたいです。

# IAM

サブスクリプションを作る前に、2つの権限の付与をやっておきます。

(1) 認証を有効化するためにGCP内部のPub/Sub用サービスアカウントに対して、`サービスアカウントトークン作成者`権限を付与します。

GCP内部のPub/Sub用のサービスアカウントは`service-{PROJECT_ID}@gcp-sa-pubsub.iam.gserviceaccount.com`という名前になっていますが、内部アカウントということでIAMの画面では検索しても出てきません。

<img width="1279" alt="gcp-iam.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/9622be93-7986-a4d1-8bef-18985d58259f.png">

IAMの追加を試してみると、上記のフォーマットだときちんと認識してくれます。
逆にドメインが違うと断られてしまいます。

正しいアカウントを記載した後、「サービス　アカウント　トークン作成者」を設定します。

<img width="736" alt="gcp-iam3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/aa9dc22e-b82f-1339-ec81-8b13f551096a.png">

(2) 続いてPub/SubからCloud Runを操作する用のIAMアカウントを作ります。

こちらはサービスアカウントの追加の方で行います。

名前は好きなもので良くて、権限の方に「Cloud Run 起動元」を追加しておきます。

# Pub/Sub

Pub/Sub側ではトピックを作りつつ、サブスクリプションを先程作ったCloud Runと紐付けます。

トピックの作成はすごい簡単で、一意な名前を入れるだけで作れるので端折ります。

## サブスクリプション

さきほど作ったトピックに対してサブスクリプションを作成します。

- サブスクリプション名
    - 好きな名前で
- 配信タイプ
    - push
    - エンドポイントURL
        - Cloud Runで発行したもの。パスがあるならそれも指定
    - 認証
        - 有効に
        - サービスアカウントにはさっきIAMの(2)の方で作ったアカウントを設定

<img width="923" alt="gcp-sub.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/43dd86da-e5e6-4c4d-e299-474aac0848fd.png">

# リクエストどうなるか

設定が完了して本当に認証が効いてるのか確認してみます。

何も考えずにURL直で叩いてみます。

```shell

[~]: curl https://xxxxxxxxxxxxxxxxxxx.a.run.app # ちゃんとしたURLを設定

<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>403 Forbidden</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Forbidden</h1>
<h2>Your client does not have permission to get URL <code>/</code> from this server.</h2>
<h2></h2>
</body></html>
```

403返ってきました。
Cloud Run側のログを見てもauthenticationに失敗したことが見て取れます。

<img width="917" alt="curl_log.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/5abe35ed-5b71-b6d3-738d-9d7610129bd8.png">

Pub/Subのtopic経由で送ると、認証が通って処理がCloud Runに渡ってきました。

<img width="990" alt="success.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/2116ec84-609f-66d5-7b2d-e5c1990f3082.png">

# おわりに

ということでPub/Subの認証の設定方法の共有でした。
とりあえずCloud Runを使えば簡単に認証が設定できるので、認証で迷った場合はPub/Sub+Cloud Runの組み合わせを試してみるといいかもしれません。

他の認証はJWTのチェックなどが必要だったりするようなので、また確認して共有してみたいと思います。