Créer un service Snowpark Container Services qui utilise les droits de l’appelant

Introduction

Dans ce tutoriel, vous découvrirez la conception d’un service, la présentation d’une UI Web, qui utilise la fonction des droits de l’appelant lors de l’exécution des requêtes SQL pour le compte des utilisateurs.

Vous créez un service (nommé query_service) qui exécute une requête fournie dans la requête. Par défaut, les conteneurs d’applications se connectent à Snowflake en tant qu’utilisateur du service en utilisant le rôle de propriétaire du service. Mais cette application utilise la fonction des droits de l’appelant pour se connecter au point de terminaison de service en tant qu’utilisateur final et en utilisant les privilèges accordés à cet utilisateur.

Lors des tests, vous utilisez le service à partir d’un navigateur Web car la fonction des droits de l’appelant n’est prise en charge que lors de l’accès à un service utilisant l’entrée réseau. La fonction des droits de l’appelant n’est pas disponible lors de l’accès à un service à l’aide d’une fonction de service.

Le service effectue les opérations suivantes :

  • Expose un point de terminaison public.

  • Lorsqu’un utilisateur se connecte au point de terminaison, le service met à sa disposition une UI Web pour lui permettre d’effectuer une requête. Le service exécute la requête dans Snowflake et renvoie les résultats. Dans ce tutoriel, vous exécutez la commande suivante SQL :

    SELECT CURRENT_USER(), CURRENT_ROLE();
    
    Copy

    La commande renvoie le nom de l’utilisateur actuellement connecté et le rôle actuellement actif, qui dépendent tous deux de l’utilisation ou non des droits de l’appelant.

    • Lorsque les droits de l’appelant sont utilisés, le service se connecte à Snowflake en tant qu’utilisateur appelant et dans le rôle par défaut de l’utilisateur. La commande renvoie votre nom d’utilisateur et votre rôle par défaut.

    • Lorsque les droits de l’appelant ne sont pas utilisés, le comportement par défaut s’applique : le service se connecte à Snowflake en tant qu’utilisateur du service et dans le rôle de propriétaire du service. Par conséquent, la commande renvoie le nom de l’utilisateur du service sous la forme : SF$SERVICE$unique-id, TEST_ROLE.

Ce tutoriel comporte deux parties :

Partie 1 : créez et testez un service. Vous téléchargez le code fourni pour ce tutoriel et suivez les instructions étape par étape :

  1. Téléchargez le code de service pour ce tutoriel.

  2. Créez une image Docker pour Snowpark Container Services, et chargez l’image dans un référentiel de votre compte.

  3. Créez un service.

  4. Communiquez avec le service en utilisant l’entrée réseau pour vous connecter au point de terminaison public que le service expose. À l’aide d’un navigateur, vous vous connectez au point de terminaison public et exécutez la commande SELECT CURRENT_USER() ;. Vérifiez la sortie de la commande pour vous assurer que le conteneur a exécuté la commande en tant qu’utilisateur connecté.

Partie 2 : comprendre le service. Cette section donne un aperçu du code du service et met en évidence la manière dont le code de l’application utilise les droits de l’appelant.

Préparer

Suivez la Configuration commune pour configurer les conditions préalables et créer les ressources Snowflake qui sont requises pour tous les tutoriels Snowpark Container Services fournis dans cette documentation.

Télécharger le code de service

Le code (une application Python) est fourni pour créer le service de requête.

  1. Téléchargez SnowparkContainerServices-Tutorials.zip.

  2. Décompressez le contenu, qui comprend un répertoire pour chaque tutoriel. Le répertoire Tutorial-6-callers-rights contient les fichiers suivants :

    • Dockerfile

    • main.py

    • templates/basic_ui.html

Créer une image et la télécharger

Construisez une image pour la plateforme linux/amd64 prise en charge par Snowpark Container Services, puis chargez l’image dans le référentiel d’images de votre compte (voir Configuration commune).

Vous aurez besoin d’informations sur le référentiel (l’URL du référentiel et le nom d’hôte du registre) avant de pouvoir construire et charger l’image. Pour plus d’informations, voir Registre et référentiels.

Obtenir des informations sur le référentiel

  1. Pour obtenir l’URL du référentiel, exécutez la commande SQL SHOW IMAGE REPOSITORIES.

    SHOW IMAGE REPOSITORIES;
    
    Copy
    • La colonne repository_url de la sortie fournit l’URL. En voici un exemple :

      <orgname>-<acctname>.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository
      
    • Le nom d’hôte dans l’URL du référentiel est le nom d’hôte du registre. En voici un exemple :

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

