Snowpark Container Services: Working with services

Snowpark Container Services enables you to easily deploy, manage, and scale containerized applications. After you create an application and upload the application image to a repository in your Snowflake account, you can run your application containers as a service or a job. This topic explains how to work with services.

A service is long-running and, like a web service, does not end automatically. After you create a service, Snowflake manages the running service. For example, if a service container stops, for whatever reason, Snowflake restarts that container so the service runs uninterrupted. If your service needs more resources, such as more compute power, Snowflake provisions additional nodes in the compute pool.

Basic service operations

Snowpark Container Services provides a set of SQL commands you can use to create and manage a service. These include: CREATE SERVICE, ALTER SERVICE, DROP SERVICE, SHOW SERVICES, and DESC[RIBE] SERVICE.

The minimum information required to create a service includes:

  • A service specification: This specification provides Snowflake with the information needed to run your service. The specification is a YAML file. You can define the specification directly within a CREATE SERVICE statement, or upload the specification to a Snowflake stage and reference the stage information in the CREATE SERVICE statement.

    In most cases, during development, you might choose inline specification. However, when you deploy the service in a production environment, it’s advisable to apply the separation of concerns design principle and upload the specification to a stage.

  • A compute pool: Snowflake runs your service in the specified compute pool.

Examples

  • Create a service using an inline specification:

    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:tutorial
           readinessProbe:
             port: 8000
             path: /healthcheck
         endpoints:
         - name: echoendpoint
           port: 8000
           public: true
       $$;
    
    Copy
  • Create a service using stage information:

    CREATE SERVICE echo_service
      IN COMPUTE POOL tutorial_compute_pool
      FROM @tutorial_stage
      SPECIFICATION_FILE='echo_spec.yaml';
    
    Copy

After creating a service, you can use the ALTER SERVICE command to suspend or resume the service.

Create multiple service instances and configure autoscaling

By default, Snowflake runs one instance of the service in the specified compute pool. To manage heavy workloads, you can run multiple service instances by setting the MIN_INSTANCES and MAX_INSTANCES properties, which specify the minimum number of instances of the service to start with and the maximum instances Snowflake can scale to when needed.

Example

CREATE SERVICE echo_service
   IN COMPUTE POOL tutorial_compute_pool
   FROM @tutorial_stage
   SPECIFICATION_FILE='echo_spec.yaml'
   MIN_INSTANCES=2
   MAX_INSTANCES=4;
Copy

When multiple service instances are running, Snowflake automatically provides a load balancer to distribute the incoming requests.

To configure Snowflake to autoscale the number of service instances running, follow these steps:

  1. Specify the memory and CPU requirements for your service instance in the service specification file. For more information, see the container.resources field.

    Example

    resources:
      requests:
       memory: <memory-reserved>
       cpu: <cpu-units>
    
    Copy
  2. When running the CREATE SERVICE command, set the MIN_INSTANCES and MAX_INSTANCES parameters. You can also use ALTER SERVICE to change these values.

Snowflake starts by creating the minimum number of service instances on the specified compute pool and then automatically scales the number of instances as needed. Snowflake uses an 80% threshold (for both CPU and memory) to scale up or scale down the number of service instances.

At any given time, one or more service instances can be running on the specified compute pool. Snowflake continuously monitors resource utilization (memory or CPU) within the compute pool, aggregating the usage data from all currently running service instances.

When the aggregated resource usage (across all service instances) surpasses 80%, Snowflake deploys an additional service instance within the compute pool. If the aggregated resource usage falls below 80%, Snowflake scales down by removing a running service instance. Snowflake uses a five-minute stabilization window to prevent frequent scaling.

Note the following scaling behaviors:

  • The scaling of service instances is constrained by the MIN_INSTANCES and MAX_INSTANCES parameters configured for the service.

  • If scaling up is necessary and the compute pool nodes lack the necessary resource capacity to start up another service instance, compute pool autoscaling can be triggered. For more information, see Autoscaling of compute pool nodes.

  • If you specify the MAX_INSTANCES and MIN_INSTANCES parameters when creating a service but don’t specify the memory and CPU requirements for your service instance in the service specification file, no autoscaling occurs; Snowflake starts with the number of instances specified by the MIN_INSTANCES property and will not autoscale.

