チュートリアル1: Snowpark Container Servicesサービスを作成する

紹介

共通セットアップ が完了すると、サービスを作成する準備が整います。このチュートリアルでは、入力されたテキストを単純にエコーバックするサービス(echo_service という名前)を作成します。たとえば、入力文字列が「Hello World」の場合、サービスは「I said, Hello World」を返します。

このチュートリアルには2つのパートがあります。

パート1: サービスを作成してテストする。 このチュートリアルで提供されるコードをダウンロードし、ステップバイステップの手順に従います。

  1. このチュートリアルのサービスコードをダウンロードします。

  2. Snowpark Container Services用のDockerイメージをビルドし、アカウントのリポジトリにイメージをアップロードします。

  3. サービスを作成し、サービス仕様ファイルとサービスを実行するコンピューティングプールを提供します。

  4. サービスと通信するためのサービス関数を作成します。

  5. サービスを使用します。サービスにエコーリクエストを送り、その応答を検証します。

パート2: サービスを理解する。このセクションでは、サービスコードの概要を説明し、さまざまなコンポーネントがどのように連携しているかを明らかにします。

1: サービスコードをダウンロードする

Echoサービスを作成するためのコード(Pythonアプリケーション)が提供されます。

  1. zipファイル をディレクトリにダウンロードします。

  2. ファイルを解凍します。チュートリアルごとに1つのディレクトリが含まれています。 Tutorial-1 ディレクトリには以下のファイルがあります。

    • Dockerfile

    • echo_service.py

    • templates/basic_ui.html

2: イメージをビルドしてアップロードする

Snowpark Container Servicesがサポートするlinux/amd64プラットフォーム用のイメージをビルドし、アカウントのイメージリポジトリにイメージをアップロードします(共通セットアップ を参照)。

イメージをビルドしてアップロードする前に、リポジトリに関する情報(リポジトリ URL とレジストリのホスト名)が必要です。詳細については、 レジストリおよびリポジトリ をご参照ください。

リポジトリに関する情報を取得する

  1. リポジトリ URL を取得するには、 SHOW IMAGE REPOSITORIES SQL コマンドを実行します。

    SHOW IMAGE REPOSITORIES;
    
    Copy
    • 出力の repository_url 列は、 URL を提供します。以下に例を示します。

      <orgname>-<acctname>.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository
      
    • リポジトリ URL のホスト名はレジストリのホスト名です。以下に例を示します。

      <orgname>-<acctname>.registry.snowflakecomputing.com
      

イメージをビルドし、リポジトリにアップロードする

  1. ターミナルウィンドウを開き、解凍したファイルのあるディレクトリに移動します。

  2. Dockerイメージをビルドするには、Docker CLI を使用して以下の docker build コマンドを実行します。このコマンドは、イメージのビルドに使用するファイルの PATH として、現在の作業ディレクトリ(.)を指定していることに注意してください。

    docker build --rm --platform linux/amd64 -t <repository_url>/<image_name> .
    
    Copy
    • image_name には、 my_echo_service_image:latest を使用します。

    docker build --rm --platform linux/amd64 -t myorg-myacct.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/my_echo_service_image:latest .
    
    Copy
  3. Snowflakeアカウントのリポジトリにイメージをアップロードします。Dockerがあなたの代わりにリポジトリにイメージをアップロードするには、まずSnowflakeでDockerを認証する必要があります。

    1. SnowflakeレジストリでDockerを認証するには、以下のコマンドを実行します。

      docker login <registry_hostname> -u <username>
      
      Copy
      • username には、Snowflakeのユーザー名を指定します。Dockerは、パスワードの入力を求めるプロンプトを表示します。

    2. イメージをアップロードするには、以下のコマンドを実行します。

      docker push <repository_url>/<image_name>
      
      Copy

      docker push myorg-myacct.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/my_echo_service_image:latest
      
      Copy

3: サービスを作成する

このセクションでは、サービスを作成し、サービスと通信するためのサービス関数も作成します。