Construire l’image et la charger dans le référentiel

  1. Ouvrez une fenêtre de terminal et accédez au répertoire contenant les fichiers que vous avez décompressés.

  2. Pour créer une image Docker, exécutez la commande docker build suivante à l’aide de la CLI Docker. Notez que la commande spécifie le répertoire de travail actuel (.) comme PATH pour les fichiers à utiliser pour la conception de l’image.

    docker build --rm --platform linux/amd64 -t <repository_url>/<image_name> .
    
    Copy
    • Pour image_name, utilisez query_service:latest.

    Exemple

    docker build --rm --platform linux/amd64 -t myorg-myacct.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/query_service:latest .
    
    Copy
  3. Chargez l’image dans le référentiel de votre compte Snowflake. Pour que Docker télécharge une image en votre nom dans votre référentiel, vous devez d’abord authentifier Docker avec Snowflake.

    1. Pour authentifier Docker auprès du registre Snowflake, exécutez la commande suivante :

      docker login <registry_hostname> -u <username>
      
      Copy
      • Pour username, indiquez votre nom d’utilisateur Snowflake. Docker vous demandera votre mot de passe.

    2. Pour charger l’image, exécutez la commande suivante :

      docker push <repository_url>/<image_name>
      
      Copy

      Exemple

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

Créez un service

Dans cette section, vous créez un service (query_service).

  1. Vérifiez que le pool de calcul est prêt et que vous êtes dans le bon contexte pour créer le service.

    1. Auparavant, vous avez défini le contexte dans l’étape Configuration commune. Pour vous assurer que vous êtes dans le bon contexte pour les instructions SQL de cette étape, exécutez ce qui suit :

    USE ROLE test_role;
    USE DATABASE tutorial_db;
    USE SCHEMA data_schema;
    USE WAREHOUSE tutorial_warehouse;
    
    Copy
    1. Pour s’assurer que le pool de calcul que vous avez créé dans la configuration commune est prêt, exécutez DESCRIBE COMPUTE POOL, et vérifiez que state est ACTIVE ou IDLE. Si le state est STARTING, vous devez attendre que le state devienne ACTIVE ou IDLE.

    DESCRIBE COMPUTE POOL tutorial_compute_pool;
    
    Copy
  2. Pour créer le service, exécutez la commande suivante en utilisant test_role :

    CREATE SERVICE query_service
      IN COMPUTE POOL tutorial_compute_pool
      FROM SPECIFICATION $$
        spec:
          containers:
          - name: main
            image: /tutorial_db/data_schema/tutorial_repository/query_service:latest
            env:
              SERVER_PORT: 8000
            readinessProbe:
              port: 8000
              path: /healthcheck
          endpoints:
          - name: execute
            port: 8000
            public: true
        capabilities:
          securityContext:
            executeAsCaller: true
        serviceRoles:
        - name: ui_usage
          endpoints:
          - execute
    $$;
    
    Copy

    Note

    Si un service portant ce nom existe déjà, utilisez la commande DROP SERVICE pour supprimer le service précédemment créé, puis créez ce service.

  3. Exécutez les commandes SQL suivantes pour obtenir des informations détaillées sur le service que vous venez de créer. Pour plus d’informations, voir Snowpark Container Services : utilisation des tâches.

    • Pour dresser la liste des services de votre compte, exécutez la commande SHOW SERVICES :

      SHOW SERVICES;
      
      Copy
    • Pour obtenir le statut de votre service, exécutez la commande SHOW SERVICE CONTAINERS IN SERVICE :

      SHOW SERVICE CONTAINERS IN SERVICE query_service;
      
      Copy
    • Pour obtenir des informations sur votre service, exécutez la commande DESCRIBE SERVICE :

      DESCRIBE SERVICE query_service;
      
      Copy

Utilisez le service

Dans cette section, vous vérifiez que les droits de l’appelant configurés pour le service fonctionnent. Vous vous connectez au point de terminaison public à partir d’un navigateur, vous exécutez une requête et vous vérifiez que la session Snowflake créée par le service opère en tant qu’utilisateur appelant et non en tant qu’utilisateur du service.

Tout d’abord, pour configurer le contexte des instructions SQL de cette section, exécutez ce qui suit :

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

