Let’s take a look at what we have disuccsed so far:

1 - Setting up MLFlow with CLoud Run with IAP

2 - Setting up Kubeflow with GKE and IAP

3 - Setting up ML endpoint for prediction with FastAPI and Cloud Run, also with Vertex AI Endpoint Serving and KServe

Now, in this last article of the MLOps series, we will look in to Streamlit app for machine learning prediction in Cloud Run and GKE.

Architecture

Here is an overview of the MLOPS workflow.

png

And in today’s post, we will focus on this part.

png

This article will cover the following topics:

1 - Deploying a Streamlit app to Cloud Run using a MLFlow endpoint deployed on Cloud Run with FastAPI

2 - Deploying a Streamlit app to Cloud Run using Vertex AI Endpoint Serving

3 - Deploying a Streamlit app to GKE using the KServe endpoint deployed on GKE

What is Streamlit?

Streamlit is an open-source app framework. It allows you to create web applications for various use cases, including machine learning models, with minimal effort. It is designed to be easy to use and allows you to create interactive web applications with just a few lines of code.

Setting up the Streamlit app with MLFlow endpoint on Cloud Run

To follow along with this part of the article, you can find the code on the GitHub repository here

Folder strucutre is as below:

.
├── Dockerfile
├── README.md
├── app
│   ├── basic_auth.py
│   ├── gcp_secrets.py
│   ├── main.py
│   └── settings.py
├── deploy
│   └── production
│       └── service.yaml
├── poetry.lock
├── pyproject.toml
└── skaffold.yaml

The settings.py file is as below, note that I have obtained the CREDENTIALS by decoding the base64 encoded string of the username and password for the MLFlow server. Since when we build the FastAPI app in our previous post, we have added the logic to pass the credentials on the header of the requests.

from pydantic_settings import BaseSettings
from gcp_secrets import GCPSecrets
import base64

secrets = GCPSecrets()


class AppConfig(BaseSettings):
    MLFLOW_TRACKING_USERNAME: str = secrets.get_secret("mlflow_server_username")
    MLFLOW_TRACKING_PASSWORD: str = secrets.get_secret("mlflow_server_password")
    MLFLOW_ENDPOINT: str = secrets.get_secret("mlflow_endpoint")
    CRED_STR: str = f"{MLFLOW_TRACKING_USERNAME}:{MLFLOW_TRACKING_PASSWORD}"
    CRED_BYTES: bytes = str.encode(CRED_STR)
    CREDENTIALS: str = base64.b64encode(CRED_BYTES).decode("utf-8")


def get_config() -> AppConfig:
    return AppConfig()


app_config = get_config()

Thus in main.py file, when requests are made, it looks something like this

if st.button("Predict"):
    response = requests.post(
        f"{app_config.MLFLOW_ENDPOINT}/invocations",
        json=data,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Basic {app_config.CREDENTIALS}",
        },
    )
    response.raise_for_status()
    prediction = response.json()

The service.yaml file is as below:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: mlflow-wine-streamlit-app
  labels:
    cloud.googleapis.com/location: us-central1
  annotations:
    run.googleapis.com/ingress: all
    run.googleapis.com/ingress-status: all
spec:
  template:
    metadata:
      annotations:
        run.googleapis.com/vpc-access-egress: all-traffic
        run.googleapis.com/vpc-access-connector: projects/gcp-prj-id-123/locations/us-central1/connectors/custom-vpc-connector
    spec:
      serviceAccountName: svc@developer.gserviceaccount.com
      containers:
      - image: us-central1-docker.pkg.dev/gcp-prj-id-123/mlflow-gcp/mlflow-wine-streamlit-app
        ports:
        - name: http1
          containerPort: 80
        env:
        - name: GCP_PROJECT_ID
          value: gcp-prj-id-123
        - name: ENV
          value: prod

Dockerfile is as below

FROM python:3.11-slim

WORKDIR /usr/src/app

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    POETRY_VERSION=1.7.1 \
    USERNAME=nonroot

RUN adduser $USERNAME
USER $USERNAME

ENV HOME=/home/$USERNAME
ENV PATH="$HOME/.local/bin:$PATH"

RUN pip install pipx
RUN pipx install poetry==${POETRY_VERSION}

COPY ./poetry.lock pyproject.toml /usr/src/app/
RUN poetry install -nv --no-root

COPY app .

EXPOSE 80

CMD ["poetry", "run", "streamlit", "run", "main.py", "--server.port=80", "--server.address=0.0.0.0"]

And the skaffold.yaml file is as below:

apiVersion: skaffold/v4beta2
kind: Config
metadata:
  name: mlflow-wine-streamlit-app
build:
  artifacts:
  - image: us-central1-docker.pkg.dev/gcp-prj-id-123/mlflow-gcp/mlflow-wine-streamlit-app
    docker:
      dockerfile: Dockerfile
    platforms:
      - "linux/amd64"
profiles:
- name: production
  manifests:
    rawYaml:
    - deploy/production/service.yaml
deploy:
  cloudrun:
    projectid: gcp-prj-id-123
    region: us-central1

To deploy the Streamlit app to Cloud Run, you can run the following command:

skaffold run -p production

png

Setting up the Streamlit app with Vertex AI endpoint on Cloud Run

To follow along with this part, you can find the code on the GitHub repository here

The set up is similar to the above, but instead of calling the MLFlow endpoint, we will call the Vertex AI endpoint.

The settings.py file will look something like this:

from pydantic_settings import BaseSettings

from gcp_secrets import GCPSecrets

secrets = GCPSecrets()


class AppConfig(BaseSettings):
    MLFLOW_TRACKING_USERNAME: str = secrets.get_secret("mlflow_server_username")
    MLFLOW_TRACKING_PASSWORD: str = secrets.get_secret("mlflow_server_password")

    GCP_CREDS: str = secrets.get_secret("svc-creds-json")

    PROJECT_ID: str = "gcp-prj-id-123"
    REGION: str = "us-central1"
    ENDPOINT_ID: str = "1234567"
    ENDPOINT_URL: str = f"https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{REGION}/endpoints/{ENDPOINT_ID}:predict"


def get_config() -> AppConfig:
    return AppConfig()


app_config = get_config()

To obtain a requests header, we will need to do the following:

SCOPES: list = ["https://www.googleapis.com/auth/cloud-platform"]
GCP_CREDS_DICT = extract_json_string(app_config.GCP_CREDS)
credentials = service_account.Credentials.from_service_account_info(
    GCP_CREDS_DICT, scopes=SCOPES
)
credentials.refresh(Request())
HEADERS = {
    "Authorization": f"Bearer {credentials.token}",
    "Content-Type": "application/json",
}

When making prediction, it will look something like this:

if st.button("Predict"):
  response = requests.post(
      f"{app_config.ENDPOINT_URL}",
      headers=HEADERS,
      json=inference_input,
  )
  response.raise_for_status()
  prediction = response.json()
  pred_result = prediction["predictions"][0]

To deploy the Streamlit app to Cloud Run, you can run the following command:

skaffold run -p production

png

Setting up the Streamlit app with KServe endpoint on GKE

To follow along with this part, you can find the code on the GitHub repository here

Let’s jump right in.

The folder structure is as below:

.
├── Dockerfile
├── README.md
├── app
│   ├── basic_auth.py
│   ├── gcp_secrets.py
│   ├── main.py
│   └── settings.py
├── deploy
│   ├── common
│   │   ├── config.yaml
│   │   ├── deployment.yaml
│   │   ├── kustomization.yaml
│   │   ├── service.yaml
│   │   └── serviceaccount.yaml
│   └── production
│       └── kustomization.yaml
├── poetry.lock
├── pyproject.toml
└── skaffold.yaml

Before we start configuring the deployment artifacts, there is a few things we need to do

For deployment, in order to pull the image from the Google Container Registry, you need to create/ make sure your node pool has the scopes cloud-platform and storage-full You can do this by running the following command:

gcloud container node-pools create default-node-pool-new \
    --cluster=kubeflow  \
    --zone=us-central1-c \
    --scopes=cloud-platform,storage-full \        
    --num-nodes=2

png

Create a json key for accessing the GCP Artifacts Registry

gcloud iam service-accounts keys create key.json \                               
  --iam-account=svc@gcp-prj-id-123.iam.gserviceaccount.com
kubectl create secret docker-registry gar-json-key \                             
  --docker-server=us-central1-docker.pkg.dev \
  --docker-username=_json_key \
  --docker-password="$(cat key.json)" \
  --docker-email=user@email.com

Reference the secret in the deployment.yaml file as shown below:

spec:
  serviceAccountName: default-editor
  imagePullSecrets:
  - name: gar-json-key

Also in serviceaccount.yaml file,

imagePullSecrets:
- name: gar-json-key

Ensure the service account has the role artifactregistry.reader

gcloud projects add-iam-policy-binding gcp-prj-id-123 \
    --member "serviceAccount:svc@gcp-prj-id-123.iam.gserviceaccount.com" \
    --role "roles/artifactregistry.reader"
