Cloud Functions の関数の作成

Cloud Functions の関数は、Node.jsPythonGoJava.NET プログラミング言語で作成できます。この関数は言語固有のランタイムで実行されます。Cloud Functions の実行環境は、選択したランタイムによって異なります。ランタイム環境の詳細については、各ランタイムの概要ページをご覧ください。

Cloud Functions の関数のタイプ

Cloud Functions には、HTTP 関数とバックグラウンド関数の 2 つのタイプがあります。

HTTP 関数

標準的な HTTP リクエストから HTTP 関数を実行します。この HTTP リクエストはレスポンスを待ち、GET、PUT、POST、DELETE、OPTIONS など、通常の HTTP リクエスト メソッドを処理します。Cloud Functions を使用する際は、TLS 証明書が自動的にプロビジョニングされるため、すべての HTTP 関数を、セキュアな接続を使用して呼び出すことができます。

詳しくは、HTTP 関数の作成をご覧ください。

例:

Node.js

const escapeHtml = require('escape-html');

/**
 * HTTP Cloud Function.
 *
 * @param {Object} req Cloud Function request context.
 *                     More info: https://expressjs.com/en/api.html#req
 * @param {Object} res Cloud Function response context.
 *                     More info: https://expressjs.com/en/api.html#res
 */
