SendGrid チュートリアルで Cloud Functions からメールを送信する

このチュートリアルでは、Cloud Functions を使って、SendGrid プラットフォームからメールを送信する方法、ウェブフック経由で SendGrid から分析用データを受信する方法、分析のためにデータを Google BigQuery で読み込ませる方法について説明します。

目標

  • SendGrid アカウントを作成する。
  • 2 つの HTTP Cloud Functions を書き込み、デプロイする。
  • 1 つの バックグラウンド Cloud ファンクションに書き込み、デプロイする。
  • SendGrid 経由でデプロイした関数からメールを送信する。
  • ウェブフックを経由して SendGrid から分析用データを受信する。
  • SendGrid にあるデータを分析のために BigQuery に読み込ませる。

料金

このチュートリアルでは、以下を含む、Cloud Platform の課金対象コンポーネントを使用しています。

  • Google Cloud Functions
  • Google BigQuery
  • Google Cloud Storage

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを作成できます。

新しい Cloud Platform ユーザーは無料トライアルをご利用いただけます。

始める前に

  1. Google アカウントにログインします。

    Google アカウントをまだお持ちでない場合は、新しいアカウントを登録します。

  2. Cloud Console のプロジェクト セレクタページで、Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタのページに移動

  3. Google Cloud プロジェクトに対して課金が有効になっていることを確認します。プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  4. Cloud Functions, Cloud Storage, and Google BigQuery API を有効にします。

    API を有効にする

  5. Cloud SDK をインストールして初期化します。
  6. gcloud コンポーネントを更新します。
    gcloud components update
  7. 開発環境を準備します。

データの流れ

SendGrid のチュートリアル アプリケーションでのデータの流れでは、次の手順が行われます。

  1. HTTP 経由で Cloud Function をトリガーすると SendGrid からメールが送信されます。
  2. SendGrid は HTTP を介して分析用データを別の Cloud Function に送信します。
  3. 分析用データは JSON(改行区切り)として Cloud Storage に保存されます。
  4. 3 番目の Cloud Function は JSON 新規ファイルによりトリガーされ、データが分析できる BigQuery に JSON ファイルが読み込まれるよう、キューに入れます。

次はこの手順を可視化した図です。

アプリケーションの準備

  1. SendGrid アカウントを作成します。SendGrid ウェブサイトから手動で行うことも、アカウントを作成して課金データを統合する Google Cloud Launcher を使って行うことも可能です。

    Cloud Launcher を使って SendGrid アカウントを作成するをご覧ください。

  2. SendGrid API キーを作成します。

    1. https://app.sendgrid.com から SendGrid アカウントにログインします。
    2. [設定] > [API キー] の順に移動します。
    3. 完全アクセス権で API キーを新規に作成します。
    4. API キーが表示されたら、これをコピーします(1 回しか表示されませんので、このチュートリアルの最後に Cloud ファンクションを呼び出せるようどこかに貼り付けておきます)。
  3. SendGrid イベント通知を作成します。

    1. https://app.sendgrid.com から SendGrid アカウントにログインします。
    2. [設定] > [メールの設定] の順に移動します。
    3. [イベント通知] タブを開きます。
    4. [編集] をクリックし、次の情報を HTTP POST URL 項目に入力します。

      Node.js 6(非推奨)

      https://YOUR_USERNAME:YOUR_PASSWORD@YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/sendgridWebhook

      ここで

      • YOUR_USERNAMEYOUR_PASSWORD は、任意のユーザー名とパスワードです。これらの 2 つの変数を次の手順で構成ファイルに入力します。
      • YOUR_REGION は関数がデプロイされるリージョンで、関数のデプロイが完了するとターミナルに表示されます。
      • YOUR_PROJECT_ID は Cloud プロジェクト ID です。 関数のデプロイが完了するとターミナルに表示されます。
    5. [すべて] を選択します。これにより、すべてのアクションが Cloud 関数に報告されます。

    6. 最後に、イベント通知機能を [オン] にします。

  4. BigQuery bq コマンドライン ツールを使用して BigQuery データセットを作成します。

    bq mk YOUR_DATASET_NAME
    

    YOUR_DATASET_NAME は、新しく作成された BigQuery データセット名です。BigQuery コンソールからデータセットを作成することもできます。

  5. JSON ファイルを保存するために Cloud Storage バケットを作成します。ここで、YOUR_EVENT_BUCKET_NAME はグローバルに固有なバケット名です。

    gsutil mb gs://YOUR_EVENT_BUCKET_NAME
    
  6. ローカルマシンにサンプルアプリのリポジトリのクローンを作成します。

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

  7. Cloud Functions のサンプルコードが含まれているディレクトリに移動します。

    Node.js 6(非推奨)

    cd nodejs-docs-samples/functions/sendgrid/

  8. アプリを構成します。

    Node.js 6(非推奨)

    config.default.json ファイルをテンプレートとして使用し、sendgrid ディレクトリに次のコンテンツを含む config.json ファイルを作成します。
    {
      "EVENT_BUCKET": "YOUR_EVENT_BUCKET_NAME",
      "DATASET": "YOUR_DATASET_NAME",
      "TABLE": "events",
      "USERNAME": "YOUR_USERNAME",
      "PASSWORD": "YOUR_PASSWORD"
    }
    • YOUR_EVENT_BUCKET_NAME は、JSON ファイルを保存する Cloud Storage バケットの名前に置き換えます。
    • YOUR_DATASET_NAME は、BigQuery データセット名に置き換えます。
    • YOUR_USERNAME は、SendGrid から受信するデータの検証に使用する任意のユーザー名に置き換えます。
    • YOUR_PASSWORD は、SendGrid から受信するデータの検証に使用する任意のパスワードに置き換えます。

