gcp
Firebase
serverless
cloudfunctions
GoogleCloudFunctions

Cloud functions for Firebaseとは

Cloud functions for FirebaseはFirebaseの機能やHTTPS要求によってトリガされたイベントに応答して、自動的に用意したバックエンドコードを実行することができます。これらのバックエンドコードはGoogleのクラウドに保存され管理&自動実行されるので、自分でサーバー管理及び拡張する必要はありません。

現在サポートしているFirebaseの機能

  • Realtime Database Triggers
    • Writes to Realtime Database
  • Firebase Authentication Triggers
    • New user authentication
  • Google Analytics for Firebase Triggers
    • Analytics conversion events
  • Cloud Storage Triggers
    • Uploads to your storage bucket
  • HTTP Triggers
    • Incoming HTTPS requests
  • Cloud Pub/Sub Triggers

サポートしている言語

現在はnode.jsのみサポート、今後他の言語もサポートする予定

環境構築

  • nodeのインストール
terminal
brew install node
  • Firebase CLIのインストール
terminal
sudo npm install -g firebase-tools
  • 任意のフォルダを作成
terminal
mkdir functions-firebase
cd functions-firebase
  • firebase にログイン
terminal
firebase login
  • プロジェクトの初期化
terminal
firebase init
  • Functions: Configure and deploy Cloud Functionsを選択
  • GCPプロジェクトの選択
  • Do you want to install dependencies with npm now? Yesを選択

デプロイ

index.jsを開いてください。そこにHTTPリクエストのイベントに応答するサンプルコードがコメントアウトされています。そのコメントアウトを解除し、function名を"sayHelloWorld"にしてください。

index.js
const functions = require('firebase-functions');

// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions

exports.sayHelloWorld = functions.https.onRequest((request, response) => {
 response.send("Hello from Firebase!");
});
terminal
firebase deploy

デプロイが終わるとfunction URLが返ってくるので、ブラウザでアクセスしてください。

terminal
Function URL (sayHelloWorld): https://us-central1.../satHelloWorld

ブラウザに"Hello from Firebase!"が表示されればデプロイ成功です。

Firebaseコンソールにログを出してみる

index.js
const functions = require('firebase-functions');

// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions

exports.sayHelloWorld = functions.https.onRequest((request, response) => {
 console.log("Hello from Firebase!");   
 response.send("Hello from Firebase!");
});
terminal
firebase deploy

またブラウザでFunction URLにアクセスしたらFirebaseコンソールのログを見てみます。

Firebaseコンソール > functions > ログ 

"Hello from Firebase!" が表示されれば成功です。

Authentication Trigger

Authentication Triggerではユーザーが作成されたときや削除された時のイベントを取ることができます。こちらのTriggerを利用すればユーザーが会員登録をした際に自動返信メールを出すことができます。

ユーザーが作成された時

新しいユーザーが作成されたらデータベースにUserデータを作成し、デフォルトのユーザーアイコンの画像URLを挿入する。

index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase)

const ref = admin.database().ref()

//Authentication Trigger
exports.createUserAccount = functions.auth.user().onCreate(event => {
    const uid = event.data.uid
    const email =  event.data.email
    const photoUrl = event.data.photoUrl || 
    'photo_url'
    const newUserRef = ref.child(`/users/${uid}`)
    return newUserRef.set({
        photoUrl: photoUrl,
        email: email
    })
})

ユーザーが削除された時

ユーザーが削除されたら削除済みフラグ("isDeleted: true")をユーザーデータに自動的にセットさせる

index.js
exports.cleanupUserData = functions.auth.user().onDelete(event => {
    const uid = event.data.uid
    const userRef = ref.child(`/users/${uid}`)
    return userRef.update({isDeleted: true})
})

Realtime Database Trigger

Realtime Database Triggerではデータベースの書き込みなどのイベントを取得することが出来ます。

regionをJapanに書き換えするサンプル

index.js
//Realtime Database Trigger
exports.changePost = functions.database
    .ref('/posts/{pushId}')
    .onWrite(event => {
        const post = event.data.val()
        if (post.chnaged) {
            return
        }
        post.chnaged = true
        post.region = "Japan"
        return event.data.ref.set(post)
})

fibreaseコンソールで/posts/{pushId}のregionに適当な値をいれて、データベース見ましょう。自動的にJapanに置き換わります。