List services

You can use various SHOW SERVICES commands to list current services:

  • SHOW SERVICES: List services in the current database and schema for which you have permissions.

  • SHOW SERVICES IN ACCOUNT: List services in your Snowflake account, regardless of which database or schema the services belong to. This is useful if you have Snowflake services created in multiple databases and schemas in your account. Like all other commands, SHOW SERVICES IN ACCOUNTS is gated by privileges, returning only the services for which the role you are using has viewing permissions.

  • SHOW SERVICES IN SCHEMA: List services in the specified schema.

Listing services in a specific compute pool is a two-step process:

  1. Execute SHOW SERVICES to get a list of services.

  2. Scan the list from the previous query (using RESULT_SCAN) for services in the specific compute pool.

Example

In the following example script, the SHOW SERVICES command first lists services in the current database and schema that have a specific string in the service name.

Next, the SELECT query filters the result set to return only the services in the specified compute pool. Note that the query uses the RESULT_SCAN function to get the result set from the preceding SHOW SERVICES command.

SHOW SERVICES like '%tutorial%' in account;
SELECT *
    FROM TABLE(RESULT_SCAN(last_query_id()))
    WHERE "compute_pool" = 'TUTORIAL_COMPUTE_POOL';
Copy

Monitoring a service

Use SYSTEM$GET_SERVICE_STATUS to get the detailed runtime status of a service. For example, you can call this function to determine whether the service is still running or whether the service failed to start. If the service failed to start, the function provides more details about the failure.

When calling this function, pass in the name of the service.

Example

CALL SYSTEM$GET_SERVICE_STATUS('echo_service');
Copy

The following are example outputs:

  • Sample output of a service that is running one instance and has one container:

    [
       {
          "status":"READY",
          "message":"Running",
          "containerName":"echo",
          "instanceId":"0",
          "serviceName":"ECHO_SERVICE",
          "image":"<account>.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/my_echo_service_image:tutorial",
          "restartCount":0,
          "startTime":"2023-01-01T00:00:00Z"
       }
    ]
    
    Copy

    instanceId is the service instance ID to which the container belongs. If you run two instances of this service, the output includes two such objects and provides the container status for each service instance. In this case, the instance IDs are 0 and 1.

  • Sample output of a service that is running one instance and has three containers:

    [
       {
          "status":"READY",
          "message":"Running",
          "containerName":"some-proxy",
          "instanceId":"0",
          "serviceName":"example_service",
          "image":"<account>.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/my_service_image_proxy:tutorial",
          "restartCount":0,
          "startTime":"2023-01-01T00:00:00Z"
       },
       {
          "status":"READY",
          "message":"Running",
          "containerName":"some-server",
          "instanceId":"0",
          "serviceName":"example_service",
          "image":"<account>.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/my_server:tutorial",
          "restartCount":0,
          "startTime":"2023-01-01T00:00:00Z"
       },
       {
          "status":"READY",
          "message":"Running",
          "containerName":"movies",
          "instanceId":"0",
          "serviceName":"example_service",
          "image":"<account>.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/my_movies:tutorial",
          "restartCount":0,
          "startTime":"2023-01-01T00:00:00Z"
       }
    ]
    
    Copy

    The output includes status information for each container. All these containers belong to the single service instance that has the instance ID 0.

SYSTEM$GET_SERVICE_STATUS takes an optional timeout_secs argument. If timeout_secs is not specified or is specified with value 0, the function immediately returns the current status. If timeout_secs is specified, Snowflake waits until the service reaches a terminal state (READY or FAILED) within the specified time before returning the service status. If the service does not reach the terminal state within the specified time, Snowflake returns the current state at the end of the specified time interval.

The timeout you specify is an approximation based on when you expect the service to be ready, and this can help identify situations such as when Snowflake is waiting for other services and jobs to stop and free up resources in the specified compute pool before starting another service. The following SYSTEM$GET_SERVICE_STATUS function specifies a 10-second timeout:

call SYSTEM$GET_SERVICE_STATUS('echo_service', 10);
Copy

Accessing local container logs

Snowflake collects whatever your code in your application container outputs to standard output or standard errors. You should ensure that your code outputs useful information to debug a service.

Snowflake provides two ways to access these container logs:

  • Using the SYSTEM$GET_SERVICE_LOGS system function: Gives access to logs from a specific container. After a container exits, you can continue to access the logs using the system function for a short time. System functions are most useful during development and testing, when you are initially authoring a service or a job. For more information, see SYSTEM$GET_SERVICE_LOGS.

  • Using an event table: An event table gives you access to logs from multiple containers across all services. Snowflake persists the logs in the event table for later access. Event tables are best used for the retrospective analysis of services and jobs. For more information, see Using an event table.

Using SYSTEM$GET_SERVICE_LOGS

You provide the service name, instance ID, container name, and optionally the number of most recent log lines to retrieve. If only one service instance is running, the service instance ID is 0. For example, the following GET_SERVICE_LOGS command retrieves the trailing 10 lines from the log of a container named echo that belongs to instance 0 of a service named echo_service:

CALL SYSTEM$GET_SERVICE_LOGS('echo_service', '0', 'echo', 10);
Copy

If you don’t know service information (such as the instance ID or container name), you can first run GET_SERVICE_STATUS to get information about the service instances and containers running in each instance.

Example output:

+--------------------------------------------------------------------------+
| SYSTEM$GET_SERVICE_LOGS                                                  |
|--------------------------------------------------------------------------|
| 10.16.6.163 - - [11/Apr/2023 21:44:03] "GET /healthcheck HTTP/1.1" 200 - |
| 10.16.6.163 - - [11/Apr/2023 21:44:08] "GET /healthcheck HTTP/1.1" 200 - |
| 10.16.6.163 - - [11/Apr/2023 21:44:13] "GET /healthcheck HTTP/1.1" 200 - |
| 10.16.6.163 - - [11/Apr/2023 21:44:18] "GET /healthcheck HTTP/1.1" 200 - |
+--------------------------------------------------------------------------+
1 Row(s) produced. Time Elapsed: 0.878s

SYSTEM$GET_SERVICE_LOGS output has the following limitations:

  • It simply merges standard output and standard error streams, which makes it impossible to distinguish between regular output and error messages.

  • It reports the captured data for a specific container in a single service instance.

  • It only reports logs for a running container. The function cannot fetch logs from a previous container that was restarted or from a container of a service that is stopped or deleted.

  • The function returns up to 100 KB of data.

Using an event table

Snowflake can capture and record standard output and standard errors from your containers into the event table configured for your account. For more information, see Logging and Tracing Overview.

For example, the following SELECT query retrieves Snowflake service and job events recorded in the past hour:

SELECT TIMESTAMP, RESOURCE_ATTRIBUTES, RECORD_ATTRIBUTES, VALUE
FROM <current-event-table-for-your-account>
WHERE timestamp > dateadd(hour, -1, current_timestamp())
AND RESOURCE_ATTRIBUTES:"snow.executable.type" = 'SnowparkContainers'
ORDER BY timestamp DESC
LIMIT 10;
Copy

Snowflake recommends that you include a timestamp in the WHERE clause of event table queries, as shown in this example. This is particularly important because of the potential volume of data generated by various Snowflake components. By applying filters, you can retrieve a smaller subset of data, which improves query performance.

You control the level of logs (all, errors only, or none) that you want persisted in an event table by using the spec.logExporters field in the service specification file.

