For the complete documentation index, see llms.txt. This page is also available as Markdown.

Deploy workspace and e6data

Deploy an e6data workspace on AKS - namespace, TLS, service accounts and RBAC, NamespaceConfig, QueryRouter, and DNS, using explicit kubectl and YAML.

With the infrastructure and platform components in place, deploy a workspace. Run this part once per workspace, in order: namespace → TLS → service accounts and RBAC → NamespaceConfig → NVMe init (optional) → QueryRouter → AccessToken (optional) → DNS.

Step 0: Set workspace variables

These commands re-derive the Part 1 values, so this works in a fresh shell. Set PREFIX, RESOURCE_GROUP, and WORKSPACE_NAME to the same values you used in Part 1.

export PREFIX="e6data"
export RESOURCE_GROUP="${PREFIX}-rg"
export LOCATION="eastus"

export WORKSPACE_NAME="my-workspace"
export NAMESPACE="${WORKSPACE_NAME}"
export TENANT_NAME="<tenant-name-from-e6data>"
export CONSOLE_HOSTNAME="e6data.yourdomain.com"   # the workspace endpoint
export IMAGE_REPOSITORY="e6labs.azurecr.io"

# Metadata storage account (from Part 1 Step 7)
export STORAGE_ACCOUNT=$(echo "${PREFIX}workspaces" | tr -d '-' | tr '[:upper:]' '[:lower:]' | cut -c1-24)

# Workspace Managed Identity (from Part 1 Step 8). PREFIX, WORKSPACE_NAME, and RESOURCE_GROUP
# must match Part 1 exactly, or this returns "ResourceNotFound".
export WORKSPACE_IDENTITY_CLIENT_ID=$(az identity show \
  --resource-group $RESOURCE_GROUP \
  --name ${PREFIX}-${WORKSPACE_NAME}-engine-identity \
  --query clientId -o tsv)

# e6data control plane - the console polls this for cluster create/suspend/resume commands
export AGENT_API_BASE_URL="https://app.e6.run"
export AGENT_POLL_INTERVAL="30s"

# Console super-admin(s) - REQUIRED, comma-separated emails. Without this, logins land with
# no admin access. Use your own login email.
export AUTHZ_SUPER_ADMINS="[email protected]"

# JWT auth - REQUIRED. The load balancer is L4, so Envoy enforces auth itself.
# Values provided by e6data during onboarding.
export JWT_ISSUER="https://app.e6.run"
export JWT_JWKS_URI="https://app.e6.run/.well-known/jwks.json"
export JWT_AUDIENCE="e6-controlplane"

# Image tags - use the versions from your e6data release notes
export CONSOLE_IMAGE_TAG="1.0.4603"
export ENVOY_IMAGE_TAG="1.37.1-pgrouter-e6.36"
export XDS_IMAGE_TAG="1.0.350"
export ENVOY_INSTANCE_TYPE="Standard_D4ps_v5"

Step 1: Create the namespace

Step 2: Create the TLS certificate

The workspace endpoint is served over HTTPS by Envoy, which terminates TLS in-cluster using a Kubernetes secret named envoy-tls. Unlike AWS - where the NLB can terminate TLS with an ACM cert - the Azure load balancer is L4 and passes through, so the certificate always lives in the cluster. Every option below produces the same envoy-tls secret covering ${CONSOLE_HOSTNAME}. Choose one.

Option A: e6data-provided wildcard certificate

If your workspace endpoint is on an e6data domain (for example *.e6.run), e6data provides a CA-signed wildcard certificate. Load it into the secret:

Option B: Customer-procured certificate

A certificate from your own CA (DigiCert, Namecheap, Sectigo, or a corporate CA). Works with any domain. Make sure fullchain.pem includes the full chain (leaf plus intermediates):

If your certificate is stored in Azure Key Vault, download it first, split it into tls.crt and tls.key, then create the secret the same way.

Option C: Let's Encrypt via cert-manager