サービスを作成するには、以下が必要です。

  • コンピューティングプール。Snowflakeは指定したコンピューティングプールでサービスを実行します。共通セットアップの一環としてコンピューティングプールを作成しました。

  • サービス仕様。この仕様は、サービスの構成と実行に必要な情報をSnowflakeに提供します。詳細については、 Snowpark Container Services: サービスの操作 をご参照ください。このチュートリアルでは、 CREATE SERVICE コマンドで仕様をインラインで提供します。また、チュートリアル2に示すように、仕様をSnowflakeステージのファイルに保存し、 CREATE SERVICE コマンドでファイル情報を提供することもできます。

サービス関数は、サービスと通信するために利用できる方法の1つです。サービス関数は、サービスエンドポイントに関連付けるユーザー定義関数(UDF)です。サービス関数が実行されると、サービスエンドポイントにリクエストを送り、応答を受け取ります。

  1. コンピューティングプールの準備ができ、サービスを作成するのに適切なコンテキストにいることを確認します。

    1. 以前は 共通セットアップ ステップでコンテキストを設定しました。このステップの SQL ステートメントのコンテキストが正しいことを確認するために、以下を実行します。

    USE ROLE test_role;
    USE DATABASE tutorial_db;
    USE SCHEMA data_schema;
    USE WAREHOUSE tutorial_warehouse;
    
    Copy
    1. 共通セットアップ で作成したコンピューティングプールが準備できていることを確認するために、 DESCRIBE COMPUTE POOL を実行し、 stateACTIVE または IDLE であることを確認します。 stateSTARTING の場合は、 stateACTIVEIDLE に変わるまで待つ必要があります。

    DESCRIBE COMPUTE POOL tutorial_compute_pool;
    
    Copy
  2. サービスを作成するには、 test_role を使用して以下のコマンドを実行します。

    CREATE SERVICE echo_service
      IN COMPUTE POOL tutorial_compute_pool
      FROM SPECIFICATION $$
        spec:
          containers:
          - name: echo
            image: /tutorial_db/data_schema/tutorial_repository/my_echo_service_image:latest
            env:
              SERVER_PORT: 8000
              CHARACTER_NAME: Bob
            readinessProbe:
              port: 8000
              path: /healthcheck
          endpoints:
          - name: echoendpoint
            port: 8000
            public: true
          $$
       MIN_INSTANCES=1
       MAX_INSTANCES=1;
    
    Copy

    注釈

    その名前のサービスがすでに存在する場合は、 DROP SERVICE コマンドを使用して以前に作成したサービスを削除してから、このサービスを作成します。

  3. 以下の SQL コマンドを実行して、先ほど作成したサービスの詳細情報を取得します。詳細については、 Snowpark Container Services: サービスの操作 をご参照ください。

    • アカウント内のサービスをリストするには、 SHOW SERVICES コマンドを実行します。

      SHOW SERVICES;
      
      Copy
    • サービスのステータスを取得するには、システム関数 SYSTEM$GET_SERVICE_STATUS を呼び出します。

      SELECT SYSTEM$GET_SERVICE_STATUS('echo_service');
      
      Copy
    • サービスに関する情報を取得するには、 DESCRIBE SERVICE コマンドを実行します。

      DESCRIBE SERVICE echo_service;
      
      Copy
  4. サービス関数を作成するには、以下のコマンドを実行します。

    CREATE FUNCTION my_echo_udf (InputText varchar)
      RETURNS varchar
      SERVICE=echo_service
      ENDPOINT=echoendpoint
      AS '/echo';
    
    Copy

    次の点に注意してください。

    • SERVICE プロパティは、 UDF と echo_service サービスを関連付けます。

    • ENDPOINT プロパティは、サービス内の UDF と echoendpoint エンドポイントを関連付けます。

    • AS 「/echo」は、エコーサーバーへの HTTP パスを指定します。このパスは、サービスコード echo_service.py にあります。

4: サービスを使用する

まず、このセクションの SQL ステートメントのコンテキストを設定し、以下を実行します。

USE ROLE test_role;
USE DATABASE tutorial_db;
USE SCHEMA data_schema;
USE WAREHOUSE tutorial_warehouse;
Copy

