> For the complete documentation index, see [llms.txt](https://docs.e6data.com/query-engine/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.e6data.com/query-engine/guides/deployment/aws-in-vpc/deploy-workspace-and-e6data.md).

# Deploy workspace and e6data

Create the workspace namespace and NodePool, deploy the e6data workspace components with Helm, wait for reconciliation, and configure DNS.

With the infrastructure and platform components in place, deploy the e6data workspace. This creates the workspace namespace, a dedicated Karpenter NodePool, and the workspace components (the `NamespaceConfig`, `QueryRouter`, and supporting resources).

## Step 1: Create the workspace namespace

```bash
export WORKSPACE_NAME="my-workspace"  # Replace with your workspace name
export NAMESPACE="${WORKSPACE_NAME}"

kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -
```

## Step 2: Create the workspace NodePool and EC2NodeClass

```bash
cat << EOF | kubectl apply -f -
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: ${WORKSPACE_NAME}-nodeclass
spec:
  amiSelectorTerms:
    - alias: al2023@latest
  instanceProfile: ${CLUSTER_NAME}-karpenter
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: ${CLUSTER_NAME}-eks
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: ${CLUSTER_NAME}-eks
  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 100Gi
        volumeType: gp3
        deleteOnTermination: true
  tags:
    Name: ${CLUSTER_NAME}-${WORKSPACE_NAME}
    workspace: ${WORKSPACE_NAME}
EOF

cat << EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: ${WORKSPACE_NAME}-nodepool
spec:
  template:
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: ${WORKSPACE_NAME}-nodeclass
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: kubernetes.io/arch
          operator: In
          values: ["arm64"]
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: [r6g, r6gd, r7g, r7gd, m7g, c7g]
        - key: topology.kubernetes.io/zone
          operator: In
          values: ["${AZ1}", "${AZ2}"]
      taints:
        - key: workspace-name
          value: ${WORKSPACE_NAME}
          effect: NoSchedule
  limits:
    cpu: 1000
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1h
    budgets:
      - nodes: "10%"
EOF

kubectl get nodepools.karpenter.sh ${WORKSPACE_NAME}-nodepool
kubectl get ec2nodeclasses.karpenter.k8s.aws ${WORKSPACE_NAME}-nodeclass
```

## Step 3: Configure the TLS certificate

Query traffic reaches the workspace over HTTPS, so you need a valid TLS certificate for your workspace hostname. Choose one of the options below. Options B and C create a Kubernetes secret named `envoy-tls`; Option A is referenced later by its ACM ARN.

### Option A: AWS ACM certificate (recommended on AWS)

AWS Certificate Manager provisions and renews the certificate, and it integrates with the Network Load Balancer:

```bash
export DOMAIN_NAME="${WORKSPACE_NAME}.yourdomain.com"

ACM_CERTIFICATE_ARN=$(aws acm request-certificate \
  --domain-name "${DOMAIN_NAME}" \
  --validation-method DNS \
  --query 'CertificateArn' --output text --region $REGION)

echo "Certificate ARN: $ACM_CERTIFICATE_ARN"

# Add the DNS validation CNAME shown by this command, then wait for validation:
aws acm describe-certificate --certificate-arn $ACM_CERTIFICATE_ARN --region $REGION \
  --query 'Certificate.DomainValidationOptions[0].ResourceRecord'

aws acm wait certificate-validated --certificate-arn $ACM_CERTIFICATE_ARN --region $REGION
```

You set `ACM_CERTIFICATE_ARN` in the workspace values in the next step.

### Option B: Let's Encrypt via cert-manager

Use cert-manager (installed earlier) to obtain and auto-renew a certificate from Let's Encrypt:

```bash
cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your-email@yourdomain.com  # Replace with your email
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            class: alb
EOF

cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: envoy-tls
  namespace: ${NAMESPACE}
spec:
  secretName: envoy-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - ${WORKSPACE_NAME}.yourdomain.com  # Replace with your domain
EOF

kubectl wait --for=condition=Ready certificate/envoy-tls -n ${NAMESPACE} --timeout=300s
```

### Option C: Customer-procured certificate

If you have a certificate from a third-party CA (DigiCert, Namecheap, Sectigo, and so on), create the `envoy-tls` secret from your certificate files. Make sure `certificate.crt` includes the full chain (your certificate plus intermediates):

```bash
kubectl create secret tls envoy-tls \
  --cert=certificate.crt \
  --key=private.key \
  --namespace ${NAMESPACE}

kubectl get secret envoy-tls -n ${NAMESPACE}
```

## Step 4: Deploy the workspace components

e6data provides the e6-workspace Helm chart. It creates the `NamespaceConfig`, `QueryRouter`, and all workspace resources. Set the variables from your onboarding details, build the values file, and apply:

```bash
# Set variables (values provided by e6data)
export TENANT_NAME="<YOUR_TENANT_NAME>"
export E6_WORKSPACE_CHART="./e6-workspace-chart"
export IMAGE_REPOSITORY="<ECR_REPO_PROVIDED_BY_E6DATA>"
export CONSOLE_IMAGE_TAG="<VERSION_PROVIDED_BY_E6DATA>"
export COMPACTION_IMAGE_TAG="<VERSION_PROVIDED_BY_E6DATA>"
export ENVOY_IMAGE_TAG="<VERSION_PROVIDED_BY_E6DATA>"
export XDS_IMAGE_TAG="<VERSION_PROVIDED_BY_E6DATA>"

# Monitoring configuration (provided by e6data)
export GREPTIME_ENDPOINT="<ENDPOINT_PROVIDED_BY_E6DATA>"
export GREPTIME_DATABASE="<DATABASE_PROVIDED_BY_E6DATA>"
export GREPTIME_USERNAME="<USERNAME_PROVIDED_BY_E6DATA>"
export GREPTIME_PASSWORD="<PASSWORD_PROVIDED_BY_E6DATA>"

# Auth configuration (provided by e6data)
export JWT_ISSUER="https://app.e6.run"
export JWT_JWKS_URI="https://app.e6.run/.well-known/jwks.json"
export JWT_AUDIENCE="e6-controlplane"

# Console configuration - the control plane the console polls for cluster commands
export AGENT_API_BASE_URL="https://app.e6.run"

# Your configuration
# GREPTIME_ENDPOINT must be the GreptimeDB ingest host (e.g. https://olly.e6azure.com),
# NOT the Grafana dashboard URL - pointing at Grafana returns a 302 redirect and
# silently drops every batch of logs and metrics.
export AUTHZ_SUPER_ADMINS="admin@yourcompany.com"  # Comma-separated list of super-admin emails
export HTTP_HOSTNAME="${WORKSPACE_NAME}.yourdomain.com"
export S3_STORAGE_BACKEND="s3a://e6-${WORKSPACE_NAME}-metadata"

# TLS (from Step 3): set ACM_CERTIFICATE_ARN if you used Option A; leave empty for Option B or C
export ACM_CERTIFICATE_ARN=""

cat > workspace-values.yaml << EOF
workspaceName: "${WORKSPACE_NAME}"
tenant: "${TENANT_NAME}"
cloud: "AWS"
workspaceType: "cloud_prem"
region: "${REGION}"
clusterName: "${CLUSTER_NAME}-eks"
storageBackend: "${S3_STORAGE_BACKEND}"
imageRepository: "${IMAGE_REPOSITORY}"

rbac:
  metriq: { enabled: true }
  laminar: { enabled: true }
  copilot: { enabled: true }

namespaceConfig:
  enabled: true
  suspended: false
  karpenterNodePool: "${WORKSPACE_NAME}-nodepool"
  tolerations:
    - key: "workspace-name"
      operator: "Equal"
      value: "${WORKSPACE_NAME}"
      effect: "NoSchedule"
  console:
    enabled: true
    replicas: 2
    image: { tag: "${CONSOLE_IMAGE_TAG}" }
    resources:
      requests: { cpu: "8", memory: "8Gi" }
      limits: { cpu: "8", memory: "10Gi" }
    environmentVariables:
      AWS_REGION: "${REGION}"
      AGENT_API_BASE_URL: "${AGENT_API_BASE_URL}"  # Control plane URL (provided by e6data)
      AGENT_POLL_INTERVAL: "5m"
      AUTHZ_DEFAULT_ROLE: "viewer"
      AUTHZ_SUPER_ADMINS: "${AUTHZ_SUPER_ADMINS}"  # Comma-separated list of admin emails
  compaction:
    enabled: true
    schedule: "0 * * * *"
    hoursAgo: 2
    image: { tag: "${COMPACTION_IMAGE_TAG}" }

queryRouter:
  enabled: true
  trafficDefaults:
    blueWeight: 100
    greenWeight: 0
  httpQuery: { enabled: true, timeout: "5m" }
  pgQuery: { enabled: true, timeout: "1h" }
  xds:
    replicas: 1
    image: { tag: "${XDS_IMAGE_TAG}" }
    pollInterval: 5
    resources: { cpu: "200m", memory: "256Mi" }
  envoy:
    replicas: 1
    maxReplicas: 10
    image: { tag: "${ENVOY_IMAGE_TAG}" }
    hpa: { enabled: true, targetCPUUtilization: 70, targetMemoryUtilization: 80 }
    resources: { cpu: "500m", memory: "512Mi" }
    service: { type: LoadBalancer }
  auth:
    domain: "${HTTP_HOSTNAME}"
    tls: { secretName: "envoy-tls" }
    jwt:
      issuer: "${JWT_ISSUER}"
      jwksUri: "${JWT_JWKS_URI}"
      audience: "${JWT_AUDIENCE}"
    mode: merged-single-port
    mergedService:
      type: LoadBalancer
      annotations:
        service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
        service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
        service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
        # If you used an ACM certificate (Step 3, Option A), uncomment the next line:
        # service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "${ACM_CERTIFICATE_ARN}"

monitoringServices:
  enabled: true
  greptimeRef:
    endpoint: "${GREPTIME_ENDPOINT}"
    database: "${GREPTIME_DATABASE}"
    username: "${GREPTIME_USERNAME}"
    password: "${GREPTIME_PASSWORD}"
  metricsBridge:
    enabled: true
    resources: { cpu: "100m", memory: "128Mi" }
    metricPushInterval: "10s"
    healthPort: 8081
EOF

# Verify the e6-operator is ready before applying the workspace CRs
kubectl wait --for=condition=Available deployment/e6operator -n e6operator --timeout=60s

# Deploy the workspace
helm template "${WORKSPACE_NAME}-ws" "${E6_WORKSPACE_CHART}" \
  --namespace "${NAMESPACE}" --values workspace-values.yaml \
  | kubectl apply --server-side --force-conflicts -f -

sleep 30
kubectl get sa "${WORKSPACE_NAME}-engine" -n "${NAMESPACE}"
kubectl get namespaceconfig "${WORKSPACE_NAME}-nsc" -n "${NAMESPACE}"
kubectl get queryrouter "${WORKSPACE_NAME}-qr" -n "${NAMESPACE}"
```

## Step 5: Wait for reconciliation

```bash
kubectl wait --for=jsonpath='{.status.phase}'=Ready \
  namespaceconfig/${WORKSPACE_NAME}-nsc -n ${NAMESPACE} --timeout=300s

kubectl wait --for=jsonpath='{.status.phase}'=Ready \
  queryrouter/${WORKSPACE_NAME}-qr -n ${NAMESPACE} --timeout=300s

kubectl get monitoringservices -n ${NAMESPACE}
```

## Step 6: Configure DNS

After the LoadBalancer is provisioned, get the NLB DNS name and create a DNS record:

```bash
for i in {1..60}; do
  NLB_DNS=$(kubectl get svc ${WORKSPACE_NAME}-qr-envoy-external -n ${NAMESPACE} \
    -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null)
  if [[ -n "$NLB_DNS" ]]; then break; fi
  echo "  Waiting... ($i/60)"
  sleep 5
done

echo "LoadBalancer DNS: $NLB_DNS"

# Create a Route 53 CNAME record (replace HOSTED_ZONE_ID with your zone ID)
if [[ -n "$HOSTED_ZONE_ID" && -n "$NLB_DNS" ]]; then
  aws route53 change-resource-record-sets \
    --hosted-zone-id "$HOSTED_ZONE_ID" \
    --change-batch '{
      "Changes": [{
        "Action": "UPSERT",
        "ResourceRecordSet": {
          "Name": "'${HTTP_HOSTNAME}'",
          "Type": "CNAME",
          "TTL": 60,
          "ResourceRecords": [{"Value": "'${NLB_DNS}'"}]
        }
      }]
    }'
else
  echo "Manual DNS setup required: create a CNAME record ${HTTP_HOSTNAME} -> ${NLB_DNS}"
fi
```

## Next

* [Register a catalog](/query-engine/guides/deployment/aws-in-vpc/register-catalog.md) - connect your data lake.
* [Run your first query](/query-engine/guides/deployment/aws-in-vpc/run-first-query.md) - verify the install.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.e6data.com/query-engine/guides/deployment/aws-in-vpc/deploy-workspace-and-e6data.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
