Edited at

Google Cloud Functionsを使ってGCSに新規アップロードされたファイルを即座に取得する

More than 1 year has passed since last update.


Google Cloud Functionsとは


  • HTTPリクエスト(POST,PUT, GET, DELETE, OPTIONS)

  • GCS の状態変更

  • Cloud Pub / Sub

をトリガーにして、事前に作成しておいたスクリプトを実行できます。

(ちなみにFirebaseにも対応したCloud Functions for Firebaseでは、上記に加え以下のものもトリガー可能です。データベースへのwrite、所持ドメイン内の任意パスへのアクセス、ユーザの作成・削除、アナリティクスのイベント、モバイルアプリのクラッシュ)


環境構築


  • Cloud Functionsのインストール



    • ここを参考に。

    • gcloudからbetaのcomponents をinstall
      gcloud components update beta



  • Emulatorインストール



    • ここを参考に。

    • nodeはnodebrewとかで入れたほうがいいかも。

    • Emulatorのインストールが完了したら、consoleでwhich functionsできることを確認。



  • 注意




簡単なサンプルをローカルで動かす


  • HTTP Trriger


    • 以下のようなjsを用意し、コマンドを実行




helloWorld.js

exports.helloWorld = (req, res) => res.send("Hello, World!");


functions start

functions deploy helloWorld --trigger-http
functions call helloWorld


  • EmulatorでホストされているFunctionsを確認

functions list


  • GCS Trriger


    • 以下のようなjsを用意し、コマンドを実行




index.js

exports.index = function (event, callback) {

const file = event.data;
if (file.resourceState === 'not_exists') {
console.log('File ' + file.name + ' not_exists.');
} else {
if (file.metageneration === '1') {
// この判定を入れることで、ファイルの新規作成なのかそれ以外(変名、削除)なのかの
// 判別が可能です。
console.log('File + ' + file.name + ' created.');
// ファイルが作成されたらやりたい処理 //
} else {
console.log('File + ' + file.name + ' metadata updated.');
}
}
callback();
};

functions start

functions deploy index --trigger-bucket=my-bucket-project.appspot.com
functions call index --data='{"name":"upload.zip", "bucket":"my-bucket-project.appspot.com", "metageneration": "1"}'

ちなみに、GCSのBucket内にディレクトリ構造を持たせている場合、

以下のようにして指定すれば再現可能です。

functions call index --data='{"name":"functions_files/upload.zip", "bucket":"my-bucket-project.appspot.com", "metageneration": "1"}'

なお、Deployのtargetはindex.js内でexportsしているmodule名称で指定します。

index.js内に複数のexportしても、それぞれDeployすれば問題ありません。

逆にindex.js以外の名称のjsを読み込むことはできなさそうです。

実行結果のログは functions logs read で確認できます。


GoogleCloudStrage, CloudFunctions, サーバーサイドAPIの連携


まずはStorageの準備

GCSに適当なバケットを作成し、そこに適当なファイルを配置。

以下はサンプル。


  • GCP Project


    • sample-project



  • バケット


    • artifacts.sample-project.appspot.com



  • 階層


    • functions_files /


      • test.zip






Cloud Functionsでhookされるjs


  • このjsでは、バックエンドサーバーへPOSTリクエストを行います。

  • パラメータとして、ファイル名とバケット名を渡します。


index.js

var request = require('request');

exports.index = function (event, callback) {
const file = event.data;

if (file.resourceState === 'not_exists') {
console.log('File ' + file.name + ' not_exists.');
callback();
return
} else {
if (file.metageneration === '1') {
var data = {
'file_name': file.name,
'bucket_name': file.bucket
};

var options = {
url: 'http://localhost:8000',
method: 'POST',
headers: {'Content-Type':'application/json'},
json: data
};

request(options, function(err, response, body) {
if (response && response.statusCode === 200 && body) {
console.log('backend request success.');
} else {
console.log('backend error.');
}
callback();
});
} else {
console.log('File + ' + file.name + ' metadata updated.');
callback();
return
}
}
};



バックエンドAPI


  • バックエンドAPIでは、localhost:8000 にてPythonサーバーを起動して、パラメータで指定されたファイルをGCSから取得してローカルサーバーに保存します。


sample.py

# -*- coding: utf-8 -*-

import json
from bottle import request, HTTPResponse
from google.cloud import storage

file_types = {
'zip' : 'application/zip'
}

class Sample():
@classmethod
def main(cls):
try:
file_name = request.json.get('file_name')
bucket_name = request.json.get('bucket_name')

if '.' in file_name:
file_ext = file_name.split('.')[len(file_name.split('.')) - 1]
else:
file_ext = ''

result, validated_result = get_validated_file(file_name, bucket_name, file_ext)

if result:
body = { 'result': 'ok' }
else:
body = { 'result': validated_result }

# レスポンスを返却する
response = HTTPResponse(status=200, body=json.dumps(body))
response.set_header('Content-Type', 'application/json')
return response

def get_file(file_name, bucket_name):
"""GCSからファイルを取得
"""

storage_client = storage.Client(project='sample-project')
bucket = storage_client.get_bucket(bucket_name)
return bucket.get_blob(file_name)

def is_accept_type(file, file_ext):
"""想定ファイルかどうかチェック
"""

if file_ext not in file_types:
return False
else:
return file.content_type == file_types[file_ext]

def get_validated_file(file_name, bucket_name, file_ext):
"""対象ファイルのバリデーション
:return: バリデーションの成否, バリデーション結果(成功ならファイルパス)
"""

file = get_file(file_name, bucket_name)

if not file or not file.exists():
return False, 'is not exist'

if not is_accept_type(file, file_ext):
return False, 'content_type'

outfile_name = '/hoge/hoge/text.zip'
# GCSのBlobオブジェクトをローカルのファイルオブジェクトとして保存
file.download_to_filename(outfile_name)

return True, outfile_name



CloudFunctions Emulatorで試す

functions deploy index --trigger-bucket=artifacts.sample-project.appspot.com

functions call index --data='{"name":"functions_files/test.zip", "bucket":"artifacts.sample-project.appspot.com", "metageneration": "1"}'

あらかじめ localhost:8000 でPythonサーバーを起動しておき、上記コマンドでfunctionsをローカルから叩いてPythonへリクエストが通れば成功。

Python側で指定ファイルがGCSに存在するかなどのチェックを行っているため、無い場合はちゃんとエラーが返ります。

これで動作確認が出来れば、GCSにファイル配置をトリガーにしてマスタインポートなどの処理が可能となります。


GCP上にCloudFunctionsをデプロイする


  • index.js があるディレクトリで以下のコマンドを実行すればDeploy可能です。

gcloud beta functions deploy index --stage-bucket test-stage-bucket --trigger-bucket=artifacts.sample-project.appspot.com

※GCFはバケットに展開されるため、stage-bucket はあらかじめ作成しておくこと。

上記にてDeploy完了したら、以後GCSをトリガーにしてFunctionsが実行されます。