コードの説明

依存関係をインポートする

アプリケーションが Google Cloud Platform サービスとやり取りするには、いくつかの依存関係をインポートする必要があります。

Node.js 6(非推奨)

const sendgrid = require('sendgrid');
const config = require('./config.json');
const uuid = require('uuid');

// Get a reference to the Cloud Storage component
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
// Get a reference to the BigQuery component
const {BigQuery} = require('@google-cloud/bigquery');
const bigquery = new BigQuery();

メール送信

次の関数は、メールの送信を行う SendGrid クライアントを作成します。

Node.js 6(非推奨)

/**
 * Returns a configured SendGrid client.
 *
 * @param {string} key Your SendGrid API key.
 * @returns {object} SendGrid client.
 */
const getClient = (key) => {
  if (!key) {
    const error = new Error(
      'SendGrid API key not provided. Make sure you have a "sg_key" property in your request querystring'
    );
    error.code = 401;
    throw error;
  }

  // Using SendGrid's Node.js Library https://github.com/sendgrid/sendgrid-nodejs
  return sendgrid(key);
};

次の関数は、メールの送信に SendGrid クライアントを使用します。

Node.js 6(非推奨)

/**
 * Send an email using SendGrid.
 *
 * Trigger this function by making a POST request with a payload to:
 * https://[YOUR_REGION].[YOUR_PROJECT_ID].cloudfunctions.net/sendEmail?sg_key=[YOUR_API_KEY]
 *
 * @example
 * curl -X POST "https://us-central1.your-project-id.cloudfunctions.net/sendEmail?sg_key=your_api_key" --data '{"to":"bob@email.com","from":"alice@email.com","subject":"Hello from Sendgrid!","body":"Hello World!"}' --header "Content-Type: application/json"
 *
 * @param {object} req Cloud Function request context.
 * @param {object} req.query The parsed querystring.
 * @param {string} req.query.sg_key Your SendGrid API key.
 * @param {object} req.body The request payload.
 * @param {string} req.body.to Email address of the recipient.
 * @param {string} req.body.from Email address of the sender.
 * @param {string} req.body.subject Email subject line.
 * @param {string} req.body.body Body of the email subject line.
 * @param {object} res Cloud Function response context.
 */
exports.sendgridEmail = async (req, res) => {
  try {
    if (req.method !== 'POST') {
      const error = new Error('Only POST requests are accepted');
      error.code = 405;
      throw error;
    }

    // Get a SendGrid client
    const client = getClient(req.query.sg_key);

    // Build the SendGrid request to send email
    const request = client.emptyRequest({
      method: 'POST',
      path: '/v3/mail/send',
      body: getPayload(req.body),
    });

    // Make the request to SendGrid's API
    console.log(`Sending email to: ${req.body.to}`);
    const response = await client.API(request);

    if (response.statusCode < 200 || response.statusCode >= 400) {
      const error = Error(response.body);
      error.code = response.statusCode;
      throw error;
    }

    console.log(`Email sent to: ${req.body.to}`);

    // Forward the response back to the requester
    res.status(response.statusCode);
    if (response.headers['content-type']) {
      res.set('content-type', response.headers['content-type']);
    }
    if (response.headers['content-length']) {
      res.set('content-length', response.headers['content-length']);
    }
    if (response.body) {
      res.send(response.body);
    } else {
      res.end();
    }

    return Promise.resolve();
  } catch (err) {
    console.error(err);
    const code =
      err.code || (err.response ? err.response.statusCode : 500) || 500;
    res.status(code).send(err);
    return Promise.reject(err);
  }
};