gcloud artifacts repositories add-iam-policy-binding mlflow-gcp \                 
    --location=us-central1 \
    --member=serviceAccount:svc@gcp-prj-id-123.iam.gserviceaccount.com \
    --role="roles/artifactregistry.reader"

Dockerfile is as below:

FROM python:3.11-slim

WORKDIR /usr/src/app

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    POETRY_VERSION=1.7.1 \
    USERNAME=nonroot

RUN adduser $USERNAME
USER $USERNAME

ENV HOME=/home/$USERNAME
ENV PATH="$HOME/.local/bin:$PATH"

RUN pip install pipx
RUN pipx install poetry==${POETRY_VERSION}

COPY ./poetry.lock pyproject.toml /usr/src/app/
RUN poetry install -nv --no-root

COPY app .

EXPOSE 8501

CMD ["poetry", "run", "streamlit", "run", "main.py", "--server.port=8501", "--server.address=0.0.0.0"]

The skafoold.yaml file is as below:

apiVersion: skaffold/v2beta28
kind: Config
metadata:
  name: kserve-k8s-streamlit-app
build:
  artifacts:
  - image: us-central1-docker.pkg.dev/gcp-prj-id-123/mlflow-gcp/kserve-k8s-streamlit-app
    docker:
      dockerfile: Dockerfile
deploy:
  kubeContext: gke_gcp-prj-id-123_us-central1-c_kubeflow
  kustomize:
    paths:
    - deploy/common
profiles:
- name: production
  deploy:
    kustomize:
      paths:
      - deploy/production

The deploy/common/config.yaml file is as below:

apiVersion: v1
kind: ConfigMap
metadata:
  name: kserve-k8s-streamlit-config
data:
  GCP_PROJECT_ID: "gcp-prj-id-123"
  ENV: "prod"

The deploy/common/deployment.yaml file is as below:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kserve-k8s-streamlit
  namespace: your-namespace
  labels:
    app: kserve-k8s-streamlit
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kserve-k8s-streamlit
  template:
    metadata:
      labels:
        app: kserve-k8s-streamlit
    spec:
      serviceAccountName: default-editor
      imagePullSecrets:
      - name: gar-json-key
      nodeSelector:
        cloud.google.com/gke-nodepool: default-node-pool-new
      containers:
      - name: kserve-k8s-streamlit
        image: us-central1-docker.pkg.dev/gcp-prj-id-123/mlflow-gcp/kserve-k8s-streamlit-app
        envFrom:
          - configMapRef:
              name: kserve-k8s-streamlit-config
        resources:
          limits:
            memory: 200Mi
            cpu: 50m
          requests:
            memory: 100Mi
            cpu: 50m
        ports:
        - containerPort: 8501
        volumeMounts:
        - name: kube-api-access
          mountPath: /var/run/secrets/kubernetes.io/serviceaccount
          readOnly: true
      volumes:
      - name: kube-api-access
        projected:
          sources:
          - serviceAccountToken:
              path: token
              expirationSeconds: 3600
          - configMap:
              name: kube-root-ca.crt
              items:
              - key: ca.crt
                path: ca.crt
          - downwardAPI:
              items:
              - path: namespace
                fieldRef:
                  fieldPath: metadata.namespace

The deploy/common/service.yaml file is as below:

apiVersion: v1
kind: Service
metadata:
  name: streamlit-app-service
  namespace: your-namespace
spec:
  selector:
    app: kserve-k8s-streamlit
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8501
  type: LoadBalancer

The deploy/common/serviceaccount.yaml file is as below:

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    iam.gke.io/gcp-service-account: svc@gcp-prj-id-123.iam.gserviceaccount.com
  name: default-editor
  namespace: your-namespace
imagePullSecrets:
- name: gar-json-key

The deploy/production/kustomization.yaml file is as below:

namespace: your-namespace
resources:
  - deployment.yaml
  - config.yaml
  - serviceaccount.yaml
  - service.yaml

To deploy the Streamlit app to GKE, you can run the following command:

skaffold run -p production

png

To get the external IP address of the service, you can run the following command:

kubectl get svc streamlit-app-service -n your-namespace

png

This will give you the external IP address of the service, which you can use to access the Streamlit app by http://<EXTERNAL-IP>:80

png

And that’s it, now we have learnt how to deploy streamlit application using various endpoints and with tools such as Cloud Run and GKE.

Thank you for reading and have a nice day!

If you want to support my work,

Buy me a coffee

Honestly, if you made it this far you already made my day :)