Cloud Functions には、しばらく関数が使われないとスリープし、スリープ後、最初のリクエストの処理に時間がかかるという性質があります。関数全てを定期的に叩くことで、リクエストの処理に時間がかかってしまう問題を避けることが可能です。本記事では、Cloud Functionsにデプロイされている関数全てを定期的に叩く方法を紹介します。

なぜ定期的に function を叩く必要があるのか

Cloud Functions (以下 GCFと書きます) には、しばらく関数が使われないとスリープし、スリープ後、最初のリクエストの処理に時間がかかるという性質があります(会社のtechblogで、なぜリクエストの処理に時間がかかるのかや、この減少を緩和する方法について解説しています)。

関数が定期的に使われていればスリーブすることはなくなると考えられます。そのため、定期的に関数を叩くことで、時折レスポンスに時間がかかってしまう現象を避けることができます。

GCFにデプロイできる関数の種類

GCFにデプロイすることができる関数には大きく分けて2種類あります。1つ目が HTTP 関数、2つ目がバックグラウンド関数です。ユーザが直接利用するのは主にHTTP関数なので、今回はデプロイされている関数の中でも全てのHTTP関数を叩けることを目標にします。

HTTP 関数にも種類があり、1つ目が通常のHTTPリクエストで起動する関数、2つ目がアプリケーションから関数名を指定して起動するCallable関数です。それぞれ異なった方法で起動する必要があるので、これにも対応していきます。

実装

それでは、デプロイされている全ての関数を叩く関数の実装について説明していきます。必要な実装は次の 3 つです。

  • 関数の一覧の取得
  • 全ての関数を起こす
  • 定期実行されるようスケジューリングする

関数の一覧を取得する

デプロイされている全ての関数を叩くために、まずCloud Functions の API を用いてデプロイされている関数のリストを取得します。GCP のほとんどのプロダクトにはAPIが提供されており、APIの利用者の権限に応じて GCP のリソース状況を参照したり、操作したりすることができます。今回のように GCP のリソース状況の参照や操作が必要な処理をしたい場合には、それが可能か見てみると良いと思います(Cloud API一覧)。

今回は projects.locations.functions.list API を使用します。この API では、GCP プロジェクトとリージョンを指定することでデプロイされている関数のリストを取得することができます。

アクセストークンを取得する

Cloud API を利用するにはアクセストークンの取得が必要です。アクセストークンを発行するサービスアカウントは次のロールを持っている必要があります (ロールに cloudfunctions.functions.list の権限がついていれば良いです)。

  • Cloud Functions Viewer

アクセストークンの取得は、API clientを用いて行います。NodeJSの場合は google-api-nodejs-client です。API Clientにサービスアカウントの情報を与え、APIの利用に必要なスコープを要求し、アクセストークンを取得します。API Clientを用いてアクセストークンを取得するコードは以下のようになります。

const serviceAccount = require('./service-account.json');

async function getAccessToken() {
    const { google } = require('googleapis');
    // ref: https://developers.google.com/identity/protocols/googlescopes
    const scopes = ['https://www.googleapis.com/auth/cloudfunctions', 'https://www.googleapis.com/auth/cloud-platform'];
    const jwtClient = new google.auth.JWT(
        serviceAccount.client_email,
        undefined,
        serviceAccount.private_key,
        scopes,
        undefined
    );
    const authorization = await jwtClient.authorize();
    return authorization.access_token;
}

デプロイされている関数のリストを取得する

次に、実際にAPIを叩いて関数のリストを取得します。APIの利用には前節で取得したアクセストークンをヘッダに乗せ、リクエストを送る必要があります。APIのレスポンスの functions パラメータに関数の情報が含まれています。

デプロイされている関数のリストを取得する関数は次のようになります。

const projectId = process.env.GCP_PROJECT;
const region = 'us-central1';

async function listFunction() {
    try {
        const accessToken = await getAccessToken();
        const endpoint = `https://cloudfunctions.googleapis.com/v1/projects/${projectId}/locations/${region}/functions`;
        const option = {
            uri: endpoint,
            method: 'GET',
            headers: {
                Authorization: `Bearer ${accessToken}`
            },
            qs: {
                pageSize: 100
            },
            json: true
        };

        // ref: https://cloud.google.com/functions/docs/reference/rest/v1/projects.locations.functions/list
        const res = await rp(option);

        console.log(`res: ${JSON.stringify(res)}`);

        return res.functions;
    } catch (err) {
        console.log(`error occurred when listing function: ${JSON.stringify(err)}`);
        return err;
    }
}

関数のリストからHTTP関数を抜き出す

APIのレスポンスから 通常のHTTP関数と Callable関数 を分けて取得します。API のレスポンスには関数名やそのトリガー情報などが含まれています。この情報を用いて、HTTP関数とその他の関数を見分けます。各関数の情報は下記のような Object で得られます。

