Install
Datastrato Enterprise installs from a Helm chart. The chart and every image it deploys are served from the Datastrato release registry on Amazon ECR, and one pull-only credential covers both. Follow the steps in order. Each one ends with something you can check before moving on, so a problem shows up at the step that caused it rather than three steps later.
The install steps are the same on every Kubernetes cluster. What differs is how the cluster meets the requirements under Before you start. Prepare the cluster covers the five tested platforms: k3s on your own hosts, Amazon EKS, Azure AKS, Google GKE, and Red Hat OpenShift.
Expect thirty minutes, most of it waiting for images to pull on the first install.
Before you start
You need these tools on the machine you install from: kubectl pointed at the target cluster,
helm 3, curl, jq, openssl, and the AWS CLI (aws). The AWS CLI is needed on every platform,
not only on AWS, because the Datastrato release registry is on Amazon ECR and step 3 uses it to turn
the pull key into a registry login. An air-gapped install that pulls from your own registry does not
need it.
You need these files from Datastrato. Your account team sends a release package as a zip file.
Unzip it into a directory of its own; step 1 calls that directory PKG.
unzip datastrato-enterprise-{version}-artifacts.zip -d datastrato-enterprise-{version}
ls datastrato-enterprise-{version}
The package contains:
| File | What it is |
|---|---|
datastrato-license-prod-{date}.txt | Your license key |
datastrato-ecr-pull-user_accessKeys.csv | A pull-only AWS access key for the release registry; keep private |
offline-toolkit-{version}.tar.gz | Image mirroring tool, needed only for an air-gapped install |
chart-release-summary-{version}.md | What changed in this release |
Your cluster needs these. See Requirements for the supported Kubernetes and Helm versions.
vm.max_map_countset to at least 262144 on every node. OpenSearch will not start without it.- An ingress controller, and its class name (for example
traefikornginx). Find it withkubectl get ingressclass. - A DNS hostname that resolves to that ingress controller, for the UI. Each further service you publish, such as the REST API or Iceberg REST in Configure, needs a hostname of its own, resolving to the same controller.
- A TLS certificate for that hostname, either as a Kubernetes TLS Secret or issued by cert-manager. Use a certificate the browser trusts. With a self-signed certificate the browser discards the sign-in cookies and login fails in a way that looks like a credential problem.
- A default StorageClass, since the built-in PostgreSQL and OpenSearch each claim a persistent
volume. Check with
kubectl get storageclass. - Room for the workloads in Workloads and resources: about 2 cores and 14Gi of memory requested, and a node with 8Gi of allocatable memory free for the Gravitino server. One server replica is the default; see Run more than one server.
Prepare the cluster shows how to provide each of these on k3s, Amazon EKS, Azure AKS, Google GKE and Red Hat OpenShift.
Workloads and resources
An install runs eight workloads, each a single pod with one container. The table gives what each pod requests, which a node must have free before the pod is scheduled, and its limit, the most it can use. The Gravitino server row is at the 8Gi that step 5 of the install sets; the chart's own default is 4Gi.
| Workload | Pod | CPU request / limit | Memory request / limit | Persistent volumes |
|---|---|---|---|---|
| Gravitino server | {release}-gravitino-* | 500m / 2 | 8Gi / 8Gi | Logs 14Gi, JDBC drivers 2Gi, Iceberg jars 1Gi |
| PostgreSQL, the metadata database, also used by Marquez | {release}-gravitino-postgresql-0 | 250m / none | 256Mi / 1Gi | Data 8Gi |
| OpenSearch, the search index | opensearch-cluster-master-0 | 500m / 2 | 4Gi / 6Gi | Data 8Gi |
| UI | {release}-ui-* | 100m / 200m | 128Mi / 192Mi | None |
| MCP server | {release}-mcp-* | 100m / 500m | 256Mi / 512Mi | None |
| Metrics service | {release}-metrics-* | 250m / 1 | 512Mi / 4Gi | Logs 2Gi |
| Marquez, the lineage service | {release}-marquez-* | 200m / 500m | 500Mi / 750Mi | None |
| Marquez web | {release}-marquez-web-* | 100m / 200m | 128Mi / 192Mi | None |
| Total | 2 / 6.4, plus PostgreSQL unbounded | about 14Gi / 21Gi | 35Gi |
Every pod schedules once the nodes have the requests free between them. Plan capacity on the limits, since the server, OpenSearch and the metrics service grow toward theirs under load. The Gravitino server must fit on one node, so a node with 8 GiB of memory in total is too small for it: part of every node's memory is reserved for the system, leaving less than 8Gi allocatable.
Three short-lived Jobs also run. The schema Job runs at each install and upgrade, a hook Job scales the server to zero before each upgrade, and the registry-token Job renews the image pull token every six hours, requesting 100m and 128Mi with a 256Mi limit. Each finishes in seconds.
Prepare the cluster
Follow the subsection for your platform, then continue with Install. Every platform except OpenShift issues certificates the same way, described in Issue certificates with cert-manager.
k3s on your own hosts
k3s supplies two of the requirements itself: Traefik as the ingress controller, with class name
traefik, listening on ports 80 and 443 of each node, and the local-path StorageClass as the
default.
Install k3s on the first node and give your user a copy of its kubeconfig. k3s writes the original as readable by root only.
curl -sfL https://get.k3s.io | sh -
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown "$USER" ~/.kube/config
kubectl get ingressclass
kubectl get storageclass
The first list shows traefik and the second shows local-path (default).
Set vm.max_map_count on every node, and persist it so it survives a reboot.
sudo sysctl -w vm.max_map_count=262144
echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-opensearch.conf
Point DNS at the node. Create an A record from your UI hostname to the public IP address of the node, or of a load balancer in front of the nodes. Allow inbound ports 80 and 443 in the host firewall or security group. Port 80 must stay open if cert-manager issues the certificate, since Let's Encrypt validates the hostname over it.
local-path volumes live on one node's disk, so the PostgreSQL and OpenSearch pods can only run on
the node that holds their data. On a multi-node k3s cluster, that node is a single point of failure
for the metadata database unless you use your own database; see Prepare for production.
Amazon EKS
An EKS cluster provides the control plane and the nodes, and none of the other requirements. It has
no ingress controller, no working default StorageClass until the EBS CSI driver is added, and its
managed nodes take no sysctl changes by hand. The steps below supply each one. Run them from the
machine you install from, with eksctl installed alongside the tools under
Before you start.
CLUSTER={cluster_name}
REGION={aws_region}
AZ_NODES={availability_zone_for_nodes}
AZ_SECOND={second_availability_zone}
-
Check the AWS account. The AWS CLI and
eksctlact as the AWS identity your shell is signed in with. That identity needs permission to create an EKS cluster and node group, IAM roles, and an IAM OIDC provider, whicheksctluses to give the EBS CSI driver its IAM role. An account whose organization denies IAM OIDC provider actions cannot create the cluster below. The account must also be able to launch the node instance type.Check the identity, instance type and vCPU quotaaws sts get-caller-identity
aws ec2 describe-instance-type-offerings --region "$REGION" --location-type availability-zone \
--filters Name=instance-type,Values=m6i.2xlarge Name=location,Values="$AZ_NODES" \
--query 'InstanceTypeOfferings[].Location' --output text
aws service-quotas get-service-quota --region "$REGION" --service-code ec2 --quota-code L-1216C47A \
--query 'Quota.Value' --output textThe first command names the account and identity, the second prints your node zone, and the quota for on-demand standard instances is at least 24, the vCPUs of three m6i.2xlarge nodes. An account on the AWS Free Plan cannot launch m6i.2xlarge at all. There, and with too little quota, the node group stays in
CREATINGwith no error untileksctlgives up after about 25 minutes. -
Create the cluster. For a new evaluation cluster:
cluster.yamlcat > cluster.yaml << EOF
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: $CLUSTER
region: $REGION
version: "1.34"
availabilityZones: [$AZ_NODES, $AZ_SECOND]
autoModeConfig:
enabled: false
iam:
withOIDC: true
addons:
- name: vpc-cni
- name: coredns
- name: kube-proxy
- name: eks-pod-identity-agent
- name: aws-ebs-csi-driver
wellKnownPolicies:
ebsCSIController: true
managedNodeGroups:
- name: ng-1
instanceType: m6i.2xlarge
availabilityZones: [$AZ_NODES]
desiredCapacity: 3
minSize: 3
maxSize: 4
volumeSize: 100
EOF
eksctl create cluster -f cluster.yaml 2>&1 | tee create-cluster.log
kubectl get nodesCreation takes about twenty minutes.
eksctlswitches yourkubectlcontext to the new cluster, andkubectl get nodesshows three nodesReady. The file was tested witheksctl0.230.0.autoModeConfigkeeps the managed node group and add-ons that a latereksctlrelease stops creating by default, andeksctlalso installs themetrics-serveradd-on, which the file does not list.If you already have a cluster, skip the file and confirm the cluster has the EBS CSI driver add-on instead:
Check the EBS CSI driver add-onaws eks describe-addon --cluster-name "$CLUSTER" --region "$REGION" --addon-name aws-ebs-csi-driver \
--query 'addon.status' --output textIt must print
ACTIVE.ResourceNotFoundExceptionmeans the cluster name or region is wrong, or the add-on is missing; install the add-on with its IAM role as described in the Amazon EKS documentation for the Amazon EBS CSI driver.The node group sits in a single Availability Zone on purpose. EBS volumes are zonal: the PostgreSQL and OpenSearch volumes are created in one zone, and their pods can run only on nodes in that zone. A node group spread across zones can reschedule those pods where their volumes cannot attach. If you run nodes in several zones, keep a node group in every zone that holds a volume. Three m6i.2xlarge nodes run the evaluation install with room to spare; see Workloads and resources for what it needs.
-
Make gp3 the default StorageClass.
gp3.yamlcat > gp3.yaml << 'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
EOF
kubectl apply -f gp3.yaml
kubectl annotate storageclass gp2 storageclass.kubernetes.io/is-default-class=false --overwrite 2>/dev/null
kubectl get storageclassOnly
gp3shows(default). -
Install an ingress controller. The example runs Traefik behind a Network Load Balancer. An NLB keeps a fixed address in each zone for its lifetime. Cross-zone load balancing lets the address in every zone reach the nodes, which here sit in one zone.
traefik-values.yamlcat > traefik-values.yaml << 'EOF'
service:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
ingressClass:
enabled: true
isDefaultClass: true
EOF
helm repo add traefik https://traefik.github.io/charts
helm repo update
helm install traefik traefik/traefik -n traefik --create-namespace -f traefik-values.yaml
kubectl -n traefik get podsThe Traefik pod shows
1/1 Running. UsetraefikasINGRESS_CLASSin step 1 of the install. -
Point DNS at the load balancer.
Read the load balancer hostnameuntil LB_HOST=$(kubectl -n traefik get svc traefik -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') \
&& [ -n "$LB_HOST" ]; do sleep 10; done
echo "$LB_HOST"Create a CNAME record, or a Route 53 alias record, from your UI hostname to that name. A new NLB name takes a few minutes to resolve. Without a domain of your own, use an sslip.io name instead, as described under Choosing
UI_HOSTin step 1 of the install.Check the path end to endcurl -s -o /dev/null -w '%{http_code}\n' http://{ui_hostname}/It prints
404. The answer comes from Traefik, which is reachable and has no routes yet. -
Set
vm.max_map_counton every node. Apply the DaemonSet in Set vm.max_map_count with a DaemonSet. -
Issue certificates. Follow Issue certificates with cert-manager.
Image pulls on EKS. The node IAM role cannot pull from the Datastrato release registry, so the
pull Secret from install step 4 is required on EKS exactly as on any other cluster. The chart pulls
from the release registry in us-east-1 by default, and global.ecrRegion switches it to the replica
in eu-west-1 or ap-northeast-1. The server image is about 3 GB, and every node without the image
repeats the pull, which on EKS includes node replacement, managed node group upgrades and scale-out. For production on EKS, mirror
the images into a registry in your own region; see Air-gapped installation.
Azure AKS
AKS needs less preparation than EKS. Its Azure Disk driver is installed with a default StorageClass
already set, and a load balancer Service receives a plain public IP address, so no load balancer
annotations are needed. What remains is the ingress controller and vm.max_map_count. Run the
steps from the machine you install from, with the Azure CLI (az) installed alongside the tools
under Before you start.
RG={resource_group}
AKS={cluster_name}
LOC={azure_region}
For a new cluster, choose a name for a new resource group; step 2 creates it. To use an existing cluster, set the group it is in.
-
Check the VM size and vCPU quota. The cluster in step 2 uses two Standard_E4bds_v5 nodes, 4 vCPUs and 32 GB each. Any size with 4 vCPUs and 32 GB works in its place; set
SIZEandFAMILYto match the size you choose. The first provider registration andaz vm list-skuscan each take a few minutes.Check the VM size and vCPU quotaSIZE=Standard_E4bds_v5
FAMILY=EBDSv5
az provider register --namespace Microsoft.Compute --wait
az provider register --namespace Microsoft.ContainerService --wait
az vm list-skus --location "$LOC" --size "$SIZE" --query "[?restrictions==\`[]\`].name" -o tsv
az vm list-usage --location "$LOC" -o table | grep -i -E "Total Regional vCPUs|$FAMILY Family"az vm list-skusprints the size when your subscription can use it in this region. If it prints nothing, pick another region or another 4 vCPU, 32 GB size, such as Standard_E4s_v4 with the familyESv4.The two nodes need 8 vCPUs, in both rows of the Limit column: the regional total and the family of your size. New subscriptions often allow 10 vCPUs per region, and some free or credit-based offers allow only 4 and refuse increases. If your limit is too low, raise it, use another subscription, or create the cluster with
--node-count 1in step 2: one 4 vCPU, 32 GB node runs the full evaluation install. To raise a limit from the CLI:Raise a vCPU limitaz extension add --name quota
az provider register --namespace Microsoft.Quota --wait
SCOPE="/subscriptions/$(az account show --query id -o tsv)/providers/Microsoft.Compute/locations/$LOC"
az quota list --scope "$SCOPE" --query "[?name=='cores'].{name:name, limit:properties.limit.value}" -o table
az quota create --resource-name cores --scope "$SCOPE" --limit-object value=8 --resource-type dedicatedRepeat
az quota createfor the family;az quota listwithout the query shows its resource name, such asstandardESv4Family. A request refused withResourceNotAvailableForOffermeans the subscription's offer does not allow increases. -
Create the cluster. Skip to the next step if you already have one. For a new evaluation cluster:
Create the AKS clusteraz group create --name "$RG" --location "$LOC" -o table
az aks create --resource-group "$RG" --name "$AKS" --location "$LOC" \
--node-count 2 --node-vm-size "$SIZE" --node-osdisk-size 100 \
--tier free --generate-ssh-keys -o table 2>&1 | tee create-cluster.log
az aks get-credentials --resource-group "$RG" --name "$AKS" --overwrite-existing
chmod 600 ~/.kube/config
kubectl get nodes
kubectl get storageclassCreation takes a few minutes and prints one row with the cluster's state,
Succeeded.--generate-ssh-keyscreates a key pair in~/.sshif you have none. Each node showsReady, anddefaultshows(default)among the storage classes listed. Memory, not CPU, is what the install mostly needs; see Workloads and resources. -
Install an ingress controller and point DNS at it.
traefik-values.yamlcat > traefik-values.yaml << 'EOF'
ingressClass:
enabled: true
isDefaultClass: true
EOF
helm repo add traefik https://traefik.github.io/charts
helm repo update
helm install traefik traefik/traefik -n traefik --create-namespace -f traefik-values.yaml
until LB_IP=$(kubectl -n traefik get svc traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}') \
&& [ -n "$LB_IP" ]; do sleep 10; done
echo "$LB_IP"Create an A record from your UI hostname to that address.
Check the path end to endcurl -s -o /dev/null -w '%{http_code}\n' http://{ui_hostname}/It prints
404, from Traefik. UsetraefikasINGRESS_CLASSin step 1 of the install. -
Set
vm.max_map_counton every node. Apply the DaemonSet in Set vm.max_map_count with a DaemonSet. -
Issue certificates. Follow Issue certificates with cert-manager.
Image pulls on AKS. The release registry is on Amazon ECR, so the pull Secret from install step 4 is required on AKS. The server image is about 3 GB; on a cluster in westus2 the first pull takes about a minute and a half, and every node without the image repeats it. For production, mirror the images into a registry in your own region, such as Azure Container Registry; see Air-gapped installation.
Google GKE
GKE needs the same preparation as AKS: an ingress controller and vm.max_map_count. Its Persistent
Disk driver is installed with standard-rwo as the default StorageClass, and a load balancer Service
receives a public IP address without annotations. Use a GKE Standard cluster. Autopilot clusters
reject privileged pods, so the vm.max_map_count DaemonSet cannot run there and OpenSearch does not
start. Run the steps from the machine you install from, with the Google Cloud CLI (gcloud) and its
gke-gcloud-auth-plugin installed alongside the tools under Before you start.
PROJECT={gcp_project_id}
GKE={cluster_name}
REGION={gcp_region}
ZONE={gcp_zone}
gcloud config set project "$PROJECT"
gcloud services enable container.googleapis.com compute.googleapis.com
-
Check quota. New projects often allow 12 vCPUs across all regions, 8 for some machine families, and 250 GB of SSD-class disk per region.
Check regional and project quotasgcloud compute regions describe "$REGION" --format=json \
| jq -r '.quotas[] | select(.metric | IN("CPUS","E2_CPUS","SSD_TOTAL_GB","IN_USE_ADDRESSES")) | "\(.metric)\t\(.usage)\t\(.limit)"'
gcloud compute project-info describe --format=json \
| jq -r '.quotas[] | select(.metric=="CPUS_ALL_REGIONS") | "\(.metric)\t\(.usage)\t\(.limit)"'Each line shows the metric, current usage and limit. Default GKE boot disks and
standard-rwovolumes both count againstSSD_TOTAL_GB. Raise limits under IAM & Admin, then Quotas, in the Google Cloud console. -
Create the cluster. Skip to the next step if you already have one. For a new evaluation cluster:
Create the GKE clustergcloud container clusters create "$GKE" --zone "$ZONE" \
--num-nodes 2 --machine-type e2-highmem-4 \
--disk-type pd-standard --disk-size 50 \
--release-channel regular 2>&1 | tee create-cluster.log
kubectl get nodes
kubectl get storageclassCreation takes about five minutes and switches your
kubectlcontext to the new cluster. Two nodes showReady, andstandard-rwoshows(default). The cluster is zonal on purpose: Persistent Disk volumes are zonal, and a regional cluster also creates the requested node count in each of three zones. The boot disks usepd-standardso the SSD quota stays free for the PostgreSQL and OpenSearch volumes. Two e2-highmem-4 nodes (4 vCPUs and 32 GB each) run the evaluation install within an 8 vCPU quota; see Workloads and resources for what it needs. -
Install an ingress controller and point DNS at it. Follow Azure AKS step 3. The commands are identical: the Traefik Service receives a public IP address, which takes about a minute, and
curlprints404once DNS points at it. -
Set
vm.max_map_counton every node. Apply the DaemonSet in Set vm.max_map_count with a DaemonSet. -
Issue certificates. Follow Issue certificates with cert-manager.
Image pulls on GKE. The release registry is on Amazon ECR, so the pull Secret from install step 4 is required on GKE. The server image is about 3 GB; on the cluster above in us-west1 the first pull took about two and a half minutes, and every node without the image repeats it. For production, mirror the images into a registry in your own region, such as Artifact Registry; see Air-gapped installation.
Red Hat OpenShift
OpenShift admits pods through security context constraints (SCCs), publishes Ingress objects through its own Router, and tunes nodes through the Node Tuning Operator, so it differs from the other platforms in four places: two SCC grants, a node tuning profile, Router-based ingress, and a pull Secret you supply yourself. The steps below were verified on Red Hat OpenShift Service on AWS (ROSA) with hosted control planes, OpenShift 4.22.
In this release the chart does not run under the default restricted-v2 SCC alone. A cluster
administrator must grant the release's namespace the anyuid and nonroot-v2 SCCs. Every workload
except OpenSearch is admitted under nonroot-v2, which allows any non-root user ID. OpenSearch is
admitted under anyuid because its volume-ownership init container runs as root.
You need cluster-admin rights and the OpenShift CLI (oc). On ROSA you also need the ROSA CLI
(rosa).
-
Create the cluster. Skip to the next step if you already have one. For a ROSA evaluation cluster, first enable ROSA with hosted control planes on the ROSA page of the AWS console and link your Red Hat account there. Run the ROSA CLI as an IAM user, since it refuses root credentials.
Set up ROSA roles and networkCLUSTER={cluster_name}
REGION={aws_region}
rosa login --use-device-code
rosa create ocm-role --mode auto --yes
rosa create user-role --mode auto --yes
rosa create account-roles --hosted-cp --mode auto --yes
rosa create oidc-config --mode auto --yes
rosa create network --param Region="$REGION" --param Name="$CLUSTER-net" \
--param AvailabilityZoneCount=1 --param VpcCidr=10.0.0.0/16
rosa list oidc-configrosa create networkprints the new public and private subnet IDs at the end, androsa list oidc-configprints the OIDC config ID. Set them asOIDC_IDandSUBNETS.Create the ROSA clusterOIDC_ID={oidc_config_id}
SUBNETS={public_subnet_id},{private_subnet_id}
rosa create cluster --cluster-name "$CLUSTER" --hosted-cp --sts --mode auto --yes \
--region "$REGION" --oidc-config-id "$OIDC_ID" --subnet-ids "$SUBNETS" \
--operator-roles-prefix "$CLUSTER" --compute-machine-type m6i.2xlarge --replicas 2
rosa logs install -c "$CLUSTER" --watch
rosa create admin -c "$CLUSTER"The cluster reports
readyafter about ten minutes, and its worker nodes follow about ten minutes later.rosa create adminprints anoc logincommand; the login takes a few minutes to become active. Run it.Wait for the cluster operatorsoc wait clusteroperators --all --for=condition=Available --timeout=30m
oc get nodesBoth workers show
Ready. The network is in one Availability Zone for the same reason as on Amazon EKS: EBS volumes are zonal. -
Set
vm.max_map_counton every node. OpenShift does not admit the privileged DaemonSet used on the other platforms, so set it through a tuning profile. On ROSA with hosted control planes, attach a tuning config to the worker machine pool:tuned-opensearch.yamlcat > tuned-opensearch.yaml << 'EOF'
profile:
- data: |
[main]
summary=Datastrato OpenSearch sysctl
include=openshift-node
[sysctl]
vm.max_map_count=262144
name: datastrato-opensearch
recommend:
- priority: 20
profile: datastrato-opensearch
EOF
rosa create tuning-configs -c "$CLUSTER" --name datastrato-opensearch --spec-path tuned-opensearch.yaml
rosa edit machinepool -c "$CLUSTER" workers --tuning-configs datastrato-opensearchOn other OpenShift clusters, create the same profile as a
Tunedresource in theopenshift-cluster-node-tuning-operatornamespace. Either way the setting applies to running nodes without replacing them.Check vm.max_map_count on every nodefor n in $(oc get nodes -o name); do
echo -n "$n: "; oc debug "$n" -q -- chroot /host sysctl -n vm.max_map_count 2>/dev/null
doneEvery node prints
262144. -
Create the namespace and grant the SCCs.
Create the namespace and grant the SCCsoc new-project datastrato
oc adm policy add-scc-to-group anyuid system:serviceaccounts:datastrato
oc adm policy add-scc-to-group nonroot-v2 system:serviceaccounts:datastratoThe grants apply to every service account the chart creates in the namespace.
-
Find your hostnames. The Router serves every hostname under the cluster's apps domain with a wildcard certificate, so no DNS record and no cert-manager are needed.
Read the cluster apps domainoc get ingresscontroller default -n openshift-ingress-operator -o jsonpath='{.status.domain}{"\n"}'Use
gravitino.followed by that domain asUI_HOSTin install step 1.
Changes to the install steps on OpenShift.
-
Step 1: set
INGRESS_CLASS=openshift-default.TLS_SECRETis not used. -
Step 4: the chart's registry-token renewal cannot run on OpenShift in this release, so create the pull Secret yourself instead of
ecr-refresh-iam:Create the pull Secret by handoc -n "$NAMESPACE" create secret docker-registry image-pull-secret \
--docker-server="$REGISTRY" --docker-username=AWS \
--docker-password="$(AWS_ACCESS_KEY_ID="$AKID" AWS_SECRET_ACCESS_KEY="$SKEY" \
aws ecr get-login-password --region us-east-1)"
unset AKID SKEYThe token in that Secret expires after about twelve hours, after which new pulls fail. For anything beyond an evaluation, mirror the images into your own registry; see Air-gapped installation.
-
Step 5: in
my-values.yaml, replace theglobalblock with the one below, and in theingressblock replace the cert-manager annotation with the Router annotation and replacetls:and its entries withtls: []:my-values.yamlglobal:
imagePullSecrets:
- name: image-pull-secret
pullSecrets:
create: false
name: image-pull-secret
ecrRefresh:
enabled: falsemy-values.yamlingress:
enabled: true
className: "openshift-default"
annotations:
route.openshift.io/termination: edge
# hosts: unchanged
tls: []The Router turns each Ingress into an edge-terminated Route on the cluster certificate. Without the
global.imagePullSecretsentry, the render in step 6 fails on the OpenSearch pull Secret check. -
Step 9: there is no Certificate resource to check.
oc get routes -n "$NAMESPACE"lists the Routes, each withedgetermination. -
Uninstall: run
helm uninstallwith--no-hooks. The chart's cleanup Job is not admitted on OpenShift.
Set vm.max_map_count with a DaemonSet
On EKS, AKS and GKE, a DaemonSet applies the OpenSearch setting to each node, including nodes added later by scaling or replacement.
cat > max-map-count.yaml << 'EOF'
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: max-map-count
namespace: kube-system
spec:
selector:
matchLabels:
app: max-map-count
template:
metadata:
labels:
app: max-map-count
spec:
initContainers:
- name: sysctl
image: public.ecr.aws/docker/library/busybox:1.36
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
containers:
- name: hold
image: public.ecr.aws/docker/library/busybox:1.36
command: ["sleep", "infinity"]
EOF
kubectl apply -f max-map-count.yaml
kubectl -n kube-system rollout status ds/max-map-count
for p in $(kubectl -n kube-system get pods -l app=max-map-count -o name); do
kubectl -n kube-system exec "$p" -c hold -- cat /proc/sys/vm/max_map_count
done
Every node prints 262144.
Issue certificates with cert-manager
Skip this if you already have a TLS Secret for your hostname. Otherwise, install cert-manager and a Let's Encrypt issuer. The hostname must resolve to your ingress controller and port 80 must be reachable from the internet, since Let's Encrypt validates the hostname over it.
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace \
--version v1.16.2 --set crds.enabled=true
kubectl -n cert-manager wait --for=condition=Available deploy --all --timeout=180s
cat > letsencrypt-issuer.yaml << 'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
EOF
kubectl apply -f letsencrypt-issuer.yaml
sleep 15
kubectl get clusterissuer letsencrypt
The issuer shows True in the READY column. If your ingress class is not traefik, change
ingressClassName to match. The values file in install step 5 names this issuer under
ingress.annotations, and cert-manager creates the Secret named by TLS_SECRET in install step 1.
During issuance, cert-manager runs one short-lived cm-acme-http-solver pod per hostname. They end in
Error when cert-manager stops them after validation, which is expected.
Install
-
Set your variables. Every later command reads these, so this is the only place you type your own values. Keep this shell open for the whole procedure. If you open a new shell, run this block again first.
Set the install variablesVERSION={chart_version}
REGISTRY=730335553010.dkr.ecr.us-east-1.amazonaws.com
NAMESPACE=datastrato
RELEASE=enterprise
PKG={path_to_release_package}
UI_HOST={ui_hostname}
INGRESS_CLASS=traefik
TLS_SECRET=gravitino-tls
METALAKE={metalake_name}
ADMIN_PASSWORD='{admin_password}'
LICENSE_FILE=$(ls "$PKG"/*license-prod-*.txt)
KEY_FILE=$(ls "$PKG"/*ecr-pull-user_accessKeys.csv)
ls -l "$LICENSE_FILE" "$KEY_FILE"The
lslists one license file and one key file. If it reports a missing file, checkPKG.Variable What to set VERSIONThe chart version of your release, as in the name of chart-release-summary-{version}.md, for example1.3.0REGISTRYLeave as shown. It is the Datastrato release registry NAMESPACELeave as datastratounless you have a reason to change it. On OpenShift it must match the namespace granted the SCCsRELEASELeave as enterprise. Later commands derive resource names from itPKGThe directory that holds the files from the release package UI_HOSTThe hostname users type into the browser. See below INGRESS_CLASStraefikif you followed Prepare the cluster;openshift-defaulton OpenShift; otherwise a name fromkubectl get ingressclassTLS_SECRETThe Secret that holds the certificate for UI_HOST. With cert-manager, keepgravitino-tls; cert-manager creates it. With your own certificate, the name of the TLS Secret you created. Not used on OpenShiftMETALAKEThe name of the first metalake, which you create in step 10. The chart needs it at install time because the Iceberg REST service, the Lance REST service and the MCP server each serve one metalake ADMIN_PASSWORDThe password of the local adminaccount. 12 to 64 characters, and no quotes, backslashes or$, since it is written into a YAML file in step 5Choosing
UI_HOST. It must resolve to your ingress controller, and with cert-manager it must also be reachable from the internet on port 80, since Let's Encrypt validates it there. Use a name in a domain you control, with the DNS record from your platform's step in Prepare the cluster. For an evaluation without a domain of your own, use sslip.io, a public DNS service that resolves any name containing an IP address to that address. Write the load balancer's public IP address with dashes:gravitino.203-0-113-10.sslip.ioresolves to203.0.113.10. On EKS the load balancer has a DNS name rather than an IP address;getent ahostsv4 "$LB_HOST"lists its addresses, which stay fixed for the life of the load balancer. On OpenShift, usegravitino.followed by the cluster's apps domain. -
Read the pull key. The CSV holds an access key ID and a secret access key, and the command below reads both into the shell without printing them.
Read the pull key into the shellread -r AKID SKEY < <(awk -F',' 'NR==1{for(i=1;i<=NF;i++){h=tolower($i); gsub(/\r/,"",h); if(h ~ /access key id/)a=i; if(h ~ /secret access key/)s=i}} NR==2{gsub(/\r/,""); print $a, $s}' "$KEY_FILE")
echo "key id: ${AKID:0:4}... secret length: ${#SKEY}"The secret length should print 40. If either value is empty, check
KEY_FILEfrom step 1. -
Log in to the registry and pull the chart. The registry username is always
AWS. The password is a token exchanged from the pull key, valid for about twelve hours. When it expires, run the login again; the key itself does not expire.Log in to the registry and pull the chartAWS_ACCESS_KEY_ID="$AKID" AWS_SECRET_ACCESS_KEY="$SKEY" \
aws ecr get-login-password --region us-east-1 \
| helm registry login --username AWS --password-stdin "$REGISTRY"
helm pull "oci://$REGISTRY/datastratocharts/enterprise-helm" --version "$VERSION"
helm show values "./enterprise-helm-$VERSION.tgz" > "chart-defaults-$VERSION.yaml"
ls -l "enterprise-helm-$VERSION.tgz"chart-defaults-$VERSION.yamlis the chart's full set of defaults, kept for reference. Do not edit it. Your settings go in a separate file in step 5, which holds only what you change, so a new chart version never forces you to merge. -
Create the namespace and the pull Secret. The cluster pulls images on its own schedule, on node restarts and rescheduling as well as at install, so it cannot rely on the token from step 3. The chart instead keeps the pull key in a Secret and renews the registry token itself, before the install and every six hours after.
Create the namespace and pull Secretkubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
kubectl -n "$NAMESPACE" create secret generic ecr-refresh-iam \
--from-literal=aws-access-key-id="$AKID" \
--from-literal=aws-secret-access-key="$SKEY" \
--dry-run=client -o yaml | kubectl apply -f -
unset AKID SKEY
kubectl -n "$NAMESPACE" get secret ecr-refresh-iam -
Write your values file. Run the block as-is. It substitutes your variables from step 1.
my-values.yamlcat > my-values.yaml << EOF
# my-values.yaml
global:
pullSecrets:
create: false
name: image-pull-secret
ecrRefresh:
enabled: true
existingSecret: ecr-refresh-iam
gravitino:
authorization:
enable: true
serviceAdmins: admin
authenticators: "basic"
admin:
initPassword: "$ADMIN_PASSWORD"
icebergRest:
dynamicConfigProvider:
metalake: "$METALAKE"
lanceRest:
gravitinoMetalake: "$METALAKE"
env:
- name: GRAVITINO_MEM
value: "-Xms4g -Xmx4g -XX:MaxMetaspaceSize=2g -XX:+ExitOnOutOfMemoryError"
resources:
requests:
memory: 8Gi
limits:
memory: 8Gi
mcp:
enabled: true
metalake: "$METALAKE"
enterpriseWeb:
env:
- name: NEXTAUTH_URL
value: "https://$UI_HOST"
- name: AUTH_TRUST_HOST
value: "true"
ingress:
enabled: true
className: "$INGRESS_CLASS"
annotations:
cert-manager.io/cluster-issuer: letsencrypt
hosts:
- host: $UI_HOST
paths:
- path: /
pathType: Prefix
backend:
service:
name: '{{ include "enterprise.fullname" . }}-ui'
port:
number: 3000
- path: /mcp
pathType: Prefix
backend:
service:
name: '{{ include "enterprise.fullname" . }}-mcp'
port:
number: 8000
tls:
- secretName: $TLS_SECRET
hosts:
- $UI_HOST
EOF
grep -n -E 'metalake|host:|secretName|password|memory|cluster-issuer' my-values.yamlThe
grepshows your hostname, metalake, TLS Secret, password, memory and issuer in place. If any line shows an empty value, a variable from step 1 was not set.Run this step once. Running it again overwrites later edits. From here on, edit
my-values.yamldirectly.What each block does:
Setting Purpose global.pullSecretsUses the Secret from step 4 and renews the registry token automatically gravitino.authenticators,gravitino.adminLocal password sign-in with an adminaccount. The account name comes fromserviceAdmins, andadmin.initPasswordsets its password at the first install only; add your identity provider later, see Authenticationgravitino.icebergRest.dynamicConfigProviderThe metalake the Iceberg REST service serves; the install is refused without it gravitino.lanceRestThe metalake the Lance REST service serves; the install is refused without it gravitino.envGRAVITINO_MEMServer heap and metaspace: a 4 GiB heap and 2 GiB of metaspace, twice the chart defaults, since metaspace grows with the number of catalogs and the features in use gravitino.resourcesMemory for the server pod. The chart refuses less than 7Gi with the heap and metaspace above; 8Gi leaves room for the JVM's own overhead mcp.metalakeThe metalake the MCP server serves enterpriseWeb.envThe address the UI is reached at. The chart creates the UI's session signing secret itself, in the Secret {release}-ui-auth, and keeps it across upgradesingressPublishes the UI and the MCP server on your hostname ingress.annotationsHas cert-manager issue the certificate into the Secret named in tls, using theletsencryptissuer from Issue certificates with cert-managerIf you supply your own certificate in a TLS Secret instead of using cert-manager, delete the
annotationsblock fromingress. -
Check the render. Rendering validates your values against the cluster without installing anything. Every configuration error the chart knows how to detect is reported here.
Render the chart against the clusterhelm template "$RELEASE" "./enterprise-helm-$VERSION.tgz" -n "$NAMESPACE" -f my-values.yaml \
--set-string gravitino.license.key="$(tr -d '\n' < "$LICENSE_FILE")" \
--dry-run=server > /tmp/render.yaml && echo "RENDER OK"It must print
RENDER OK. Otherwise the message names the value that is wrong or missing; fixmy-values.yamland run it again.The license key is passed on the command line rather than stored in the values file, and
trstrips the file's trailing newline, which the license check otherwise rejects as malformed. -
Install.
Install the charthelm upgrade --install "$RELEASE" "./enterprise-helm-$VERSION.tgz" -n "$NAMESPACE" -f my-values.yaml \
--set-string gravitino.license.key="$(tr -d '\n' < "$LICENSE_FILE")" \
--timeout 15m 2>&1 | tee helm-install.logHelm prints the release notes when it finishes. The pods are still starting at that point.
-
Wait for the pods.
Wait for the Gravitino rolloutkubectl -n "$NAMESPACE" rollout status deployment/"$RELEASE"-gravitino --timeout=15m
kubectl -n "$NAMESPACE" get podsWhen the rollout reports success, every pod should be
Runningwith all containers ready, except the registry-token Job and the schema Job, which showCompleted. Expect these workloads: the Gravitino server, its PostgreSQL, the UI, the MCP server, the metrics service, OpenSearch, Marquez, and Marquez web. The UI and MCP pods wait inInituntil the Gravitino server is ready, and the metrics service sits at0/1for a while after starting; on a fresh cluster everything is ready within about four minutes. A schema Job pod may showErroronce, if it starts before the database's name resolves, and be retried; that is normal when the Job showsComplete.Anything in
ImagePullBackOff,CrashLoopBackOfforInit:Errorhas a cause listed under Troubleshoot. -
Check the certificate and the server.
Check the certificate and the serverkubectl -n "$NAMESPACE" get certificate 2>/dev/null
kubectl -n "$NAMESPACE" port-forward svc/"$RELEASE"-gravitino 8090:8090 >/dev/null 2>&1 &
sleep 3
curl -s -u "admin:$ADMIN_PASSWORD" http://localhost:8090/api/metalakes | jq .If you use cert-manager, the certificate shows
Truein the READY column. Thecurlreturns{"code": 0, "metalakes": []}. An empty list is correct, since the install creates no metalakes. -
Create the metalake. Use the same name you set as
METALAKEin step 1.Create the metalakecurl -s -u "admin:$ADMIN_PASSWORD" -X POST -H "Content-Type: application/json" \
-d "{\"name\":\"$METALAKE\",\"comment\":\"\",\"properties\":{}}" \
http://localhost:8090/api/metalakes | jq '{code, name: .metalake.name}'The response shows
"code": 0and your metalake name. -
Sign in to the UI. Open
https://{ui_hostname}in a browser and sign in asadminwith the password from step 1.
The install is complete. Keep the port forward running if you are continuing to
Get started to connect a first catalog; otherwise stop it with
kill %1.
Prepare for production
The procedure above is the same one a production install follows. Production is not a separate
install; it is the same my-values.yaml with the changes below, applied by running step 7 again.
An evaluation can therefore grow into a production deployment without being reinstalled, apart from
moving the metadata database, which is easiest to decide before you load real metadata.
| Area | Evaluation, as installed above | Production |
|---|---|---|
| License | Trial key with a short expiry | Production key from your account team; replace it on the command line in step 7 |
| Sign-in | Local admin account only | Your identity provider, with oauth,basic so the local admin account remains for recovery; see Configure |
| Users and groups | Created by hand | Provisioned from your directory through SCIM; see Configure |
| Credentials | Admin password written into my-values.yaml | Held in Secrets you create and name in the values file, so it carries no credential; see Secrets |
| Metadata database | PostgreSQL deployed by the chart | A database you operate and back up, with gravitino.postgresql.enabled: false; see Metadata database |
| Images | Pulled from the Datastrato release registry | Mirrored into your own registry with the offline toolkit, with global.registry pointed at it; see Air-gapped installation |
| Memory | 8Gi request and limit for the server | The same, with requests.memory kept equal to limits.memory so the server is the last pod evicted under node pressure. Raise both if you raise the heap in GRAVITINO_MEM |
| Backups | None | The metadata database and lineage databases, the release values and the audit log; see Backup and restore |
| Replicas | One | One, or several for query traffic; see Run more than one server |
The TLS requirement is the same for both: a certificate the browser trusts. An evaluation on a self-signed certificate fails at sign-in, so it cannot be deferred to production.
Run more than one server
Several server replicas can run behind the chart's Service, which spreads query traffic across them and keeps the IRC and the API answering while one pod restarts. Each replica keeps its own caches and drops its copies of anything changed through another replica within a few seconds, by checking a change log in the metadata database.
Several replicas suit query traffic:
- Reading metadata through the IRC, the Gravitino REST API, the connectors and the UI, including loading tables, listing objects and scan planning.
- Writing Iceberg tables. Each commit swaps the table's metadata pointer in the catalog backend in
one step, so a commit that conflicts with one made through another replica is rejected with
409, as it is on a single server. - Creating and dropping Iceberg tables through the IRC. After each one, the server checks its own record of the table against the catalog backend and corrects it.
Make administrative changes one at a time: creating, changing or dropping metalakes, catalogs, schemas and Iceberg namespaces, and changes to users, groups, roles, grants, owners, tags and policies. Each server puts these changes in order with a lock held in its own memory, so two that arrive at the same moment through different replicas are not ordered against each other.
gravitino:
replicas: 3
logPersistence:
accessModes:
- ReadWriteMany
storageClassName: {rwx_storage_class}
The chart refuses more than one replica on a ReadWriteOnce logs volume, so the logs need a
storage class that supports ReadWriteMany, such as Amazon EFS, Azure Files, Google Filestore or
NFS. Each replica writes to a directory of its own on it. A JDBC driver
volume needs ReadWriteMany for the same reason. Run the metadata database outside the cluster,
as in High availability, and check
that your license key allows the number of replicas; see Licensing.
Configure
Every change is made in my-values.yaml and applied by running step 7 again with the same
command. Keep the license on the command line each time.
| To do this | Set |
|---|---|
| Sign in through your identity provider | gravitino.authenticators: "oauth,basic", gravitino.authenticator.oauth.*, and gravitino.browserSso; see Authentication |
| Publish the Gravitino REST API to clients outside the cluster | ingress.api.enabled, ingress.api.host, and ingress.api.tls; see Publish the REST API |
| Publish the Iceberg REST service to engines | ingress.iceberg.enabled, ingress.iceberg.host, and ingress.iceberg.tls |
| Set the Iceberg REST default catalog | gravitino.icebergRest.dynamicConfigProvider.defaultCatalogName |
| Provision users and groups from a directory | gravitino.scim.enabled, ingress.scim.enabled and ingress.scim.host; see SCIM provisioning |
| Publish the Lance REST service to clients | ingress.lance.enabled, ingress.lance.host, and ingress.lance.tls |
| Publish the MCP server on a hostname of its own | ingress.mcp.enabled and ingress.mcp.host, with the /mcp path removed from ingress.hosts |
| Preview table data through your own Trino | gravitino.preview.enabled and gravitino.preview.jdbcUrl |
| Supply JDBC drivers that cannot ship in the image, such as MySQL | gravitino.jdbcDrivers.enabled and gravitino.jdbcDrivers.mode |
| Use your own metadata database | See Metadata database |
| Expose the server without an ingress controller | gravitino.service.type: LoadBalancer; see Expose a Service directly |
The chart deploys PostgreSQL for the metadata database by default. Its data sits on a persistent volume that survives upgrades and uninstalls. For production, a database you operate and back up is the better choice.
The admin password is set once, at first install. The chart keeps it in a Secret that survives
upgrades, so changing gravitino.admin.initPassword later has no effect; change the password through
the UI or the API instead.
gravitino.env replaces the chart's environment list rather than adding to it. When you add a
variable, keep the GRAVITINO_MEM entry in the same list.
Publish the REST API
Clients outside the cluster, such as the Java and Python SDKs and the Spark, Trino and Flink
connectors, call the Gravitino REST API. Publish it on a hostname of its own with ingress.api.
The UI hostname also answers /api, but those calls pass through the UI pod, which forwards them
for the browser's signed-in session. Under load from clients the UI pod limits throughput, and
calls fail while it restarts. The chart rejects an ingress.api.host equal to the UI hostname.
ingress:
api:
enabled: true
host: {api_hostname}
tls:
- secretName: api-tls
hosts:
- {api_hostname}
Add these under the existing ingress block in my-values.yaml rather than as a second ingress
key, and run step 7 again. Create a DNS record for {api_hostname} pointing at the same ingress
controller as the UI hostname. With cert-manager, the issuer annotation on ingress covers the new
host and cert-manager creates api-tls.
Point every client outside the cluster at https://{api_hostname}. The SDKs and connectors append
/api themselves, so give the address without it:
| Client | Setting |
|---|---|
| Java SDK | GravitinoClient.builder(uri) |
| Python SDK | GravitinoClient(uri=...) |
| Spark connector | spark.sql.gravitino.uri |
| Trino connector | gravitino.uri |
| Flink connector | table.catalog-store.gravitino.gravitino.uri |
Check that the hostname reaches the server:
curl -s -o /dev/null -w '%{http_code}\n' https://{api_hostname}/api/version
It prints 200, or 401 when the call needs credentials.
MCP server
The mcp block configures the MCP server. The chart deploys it by
default, and the top-level ingress publishes it at /mcp on the UI hostname. To publish it on a
hostname of its own instead, use ingress.mcp; the chart rejects mcp.ingress.
| Setting | Description | Default |
|---|---|---|
mcp.enabled | Deploy the MCP server | true |
mcp.metalake | Metalake a tool call uses when it names none. Leave empty to have agents name the metalake on every call | (none) |
mcp.gravitinoUri | Gravitino server the MCP server calls. Leave empty to use the Gravitino server in the same release | (same release) |
mcp.extraArgs | Additional server arguments, such as ["--include-tool-tags", "catalog,schema,table"]; see MCP tools | [] |
mcp.auth.token, mcp.auth.existingSecret | Static Bearer token for the service identity | (none) |
mcp.auth.oauth.* | OAuth client credentials for the service identity | (none) |
mcp.auth.oauth.noServiceIdentityFallback | Reject requests that carry no Authorization header instead of using the service identity | false |
mcp.tls.enabled, mcp.tls.existingSecret | Serve HTTPS from the pod itself, from a kubernetes.io/tls Secret. Not needed when the ingress terminates TLS | false |
mcp.replicaCount | Number of replicas. The server keeps no state, so replicas scale freely | 1 |
mcp.extraVolumes, mcp.extraVolumeMounts | Additional volumes, such as one that keeps the audit log across restarts | [] |
Each agent normally sends its own user's credentials, and the server forwards them to Gravitino. A
service identity covers requests that arrive without an Authorization header, and every such
request then acts as that one identity. There are two ways to provide it:
- A static token, in
mcp.auth.tokenor in the Secret named bymcp.auth.existingSecret. It is always sent as aBearertoken, so it must be an OAuth access token, and it stops working when that token expires. - OAuth client credentials, in
mcp.auth.oauth.tokenEndpoint,clientId, andclientSecret(orexistingSecret), which must be set together. The server fetches its own token and refreshes it, so a long-running server does not start failing when a token expires.
A static token wins when both are set. Set mcp.auth.oauth.noServiceIdentityFallback to true to
keep the client credentials while still rejecting requests that carry no identity of their own.
Set other server properties
Set any Gravitino server property that has no dedicated chart value under
gravitino.additionalConfigItems in my-values.yaml, and apply it by running step 7 again. The
chart writes each entry into the server's gravitino.conf as key = value:
gravitino:
additionalConfigItems:
gravitino.cache.lockSegments: "16"
Every value must be a quoted string in my-values.yaml; the chart fails to render an unquoted
number or boolean. To set a value on the command line, use --set-string rather than --set,
and escape the dots in the property name:
--set-string 'gravitino.additionalConfigItems.gravitino\.cache\.lockSegments=16'.
Use a property's dedicated chart value when one exists, and use additionalConfigItems only for
properties without one, because setting a property both ways writes it twice into
gravitino.conf.
Google Cloud credentials
Catalogs on Google Cloud Storage or BigQuery read a service account key from the pod. Create the Secret before installing or upgrading, since the chart refuses to render a mount for a Secret that does not exist.
kubectl -n "$NAMESPACE" create secret generic gcs-service-account \
--from-file=key.json={path_to_service_account_key}
gravitino:
env:
- name: GRAVITINO_MEM
value: "-Xms4g -Xmx4g -XX:MaxMetaspaceSize=2g -XX:+ExitOnOutOfMemoryError"
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /etc/gcs/key.json
extraVolumes:
- name: gcs-credentials
secret:
secretName: gcs-service-account
items:
- key: key.json
path: key.json
extraVolumeMounts:
- name: gcs-credentials
mountPath: /etc/gcs
readOnly: true
S3 and Azure credentials are catalog properties rather than pod configuration. See Object storage.
Air-gapped installation
The offline toolkit in the release package copies every image the chart uses into your own
registry. Its README covers the download, upload and push modes. Push the chart to the same
registry, then set global.registry in my-values.yaml to redirect every image there. With images
in your own registry, global.pullSecrets.ecrRefresh and the ecr-refresh-iam Secret are no longer
needed; supply whatever pull credential your own registry requires instead.
Upgrade
To move to a new version, follow Upgrades. It backs up the metadata database and
records what the server holds before running helm upgrade with your existing my-values.yaml, and
it covers rolling back.
Uninstall
helm uninstall "$RELEASE" -n "$NAMESPACE"
Uninstalling leaves the persistent volumes of the built-in PostgreSQL and OpenSearch in place, along with the admin credentials and database password Secrets (see Secrets), so a reinstall under the same release name picks up the existing data. The logs volume is deleted, and the audit log on it; copy or ship the audit log first, as described in Backup and restore. For a clean slate, delete the namespace as well.
kubectl delete namespace "$NAMESPACE"
Nothing in your connected sources is affected either way, since Gravitino never held that data.
Removing an evaluation cluster. On EKS, AKS and GKE, delete the namespace and wait for its volumes to go before you delete the cluster. Deleting the cluster first can leave the PostgreSQL and OpenSearch cloud disks behind, still billed, and on EKS can also leave the ingress load balancer. Delete the ingress controller release too, so its load balancer is removed while the cluster can still clean it up.
kubectl delete namespace "$NAMESPACE" --timeout=5m
helm uninstall traefik -n traefik
kubectl get pv
kubectl get pv prints No resources found once every volume is deleted. Then delete the cluster
with your platform's command (eksctl delete cluster, az group delete,
gcloud container clusters delete, or rosa delete cluster). On ROSA, also delete the operator
roles, the OIDC config and the network stack afterwards, since the network's NAT gateway is billed
by the hour. On Azure, a NetworkWatcherRG resource group may remain after az group delete.
Azure creates it automatically with the cluster's network; it costs nothing and can stay.
Troubleshoot
| Symptom | Cause and fix |
|---|---|
| Render fails saying a license key is required | The --set-string gravitino.license.key argument is missing or LICENSE_FILE is empty. Run the step 1 block again and check its ls output |
| Render fails on the Gravitino server's memory | gravitino.resources is missing or below 7Gi. Set the memory request and limit as in step 5 |
Render fails naming dynamicConfigProvider.metalake | gravitino.icebergRest.dynamicConfigProvider.metalake is unset in my-values.yaml |
Render fails naming lanceRest.gravitinoMetalake | gravitino.lanceRest.gravitinoMetalake is unset in my-values.yaml. Set it to the same metalake as METALAKE, as in step 5 |
Render fails with admin.username was removed or admin.password was renamed to admin.initPassword | The values file was written for an earlier release. Delete gravitino.admin.username and rename gravitino.admin.password to gravitino.admin.initPassword, as in step 5 |
Render fails with Do not set AUTH_SECRET or NEXTAUTH_SECRET in enterpriseWeb.env | Delete that entry from enterpriseWeb.env. The chart creates the session secret itself; to supply your own, set enterpriseWeb.authSecret.existingSecret |
Render fails with gravitino.scim.ingress.enabled is not supported | Use ingress.scim.enabled and ingress.scim.host instead, as in Configure |
Render fails naming a Secret and giving a kubectl create secret command | A Secret referenced by extraVolumes does not exist yet. Run the command it prints |
Server logs report INVALID_FORMAT for the license | The key was passed with its trailing newline. Pass it through tr -d '\n' as in step 7 |
Pods in ImagePullBackOff | The ecr-refresh-iam Secret is missing or holds the wrong key. Repeat step 4, then delete the failing pods |
| OpenSearch restarting | vm.max_map_count is below 262144 on its node |
| Sign-in returns to the login page, or shows a configuration error | The browser does not trust the certificate, or NEXTAUTH_URL does not match the address in the browser |
| Gravitino pod restarts after running for a day or more | Metaspace exhausted. Raise -XX:MaxMetaspaceSize in GRAVITINO_MEM above the 2g set in step 5, and raise the memory request and limit by the same amount. The chart's own default, used when GRAVITINO_MEM is not set, is 1g |
| After a reinstall, old catalogs and users are back, or the admin password in the values file is rejected | The release reused the kept PostgreSQL volume, which holds the old metadata and the old admin password. Sign in with the old password, or delete the namespace for a clean start, as described under Uninstall |
The server pod stays in Init and then fails | The schema Job could not reach or prepare the metadata database. Read its log; see Metadata database |
The server pod stays Pending with Insufficient memory | No node has 8Gi free for the server's memory request. Add a larger node, or free memory on an existing one |
curl in step 9 returns connection refused | The port forward has stopped. Run the port-forward line again |
PersistentVolumeClaims stay Pending on EKS | The EBS CSI driver add-on is missing, or no StorageClass is marked default. See Amazon EKS steps 2 and 3 |
A pod stays Pending with a volume node affinity conflict on EKS | No node is running in the Availability Zone of its EBS volume. Add a node group in that zone |
| Certificate never becomes ready | Let's Encrypt cannot reach port 80 on the hostname. Check that DNS points at the ingress controller and port 80 is open |
az aks create fails with a quota error | The subscription's vCPU quota is too low for the node size and count. See Azure AKS step 1 |
OpenSearch never starts on GKE and the max-map-count DaemonSet is rejected | The cluster is GKE Autopilot, which rejects privileged pods. Use a GKE Standard cluster |
On OpenShift, a Job or pod is never created and events show unable to validate against any security context constraint | The namespace lacks the SCC grants. See Red Hat OpenShift step 3 |
On OpenShift, the registry-token Job's ecr-get-login-password container shows container has runAsNonRoot and image will run as root | Registry-token renewal is enabled. Disable it and supply the pull Secret as described under Red Hat OpenShift |
Render fails with opensearch.imagePullSecrets lists "image-pull-secret", but this release will not create that Secret | Registry-token renewal is disabled and the Secret is supplied by hand. Add image-pull-secret under global.imagePullSecrets |
kubectl or helm fails with tls: bad record MAC | Packets are corrupted on the network path from your machine, usually an MTU mismatch behind a VPN or under WSL. Lower the interface MTU, for example sudo ip link set dev eth0 mtu 1350, and retry |