The events table includes the following columns, which provide useful information regarding the logs collected by Snowflake from your container:

  • TIMESTAMP: Shows when Snowflake collected the log.

  • RESOURCE_ATTRIBUTES: Each object in this column identifies which Snowflake service and container in the service generated the log. For example, it furnishes details such as the service name, container name, and compute pool name that were specified when the service was run.

    {
    "snow.containers.compute_pool.id": 549816068,
    "snow.containers.compute_pool.name": "TUTORIAL_COMPUTE_POOL",
    "snow.containers.container.name": "echo",
    "snow.containers.instance.name": "0",
    "snow.containers.restart.id": "cd0cf2",
    "snow.database.id": 549816076,
    "snow.database.name": "TUTORIAL_DB",
    "snow.executable.id": 980991015,
    "snow.executable.name": "ECHO_SERVICE",
    "snow.executable.type": "SnowparkContainers",
    "snow.schema.id": 549816076,
    "snow.schema.name": "DATA_SCHEMA"
    }
    
    Copy
  • RECORD_ATTRIBUTES: For a Snowflake service, it identifies an error source (standard output or standard error).

    { "log.iostream": "stdout" }
    
    Copy
  • VALUE: Standard output and standard error are broken into lines, and each line generates a record in the event table.

    "echo-service [2023-10-23 17:52:27,429] [DEBUG] Sending response: {'data': [[0, 'Joe said hello!']]}"
    

Configuring an event table

For more information, see Logging and Tracing Overview.

Using a service

You can grant usage on future services (services not yet created) to a role. For example:

GRANT USAGE ON FUTURE SERVICES IN [DATABASE <name> | SCHEMA <name>]TO ROLE <name>
Copy

After creating a service, you can use any of the following three supported methods to communicate with it:

  • Service function: Create a service function (UDF) that is associated with the service. Then, use the service function to communicate with a service from an SQL query. For an example, see Tutorial 1.

  • Ingress: Use public endpoints that are exposed by the service to access the service from outside Snowflake. In this case, Snowflake manages access control. For an example, see Tutorial 1.

  • Service-to-service communications: Use a Snowflake-assigned service DNS name for service-to-service communication. For an example, see Tutorial 3.

The following sections provide details.

Service functions: Using a service from an SQL query

A service function is a user-defined function (UDF) you create using CREATE FUNCTION. However, instead of writing the UDF code directly, you associate the UDF with your service. For example, in Tutorial 1, you create a service named echo_service that exposes one endpoint (echoendoint) as defined in the service specification:

spec:

  endpoints:
  - name: echoendpoint
    port: 8080
Copy

echoendpoint is a user-friendly endpoint name that represents the corresponding port (8080). To communicate with this service endpoint, you create a service function by providing the SERVICE and ENDPOINT parameters as shown:

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

The AS parameter provides the HTTP path to the service code. You get this path value from the service code (for example, see service.py in Tutorial 1).

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

When you invoke the service function, Snowflake directs the request to the associated service endpoint and path.

Note

A service function is used to communicate with a service, and not with a job. In other words, you can only associate a service (not a job) with a service function.

When you run multiple instances of your service, you can create a service function by specifying the optional MAX_BATCH_ROWS parameter to limit the batch size, the maximum rows that Snowflake sends in a batch to the service. For example, suppose MAX_BATCH_ROWS is 10 and you call my_echo_udf service function with 100 input rows. Snowflake partitions the input rows into batches, with each batch having at most 10 rows, and sends a series of requests to the service with the batch of rows in the request body. Configuring batch size can help when processing takes nontrivial time, and distributing rows across all available servers can also help.

You can use ALTER FUNCTION to alter a service function. The following ALTER FUNCTION command changes the service endpoint to which it associates and the batch size:

ALTER FUNCTION my_echo_udf(VARCHAR)
   SET SERVICE=other_service
   ENDPOINT=otherendpoint
   MAX_BATCH_ROWS=100
Copy

Data exchange format

For data exchange between a service function and a container, Snowflake follows the same format that external functions use (see Data Formats). For example, suppose you have data rows stored in a table (input_table):

"Alex", "2014-01-01 16:00:00"
"Steve", "2015-01-01 16:00:00"

To send this data to your service, you invoke the service function by passing these rows as parameters:

SELECT service_func(col1, col2) FROM input_table;
Copy

Snowflake sends a series of requests to the container, with batches of data rows in the request body in this format:

{
   "data":[
      [
         0,
         "Alex",
         "2014-01-01 16:00:00"
      ],
      [
         1,
         "Steve",
         "2015-01-01 16:00:00"
      ],
      …
      [
         <row_index>,
         "<column1>",
         "<column2>"
      ],
   ]
}
Copy

The container then returns the output in the following format:

{
   "data":[
      [0, "a"],
      [1, "b"],
      …
      [ row_index,  output_column1]
   ]
}
Copy

The example output shown assumes that the result is a one-column table with rows (“a”, “b” …).

When multiple service instances are running, you can create a service function using the MAX_BATCH_ROWS parameter to distribute the input rows for processing across all available servers.

Privileges required to create and manage service functions

To create and manage service functions, a role needs the following privileges:

  • To create a service function: The current role must have the USAGE privilege on the service being referenced.

  • To alter a service function: You can alter a service function and associate it with another service. The current role must have the USAGE privilege on the new service.

  • To use a service function: The current role must have the USAGE privilege on the service function, and the service function owner role must have the USAGE privilege on the associated service.

The following example script shows how you might grant permission to use a service function:

USE ROLE service_owner;
GRANT USAGE ON service service_db.my_schema.my_service TO ROLE func_owner;

USE ROLE func_owner;
CREATE OR REPLACE test_udf(v VARCHAR)
  RETURNS VARCHAR
  SERVICE=service_db.my_schema.my_service
  ENDPOINT=endpointname1
  AS '/run';

SELECT test_udf(col1) FROM some_table;

ALTER FUNCTION test_udf(VARCHAR) SET
  SERVICE = service_db.other_schema.other_service
  ENDPOINT=anotherendpoint;

GRANT USAGE ON FUNCTION test_udf(varchar) TO ROLE func_user;
USE ROLE func_user;
SELECT my_test_udf('abcd');
Copy

Ingress: Using a service from outside Snowflake

A service can expose one or more endpoints as public to allow users to use the service from the public web.

Note

For Public Private Preview, the ACCOUNTADMIN of your Snowflake account must execute the following command:

CREATE SECURITY INTEGRATION SNOWSERVICES_INGRESS_OAUTH
TYPE=oauth
OAUTH_CLIENT=snowservices_ingress
ENABLED=true;
Copy

Mark the endpoint as public in your service specification file:

spec
  ...
  endpoints
  - name: <endpoint name>
    port: <port number>
    public: true
Copy

For more information about service specifications, see Specification Reference.

Public endpoint access and authentication

Snowpark Container Services requires Snowflake OAuth to authenticate requests to public endpoints. For example, you will be required to sign in using username and password. Behind the scenes, your sign-in generates an OAuth token from Snowflake. The OAuth token is then used to send a request to the service endpoint.

Note

  • Because Snowpark Containers uses Snowflake OAuth to enable ingress, the default role of the user cannot be any of the privileged roles, including ACCOUNTADMIN, SECURITYADMIN, and ORGADMIN. For more information, see Blocking Specific Roles from Using the Integration.

  • Not everyone can access the public endpoints exposed by a service. Only users in the same Snowflake account having a role with the USAGE privilege on a service can access the public endpoints of the service.

You can access the public endpoint using a browser or programmatically:

  • Accessing a public endpoint by using a browser: When a browser is used to access a public endpoint, there is an automatic redirect for user authentication. You can explore Tutorial 1 to test this experience.

  • Accessing a public endpoint programmatically: The following Python example code uses the Snowflake Connector for Python to first generate a session token that represents your identity. The code then uses the session token to log in to the public endpoint.

    import snowflake.connector
    import requests
    
    ctx = snowflake.connector.connect(
       user="<username>",# username
       password="<password>", # insert password here
       account="<orgname>-<acct-name>",
       session_parameters={
          'PYTHON_CONNECTOR_QUERY_RESULT_FORMAT': 'json'
       })
    
    # Obtain a session token.
    token_data = ctx._rest._token_request('ISSUE')
    token_extract = token_data['data']['sessionToken']
    
    # Create a request to the ingress endpoint with authz.
    token = f'\"{token_extract}\"'
    headers = {'Authorization': f'Snowflake Token={token}'}
    # Set this to the ingress endpoint URL for your service
    url = 'http://<ingress_url>'
    
    # Validate the connection.
    response = requests.get(f'{url}', headers=headers)
    print(response.text)
    
    # Insert your code to interact with the application here
    
    Copy

    In the code:

    • If you don’t know your account information (<orgname>-<acctname>), see the Tutorial common setup.

    • You can get the ingress_url of the public endpoint exposed by the service by using SHOW ENDPOINTS.