{
  "name": string,
  "entryPoint": string,
  "runtime": string,
  "timeout": string,
  "availableMemoryMb": number,
  "serviceAccountEmail": string,
  "updateTime": string,
  "versionId": string,
  "labels": {
    string: string,
    ...
  },
  "environmentVariables": {
    string: string,
    ...
  },
  "network": string,
  "maxInstances": number,

   // Union field trigger can be only one of the following:
  "httpsTrigger": {
    object(HttpsTrigger)
  },
  "eventTrigger": {
    object(EventTrigger)
  },

  // etc...
}

参考: https://cloud.google.com/functions/docs/reference/rest/v1/projects.locations.functions#CloudFunction

httpsTrigger パラメータが存在すれば HTTP 関数です。また、通常の HTTP 関数か Callable 関数かは labels の deployment-callable パラメータが “true” であるかどうかで判断できます。 (このパラメータの付け方の仕様はドキュメントには書いていないため、心配な場合は、Callable関数の関数名に Call をプレフィックスとしてつけ、関数名で見分けるなどの対応が必要です)

API のレスポンスから、通常のHTTP関数の url と Callable 関数の関数名を抽出するコードは下記のようになります。

const urls = new Array();
const callableFuncNames = new Array();

const funcs = await listFunction();
console.log(`funcs: ${JSON.stringify(funcs)}`)
funcs.forEach(func => {
    if(!func.httpsTrigger){
        console.log(`function: ${func.name} is not https function`)
        return
    }
    const url = func.httpsTrigger.url;
    console.log(`funcname: ${func.name}, deployment-callable: ${func.labels["deployment-callable"]}`);
    if(func.labels["deployment-callable"] === "true" ){
        callableFuncNames.push(url.split('/').slice(-1)[0]);
    }else {
        urls.push(url);
    }
    console.log(`function: ${func.name} is target`);
})

全ての関数を起こす

前節で取得したリストにある関数を起動していきます。通常のHTTP関数は request-promise モジュールで、 Callable関数は Firebase の SDK で起動します。

通常のHTTP関数を起動する

通常の HTTP 関数を起動します。 request-promiseの使い方についてはよく知られているので割愛します。

実装は下記のようになります。

async function callHttpFunctions(urls){
    for (const url of urls){
        try{
            const option = {
                uri: url,
                method: "POST",
                body:{
        
                },
                json: true
            };
            const res = await rp(option);
            console.log(`function: ${url} is called.`)
        }catch(err){
            console.log(`error: ${JSON.stringify(err)}, url: ${url}`);
        }
    }
}

NodeJS で Callable 関数を起動する

Firebase の SDK を使って Callable 関数を起動する方法について説明します。公式ドキュメントでも解説されていますが、GCFから関数を呼び出す場合は、ウェブアプリケーションから関数を呼ぶ場合と同様の呼び出し方になります。該当ドキュメントはこのページになります。

Firebase SDKを初期化し、 firebase.functions().httpsCallable() を使って呼び出します。Callable 関数を呼び出す httpsCallable 関数は Firebase SDKにしかありません。Admin SDKと間違えないよう注意して下さい。あとは前節の処理と同様に全ての関数を呼び出します。

実装は下記のようになります。

const firebase = require('firebase');
firebase.initializeApp({
    projectId: process.env.GCP_PROJECT
})

async function callCallableFunctions(funcNames){
    for(funcName of funcNames){
        try{
            await firebase.functions().httpsCallable(funcName)()
            console.log(`function: ${funcName} called.`)
        }catch(err){
            console.log(`error: ${JSON.stringify(err)}, funcName: ${funcName}`);
        }
    }
}

定期実行されるようスケジューリングする

最後に、上述してきた処理を 1 つの関数にまとめてデプロイし、Cloud Scheduler でスケジューリングします。Cloud Scheduler の使い方については過去記事で紹介しているので、これを参考に設定します。 (Cloud Scheduler で Firestore を定期的にバックアップする)。

上述してきた処理を 1 つの関数にまとめると次の gist のようになります。

gist: https://gist.github.com/soichisumi/765c4f6a0aa4400487efdad308149021

以上で、WarmupFunctions トピックでメッセージを受け取る毎に us-central1 へデプロイされている全ての HTTP 関数を起動する関数を実装することができました。

まとめ

Cloud Functions へ既にデプロイされている関数全てを定期的に叩く関数の実装方法を紹介しました。GCF の Cloud API を使うことで現在デプロイされている関数の情報を取得することができます。本記事では、これを用いて全ての関数を起こす関数の実装方法を紹介しました。GCF の Cold Start 対策や Cloud APIを利用した関数の実装の参考になれば幸いです。