cert-manager obtains and auto-renews a free Let's Encrypt certificate using a DNS01 challenge through Azure DNS. This requires the letsencrypt-prod ClusterIssuer (a public domain in an Azure DNS zone you control, plus a cert-manager identity with DNS Zone Contributor):

Option D: Azure Key Vault + Secrets Store CSI

The closest Azure analog to AWS ACM: Key Vault owns and auto-rotates the certificate, and the Secrets Store CSI driver syncs it into the cluster as the envoy-tls secret. Best for teams already standardized on Key Vault for certificate lifecycle. Enable the azure-keyvault-secrets-provider addon, then create a SecretProviderClass that targets your certificate and sets secretObjects to sync a kubernetes.io/tls secret named envoy-tls. The exact SecretProviderClass is environment-specific - ask your e6data onboarding engineer for a template.

Step 3: Create service accounts and RBAC

Create the core service accounts (engine, monitoring, console, query-router) - each annotated with the Workload Identity client ID from Part 1 - plus their Roles and Bindings. Save as rbac.yaml and apply with envsubst < rbac.yaml | kubectl apply -f -.

This creates the core service accounts. When you enable add-ons (Laminar, Copilot, Metriq), add their service accounts (and Laminar's Role) here, and add a federated credential for each against the workspace identity (Part 1 Step 8).

Step 4: Apply the NamespaceConfig

The NamespaceConfig is the workspace's root custom resource. Once applied, the e6-operator creates the console, the compaction job, and (on demand) the MetadataServices:

storageBackend uses the wasbs://<container>@<account>.dfs.core.windows.net URI from Part 1 Step 7. The serviceAccounts block lists the core components; add laminar, copilot, and metriq keys when you enable those add-ons.

Step 5: Engine NodeClass and NodePool

Already created in Part 1 Step 6C.5 (Karpenter). If you chose a static pool or NAP instead, make sure its nodes carry the label and taint workspace-name=${WORKSPACE_NAME} so the NamespaceConfig's nodeSelector and tolerations match.

Step 6: NVMe init DaemonSet (only for NVMe VM families)

If your engine NodePool includes the L family (Lsv3/Lasv3, local NVMe), this DaemonSet formats and RAIDs the NVMe drives and mounts them at /app/tmp:

Step 7: Deploy the QueryRouter

The QueryRouter deploys Envoy (data path, TLS termination, JWT enforcement) and the xDS control plane. The external load balancer is L4, so JWT auth in the QueryRouter is required - Envoy authenticates every request. The merged-single-port mode fronts gRPC, HTTP, and PostgreSQL on port 443 behind one load balancer.

Public by default. With no azure-load-balancer-internal annotation, Azure gives the load balancer a public IP (JWT auth still protects every request). Point your DNS A-record at that public IP.

To instead restrict it to a private endpoint reachable only inside your VNet, add service.beta.kubernetes.io/azure-load-balancer-internal: "true" to mergedService.annotations, and reach it through the VNet (jumpbox, VPN, or peering). The DNS record then points at the private IP.

Step 8: Create an AccessToken (optional)

Static tokens for HTTP/JDBC query authentication:

Step 9: Configure DNS

Point the workspace hostname at the load balancer's IP address:

Use the EXTERNAL-IP (.status.loadBalancer.ingress[0].ip), not the ClusterIP. With the public default this is a real public address - not a 172.20.x (cluster-internal ClusterIP). A 10.x private VNet IP appears only if you kept the load balancer internal.

For a private/internal load balancer, put the A-record in a Private DNS zone (or an internal resolver), since a public record pointing at a 10.x address only resolves usefully from inside the VNet.

Verify the deployment

Expected pods once active: console-*, ${WORKSPACE_NAME}-qr-envoy-*, ${WORKSPACE_NAME}-qr-xds-*, and mds-schema-* / mds-storage-* (after the first MetadataServices is created). Metadata pods may take 1–2 minutes to appear.

Next

If something doesn't come up cleanly, see Troubleshooting.

Last updated

Was this helpful?