これで、Echoサービスと通信できるようになりました。

  1. サービス関数の使用: クエリ内でサービス関数を呼び出すことができます。例のサービス関数(my_echo_udf)は、単一の文字列または文字列のリストを入力として受け取ることができます。

    例1.1: 単一の文字列を渡す

    • my_echo_udf サービス関数を呼び出すには、次の SELECT ステートメントを実行し、1つの入力文字列('hello')を渡します。

      SELECT my_echo_udf('hello!');
      
      Copy

      Snowflakeは、 POST リクエストをサービスエンドポイント(echoendpoint)に送信します。リクエストを受信すると、サービスは応答に入力文字列をエコーします。

      +--------------------------+
      | **MY_ECHO_UDF('HELLO!')**|
      |------------------------- |
      | Bob said hello!          |
      +--------------------------+
      

    例1.2: 文字列のリストを渡す

    サービス関数に文字列のリストを渡すと、Snowflakeはこれらの入力文字列を一括して、一連の POST リクエストをサービスに送信します。サービスがすべての文字列を処理した後、Snowflakeは結果を結合して返します。

    以下の例では、テーブル列を入力としてサービス関数に渡します。

    1. 複数の文字列からなるテーブルを作成します。

      CREATE TABLE messages (message_text VARCHAR)
        AS (SELECT * FROM (VALUES ('Thank you'), ('Hello'), ('Hello World')));
      
      Copy
    2. テーブルが作成されたことを確認します。

      SELECT * FROM messages;
      
      Copy
    3. サービス関数を呼び出すには、次の SELECT ステートメントを実行し、入力としてテーブル行を渡します。

      SELECT my_echo_udf(message_text) FROM messages;
      
      Copy

      出力:

      +---------------------------+
      | MY_ECHO_UDF(MESSAGE_TEXT) |
      |---------------------------|
      | Bob said Thank you        |
      | Bob said Hello            |
      | Bob said Hello World      |
      +---------------------------+
      
  2. ウェブブラウザーの使用: サービスはエンドポイントを公開します(CREATE SERVICE コマンドで提供されるインライン仕様を参照)。したがって、サービスがインターネットに公開しているウェブ UI にログインし、ウェブブラウザーからサービスにリクエストを送信できます。

    1. サービスが公開しているパブリックエンドポイントの URL を見つけます。

      SHOW ENDPOINTS IN SERVICE echo_service;
      
      Copy

      応答の ingress_url 列は URL を提供します。

      p6bye-myorg-myacct.snowflakecomputing.app
      
    2. /ui をエンドポイント URL に追加し、ウェブブラウザーに貼り付けます。これにより、サービスは ui() 関数を実行します(echo_service.py を参照)。

      エンドポイント URL に初めてアクセスするときは、Snowflakeにログインするようにリクエストされることに注意してください。このテストでは、サービスの作成に使用したユーザーと同じユーザーを使用し、そのユーザーが必要な権限を持っていることを確認します。

      Web form to communicate with echo service.
    3. Input ボックスに文字列「Hello」を入力し、 Return を押します。

      Web form showing response from the Echo service.

5: クリーンアップする

チュートリアル2 または チュートリアル3 に進む予定がない場合は、作成した請求対象リソースを削除する必要があります。詳細については、 チュートリアル3 のステップ5をご参照ください。

6: サービスコードの確認

このセクションでは、以下のトピックを取り上げます。

チュートリアル1コードの検証

ステップ1でダウンロードしたzipファイルには、以下のファイルが含まれています。

  • Dockerfile

  • echo_service.py

  • templates/basic_ui.html

サービスを作成する際にもサービス仕様を使用します。次のセクションでは、これらのコードコンポーネントがどのように連携してサービスを作成するかを説明します。

echo_service.pyファイル

このPythonファイルには、入力テキストを返す(エコーバックする)最小の HTTP サーバーを実装するコードが含まれています。このコードでは主に、Snowflakeサービス関数からのエコーリクエストの処理と、エコーリクエストを送信するためのウェブユーザーインターフェイス(UI)の提供という2つのタスクを実行します。