Le service expose un point de terminaison public (voir la spécification en ligne fournie dans la commande CREATE SERVICE) ; par conséquent, connectez-vous d’abord au point de terminaison à l’aide d’un navigateur web, puis utilisez l’UI Web que le service expose à Internet pour envoyer des requêtes au point de terminaison de service.

  1. Trouvez l’URL du point de terminaison public auquel le service est exposé :

    SHOW ENDPOINTS IN SERVICE query_service;
    
    Copy

    La colonne ingress_url de la réponse fournit l’URL.

    Exemple

    p6bye-myorg-myacct.snowflakecomputing.app
    
  2. Ajoutez /ui à l’URL du point de terminaison et collez-la dans le navigateur Web. Le service exécute alors la fonction ui() (voir main.py).

    Notez que la première fois que vous accédez à l’URL du point de terminaison, il vous sera demandé de vous connecter à Snowflake.

  3. Utilisez le même utilisateur que celui que vous avez utilisé pour créer le service. Une fois la connexion réussie, le service affiche l’UI Web suivant.

    Formulaire Web pour communiquer avec le service de requête.

    Saisissez la commande suivante dans la zone de texte et appuyez sur la touche Entrée pour voir les résultats.

    SELECT CURRENT_USER(), CURRENT_ROLE()DONE;
    
    Copy

    Parce que vous avez inclus la capacité executeAsCaller dans la spécification du service, lorsqu’une requête arrive, Snowflake insère l’en-tête Sf-Context-Current-User-Token dans la requête et transmet ensuite la requête à votre point de terminaison de service.

    À des fins d’illustration, le code de service de ce tutoriel exécute la requête à la fois en tant qu’appelant et en tant qu’utilisateur du service.

    Formulaire Web pour communiquer avec le service de requête.
    • Exécute la requête pour le compte de l’appelant (utilisateur entrant) : dans ce cas, le code utilise le jeton utilisateur que Snowflake fournit pour construire un jeton de connexion pour se connecter à Snowflake. Le service utilise donc les droits de l’appelant. Snowflake exécute la requête pour le compte de l’appelant, en affichant le nom de l’appelant et le nom de son rôle actif dans le résultat de la requête. Par exemple :

      ['TESTUSER, PUBLIC']
      
    • Exécute la requête au nom de l’utilisateur du service : dans ce cas, le code n’utilise pas le jeton utilisateur que Snowflake fournit dans la requête lors de la construction du jeton de connexion pour se connecter à Snowflake. Ainsi, le service n’utilise pas les droits de l’appelant, ce qui amène Snowflake à exécuter la requête au nom de l’utilisateur du service. Le résultat de la requête indique le nom de l’utilisateur du service (qui est le même que le nom du service) et le rôle actif.

      ['QUERY_SERVICE, TEST_ROLE']
      

Lorsque le service exécute la requête (SELECT CURRENT_USER(), CURRENT_ROLE();) pour le compte de l’appelant, Snowflake n’a pas besoin de l’entrepôt de l’utilisateur pour exécuter cette simple requête. Par conséquent, le service n’a pas eu besoin d’appelant pour obtenir des. Dans la section suivante, le service exécute une requête non triviale au nom de l’utilisateur appelant qui exige que vous accordiez les autorisations de l’appelant au service.

Note

Vous pouvez accéder au point de terminaison d’entrée de manière programmatique. Pour un exemple de code, voir Accès aux points de terminaison publics depuis l’extérieur de Snowflake et authentification. Notez que vous devez ajouter /ui à l’URL du point de terminaison dans le code afin que Snowflake puisse acheminer la demande vers la fonction ui() dans le code de service.

Utiliser le service avec des autorisations à l’appelant

Dans cette section, le service exécute la requête suivante pour le compte de l’appelant (l’utilisateur qui se connecte au point de terminaison d’entrée du service).

SELECT * FROM ingress_user_db.ingress_user_schema.ingress_user_table;
Copy

Le service n’a pas l’autorisation d’accéder à la table et n’a pas l’autorisation d’exécuter la requête dans l’entrepôt par défaut. Pour permettre au service d’exécuter cette requête au nom de l’appelant, vous accordez les autorisations nécessaires de l’appelant au service.

