ベスパリブ

ベスパもってないです。バイク買いました。

Cloud FunctionsのSendGridのチュートリアルの備忘録

GCP(Google Cloud Platform)のCloud FunctionsのSendGridのチュートリアルをしました。

cloud.google.com

ちなみにCloud Functionsのチュートリアルのリンク↓

チュートリアル  |  Cloud Functions  |  Google Cloud

チュートリアルになかなか手こずったので備忘録です。

以下の章はチュートリアルの章のタイトルと同じです。

始める前に

書いてあるとおりにします。

データの流れを可視化

読みます。なるほど。

アプリケーションの準備

書いてあるとおりに進めます。 SendGridのAPIキーは一度しか表示されないので、どこかにコピペしておきます。

https://[YOUR_USERNAME]:[YOUR_PASSWORD]@[YOUR_REGION].[YOUR_PROJECT_ID].cloudfunctions.net/sendgridWebhook
  • [YOUR_USERNAME]と[YOUR_PASSWORD]は任意のユーザ名とパスワードです。適当にわかりやすいのにしてよいです。

  • [YOUR_PROJECT_ID]はCloud プロジェクトIDで、GCPのホームの「ダッシュボード」メニューの「プロジェクト情報」に記載されている「プロジェクトID」です。

  • [YOUR_REGION]は関数がデプロイされる領域らしいです。どう選べば良いのかわかりませんが、GCEのVMインスタンスと同じリージョンにしました。リージョンの確認は、GCEのページに行ってVMインスタンスの詳細に「ゾーン」の項目があるのですが、それを見ればわかります。ゾーンが「us-central1-c」なら、リージョンは「us-central1」です。

注意としては、「SELECT ACTIONS」の項目にチェックを入れないと、SendGridがメールを送信するときにWebhookイベントが発生しません。これチュートリアルに記述がまんま抜けているので注意(2019/01/11現在)。選択するACTIONはおそらく「Processed」と「Delivered」だけで良いが、チュートリアルなので「ALL」で良い。

f:id:takeg:20190111111250p:plain
SELECT ACTIONSの有効化も忘れずにする

こっちの日本語ドキュメントのほうが正確に書いてあります。

Event Webhookでイベントを受信する - ドキュメント | SendGrid

そのほかは書いてあるとおりに進めます。

コードを理解する

コードの説明がざっくり書いてあります。

関数のデプロイ

ここも書いてあるとおりに進めます。

$ gcloud beta functions deploy sendgridEmail --trigger-http
$ gcloud beta functions deploy sendgridWebhook --trigger-http
$ gcloud beta functions deploy sendgridLoad --trigger-bucket [YOUR_EVENT_BUCKET_NAME]

[YOUR_STAGING_BUCKET_NAME]とか書いてありますが、別に使いません。何かの間違いでしょう。

メールを送信

$ curl -X POST "https://[YOUR_REGION].[YOUR_PROJECT_ID].cloudfunctions.net/sendgridEmail?sg_key=[YOUR_SENDGRID_KEY]" --data '{"to":"[YOUR_SENDER_ADDR]","from":"[YOUR_RECIPIENT_ADDR]","subject":"Hello from Sendgrid!","body":"Hello World!"}' --header "Content-Type: application/json"
  • [YOUR_SENDER_ADDR]は送信者のアドレスです。よって"from"の方です(チュートリアルが間違っている)。こっちにSendGridアカウントのメールアドレスを書きます。

  • [YOUR_RECIPIENT_ADDR]は受信者のアドレスです。メールの受信を確認できるアドレスを入れます。

たとえば次のような感じになります。

$ curl -X POST "https://us-central1.my_project/sendgridEmail?sg_key=hogehogehogehogehoge" --data '{"to":"to@example.com","from":"from@example.com","subject":"Hello from Sendgrid!","body":"Hello World!"}' --header "Content-Type: application/json"

これを実行すると、以下のエラーが発生しました。

curl: (51) SSL: no alternative certificate subject name matches target host name 'us-central1.my_project.cloudfunctions.net'

このエラーメッセージでググると、Stack Overflowがヒットします。