分析用データの受信

次の関数はユーザーが選択したユーザー名とパスワードを確認し、受信した SendGrid リクエストを認証します。

Node.js 6(非推奨)

/**
 * Verify that the webhook request came from sendgrid.
 *
 * @param {string} authorization The authorization header of the request, e.g. "Basic ZmdvOhJhcg=="
 */
const verifyWebhook = (authorization) => {
  const basicAuth = Buffer.from(
    authorization.replace('Basic ', ''),
    'base64'
  ).toString();
  const parts = basicAuth.split(':');
  if (parts[0] !== config.USERNAME || parts[1] !== config.PASSWORD) {
    const error = new Error('Invalid credentials');
    error.code = 401;
    throw error;
  }
};

次の関数は SendGrid から分析用データを受信し、JSON(改行区切り)として Cloud Storage に保存します。

Node.js 6(非推奨)

/**
 * Receive a webhook from SendGrid.
 *
 * See https://sendgrid.com/docs/API_Reference/Webhooks/event.html
 *
 * @param {object} req Cloud Function request context.
 * @param {object} res Cloud Function response context.
 */
exports.sendgridWebhook = async (req, res) => {
  try {
    if (req.method !== 'POST') {
      const error = new Error('Only POST requests are accepted');
      error.code = 405;
      throw error;
    }

    verifyWebhook(req.get('authorization') || '');

    const events = req.body || [];

    // Make sure property names in the data meet BigQuery standards
    fixNames(events);

    // Generate newline-delimited JSON
    // See https://cloud.google.com/bigquery/data-formats#json_format
    const json = events.map((event) => JSON.stringify(event)).join('\n');

    // Upload a new file to Cloud Storage if we have events to save
    if (json.length) {
      const bucketName = config.EVENT_BUCKET;
      const unixTimestamp = new Date().getTime() * 1000;
      const filename = `${unixTimestamp}-${uuid.v4()}.json`;
      const file = storage.bucket(bucketName).file(filename);

      console.log(`Saving events to ${filename} in bucket ${bucketName}`);

      await file.save(json);
      console.log(`JSON written to ${filename}`);
    }
    res.status(200).end();
  } catch (err) {
    console.error(err);
    res.status(err.code || 500).send(err);
    return Promise.reject(err);
  }
};

データの BigQuery へのインポート

最後に、次の関数は、JSON(改行区切り)データを BigQuery にインポートします。

Node.js 6(非推奨)

