From 31afd99c0969002fe04982e40cf7a857960f7abf Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Wed, 18 Dec 2024 22:34:46 +0100 Subject: [PATCH] fix: Updated python-helm-demo example to use MinIO instead of GS (#4691) * Updated python-helm-demo example to use MinIO instead of GS Signed-off-by: Daniele Martinoli * Update examples/python-helm-demo/README.md Co-authored-by: Francisco Arceo Signed-off-by: Daniele Martinoli * Adding explicit wait to container to validate CI failures Signed-off-by: Daniele Martinoli * restored original conftest Signed-off-by: Daniele Martinoli --------- Signed-off-by: Daniele Martinoli Co-authored-by: Francisco Arceo --- examples/python-helm-demo/README.md | 159 +++++++++++++----- .../data/driver_stats_with_string.parquet | Bin 35310 -> 35693 bytes .../feature_repo/feature_store.yaml | 10 -- .../feature_repo/feature_store.yaml.template | 9 + examples/python-helm-demo/minio-dev.yaml | 128 ++++++++++++++ examples/python-helm-demo/minio.env | 7 + .../online_feature_store.yaml.template | 7 + .../python-helm-demo/test/feature_store.yaml | 7 + .../test_python_fetch.py | 10 +- 9 files changed, 287 insertions(+), 50 deletions(-) delete mode 100644 examples/python-helm-demo/feature_repo/feature_store.yaml create mode 100644 examples/python-helm-demo/feature_repo/feature_store.yaml.template create mode 100644 examples/python-helm-demo/minio-dev.yaml create mode 100644 examples/python-helm-demo/minio.env create mode 100644 examples/python-helm-demo/online_feature_store.yaml.template create mode 100644 examples/python-helm-demo/test/feature_store.yaml rename examples/python-helm-demo/{feature_repo => test}/test_python_fetch.py (73%) diff --git a/examples/python-helm-demo/README.md b/examples/python-helm-demo/README.md index 90469e746d..078550ae39 100644 --- a/examples/python-helm-demo/README.md +++ b/examples/python-helm-demo/README.md @@ -3,87 +3,168 @@ For this tutorial, we set up Feast with Redis. -We use the Feast CLI to register and materialize features, and then retrieving via a Feast Python feature server deployed in Kubernetes +We use the Feast CLI to register and materialize features from the current machine, and then retrieving via a +Feast Python feature server deployed in Kubernetes ## First, let's set up a Redis cluster 1. Start minikube (`minikube start`) -2. Use helm to install a default Redis cluster +1. Use helm to install a default Redis cluster ```bash helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update helm install my-redis bitnami/redis ``` ![](redis-screenshot.png) -3. Port forward Redis so we can materialize features to it +1. Port forward Redis so we can materialize features to it ```bash kubectl port-forward --namespace default svc/my-redis-master 6379:6379 ``` -4. Get your Redis password using the command (pasted below for convenience). We'll need this to tell Feast how to communicate with the cluster. +1. Get your Redis password using the command (pasted below for convenience). We'll need this to tell Feast how to communicate with the cluster. ```bash export REDIS_PASSWORD=$(kubectl get secret --namespace default my-redis -o jsonpath="{.data.redis-password}" | base64 --decode) echo $REDIS_PASSWORD ``` +## Then, let's set up a MinIO S3 store +Manifests have been taken from [Deploy Minio in your project](https://ai-on-openshift.io/tools-and-applications/minio/minio/#deploy-minio-in-your-project). + +1. Deploy MinIO instance: + ``` + kubectl apply -f minio-dev.yaml + ``` + +1. Forward the UI port: + ```console + kubectl port-forward svc/minio-service 9090:9090 + ``` +1. Login to (localhost:9090)[http://localhost:9090] as `minio`/`minio123` and create bucket called `feast-demo`. +1. Stop previous port forwarding and forward the API port instead: + ```console + kubectl port-forward svc/minio-service 9000:9000 + ``` + ## Next, we setup a local Feast repo -1. Install Feast with Redis dependencies `pip install "feast[redis]"` -2. Make a bucket in GCS (or S3) -3. The feature repo is already setup here, so you just need to swap in your GCS bucket and Redis credentials. - We need to modify the `feature_store.yaml`, which has two fields for you to replace: +1. Install Feast with Redis dependencies `pip install "feast[redis,aws]"` +1. The feature repo is already setup here, so you just need to swap in your Redis credentials. + We need to modify the `feature_store.yaml`, which has one field for you to replace: + ```console + sed "s/_REDIS_PASSWORD_/${REDIS_PASSWORD}/" feature_repo/feature_store.yaml.template > feature_repo/feature_store.yaml + cat feature_repo/feature_store.yaml + ``` + + Example repo: ```yaml - registry: gs://[YOUR GCS BUCKET]/demo-repo/registry.db + registry: s3://localhost:9000/feast-demo/registry.db project: feast_python_demo - provider: gcp + provider: local online_store: type: redis - # Note: this would normally be using instance URL's to access Redis - connection_string: localhost:6379,password=[YOUR PASSWORD] + connection_string: localhost:6379,password=**** offline_store: type: file entity_key_serialization_version: 2 ``` -4. Run `feast apply` from within the `feature_repo` directory to apply your local features to the remote registry - - Note: you may need to authenticate to gcloud first with `gcloud auth login` -5. Materialize features to the online store: +1. To run `feast apply` from the current machine we need to define the AWS credentials to connect the MinIO S3 store, which +are defined in [minio.env](./minio.env): + ```console + source minio.env + cd feature_repo + feast apply + ``` +1. Let's validate the setup by running some queries + ```console + feast entities list + feast feature-views list + ``` +1. Materialize features to the online store: ```bash + cd feature_repo CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S") feast materialize-incremental $CURRENT_TIME ``` ## Now let's setup the Feast Server -1. Add the gcp-auth addon to mount GCP credentials: - ```bash - minikube addons enable gcp-auth - ``` -2. Add Feast's Python/Go feature server chart repo +1. Add Feast's Python feature server chart repo ```bash helm repo add feast-charts https://feast-helm-charts.storage.googleapis.com helm repo update ``` -3. For this tutorial, because we don't have a direct hosted endpoint into Redis, we need to change `feature_store.yaml` to talk to the Kubernetes Redis service - ```bash - sed -i '' 's/localhost:6379/my-redis-master:6379/g' feature_store.yaml - ``` -4. Install the Feast helm chart: `helm install feast-release feast-charts/feast-feature-server --set feature_store_yaml_base64=$(base64 feature_store.yaml)` - > **Dev instructions**: if you're changing the java logic or chart, you can do - 1. `eval $(minikube docker-env)` - 2. `make build-feature-server-dev` - 3. `helm install feast-release ../../../infra/charts/feast-feature-server --set image.tag=dev --set feature_store_yaml_base64=$(base64 feature_store.yaml)` -5. (Optional): check logs of the server to make sure it’s working +1. For this tutorial, we'll use a predefined configuration where we just needs to inject the Redis service password: + ```console + sed "s/_REDIS_PASSWORD_/$REDIS_PASSWORD/" online_feature_store.yaml.template > online_feature_store.yaml + cat online_feature_store.yaml + ``` + As you see, the connection points to `my-redis-master:6379` instead of `localhost:6379`. + +1. Install the Feast helm chart: + ```console + helm upgrade --install feast-online feast-charts/feast-feature-server \ + --set fullnameOverride=online-server --set feast_mode=online \ + --set feature_store_yaml_base64=$(base64 -i 'online_feature_store.yaml') + ``` +1. Patch the deployment to include MinIO settings: + ```console + kubectl patch deployment online-server --type='json' -p='[ + { + "op": "add", + "path": "/spec/template/spec/containers/0/env/-", + "value": { + "name": "AWS_ACCESS_KEY_ID", + "value": "minio" + } + }, + { + "op": "add", + "path": "/spec/template/spec/containers/0/env/-", + "value": { + "name": "AWS_SECRET_ACCESS_KEY", + "value": "minio123" + } + }, + { + "op": "add", + "path": "/spec/template/spec/containers/0/env/-", + "value": { + "name": "AWS_DEFAULT_REGION", + "value": "default" + } + }, + { + "op": "add", + "path": "/spec/template/spec/containers/0/env/-", + "value": { + "name": "FEAST_S3_ENDPOINT_URL", + "value": "http://minio-service:9000" + } + } + ]' + kubectl wait --for=condition=available deployment/online-server --timeout=2m + ``` +1. (Optional): check logs of the server to make sure it’s working ```bash - kubectl logs svc/feast-release-feast-feature-server + kubectl logs svc/online-server ``` -6. Port forward to expose the grpc endpoint: +1. Port forward to expose the grpc endpoint: ```bash - kubectl port-forward svc/feast-release-feast-feature-server 6566:80 + kubectl port-forward svc/online-server 6566:80 ``` -7. Run test fetches for online features:8. - - First: change back the Redis connection string to allow localhost connections to Redis +1. Run test fetches for online features:8. ```bash - sed -i '' 's/my-redis-master:6379/localhost:6379/g' feature_store.yaml + source minio.env + cd test + python test_python_fetch.py ``` - - Then run the included fetch script, which fetches both via the HTTP endpoint and for comparison, via the Python SDK - ```bash - python test_python_fetch.py + + Output example: + ```console + --- Online features with SDK --- + WARNING:root:_list_feature_views will make breaking changes. Please use _list_batch_feature_views instead. _list_feature_views will behave like _list_all_feature_views in the future. + conv_rate : [0.6799587607383728, 0.9761165976524353] + driver_id : [1001, 1002] + + --- Online features with HTTP endpoint --- + conv_rate : [0.67995876 0.9761166 ] + driver_id : [1001 1002] ``` \ No newline at end of file diff --git a/examples/python-helm-demo/feature_repo/data/driver_stats_with_string.parquet b/examples/python-helm-demo/feature_repo/data/driver_stats_with_string.parquet index 83b8c31aa51a5bc273fdce897fbe8a8473179d92..ae8f17e45d3b840f99f0cee550030a913c50b232 100644 GIT binary patch delta 24528 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 delta 25529 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 + quay.io/minio/minio:RELEASE.2023-06-19T19-52-50Z + args: + - server + - /data + - --console-address + - :9090 + restartPolicy: Always + terminationGracePeriodSeconds: 30 + dnsPolicy: ClusterFirst + securityContext: {} + schedulerName: default-scheduler + strategy: + type: Recreate + revisionHistoryLimit: 10 + progressDeadlineSeconds: 600 +--- +kind: Service +apiVersion: v1 +metadata: + name: minio-service +spec: + ipFamilies: + - IPv4 + ports: + - name: api + protocol: TCP + port: 9000 + targetPort: 9000 + - name: ui + protocol: TCP + port: 9090 + targetPort: 9090 + internalTrafficPolicy: Cluster + type: ClusterIP + ipFamilyPolicy: SingleStack + sessionAffinity: None + selector: + app: minio \ No newline at end of file diff --git a/examples/python-helm-demo/minio.env b/examples/python-helm-demo/minio.env new file mode 100644 index 0000000000..b19ec5083f --- /dev/null +++ b/examples/python-helm-demo/minio.env @@ -0,0 +1,7 @@ +export AWS_ACCESS_KEY_ID=minio +export AWS_DEFAULT_REGION=default +#export AWS_S3_BUCKET=feast-demo +#export AWS_S3_ENDPOINT=http://localhost:9000 +export FEAST_S3_ENDPOINT_URL=http://localhost:9000 +export AWS_SECRET_ACCESS_KEY=minio123 + diff --git a/examples/python-helm-demo/online_feature_store.yaml.template b/examples/python-helm-demo/online_feature_store.yaml.template new file mode 100644 index 0000000000..7acb9582c5 --- /dev/null +++ b/examples/python-helm-demo/online_feature_store.yaml.template @@ -0,0 +1,7 @@ +project: feast_python_demo +provider: local +registry: s3://feast-demo/registry.db +online_store: + type: redis + connection_string: my-redis-master:6379,password=_REDIS_PASSWORD_ +entity_key_serialization_version: 2 \ No newline at end of file diff --git a/examples/python-helm-demo/test/feature_store.yaml b/examples/python-helm-demo/test/feature_store.yaml new file mode 100644 index 0000000000..13e99873ee --- /dev/null +++ b/examples/python-helm-demo/test/feature_store.yaml @@ -0,0 +1,7 @@ +registry: s3://feast-demo/registry.db +project: feast_python_demo +provider: local +online_store: + path: http://localhost:6566 + type: remote +entity_key_serialization_version: 2 \ No newline at end of file diff --git a/examples/python-helm-demo/feature_repo/test_python_fetch.py b/examples/python-helm-demo/test/test_python_fetch.py similarity index 73% rename from examples/python-helm-demo/feature_repo/test_python_fetch.py rename to examples/python-helm-demo/test/test_python_fetch.py index f9c7c62f4f..715912422f 100644 --- a/examples/python-helm-demo/feature_repo/test_python_fetch.py +++ b/examples/python-helm-demo/test/test_python_fetch.py @@ -1,6 +1,7 @@ from feast import FeatureStore import requests import json +import pandas as pd def run_demo_http(): @@ -14,7 +15,14 @@ def run_demo_http(): r = requests.post( "http://localhost:6566/get-online-features", data=json.dumps(online_request) ) - print(json.dumps(r.json(), indent=4, sort_keys=True)) + + resp_data = json.loads(r.text) + records = pd.DataFrame.from_records( + columns=resp_data["metadata"]["feature_names"], + data=[[r["values"][i] for r in resp_data["results"]] for i in range(len(resp_data["results"]))] + ) + for col in sorted(records.columns): + print(col, " : ", records[col].values) def run_demo_sdk():