api - Fix CURL (51) SSL error: no alternative certificate subject name matches - Stack Overflow

-kオプションでSSL証明書の検証をオフにできるそうです。とりあえずそうしてみます。

$ curl -kX POST "https://us-central1.my_project/sendgridEmail?sg_key=hogehogehogehogehoge" --data '{"to":"to@example.com","from":"from@example.com","subject":"Hello from Sendgrid!","body":"Hello World!"}' --header "Content-Type: application/json"

これを実行すると、今度は以下の結果が返ってきました。

<!DOCTYPE html>
<html lang=en>
  <meta charset=utf-8>
  <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  <title>Error 404 (Page not found)!!1</title>
  ...

Error 404!! ページが存在しないと言われました。なんで?

結果から言うと、URLのタイポでした。タイポには気をつけましょう。

Cloud FunctionsのHTTPトリガーのURLの確認は、Cloud Functionsのページに行けばデプロイした関数がリストされているので該当の関数をクリックし(今回はsendgridEmail)、「トリガー」メニューの画面で確認することができます。

f:id:takeg:20190110190658p:plain
sendgridEmail関数のURLの確認

さて、タイポを修正して再度実行すると、今度はSendGridからのメールを受信することができました。やったね。

Cloud Functionsの実行ログを見てみます。

$ gcloud beta functions logs read --limit 100

....
D      sendgridEmail  vra---gkc     2019-01-10 18:09:22.766  Function execution started
I      sendgridEmail  vra---gkc     2019-01-10 18:09:22.773  Sending email to: to@example.com
I      sendgridEmail  vra---gkc     2019-01-10 18:09:23.120  Email sent to: to@example.com
D      sendgridEmail  vra---gkc     2019-01-10 18:09:23.122  Function execution took 356 ms, finished with status code: 202

sendgridEmailが実行されたあと、SendGridがメールを送信してくれます。そのときにSendGrid側のWebhookイベントが同時に発生し、 https://[YOUR_USERNAME]:[YOUR_PASSWORD]@[YOUR_REGION].[YOUR_PROJECT_ID].cloudfunctions.net/sendgridWebhook宛にPOSTを送信します。POSTを受信するとGCP側のCloud FunctionsのsendgridWebhookが実行されます。するとGoogle Cloud StrageにJSONファイルが保存されます。

f:id:takeg:20190111112837p:plain
Google Cloud StrageにJSONファイルが保存される

sendgridWebhookの実行ログは以下のようになるはずです。

D      sendgridWebhook  n64---gwu     2019-01-11 02:05:42.699  Function execution started
I      sendgridWebhook  n64---gwu     2019-01-11 02:05:42.707  Saving events to 1547172342706000-6630d5c0-d6f8-40d6-a630-9a844e4239b3.json in bucket gcf_sendgrid_tutorial_bucket
I      sendgridWebhook  n64---gwu     2019-01-11 02:05:42.999  JSON written to 1547172342706000-6630d5c0-d6f8-40d6-a630-9a844e4239b3.json
D      sendgridWebhook  n64---gwu     2019-01-11 02:05:43.001  Function execution took 302 ms, finished with status code: 200

もしログがいつまで経っても現れない場合、SendGrid側のWebhookが発生していない可能性があります。「アプリケーションの準備」の章を見直してみてください。

sendgridLoadのエラー

ここまでやったら、sendgridEmailからsendgridWebhookまでは実行が完了するのですが、sendgridLoadで以下のエラーが出ました。

D      sendgridLoad     353717043065300  2019-01-11 11:29:34.810  Function execution started
I      sendgridLoad     353717043065300  2019-01-11 11:29:35.217  Starting job for 1547206173966000-13e6adf5-271c-42c5-bac3-116d89ef3ea6.json
I      sendgridLoad     353717043065300  2019-01-11 11:29:35.218  Job failed for 1547206173966000-13e6adf5-271c-42c5-bac3-116d89ef3ea6.json
E      sendgridLoad     353717043065300  2019-01-11 11:29:35.241  TypeError: table.import is not a function
                                                                      at Promise.resolve.then.then (/user_code/index.js:340:26)
                                                                      at process._tickDomainCallback (internal/process/next_tick.js:135:7)
