Cloud Functions for Firebase + Secret Manager でシークレット情報を扱う

この記事は [Firebase Advent Calendar 2020](https://qiita.com/advent-calendar/2020/firebase) 10日目の記事です。

先日 [Cloud Functions for Firebase + Cloud KMS でシークレット情報を扱う](https://qiita.com/hirok115/items/dca9754d015b0308a149) という記事を書きました。
今回は、今年 GA になった Secret Manager を使ってみたいと思います。

# はじめに
サービスを開発し始めると、外部サービスのシークレットキーや認証用のID/PASSWORDなど、秘匿しなければいけない情報を扱いたいケースが多々あります。
これらの情報はソース管理するべきではない情報であるため、環境変数等を利用して対応するケースも多いかと思います。
Cloud Functions for Firebase にも [環境変数](https://firebase.google.com/docs/functions/config-env?hl=ja) はありますが、シークレット情報を直接扱うことには適していません。

また、先日記載した Cloud KMS を使う方法には、いくつか難点がありました。

- Cloud KMS は鍵の管理のため、暗号化/復号化処理を自分で実装しないといけないこと。
- 鍵のローテーションに応じて古いバージョンを廃止する場合、再度暗号化が必要なこと。

[Secret Manager](https://cloud.google.com/secret-manager) では、この辺を含めて自動でやってくれる範囲が広く、エンジニアからするとより楽にシークレット情報を取り扱うことができます。

この記事では、**Secret Manger を使ってシークレット情報を Cloud Functions for Firebase から取り扱う方法**を記載します。

### Secret Manager について
[Secret Manager](https://cloud.google.com/secret-manager) とは、GCP で提供されているサービスの一つで、シークレット情報を管理するための安全で便利なストレージシステムです。
[Cloud Functionsのドキュメント](https://cloud.google.com/functions/docs/env-var?hl=ja) によると、シークレット情報の取り扱いのベストプラクティスとして [Secret Manager](https://cloud.google.com/secret-manager) が紹介されています。


# 対象読者
- Cloud Functions for Firebase を利用したことがある方
- GCP の扱いに少しでも慣れている方  
GCP コンソール の細かい操作方法は省略している箇所がありますので、GCPを触ったことない方は分かりにくいかもしれません。


# Secret Manager の設定
お試しで実践される方は、新規GCPプロジェクトを作成して頂ければと思います。

### APIの有効化
Secret Manager を使うためには、API を有効化する必要があります。
GCP コンソール → APIとサービス より、**Secret Manager API** を有効化します。

![Secret Manager API の有効化](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/d876cd6d-5ed9-32f4-6d65-5752b46cff73.png)

### シークレットの作成
実際にシークレットを作成してみましょう。
GCP コンソール → セキュリティ → シークレットマネージャー よりシークレットを作成します。
![シクレットの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/76909523-76df-0db7-051b-583b5d882500.png)

以下を入力して、シークレットを作成 しましょう。
- シークレットの名前
- シークレットの値 (ファイルを直接インポートすることも可能です。)
- リージョン (必要に応じてリージョンを設定してください。)

![シークレットの作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/dde69a63-8b42-39ea-2dad-215c7623173c.png)

ステータスが 有効 になっていれば完了です。
![シークレットのステータス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/9f590fce-eb17-d8f3-4f7d-a29b05c2524d.png)



# Cloud Functions for Firebase で復号化処理の作成
### 必要なモジュールのインストール
Functions 用の環境がない方は、firebase init 等で事前に環境を生成してください。
Secret Manager を扱うために必要なモジュールをインストールします。

```
npm i @google-cloud/secret-manager
```

Secret Manager の情報を環境変数に設定します。

```
// プロジェクトIDはシークレットを作成したGCPのプロジェクトIDを設定します。
$ firebase functions:config:set project.id="YOUR_PROJECT_ID"

// シークレットの作成の際に入力した名前を設定します。
$ firebase functions:config:set secret.name="secret-sample"

// 作成したシークレットのバージョンを設定します。
$ firebase functions:config:set secret.version="1"
```

設定した内容を確認します。

```
$ firebase functions:config:get
{
  "secret": {
    "name": "secret-sample",
    "version": "1"
  },
  "project": {
    "id": "YOUR_PROJECT_ID"
  }
}
```

実際の処理を書いていきましょう。
今回も HTTP トリガーとします。

```Node.js
import * as functions from 'firebase-functions';
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';

export const helloWorld = functions
    .region("asia-northeast1")
    .https
    .onRequest(async (request, response) => {

        // 環境変数から設定した情報を取得します。
        const projectId = functions.config().project.id;
        const secretName = functions.config().secret.name;
        const secretVersion = functions.config().secret.version;

        // 復号化します。
        const client = new SecretManagerServiceClient();
        const name = client.secretVersionPath(projectId, secretName, secretVersion);
        const [version] = await client.accessSecretVersion({
            name: name,
        });
        const secretValue = version.payload?.data?.toString();

        response.send(`Hello World : ${secretValue}`);
    });
```

デプロイして動作を確認してみましょう。

```
// デプロイ
$ firebase deploy --only functions
...

// 動作確認
$ curl https://asia-northeast1-YOUR_PROJECT_ID.cloudfunctions.net/helloWorld
Error: could not handle the request
```

Functions のログをみてみると以下のようにエラーが出ているのが確認できます。
Cloud KMS の時と同様に、IAM で権限が必要なようです。

```
Error: 7 PERMISSION_DENIED: Permission 'secretmanager.versions.access' denied for resource 'projects/YOUR_PROJECT_ID/secrets/secret-sample/versions/1'
```

# 権限の確認と設定
### Functions を実行しているサービスアカウントの確認
GCPコンソール → Cloud Functions → helloWorld → 詳細タブ を開きます。
サービスアカウントが記載されています。
YOUR_PROJECT_ID@appspot.gserviceaccount.com というサービスアカウントで実行されていることがわかります。

![サービスアカウントの確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/c8b1215a-de4b-bc16-8cf2-631202d39ec0.jpeg)


### 権限の設定
上記で確認したサービスアカウントに、権限を設定します。
GCP コンソール → セキュリティ → シークレットマネージャー より、作成したシークレットにチェックを入れ、メンバーを追加します。
![メンバーの追加](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/32ddd5d2-6019-da51-b50f-9d3db191e934.png)

以下を設定し、保存します。
- 新しいメンバーに **Functions を実行しているサービスアカウント** を入力する
- ロールに **Secret Manager のシークレットアクセサー** を設定する

![ロールの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143332/b6f539b0-0cc8-e7d8-c568-8d9000900727.png)

# シークレット情報へのアクセスの確認
権限設定ができたので、再度確認してみましょう。

```
// 動作確認
$ curl https://asia-northeast1-YOUR_PROJECT_ID.cloudfunctions.net/helloWorld
Hello World : some secret value
```

できました！

# まとめ
[Secret Manager](https://cloud.google.com/secret-manager) を利用して、Cloud Functions for Firebase からシークレット情報にアクセスする一例を書きました。
実際に運用する際には、まだまだ留意する点があるとは思いますが、[Cloud KMS](https://cloud.google.com/security-key-management) よりも簡単に扱えるのが感じられるかと思います。