exports.helloHttp = (req, res) => {
  res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`);
};

Python

from flask import escape

def hello_http(request):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response>.
    """
    request_json = request.get_json(silent=True)
    request_args = request.args

    if request_json and 'name' in request_json:
        name = request_json['name']
    elif request_args and 'name' in request_args:
        name = request_args['name']
    else:
        name = 'World'
    return 'Hello {}!'.format(escape(name))

Go


// Package helloworld provides a set of Cloud Functions samples.
package helloworld

import (
	"encoding/json"
	"fmt"
	"html"
	"net/http"
)

// HelloHTTP is an HTTP Cloud Function with a request parameter.
func HelloHTTP(w http.ResponseWriter, r *http.Request) {
	var d struct {
		Name string `json:"name"`
	}
	if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
		fmt.Fprint(w, "Hello, World!")
		return
	}
	if d.Name == "" {
		fmt.Fprint(w, "Hello, World!")
		return
	}
	fmt.Fprintf(w, "Hello, %s!", html.EscapeString(d.Name))
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.logging.Logger;

public class HelloHttp implements HttpFunction {
  private static final Logger logger = Logger.getLogger(HelloHttp.class.getName());

  private static final Gson gson = new Gson();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Check URL parameters for "name" field
    // "world" is the default value
    String name = request.getFirstQueryParameter("name").orElse("world");

    // Parse JSON request and check for "name" field
    try {
      JsonElement requestParsed = gson.fromJson(request.getReader(), JsonElement.class);
      JsonObject requestJson = null;

      if (requestParsed != null && requestParsed.isJsonObject()) {
        requestJson = requestParsed.getAsJsonObject();
      }

      if (requestJson != null && requestJson.has("name")) {
        name = requestJson.get("name").getAsString();
      }
    } catch (JsonParseException e) {
      logger.severe("Error parsing JSON: " + e.getMessage());
    }

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Hello %s!", name);
  }
}

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

namespace HelloHttp
{
    public class Function : IHttpFunction
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public async Task HandleAsync(HttpContext context)
        {
            HttpRequest request = context.Request;
            // Check URL parameters for "name" field
            // "world" is the default value
            string name = ((string) request.Query["name"]) ?? "world";

            // If there's a body, parse it as JSON and check for "name" field.
            using TextReader reader = new StreamReader(request.Body);
            string text = await reader.ReadToEndAsync();
            if (text.Length > 0)
            {
                try
                {
                    JsonElement json = JsonSerializer.Deserialize<JsonElement>(text);
                    if (json.TryGetProperty("name", out JsonElement nameElement) &&
                        nameElement.ValueKind == JsonValueKind.String)
                    {
                        name = nameElement.GetString();
                    }
                }
                catch (JsonException parseException)
                {
                    _logger.LogError(parseException, "Error parsing JSON request");
                }
            }

            await context.Response.WriteAsync($"Hello {name}!");
        }
    }
}

バックグラウンド関数

バックグラウンド関数を使用すると、Pub/Sub トピックのメッセージや Cloud Storage バケットの変更など、Cloud インフラストラクチャのイベントを処理できます。

詳しくは、バックグラウンド関数の作成をご覧ください。

例:

Node.js

/**
 * Background Cloud Function to be triggered by Pub/Sub.
 * This function is exported by index.js, and executed when
 * the trigger topic receives a message.
 *
 * @param {object} message The Pub/Sub message.
 * @param {object} context The event metadata.
 */
exports.helloPubSub = (message, context) => {
  const name = message.data
    ? Buffer.from(message.data, 'base64').toString()
    : 'World';

  console.log(`Hello, ${name}!`);
};

Python

def hello_pubsub(event, context):
    """Background Cloud Function to be triggered by Pub/Sub.
    Args:
         event (dict):  The dictionary with data specific to this type of
         event. The `data` field contains the PubsubMessage message. The
         `attributes` field will contain custom attributes if there are any.
         context (google.cloud.functions.Context): The Cloud Functions event
         metadata. The `event_id` field contains the Pub/Sub message ID. The
         `timestamp` field contains the publish time.
    """
    import base64

    print("""This Function was triggered by messageId {} published at {}
    """.format(context.event_id, context.timestamp))

    if 'data' in event:
        name = base64.b64decode(event['data']).decode('utf-8')
    else:
        name = 'World'
    print('Hello {}!'.format(name))

Go


// Package helloworld provides a set of Cloud Functions samples.
package helloworld

import (
	"context"
	"log"
)

// PubSubMessage is the payload of a Pub/Sub event.
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// HelloPubSub consumes a Pub/Sub message.
func HelloPubSub(ctx context.Context, m PubSubMessage) error {
	name := string(m.Data) // Automatically decoded from base64.
	if name == "" {
		name = "World"
	}
	log.Printf("Hello, %s!", name)
	return nil
}

Java


import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import functions.eventpojos.PubSubMessage;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;

public class HelloPubSub implements BackgroundFunction<PubSubMessage> {
  private static final Logger logger = Logger.getLogger(HelloPubSub.class.getName());

  @Override
  public void accept(PubSubMessage message, Context context) {
    String name = "world";
    if (message != null && message.getData() != null) {
      name = new String(
          Base64.getDecoder().decode(message.getData().getBytes(StandardCharsets.UTF_8)),
          StandardCharsets.UTF_8);
    }
    logger.info(String.format("Hello %s!", name));
    return;
  }
}

C#

using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.PubSub.V1;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;

namespace HelloPubSub
{
    public class Function : ICloudEventFunction<MessagePublishedData>
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public Task HandleAsync(CloudEvent cloudEvent, MessagePublishedData data, CancellationToken cancellationToken)
        {
            string nameFromMessage = data.Message?.TextData;
            string name = string.IsNullOrEmpty(nameFromMessage) ? "world" : nameFromMessage;
            _logger.LogInformation("Hello {name}", name);
            return Task.CompletedTask;
        }
    }
}

ソースコードの構造化

Cloud Functions で関数の定義を見つけられるよう、ランタイムごとに、ソースコードの構造に対する要件があります。

Node.js

Node.js ランタイムでは、Node.js モジュールから関数のソースコードをエクスポートする必要があります。これにより、Cloud Functions は require() 呼び出しで読み込みを行います。読み込むモジュールを決めるために、Cloud Functions は package.jsonファイルの main フィールドを使用します。たとえば、main フィールドが指定されていない場合、Cloud Functions は index.js からコードを読み込みます。

たとえば、ソースコードでは次の構成が有効です。

  • 1 つ以上の関数をエクスポートする、関数のルート ディレクトリにある 1 つの index.js

    .
    └── index.js
    
  • コードを foo.js ファイルからインポートし、1 つ以上の関数をエクスポートする index.js ファイル。

    .
    ├── index.js
    └── foo.js
    
  • "main": "app.js" を含む package.json で 1 つ以上の関数を書き出す app.js

    .
    ├── app.js
    └── package.json
    

Python

Python ランタイムの場合、関数のエントリポイントは、関数のルート ディレクトリにある main.py という名前の Python ソースファイルで定義する必要があります。

たとえば、ソースコードでは次の構成が有効です。

  • 1 つ以上の関数を定義する関数のルート ディレクトリにある 1 つの main.py ファイル:

    .
    └── main.py
    
  • 依存関係を指定した requirements.txt ファイルを含む main.py ファイル:

    .
    ├── main.py
    └── requirements.txt
    
  • ローカルの依存関係からコードをインポートする main.py ファイル。

    .
    ├── main.py
    └── mylocalpackage/
        ├── __init__.py
        └── myscript.py
    

Go

Go ランタイムの場合、関数はプロジェクト ルートの Go パッケージに存在している必要があります。関数を package main に含めることはできません。サブパッケージは、Go モジュールを使用している場合にのみサポートされます。

たとえば、ソースコードでは次の構成が有効です。

  • プロジェクトのルートにあり、1 つ以上の関数をエクスポートするパッケージ。

    .
    └── function.go
    
  • プロジェクトのルートにあり、サブパッケージからコードをインポートし、1 つ以上の関数をエクスポートするパッケージ:

    .
    ├── function.go
    ├── go.mod
    └── shared/
        └── shared.go
    
  • プロジェクトのルートにあり、package main を定義するサブディレクトリを持つパッケージ。

    .
    ├── cmd/
    |   └── main.go
    └── function.go
    

Java

Java ランタイムの場合、src/main/java/ サブディレクトリと pom.xml ファイルを含む最上位の関数ディレクトリを作成する必要があります。テストは src/test/java/ サブディレクトリに配置することをおすすめします。

.
├── pom.xml
└── src/
    ├── main/
    |   └── java/
    |       └── MyFunction.java
    └── test
        └── java/
            └── MyFunctionTest.java

.java ファイルでパッケージ(functions など)が宣言されている場合、ディレクトリ階層は次のようになります。

.
├── pom.xml
└── src/
    ├── main/
    |   └── java/
    |       └── functions/
    |               └── MyFunction.java
    └── test/
        └── java/
                └── functions/
                    └── MyFunctionTest.java

ほとんどの Java 関数と同様に特定のパッケージで関数を定義する場合、デプロイ時に関数を --entry-point 値の一部として指定する必要があります。

複数の関数をグループ化する

複数の関数を 1 つのプロジェクトにグループ化する場合、すべての関数が同じ依存関係のセットを共有する結果になる可能性があります。ただし、一部の関数は共有依存関係の全部は必要としない場合があります。

各関数を、独自の src/main/java サブディレクトリと pom.xml ファイルを持つ、上記のような独自の最上位ディレクトリに配置することをおすすめします。このアプローチにより、特定の関数に必要な依存関係の数が最小限に抑えられ、関数が必要とするメモリ量が削減されます。

さらに、Functions Framework を介して関数をローカルで実行する場合、関数が分かれていれば関数を個別に指定することも簡単にできます。これは、ローカルでの開発とテストに役立ちます。

C#

.NET ランタイムでは、他の .NET ソースコードと同じようにプロジェクトを構成できます。

テンプレートを使用して C# 関数を作成すると、ファイル システムと同じレベルに次のファイルが作成されます。

  • Function.cs という関数のソースファイル。
  • 拡張子が .csproj のプロジェクト ファイル。

例:

.
├── Function.cs
└── HelloHttp.csproj

テンプレートを使用して F# 関数と Visual Basic 関数を作成する場合にも、同じパターンが適用されます。F# の場合、ファイル名は Function.fs で、プロジェクト ファイルの拡張子は .fsproj です。Visual Basic の場合、ファイル名は CloudFunction.vb、プロジェクト ファイルの拡張子は .vbproj です。

ただし、これらのパターンは単なる規則で、要件ではありません。他の標準の C#、F#、Visual Basic プロジェクトと同様に、複数のソースファイル、リソースなどを含むコードを作成できます。

また、プロジェクトごとに 1 つの関数のみを実装する場合は、関数名を指定せずにその関数のホスティングを開始できます。これにより、ローカルでの開発とデバッグが容易になります。

依存関係の指定

通常、関数の依存関係は、使用しているランタイムに基づいて指定します。詳細については、該当するページをご覧ください。

Cloud Functions の命名方法

Cloud Functions には、デプロイ時に name プロパティが設定されます。設定後は変更できません。関数の名前は識別子として使用されるため、リージョン内で一意にする必要があります。詳細については、デプロイのドキュメントをご覧ください。

次のステップ