Pour illustrer ce scénario, vous créez un nouveau rôle (ingress_user_role) et une table (ingress_user_table) accessible au nouveau rôle mais pas au rôle de propriétaire du service (test_role). Par conséquent, lorsque le service tente d’exécuter la requête en utilisant les identifiants du service, Snowflake renvoie une erreur. Mais lorsque le service exécute la requête pour le compte de l’utilisateur, Snowflake exécute la requête et renvoie le résultat.

Créer des rôles et des ressources

  1. Créez un rôle (ingress_user_role) et une base de données (ingress_user_db) auxquels seul ce rôle peut accéder. Vous accordez ensuite ce rôle à votre utilisateur, afin qu’il puisse se connecter au point de terminaison public du service et effectuer des requêtes dans cette table.

    USE ROLE accountadmin;
    
    CREATE ROLE ingress_user_role;
    GRANT ROLE ingress_user_role TO USER <your_user_name>;
    
    GRANT USAGE ON WAREHOUSE tutorial_warehouse TO ROLE ingress_user_role;
    
    CREATE DATABASE IF NOT EXISTS ingress_user_db;
    GRANT OWNERSHIP ON DATABASE ingress_user_db TO ROLE ingress_user_role COPY CURRENT GRANTS;
    
    Copy
  2. Créez une table (ingress_user_table) à laquelle seul le rôle ingress_user_role peut accéder.

    USE ROLE ingress_user_role;
    
    CREATE SCHEMA IF NOT EXISTS ingress_user_db.ingress_user_schema;
    USE WAREHOUSE tutorial_warehouse;
    CREATE TABLE ingress_user_db.ingress_user_schema.ingress_user_table (col string) AS (
        SELECT 'this table is only accessible to the ingress_user_role'
    );
    
    Copy

    Notez que lorsque le service tente d’interroger la table pour le compte de l’appelant, il n’opère qu’en tant que test_role, le rôle qui a été utilisé pour créer le service (le rôle de propriétaire du service). Ce rôle n’a pas l’autorisation d’accéder à la table des utilisateurs.

  3. Accordez des autorisations à l’appelant pour le rôle de propriétaire du service (test_role) pour interroger les tables de la base de données ingress_user_db. Ce privilège permet au service d’interroger les tables de cette base de données uniquement si les conditions suivantes sont remplies :

    USE ROLE accountadmin;
    
    GRANT CALLER USAGE ON DATABASE ingress_user_db TO ROLE test_role;
    GRANT INHERITED CALLER USAGE ON ALL SCHEMAS IN DATABASE ingress_user_db TO ROLE test_role;
    GRANT INHERITED CALLER SELECT ON ALL TABLES IN DATABASE ingress_user_db TO ROLE test_role;
    GRANT CALLER USAGE ON WAREHOUSE tutorial_warehouse TO ROLE test_role;
    SHOW CALLER GRANTS TO ROLE test_role;
    
    Copy
  4. Configurez l’entrepôt par défaut et les rôles par défaut secondaires.

    Lorsqu’une session est créée pour un utilisateur, Snowflake active le rôle principal par défaut, les rôles secondaires par défaut et l’entrepôt par défaut de l’utilisateur connecté. Dans ce tutoriel,

    • vous définissez DEFAULT_SECONDARY_ROLES sur ALL afin que lorsqu’une session est créée pour l’utilisateur actuel, Snowflake définisse les rôles secondaires actuels comme étant tous les rôles accordés à l’utilisateur.

    • Vous avez également défini l’entrepôt par défaut sur tutorial_warehouse où les requêtes ingress_user_table sont exécutées.

    ALTER USER SET DEFAULT_SECONDARY_ROLES = ('ALL');
    ALTER USER SET DEFAULT_WAREHOUSE = TUTORIAL_WAREHOUSE;
    
    Copy

    Remarques :

    • Dans ce tutoriel, vous vous connectez au point de terminaison public du service. L’utilisateur a le test_role pour rôle principal et ingress_user_role pour rôle secondaire. Cela permet à la session de faire tout ce que le ingress_user_role autorise.

    • Le rôle par défaut et l’entrepôt par défaut n’affectent que le rôle et l’entrepôt activés lorsque le service établit une session au nom de votre utilisateur. Une fois la session de droits de l’appelant établie, vous ne pouvez pas modifier le rôle, mais vous pouvez modifier l’entrepôt.