Cloud Storage Trigger

CloudStorageにファイルが追加された時のイベントを取得することができます。下記のサンプルコードは"/image/"ディレクトリに画像が追加された時、自動的にサムネイル画像を生成します。

下準備

package.jsonがあるディレクトリで以下のコマンドを実行し必要なライブラリを導入します。

terminal
npm install --save @google_cloud/storage
npm install --save child-process-promise

package.jsonにこれらが入っていれば成功です。

package.json
"dependencies": {
    "@google-cloud/storage": "^1.2.0",
    "child-process-promise": "^2.2.1"
}

画像が追加されたら自動的にサムネイルを生成するコード

index.js
const functions = require('firebase-functions')
const gcs = require('@google-cloud/storage')();
const spawn = require('child-process-promise').spawn

//Cloud Storage Trigger
exports.generateThumbnail = functions.storage.object()
    .onChange(event => {
        const object = event.data
        const filePath = object.name
        const fileName = filePath.split('/').pop()
        const fileBucket = object.bucket
        const bucket = gcs.bucket(fileBucket)
        const tempFilePath = `/tmp/${fileName}`

        if (fileName.startsWith('thumb_')){
            console.log('Already a Thumbnail.')
            return 
        }

        if(!object.contentType.startsWith('image/')){
            console.log('This is not an image.')
        }

        if(object.resourceState === 'not_exists'){
            console.log('This is a deletion event.')
        }

        return bucket.file(filePath).download({
            destination: tempFilePath
        }).then(() => {
            console.log('Image downloaded locally to', tempFilePath)
            return spawn('convert', [tempFilePath, '-thumbnail', '200x200>',
                tempFilePath])
        }).then(() => {
            console.log('Thumbnail created')
            const thumbFilePath = filePath.replace(/(\/)?([^\/]*)$/,
            '$1thumb_$2')

            return bucket.upload(tempFilePath, {
                destination: thumbFilePath
            })
        })
    })

さらに応用

サムネイル画像が追加されたらRealtime Databaseに画像のURLを追加します。

下準備

Firebaseコンソールの歯車アイコン>プロジェクトの設定>サービスアカウント>新しい秘密鍵の生成をクリックしjsonファイルを落としindex.jsと同じディレクトリにいれます。

画像のURLをRealtime Databaseに保存するサンプルコード

index.js
const functions = require('firebase-functions')
const gcs = require('@google-cloud/storage')({keyFilename: 
    'Firebaseコンソールでダウンロードしたjsonファイル.json'})
const spawn = require('child-process-promise').spawn
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)

//Cloud Storage Trigger
exports.generateThumbnail = functions.storage.object()
    .onChange(event => {
        const object = event.data
        const filePath = object.name
        const fileName = filePath.split('/').pop()
        const fileBucket = object.bucket
        const bucket = gcs.bucket(fileBucket)
        const tempFilePath = `/tmp/${fileName}`
        const ref = admin.database().ref()
        const file = bucket.file(filePath)
        const thumbFilePath = filePath.replace(/(\/)?([^\/]*)$/,
            '$1thumb_$2')

        if (fileName.startsWith('thumb_')){
            console.log('Already a Thumbnail.')
            return 
        }

        if(!object.contentType.startsWith('image/')){
            console.log('This is not an image.')
        }

        if(object.resourceState === 'not_exists'){
            console.log('This is a deletion event.')
        }

        return bucket.file(filePath).download({
            destination: tempFilePath
        }).then(() => {
            console.log('Image downloaded locally to', tempFilePath)
            return spawn('convert', [tempFilePath, '-thumbnail', '200x200>',
                tempFilePath])
        }).then(() => {
            console.log('Thumbnail created')

            return bucket.upload(tempFilePath, {
                destination: thumbFilePath
            })
        }).then(() => {
            const config = {
                action: 'read',
                expires: '03-09-2491'
            }
            const thumbFile = bucket.file(thumbFilePath)
            return Promise.all([
                thumbFile.getSignedUrl(config),
                file.getSignedUrl(config)
            ])
        }).then(results => {
            const thumbResult = results[0]
            const originalResult = results[1]
            const thumbFileUrl = thumbResult[0]
            const fileUrl = originalResult[0]
            return ref.child('posts').push({path: fileUrl, thumbnail: thumbFileUrl})
        })
    })