/**
 * 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 = async (event) => {
  const file = event.data;

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

  try {
    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'
      );
    }

    const [table] = await getTable();

    const fileObj = storage.bucket(file.bucket).file(file.name);
    console.log(`Starting job for ${file.name}`);
    const metadata = {
      autodetect: true,
      sourceFormat: 'NEWLINE_DELIMITED_JSON',
    };
    const [job] = await table.import(fileObj, metadata);

    await job.promise();
    console.log(`Job complete for ${file.name}`);
  } catch (err) {
    console.log(`Job failed for ${file.name}`);
    return Promise.reject(err);
  }
};

関数のデプロイ

  1. HTTP トリガーを使用してメール送信関数をデプロイするには、sendgrid ディレクトリで次のコマンドを実行します。

    Node.js 6(非推奨)

    /**
     * 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 = async (event) => {
      const file = event.data;
    
      if (file.resourceState === 'not_exists') {
        // This was a deletion event, we don't want to process this
        return;
      }
    
      try {
        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'
          );
        }
    
        const [table] = await getTable();
    
        const fileObj = storage.bucket(file.bucket).file(file.name);
        console.log(`Starting job for ${file.name}`);
        const metadata = {
          autodetect: true,
          sourceFormat: 'NEWLINE_DELIMITED_JSON',
        };
        const [job] = await table.import(fileObj, metadata);
    
        await job.promise();
        console.log(`Job complete for ${file.name}`);
      } catch (err) {
        console.log(`Job failed for ${file.name}`);
        return Promise.reject(err);
      }
    };

  2. HTTP トリガーを使用して SendGrid webhook 関数をデプロイするには、sendgrid ディレクトリで次のコマンドを実行します。

    Node.js 6(非推奨)

    /**
     * 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 = async (event) => {
      const file = event.data;
    
      if (file.resourceState === 'not_exists') {
        // This was a deletion event, we don't want to process this
        return;
      }
    
      try {
        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'
          );
        }
    
        const [table] = await getTable();
    
        const fileObj = storage.bucket(file.bucket).file(file.name);
        console.log(`Starting job for ${file.name}`);
        const metadata = {
          autodetect: true,
          sourceFormat: 'NEWLINE_DELIMITED_JSON',
        };
        const [job] = await table.import(fileObj, metadata);
    
        await job.promise();
        console.log(`Job complete for ${file.name}`);
      } catch (err) {
        console.log(`Job failed for ${file.name}`);
        return Promise.reject(err);
      }
    };

  3. Cloud Storage トリガーを使用して BigQuery にデータを読み込む関数をデプロイするには、sendgrid ディレクトリで次のコマンドを実行します。

    Node.js 6(非推奨)

    /**
     * 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 = async (event) => {
      const file = event.data;
    
      if (file.resourceState === 'not_exists') {
        // This was a deletion event, we don't want to process this
        return;
      }
    
      try {
        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'
          );
        }
    
        const [table] = await getTable();
    
        const fileObj = storage.bucket(file.bucket).file(file.name);
        console.log(`Starting job for ${file.name}`);
        const metadata = {
          autodetect: true,
          sourceFormat: 'NEWLINE_DELIMITED_JSON',
        };
        const [job] = await table.import(fileObj, metadata);
    
        await job.promise();
        console.log(`Job complete for ${file.name}`);
      } catch (err) {
        console.log(`Job failed for ${file.name}`);
        return Promise.reject(err);
      }
    };

    YOUR_EVENT_BUCKET_NAME は、JSON ファイルを保存する Cloud Storage バケットの名前です。

メール送信

  1. 次のコマンドでメールを送信します。

    Node.js 6(非推奨)

    curl -X POST "https://YOUR_REGION-YOUR_PROJECT_ID.cloudfunctions.net/sendgridEmail?sg_key=YOUR_SENDGRID_KEY" --data '{"to":"YOUR_RECIPIENT_ADDR","from":"YOUR_SENDER_ADDR","subject":"Hello from Sendgrid!","body":"Hello World!"}' --header "Content-Type: application/json"

    ここで

    • YOUR_REGION は関数がデプロイされるリージョンです。関数のデプロイが完了するとターミナルに表示されます。
    • YOUR_PROJECT_ID は Cloud プロジェクト ID です。関数のデプロイが完了するとターミナルに表示されます。
    • YOUR_SENDGRID_KEY は、ユーザーの SendGrid API キーです。
    • YOUR_RECIPIENT_ADDR は受信者のメールアドレスです。
    • YOUR_SENDER_ADDR は、ユーザーの SendGrid アカウントのメールアドレスです。
  2. 実行した内容が完了していることをログで確認します。

    gcloud functions logs read --limit 100
    
  3. 構成ファイルの EVENT_BUCKET 値で指定された Cloud Storage バケットに保存された JSON ファイルを確認できます。

  4. 次の URL から BigQuery にインポートした分析用データを表示できます。

    https://bigquery.cloud.google.com/table/YOUR_PROJECT_ID:YOUR_DATASET_NAME.events
    

    ここで

    • YOUR_PROJECT_ID はユーザーの Google Cloud プロジェクト ID です。
    • YOUR_DATASET_NAME は、以前に構成した BigQuery データセット名です。

クリーンアップ

このチュートリアルで使用するリソースについて、Google Cloud Platform アカウントへの課金が発生しないようにする手順は次のとおりです。

プロジェクトの削除

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。

プロジェクトを削除するには:

  1. Cloud Console で [リソースの管理] ページに移動します。

    [リソースの管理] ページに移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

Cloud Functions の削除

Cloud Functions を削除しても、Cloud Storage または BigQuery に保存されたリソースは削除されません。

Cloud ファンクションを削除するには、次のコマンドを実行します。

gcloud functions delete NAME_OF_FUNCTION

NAME_OF_FUNCTION は削除する関数の名前です。

Google Cloud Console から Cloud Functions の関数を削除することもできます。