Utiliser le service et tester les autorisations accordées à l’appelant

  1. Trouvez l’URL du point de terminaison public auquel le service est exposé :

    SHOW ENDPOINTS IN SERVICE tutorial_db.data_schema.query_service;
    
    Copy

    La colonne ingress_url de la réponse fournit l’URL.

    Exemple

    p6bye-myorg-myacct.snowflakecomputing.app
    
  2. Ajoutez /ui à l’URL du point de terminaison et collez-la dans le navigateur Web. Le service exécute alors la fonction ui() (voir echo_service.py) : notez que la première fois que vous accédez à l’URL du point de terminaison, il vous sera demandé de vous connecter à Snowflake. Pour ce test, utilisez le même utilisateur que celui que vous avez utilisé pour créer le service afin de vous assurer que l’utilisateur dispose des privilèges nécessaires :

  3. Utilisez le même utilisateur que celui que vous avez utilisé pour créer le service. Une fois la connexion réussie, le service affiche l’UI Web suivant.

    Formulaire Web pour communiquer avec le service de requête.

    Saisissez la commande suivante dans la zone de texte et appuyez sur la touche Entrée pour voir les résultats.

    SELECT * FROM ingress_user_db.ingress_user_schema.ingress_user_table;
    
    Copy

    À des fins d’illustration, le code de service de ce tutoriel exécute la requête à la fois en tant qu’appelant et en tant qu’utilisateur du service.

    • Exécute la requête pour le compte de l’appelant (utilisateur entrant) : dans ce cas, le code utilise le jeton utilisateur fourni par Snowflake pour construire un jeton de connexion pour se connecter à Snowflake. Le service utilise donc les droits de l’appelant. Snowflake exécute la requête pour le compte de l’appelant. Comme l’appelant utilise le ingress_user_role role qui a le privilège d’interroger la table ingress_user_table, la requête renvoie une seule ligne dans le résultat :

      ['this table is only accessible to ingress_user_role']
      
    • Exécute la requête au nom de l’utilisateur du service : dans ce cas, le code n’utilise pas le jeton utilisateur que Snowflake fournit dans la requête lors de la construction du jeton de connexion pour se connecter à Snowflake. Ainsi, Snowflake exécute la requête pour le compte de l’utilisateur du service. Comme le propriétaire du service utilise le test_role par défaut, qui n’a pas l’autorisation d’interroger la table, une erreur s’affiche :

      Encountered an error when executing query:... SQL compilation error: Database 'INGRESS_USER_DB' does not exist or not authorized.
      
    Formulaire Web pour communiquer avec le service echo.

Nettoyage

Vous devez supprimer les ressources facturables que vous avez créées. Pour plus d’informations, voir l’étape 5 dans le tutoriel 3.

Révision du code de service

Cette section couvre les sujets suivants :

Examen du code du tutoriel

Le fichier zip que vous avez chargé à l’étape 1 comprend les fichiers suivants :

  • Dockerfile

  • main.py

  • templates/basic_ui.html

Vous utilisez également la spécification de service lors de la création du service. La section suivante explique comment ces composants de code fonctionnent ensemble pour créer le service.

fichier main.py

Ce fichier Python contient le code qui implémente un serveur HTTP minimal qui exécute une requête dans la requête et renvoie les résultats de la requête. Le code fournit une interface Web utilisateur (UI) pour la soumission des requêtes d’écho.

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

from snowflake.snowpark import Session
from snowflake.snowpark.exceptions import *

# Environment variables below will be automatically populated by Snowflake.
SNOWFLAKE_ACCOUNT = os.getenv("SNOWFLAKE_ACCOUNT")
SNOWFLAKE_HOST = os.getenv("SNOWFLAKE_HOST")
SNOWFLAKE_DATABASE = os.getenv("SNOWFLAKE_DATABASE")
SNOWFLAKE_SCHEMA = os.getenv("SNOWFLAKE_SCHEMA")

# Custom environment variables
SNOWFLAKE_USER = os.getenv("SNOWFLAKE_USER")
SNOWFLAKE_PASSWORD = os.getenv("SNOWFLAKE_PASSWORD")
SNOWFLAKE_ROLE = os.getenv("SNOWFLAKE_ROLE")
SNOWFLAKE_WAREHOUSE = os.getenv("SNOWFLAKE_WAREHOUSE")

SERVICE_HOST = os.getenv("SERVER_HOST", "0.0.0.0")
SERVER_PORT = os.getenv("SERVER_PORT", 8080)


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


def get_login_token():
    """
    Read the login token supplied automatically by Snowflake. These tokens
    are short lived and should always be read right before creating any new connection.
    """
    with open("/snowflake/session/token", "r") as f:
        return f.read()