from flask import Flask
from flask import request
from flask import make_response
from flask import render_template
import logging
import os
import sys

SERVICE_HOST = os.getenv('SERVER_HOST', '0.0.0.0')
SERVER_PORT = os.getenv('SERVER_PORT', 8080)
CHARACTER_NAME = os.getenv('CHARACTER_NAME', 'I')


def get_logger(logger_name):
  logger = logging.getLogger(logger_name)
  logger.setLevel(logging.DEBUG)
  handler = logging.StreamHandler(sys.stdout)
  handler.setLevel(logging.DEBUG)
  handler.setFormatter(
    logging.Formatter(
      '%(name)s [%(asctime)s] [%(levelname)s] %(message)s'))
  logger.addHandler(handler)
  return logger


logger = get_logger('echo-service')

app = Flask(__name__)


@app.get("/healthcheck")
def readiness_probe():
  return "I'm ready!"


@app.post("/echo")
def echo():
  '''
  Main handler for input data sent by Snowflake.
  '''
  message = request.json
  logger.debug(f'Received request: {message}')

  if message is None or not message['data']:
    logger.info('Received empty message')
    return {}

  # input format:
  #   {"data": [
  #     [row_index, column_1_value, column_2_value, ...],
  #     ...
  #   ]}
  input_rows = message['data']
  logger.info(f'Received {len(input_rows)} rows')

  # output format:
  #   {"data": [
  #     [row_index, column_1_value, column_2_value, ...}],
  #     ...
  #   ]}
  output_rows = [[row[0], get_echo_response(row[1])] for row in input_rows]
  logger.info(f'Produced {len(output_rows)} rows')

  response = make_response({"data": output_rows})
  response.headers['Content-type'] = 'application/json'
  logger.debug(f'Sending response: {response.json}')
  return response


@app.route("/ui", methods=["GET", "POST"])
def ui():
  '''
  Main handler for providing a web UI.
  '''
  if request.method == "POST":
    # getting input in HTML form
    input_text = request.form.get("input")
    # display input and output
    return render_template("basic_ui.html",
      echo_input=input_text,
      echo_reponse=get_echo_response(input_text))
  return render_template("basic_ui.html")


def get_echo_response(input):
  return f'{CHARACTER_NAME} said {input}'

if __name__ == '__main__':
  app.run(host=SERVICE_HOST, port=SERVER_PORT)
Copy

コードで、

  • echo 関数は、Snowflakeサービス関数がサービスと通信できるようにします。この関数は、 @app.post() 装飾を次のように指定します。

    @app.post("/echo")
    def echo():
    
    Copy

    エコーサーバーが HTTP POST リクエストと /echo パスを受信すると、サーバーはリクエストをこの関数にルーティングします。この関数は実行され、リクエスト本文の文字列を応答にエコーバックします。

    Snowflakeサービス関数からの通信をサポートするために、このサーバーは外部関数を実装します。つまり、サーバー実装は、 SQL 関数を処理するために特定の入出力データ形式に従います。これは 外部関数 が使用する 入出力データ形式 と同じです。

  • コードの ui 関数セクションは、ウェブフォームを表示し、ウェブフォームから送信されたエコーリクエストを処理します。この関数は、 @app.route() デコレーターを使用して、 /ui に対するリクエストをこの関数で処理するように指定します。

    @app.route("/ui", methods=["GET", "POST"])
    def ui():
    
    Copy

    Echoサービスは、 echoendpoint エンドポイントを公開し(サービス仕様を参照)、ウェブ上でサービスと通信できるようにします。ブラウザーで/uiを付加したパブリックエンドポイントの URL をロードすると、ブラウザーはこのパスに対する HTTP GET リクエストを送信し、サーバーはリクエストをこの関数にルーティングします。この関数は実行され、ユーザーが文字列を入力するための単純な HTML フォームを返します。

    ユーザーが文字列を入力してフォームを送信した後、ブラウザーはこのパスに対する HTTP ポストリクエストを送信し、サーバーはリクエストをこの同じ関数にルーティングします。この関数は実行され、元の文字列を含む HTTP 応答を返します。

  • readiness_probe 関数は、 @app.get() デコレーターを使用して、 /healthcheck に対するリクエストがこの関数で処理されることを指定します。

    @app.get("/healthcheck")
    def readiness_probe():
    
    Copy

    この関数により、Snowflakeはサービスの準備状況を確認することができます。コンテナーが起動すると、Snowflakeはアプリケーションが動作していることと、サービスがリクエストに対応する準備ができていることを確認します。Snowflakeは、このパスで HTTP GET リクエストを送信し(healthプローブ、readinessプローブとして)、正常なコンテナーだけがトラフィックを提供できるようにします。この関数は何でもできます。

  • get_logger 関数はログのセットアップに役立ちます。