User-specific headers in ingress requests

When a request for an ingress endpoint arrives, Snowflake automatically passes the following headers along with the HTTP request to the container.

Sf-Context-Current-User: <user_name>
Sf-Context-Current-User-Email: <email>
Copy

You container code can optionally read these headers, know who the caller is, and apply context-specific customization for different users.

Service-to-service communications

Services can communicate with each other using the DNS name that Snowflake automatically assigns to each service. For an example, see Tutorial 3.

The DNS name format is:

<service-name>.<schema-name>.<db-name>.snowflakecomputing.internal

Use SHOW SERVICES (or DESCRIBE SERVICE) to get the DNS name of a service. The preceding DNS name is a full name. Services created in the same schema can communicate using just the <service-name>. Services that are in the same database but different schemas must provide the schema name, such as <service-name>.<schema-name>.

Snowflake allows network communications between services created by the same role and blocks network communications between services created by different roles. If you want to prevent your services from communicating with each other (for reasons such as security), use different Snowflake roles to create those services.

DNS names have the following limitations:

  • Your database, schema, or service names must be valid DNS labels. (See also https://www.ietf.org/rfc/rfc1035.html#section-2.3.1). Otherwise, creating a service will fail.

  • Snowflake replaces an underscore (_) in the names (database, schema, and service name) by a dash (-) in the DNS name.

  • After creating a service, do not change the database or the schema name, because Snowflake will not update the DNS name of the service.

  • A DNS name is only for internal communications within Snowflake between services running in the same account. It is not accessible from the internet.

Updating the code for your service

After creating a service, use ALTER SERVICE to update the service code. You first upload modified application code to your image repository and then call ALTER SERVICE providing the service specification either inline or specifying the path to a specification file in Snowflake stage. For example:

  • Provide specification inline (partial specification is shown).

    ALTER SERVICE echo_service
    FROM SPECIFICATION $$
    spec:
      
      
    $$;
    
    Copy
  • Provide Snowflake stage file path:

    ALTER SERVICE echo_service
    FROM @tutorial_stage
    SPECIFICATION_FILE='echo_spec.yaml';
    
    Copy

Upon receiving the request, Snowflake redeploys the service using the new code.

ALTER SERVICE always uses the most recent version of the image from your repository. For example, if you upload multiple versions of an image named “echo_service:latest” or “echo_service:sometag”, Snowflake will use the latest uploaded image version.

You can use the DESCRIBE SERVICE command to find the image version the service is running. If you are the service owner, DESCRIBE SERVICE output includes the service specification, where you get the image digest (SHA256 of the image). For example:

spec:
containers:
- name: "echo"
   image: "orgname-acctname.registry-dev.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/my_echo_service_image:latest"
   sha256: "@sha256:8d912284f935ecf6c4753f42016777e09e3893eed61218b2960f782ef2b367af"
   env:
      SERVER_PORT: "8000"
      CHARACTER_NAME: "Bob"
   readinessProbe:
      port: 8000
      path: "/healthcheck"
endpoints:
- name: "echoendpoint"
   port: 8000
   public: true
Copy

Note

If you suspend and resume a service, Snowflake deploys the same image version, it is not a service update operation.

Privileges

Privilege

Usage

Notes

USAGE

To communicate with a service. Required for creating a service function, using public endpoints, and connecting from another service.

MONITOR

To monitor a service and get runtime status.

OPERATE

To suspend or resume a service.

OWNERSHIP

Full control over the service. Only a single role can hold this privilege on a specific object at a time.

ALL [ PRIVILEGES ]

Grants all privileges, except OWNERSHIP, on the service.