def get_connection_params(ingress_user_token=None):
    """
    Construct Snowflake connection params from environment variables.
    """
    if os.path.exists("/snowflake/session/token"):
        if ingress_user_token:
            logger.info("Creating a session on behalf of user.")
            token = get_login_token() + "." + ingress_user_token
        else:
            logger.info("Creating a session as service user.")
            token = get_login_token()

        return {
            "account": SNOWFLAKE_ACCOUNT,
            "host": SNOWFLAKE_HOST,
            "authenticator": "oauth",
            "token": token,
            "warehouse": SNOWFLAKE_WAREHOUSE,
            "database": SNOWFLAKE_DATABASE,
            "schema": SNOWFLAKE_SCHEMA,
        }
    else:
        return {
            "account": SNOWFLAKE_ACCOUNT,
            "host": SNOWFLAKE_HOST,
            "user": SNOWFLAKE_USER,
            "password": SNOWFLAKE_PASSWORD,
            "role": SNOWFLAKE_ROLE,
            "warehouse": SNOWFLAKE_WAREHOUSE,
            "database": SNOWFLAKE_DATABASE,
            "schema": SNOWFLAKE_SCHEMA,
        }


logger = get_logger("query-service")
app = Flask(__name__)


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


@app.route("/ui", methods=["GET", "POST"])
def ui():
    """
    Main handler for providing a web UI.
    """
    if request.method == "POST":
        # get ingress user token
        ingress_user = request.headers.get("Sf-Context-Current-User")
        ingress_user_token = request.headers.get("Sf-Context-Current-User-Token")

        if ingress_user:
            logger.info(f"Received a request from user {ingress_user}")

        # getting input in HTML form
        query = request.form.get("query")
        if query:
            logger.info(f"Received a request for query: {query}.")
            query_result_ingress_user = (
                run_query(query, ingress_user_token)
                if ingress_user_token
                else "Token is missing. Can't execute as ingress user."
            )
            query_result_service_user = run_query(query)
            return render_template(
                "basic_ui.html",
                query_input=query,
                query_result_ingress_user=query_result_ingress_user,
                query_result_service_user=query_result_service_user,
            )
    return render_template("basic_ui.html")


@app.route("/query", methods=["GET"])
def query():
    """
    Main handler for providing programmatic access.
    """
    # get ingress user token
    query = request.args.get("query")
    logger.info(f"Received query request: {query}.")
    if query:
        ingress_user = request.headers.get("Sf-Context-Current-User")
        ingress_user_token = request.headers.get("Sf-Context-Current-User-Token")

        if ingress_user:
            logger.info(f"Received a request from user {ingress_user}")

        res = run_query(query, ingress_user_token)
        return str(res)
    return "DONE"


def run_query(query, ingress_user_token=None):
    # start a Snowflake session as the ingress user
    try:
        with Session.builder.configs(
            get_connection_params(ingress_user_token)
        ).create() as session:
            logger.info(
                f"Snowflake connection established (id={session.session_id}). Now executing query: {query}."
            )
            try:
                res = session.sql(query).collect()
                logger.info(f"Query execution done: {query}.")
                return (
                    "[Empty Result]"
                    if len(res) == 0
                    else [", ".join(row) for row in res]
                )
            except Exception as e:
                return "Encountered an error when executing query: " + str(e)
    except Exception as e:
        return "Encountered an error when connecting to Snowflake: " + str(e)

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