Dockerfile

このファイルには、Dockerを使用してイメージを構築するためのすべてのコマンドが含まれています。

ARG BASE_IMAGE=python:3.10-slim-buster
FROM $BASE_IMAGE
COPY echo_service.py ./
COPY templates/ ./templates/
RUN pip install --upgrade pip && \\
pip install flask
CMD ["python", "echo_service.py"]
Copy

Dockerfileには、FlaskライブラリをDockerコンテナーにインストールする手順が含まれています。 echo_service.py のコードは、 HTTP のリクエストを処理するためにFlaskライブラリに依存しています。

/template/basic_ui.html

Echoサービスは、 echoendpoint エンドポイントを公開し(サービス仕様を参照)、ウェブ上でサービスと通信できるようにします。ブラウザーで /ui を付加したパブリックエンドポイント URL をロードすると、Echoサービスはこのフォームを表示します。フォームに文字列を入力して送信すると、サービスはその文字列を HTTP 応答として返します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Welcome to echo service!</title>
  </head>
  <body>
    <h1>Welcome to echo service!</h1>
    <form action="{{ url_for("ui") }}" method="post">
      <label for="input">Input:<label><br>
      <input type="text" id="input" name="input"><br>
    </form>
    <h2>Input:</h2>
    {{ echo_input }}
    <h2>Output:</h2>
    {{ echo_reponse }}
  </body>
</html>
Copy

サービス仕様

Snowflakeは、この仕様で提供された情報を使用して、サービスを構成および実行します。

spec:
  containers:
  - name: echo
    image: /tutorial_db/data_schema/tutorial_repository/my_echo_service_image:latest
    env:
      SERVER_PORT: 8000
      CHARACTER_NAME: Bob
    readinessProbe:
      port: 8000
      path: /healthcheck
  endpoints:
  - name: echoendpoint
    port: 8000
    public: true
Copy

サービス仕様で、

  • containers.image は、Snowflakeがコンテナーを開始するためのイメージを指定します。

  • オプションの endpoints フィールドは、サービスが公開するエンドポイントを指定します。

    • name は、コンテナーがリッスンしている TCP ネットワークポートのユーザーフレンドリーな名前を指定します。このユーザーフレンドリーなエンドポイント名を使用して、対応するポートにリクエストを送信します。 env.SERVER_PORT は、このポート番号を制御することに注意してください。

    • エンドポイントも public として構成されています。これにより、パブリックウェブからこのエンドポイントへのトラフィックが許可されます。

  • オプションの containers.env フィールドは、Snowflakeがコンテナー内のすべてのプロセスに渡す環境変数を上書きする方法を説明するために追加されています。たとえば、サービスコード(echo_service.py)は、以下のようにデフォルト値で環境変数を読み取ります。

    CHARACTER_NAME = os.getenv('CHARACTER_NAME', 'I')
    SERVER_PORT = os.getenv('SERVER_PORT', 8080)
    
    Copy

    仕組みは次のとおりです。

    • Echoサービスは、リクエスト本文に文字列(例: 「Hello」)を含む HTTP POST リクエストを受信すると、デフォルトで「I said Hello」を返します。このコードでは、環境変数 CHARACTER_NAME を使用して、「said」の前の単語を決定しています。デフォルトでは、 CHARACTER_NAME は「I」に設定されています。

      サービス仕様の CHARACTER_NAME デフォルト値は上書きすることができます。たとえば、値を「Bob」に設定すると、Echoサービスは「Bob said Hello」という応答を返します。

    • 同様に、サービス仕様は、サービスがリッスンするポート(SERVER_PORT)をデフォルトのポート8080から8000に上書きします。

  • readinessProbe フィールドは、Snowflakeがreadinessプローブに HTTP GET リクエストを送信するために使用できる portpath を識別して、サービスがトラフィックを処理する準備ができていることを確認します。

    サービスコード(echo_python.py)は、以下のようにreadinessプローブを実装します。

    @app.get("/healthcheck")
    def readiness_probe():
    
    Copy

    そのため、仕様ファイルにはそれに応じて container.readinessProbe フィールドが含まれます。