D      sendgridLoad     353717043065300  2019-01-11 11:29:35.251  Function execution took 442 ms, finished with status: 'error'

TypeError: table.import is not a function

tableにはimportという関数がないと言われました。

あまり良くわかっていないのですが、 @google-cloud/bigquery 2.0.5 » Class: Table  |  Node.js  |  Google Cloud を見ると、importという関数はないようです。loadの間違い?

なんか色々こねこねして最終的にできたsendgridLoadのコードが以下。自分でも何をやっているのかよくわかっていない。

// [START functions_sendgrid_load]
/**
 * Cloud Function triggered by Cloud Storage when a file is uploaded.
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data A Cloud Storage file object.
 * @param {string} event.data.bucket Name of the Cloud Storage bucket.
 * @param {string} event.data.name Name of the file.
 * @param {string} [event.data.timeDeleted] Time the file was deleted if this is a deletion event.
 * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource
 */
exports.sendgridLoad = event => {
  const file = event.data;

  if (file.resourceState === 'not_exists') {
    // This was a deletion event, we don't want to process this
    return;
  }

  return Promise.resolve()
    .then(() => {
      if (!file.bucket) {
        throw new Error(
          'Bucket not provided. Make sure you have a "bucket" property in your request'
        );
      } else if (!file.name) {
        throw new Error(
          'Filename not provided. Make sure you have a "name" property in your request'
        );
      }

      return getTable();
    })
    .then(([table]) => {
      const fileObj = storage.bucket(file.bucket).file(file.name);
      console.log(`Starting job for ${file.name}`);
      const metadata = {
        autodetect: true,
        sourceFormat: 'NEWLINE_DELIMITED_JSON',
      };
      // tableのプロパティを全て出力させる
      for(var n in table){
        console.log("table: " + n);
      }
      // Error: table.import is not function ... why?
      console.log("table has import: " + ("import" in table)); 

      //return table.import(fileObj, metadata);
      return table.load(fileObj, metadata);
    })
    //.then(([job]) => job.promise())
    .then(([job]) => {
      for(var n in job){
            // jobのプロパティを全て出力させる
        console.log("job: " + n);
      }
    })
    .then(() => console.log(`Job complete for ${file.name}`))
    .catch(err => {
      console.log(`Job failed for ${file.name}`);
      return Promise.reject(err);
    });
};
// [END functions_sendgrid_load]

以下はその実行結果

D      sendgridLoad     357398789515366  2019-01-15 04:02:31.061  Function execution started
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.245  Starting job for 1547524950091000-396529d3-8fad-4bcb-b527-f1c24a3bb25b.json
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: domain
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: _events
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: _eventsCount
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: _maxListeners
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: metadata
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: baseUrl
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: parent
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: id
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: createMethod
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: methods
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: interceptors
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: Promise
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: requestModule
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: bigQuery
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: dataset
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: createReadStream
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: getRows_
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: setMaxListeners
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: getMaxListeners
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: emit
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: addListener
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: on
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: prependListener
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: once
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: prependOnceListener
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: removeListener
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: removeAllListeners
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: listeners
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: listenerCount
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table: eventNames
I      sendgridLoad     357398789515366  2019-01-15 04:02:32.247  table has import: false
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.446  job: kind
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.447  job: etag
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.447  job: id
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.447  job: selfLink
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.447  job: jobReference
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.447  job: configuration
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.447  job: status
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.447  job: statistics
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.447  job: user_email
I      sendgridLoad     357398789515366  2019-01-15 04:02:34.447  Job complete for 1547524950091000-396529d3-8fad-4bcb-b527-f1c24a3bb25b.json
D      sendgridLoad     357398789515366  2019-01-15 04:02:34.455  Function execution took 3395 ms, finished with status: 'ok'

よくわかってないですけど、実行が正常終了したっぽいです。jobの正体がイマイチ謎ですが、

Jobs  |  BigQuery  |  Google Cloud

ということでいいのかな?

sendgridLoadが動作するとBigQueryにデータが格納されていきます。

f:id:takeg:20190115144940p:plain

ということで、とりあえずチュートリアルは終了です。