Dans le code :

  • La fonction ui affiche le formulaire web suivant et traite les requêtes soumises à partir du formulaire web.

    Formulaire Web pour communiquer avec le service de requête.

    Cette fonction utilise le décorateur @app.route() pour spécifier que les requêtes pour /ui sont traitées par cette fonction :

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

    Le service de requête expose publiquement le point de terminaison execute (voir la spécification de service en ligne que vous avez fournie lors de la création du service), ce qui permet de communiquer avec le service sur le Web. Lorsque vous chargez l’URL du point de terminaison public avec /ui ajouté dans votre navigateur, celui-ci envoie une requête HTTP GET pour ce chemin, et le serveur achemine la requête vers cette fonction. La fonction s’exécute et renvoie un simple formulaire HTML permettant à l’utilisateur de saisir une requête.

    Après que l’utilisateur a saisi une requête et soumis le formaulaire, le navigateur envoie une requête HTTP POST pour ce chemin. Comme la spécification du service inclut la capacité executeAsCaller, Snowflake ajoute l’en-tête Sf-Context-Current-User-Token à la requête entrante et transmet la requête à cette même fonction (voir Connexion à Snowflake en utilisant les droits de l’appelant).

    Le code exécute deux fois la fonction run_query :

    • En tant qu’utilisateur d’entrée. Dans ce cas, le jeton de connexion est la concaténation du jeton OAuth et du jeton de l’utilisateur d’entrée.

      token = get_login_token() + "." + ingress_user_token
      
      Copy
    • En tant qu’utilisateur du service. Dans ce cas, le jeton de connexion n’est que le jeton OAuth.

      token = get_login_token()
      
      Copy
  • La fonction readiness_probe utilise le décorateur @app.get() pour spécifier que les requêtes pour /healthcheck sont traitées par cette fonction :

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

    Cette fonction permet à Snowflake de vérifier l’état de préparation du service. Lorsque le conteneur démarre, Snowflake veut confirmer que l’application fonctionne et que le service est prêt à répondre aux requêtes. Snowflake envoie une requête HTTP GET avec ce chemin (en tant que sonde de santé, probe readiness) pour s’assurer que seuls les conteneurs sains servent le trafic. La fonction peut faire ce que vous voulez.

  • La fonction get_logger permet de mettre en place la journalisation.

Dockerfile

Ce fichier contient toutes les commandes pour construire une image en utilisant Docker.

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

Le Dockerfile contient des instructions pour installer la bibliothèque Flask dans le conteneur Docker. Le code de main.py s’appuie sur la bibliothèque Flask pour traiter les requêtes HTTP.

/template/basic_ui.html

Le service de requête expose publiquement le point de terminaison echoendpoint (voir la spécification du service), ce qui permet de communiquer avec le service sur le Web. Lorsque vous chargez l’URL du point de terminaison public avec /ui en annexe dans votre navigateur, le service de requête affiche cette forme.

Formulaire Web pour communiquer avec le service de requête.

Vous pouvez saisir une requête dans le formulaire et soumettre le formulaire, et le service renvoie le résulata dans une réponse HTTP.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Welcome to the query service!</title>
  </head>
  <body>
    <h1>Welcome to the query service!</h1>
    <form action="{{ url_for("ui") }}" method="post">
      <label for="query">query:<label><br>
      <input type="text" id="query" name="query" size="50"><br>
    </form>
    <h2>Query:</h2>
    {{ query_input }}
    <h2>Result (executed on behalf of ingress user):</h2>
    {{ query_result_ingress_user }}
    <h2>Result (executed as service user):</h2>
    {{ query_result_service_user }}
  </body>
</html>
Copy

Spécification du service

Snowflake utilise les informations que vous fournissez dans cette spécification pour configurer et faire fonctionner votre service.

spec:
  containers:
  - name: main
    image: /tutorial_db/data_schema/tutorial_repository/query_service:latest
    env:
      SERVER_PORT: 8000
    readinessProbe:
      port: 8000
      path: /healthcheck
  endpoints:
  - name: execute
    port: 8000
    public: true
capabilities:
  securityContext:
    executeAsCaller: true
serviceRoles:
- name: ui_usage
  endpoints:
  - execute
Copy

Dans la spécification de service, les champs spec, capabilities et serviceRoles sont les champs de premier niveau.

  • spec fournit des détails sur les spécifications (voir Référence Spécification de service). Notez que le service expose un point de terminaison public (execute) qui permet l’accès au service à partir du Web public.

  • capabilities Spécifie la capacité executeAsCaller. Cela indique à Snowflake que l’application a l’intention d’utiliser les droits de l’appelant.

  • serviceRoles spécifie un rôle de service (ui_usage) et un point de terminaison (execute) pour lesquels le privilège USAGE doit être accordé.

  • Le champ readinessProbe identifie les port et path que Snowflake peut utiliser pour envoyer une requête HTTP GET à la probe readiness afin de vérifier que le service est prêt à gérer le trafic.

    Le code de service (echo_python.py) met en œuvre la probe readiness comme suit :

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

    Le fichier de spécification inclut donc le champ container.readinessProbe en conséquence.

Pour plus d’informations sur les spécifications de service, voir Référence Spécification de service.

Quelle est la prochaine étape ?

Maintenant que vous avez terminé ce tutoriel, vous pouvez retourner à Travailler avec les services pour explorer d’autres sujets.