サービス仕様の詳細については、 サービス仕様リファレンス をご参照ください。

サービス関数に対する理解

サービス関数は、サービスと通信する方法の1つです(サービスの使用 を参照)。サービス関数は、サービスエンドポイントに関連付けるユーザー定義関数(UDF)です。サービス関数が実行されると、関連するサービスエンドポイントにリクエストを送信し、応答を受信します。

以下のパラメーターを使用して CREATE FUNCTION コマンドを実行し、以下のサービス関数を作成します。

CREATE FUNCTION my_echo_udf (InputText VARCHAR)
  RETURNS VARCHAR
  SERVICE=echo_service
  ENDPOINT=echoendpoint
  AS '/echo';
Copy

次の点に注意してください。

  • my_echo_udf 関数は、入力として文字列を取り、文字列を返します。

  • SERVICE プロパティはサービス(echo_service)を識別し、 ENDPOINT プロパティはユーザーフレンドリーなエンドポイント名(echoendpoint)を識別します。

  • AS 「/echo」は、サービスへのパスを指定します。 echo_service.py では、 @app.post デコレーターがこのパスを echo 関数に関連付けます。

この関数は、指定された SERVICE の特定の ENDPOINT に接続します。この関数を呼び出すと、Snowflakeはサービスコンテナー内の /echo パスにリクエストを送信します。

ローカルでのイメージの構築とテスト

Snowflakeアカウントのリポジトリにアップロードする前に、ローカルでDockerイメージをテストすることができます。ローカルテストでは、コンテナーはスタンドアロンで実行されます(Snowflakeが実行するサービスではありません)。

チュートリアル1のDockerイメージをテストするには、

  1. Dockerイメージを作成するには、Docker CLI で以下のコマンドを実行します。

    docker build --rm -t my_service:local .
    
    Copy
  2. コードを起動するには、以下のコマンドを実行します。

    docker run --rm -p 8080:8080 my_service:local
    
    Copy
  3. 以下のいずれかの方法で、サービスにエコーリクエストを送信します。

    • cURL コマンドの使用:

      別のターミナルウィンドウで、 cURL を使用し、ポート8080に以下の POST リクエストを送信します。

      curl -X POST http://localhost:8080/echo \
        -H "Content-Type: application/json" \
        -d '{"data":[[0, "Hello friend"], [1, "Hello World"]]}'
      
      Copy

      リクエスト本文には2つの文字列が含まれていることに注意してください。この cURL コマンドは、サービスがリッスンしているポート8080に POST リクエストを送信します。データ中の0は、リスト中の入力文字列のインデックスです。Echoサービスは、入力された文字列を次のようにエコーします。

      {"data":[[0,"I said Hello Friend"],[1,"I said Hello World"]]}
      
    • ウェブブラウザーの使用:

      1. 同じコンピューターのブラウザーで、 http://localhost:8080/ui を開きます。

        これにより、サービスがリッスンしているポート8080に GET リクエストが送信されます。サービスは ui() 関数を実行し、次のように HTML フォームを表示します。

        Web form to communicate with echo service.
      2. Input ボックスに文字列「Hello」を入力し、 Return を押します。

        Web form showing response from the Echo service.

次の内容

これでジョブを実行する チュートリアル2 をテストできます。