Snowpark Container Servicesでシークレットを使う


Snowpark Container ServicesはSnowflakeのインフラ上でKubernetesクラスタを構築できる機能です。  
略してSPCSと呼ばれることもあります。  

## 本記事のサマリ

Snowpark Container Servicesのコンテナでパスワードのようなシークレットを扱いたい場合はシークレットオブジェクトを使う。  
シークレットオブジェクトを作成して、そのシークレットオブジェクトを```CREATE SERVICE```するときに指定すると、コンテナにはその指定のオプションに応じて環境変数かローカルファイルとしてシークレットが渡される。  

## 問題

コンテナからSnowflakeや外部サービスに接続するときにユーザ名とパスワードを使いたいのですが、組織のルールでそれらをコンテナ内、あるいはコンテナを起動するためのコマンドリストに記載できないことがあります。  
このような組織では多くの場合、コンテナを起動するときにユーザ名とパスワードを手動で入力するか、セキュアな場所に記載しておいたユーザ名とパスワードをコンテナに自動的に渡すことが求められます。  

## 公式ドキュメント

シークレットオブジェクトを使うことで対応できます。  
[ここ](https://docs.snowflake.com/en/developer-guide/snowpark-container-services/additional-considerations-services-jobs#passing-secrets-as-environment-variables)に記載があります。  

https://docs.snowflake.com/en/developer-guide/snowpark-container-services/additional-considerations-services-jobs#passing-secrets-as-environment-variables

```CREATE SECRET```については[こちら](https://docs.snowflake.com/ja/sql-reference/sql/create-secret)。

https://docs.snowflake.com/ja/sql-reference/sql/create-secret

## 試してみる

シークレットを環境変数として渡す方法を試してみます。  

まず、コンテナ内の環境変数を確認するためのPythonプログラムを作成しておきます。  
今回は[SPCSのチュートリアル](https://docs.snowflake.com/en/developer-guide/snowpark-container-services/overview-tutorials)のサンプルプログラムを改造してブラウザから確認できるものを作成してみました。  

```py:test.py
from flask import Flask
from flask import request
from flask import make_response
from flask import render_template
import os
import sys

# ホストOSからアクセスするために0.0.0.0でListenする
SERVICE_HOST = os.getenv('SERVER_HOST', '0.0.0.0')
# デフォルトポート：8080
SERVICE_PORT = os.getenv('SERVER_PORT', 8080)

app = Flask(__name__)

# 環境変数を取得して配列に追加する
def enum_env():
    result = []
    for env_key, env_value in os.environ.items():
        result.append([env_key, env_value])
    return result

# トップディレクトリにアクセスしたときに呼び出される関数を定義
@app.route("/", methods=["GET", "POST"])
def show_env():
    # 出力データを生成
    output_rows = enum_env()

    # 出力データをレスポンスに変換
    response = make_response({"data": output_rows})
    response.headers['Content-type'] = 'application/json'
    return response

if __name__ == '__main__':
    app.run(host=SERVICE_HOST, port=SERVICE_PORT)
```

このPythonプログラムをコンテナ化します。  

```dockerfile:Dockerfile
ARG BASE_IMAGE=python:3.10-slim-buster
FROM $BASE_IMAGE
COPY test.py ./
RUN pip install --upgrade pip && \
    pip install flask
CMD ["python3", "test.py"]
```

コンテナをビルドしてSnowflakeのイメージリポジトリにプッシュします。ローカルで動作確認してもいいかもしれません。  

```sh
# ビルドする
docker build --rm=true --tag=test:latest .
docker images
# ローカルで動作確認する
docker run -p 8080:8080 test:latest
# SHOW IMAGE REPOGITORIESでリポジトリURLを確認してタグに付与する
docker tag test:latest xxxxxxx-test-mumbai.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/test:latest
docker images
# イメージリポジトリにログインする
docker login xxxxxxx-test-mumbai.registry.snowflakecomputing.com -u sakatoku
# プッシュする
docker push xxxxxxx-test-mumbai.registry.snowflakecomputing.com/tutorial_db/data_schema/tutorial_repository/test:latest

```

次からはSnowflakeでの操作です。
まず、シークレットオブジェクトを作成します。  
シークレットオブジェクトはいずれかのスキーマの配下に作成することになります。  

```sql:Snowsight
CREATE SECRET TUTORIAL_DB.DATA_SCHEMA.SECRET_CONNECTION_MUMBAI
    TYPE = password
    USERNAME = 'sakatoku'
    PASSWORD = 'fugafuga';
```

```CREATE SERVICE```するときにシークレットオブジェクトと参照するキー、そのシークレットをどの環境変数に渡すかを指定します。  
参照するキーというのは上記のシークレットオブジェクトであれば```USERNAME```と```PASSWORD```です。  
今回はそれぞれ```MUMBAI_USERNAME```と```MUMBAI_PASSWORD```という環境変数に渡すように指定します。  

```sql:Snowsight
-- サービスを起動する
CREATE SERVICE test
  IN COMPUTE POOL tutorial_compute_pool
  FROM SPECIFICATION $$
    spec:
      containers:
      - name: test
        image: /tutorial_db/data_schema/tutorial_repository/test:latest
        secrets:
        - snowflakeSecret: TUTORIAL_DB.DATA_SCHEMA.SECRET_CONNECTION_MUMBAI
          secretKeyRef: USERNAME
          envVarName: MUMBAI_USERNAME
        - snowflakeSecret: TUTORIAL_DB.DATA_SCHEMA.SECRET_CONNECTION_MUMBAI
          secretKeyRef: PASSWORD
          envVarName: MUMBAI_PASSWORD
      endpoints:
      - name: testendpoint
        port: 8080
        public: true
      $$
   MIN_INSTANCES=1
   MAX_INSTANCES=1;

-- このサービスのパブリックURL(ingress_url)を確認する。サービスを起動してから1～2分で確認できるようになる
SHOW ENDPOINTS IN SERVICE test;
```

確認したパブリックURLにブラウザでアクセスすると、コンテナ内の環境変数を列挙したJSONが得られました。  
```MUMBAI_USERNAME```と```MUMBAI_PASSWORD```が渡されていることが分かります。  

```json
{
  "data": [
    [ "PATH", "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ],
    [ "HOSTNAME", "statefulset-0" ],
    [ "LANG", "C.UTF-8" ],
    [ "GPG_KEY", "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD" ],
    [ "PYTHON_VERSION", "3.10.12" ],
    [ "PYTHON_PIP_VERSION", "23.0.1" ],
    [ "PYTHON_SETUPTOOLS_VERSION", "65.5.1" ],
    [ "PYTHON_GET_PIP_URL", "https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.py" ],
    [ "PYTHON_GET_PIP_SHA256", "96461deced5c2a487ddc65207ec5a9cffeca0d34e7af7ea1afc470ff0d746207" ],
    [ "NVIDIA_VISIBLE_DEVICES", "none" ],
    [ "SNOWFLAKE_DATABASE", "TUTORIAL_DB" ],
    [ "SNOWFLAKE_HOST", "snowflake.ap-south-1.aws.snowflakecomputing.com" ],
    [ "SNOWFLAKE_SERVICE_NAME", "TEST" ],
    [ "MUMBAI_USERNAME", "sakatoku" ],
    [ "MUMBAI_PASSWORD", "fugafuga" ],
    [ "SNOWFLAKE_ACCOUNT", "XX12345" ],
    [ "SNOWFLAKE_PORT", "443" ],
    [ "SNOWFLAKE_SCHEMA", "DATA_SCHEMA" ],
    [ "KUBERNETES_PORT_443_TCP_PORT", "443" ],
    [ "KUBERNETES_PORT_443_TCP_ADDR", "10.96.0.1" ],
    [ "SERVICE_PORT_8080_TCP_PORT", "8080" ],
    [ "KUBERNETES_PORT", "tcp://10.96.0.1:443" ],
    [ "KUBERNETES_PORT_443_TCP_PROTO", "tcp" ],
    [ "SERVICE_PORT_8080_TCP_ADDR", "10.99.11.197" ],
    [ "KUBERNETES_SERVICE_PORT", "443" ],
    [ "KUBERNETES_PORT_443_TCP", "tcp://10.96.0.1:443" ],
    [ "SERVICE_SERVICE_HOST", "10.99.11.197" ],
    [ "SERVICE_SERVICE_PORT_TESTENDPOINT", "8080" ],
    [ "SERVICE_PORT", "tcp://10.99.11.197:8080" ],
    [ "SERVICE_SERVICE_PORT", "8080" ],
    [ "SERVICE_PORT_8080_TCP", "tcp://10.99.11.197:8080" ],
    [ "SERVICE_PORT_8080_TCP_PROTO", "tcp" ],
    [ "KUBERNETES_SERVICE_HOST", "10.96.0.1" ],
    [ "KUBERNETES_SERVICE_PORT_HTTPS", "443" ],
    [ "HOME", "/root" ],
    [ "WERKZEUG_SERVER_FD", "3" ]
  ]
}
```

:::note info
上記は見やすいように整形しています。
:::

## 別解

Snowflakeに接続するだけであれば、コンテナ内に自動的に生成されるトークンを利用することが推奨されているようです。  

```py:
def get_login_token():
  with open("/snowflake/session/token", "r") as f:
    return f.read()
```

もちろん、ローカルで動作確認するときには動きません。  
そのため、SPCSのチュートリアルではローカルで動作確認するときには```docker```コマンドのオプションでユーザ名とパスワードを渡しています。これに対応してサンプルプログラムでは```/snowflake/session/token```の有無で接続処理を分岐させています。  

## おわりに

シークレットオブジェクトを使うことで、パスワードのハードコーディングが避けられるので安心ですね。  

## 本記事について

本記事は[アウトプットガチ勢が作った高速記事作成フレームワーク](https://qiita.com/Sicut_study/items/9b20ac5ded003cb6d55a)を参考にしています。  
高速アウトプットを身に着けたい！