> ## Documentation Index
> Fetch the complete documentation index at: https://enterprise-docs.crewai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Azure AKS + Entra ID + Wharf + Studio V2

> Complete deployment guide for CrewAI Platform on Azure AKS with Microsoft Entra ID SSO, Wharf trace collection, and Studio V2.

## Overview

This guide covers a full production deployment of CrewAI Platform on Azure AKS using:

* **Azure Database for PostgreSQL Flexible Server** (PostgreSQL 16) for all databases including Wharf
* **Azure Blob Storage** for object storage
* **Azure Container Registry (ACR)** for crew container images
* **NGINX Ingress Controller** for ingress with cert-manager TLS termination
* **Microsoft Entra ID** for SSO authentication
* **Wharf** for OTLP trace and span collection
* **Studio V2** for the AI-powered crew builder (post-install)

Entra ID is the natural auth choice for Azure deployments — your users are already in Azure AD, so there is no separate identity provider to configure.

This is a self-contained guide. All required configuration is included here — no cross-referencing other guides for values.

## Prerequisites Checklist

Complete every item before running `helm install`.

### Azure Infrastructure

| Component                        | Requirement                                                             |
| -------------------------------- | ----------------------------------------------------------------------- |
| AKS cluster                      | Kubernetes 1.32.0+, AMD64 worker nodes                                  |
| NGINX Ingress Controller         | Installed and running                                                   |
| cert-manager                     | Installed with a ClusterIssuer configured                               |
| Azure PostgreSQL Flexible Server | PostgreSQL 16, accessible from AKS worker nodes                         |
| Azure Blob Storage               | Container created                                                       |
| Azure Container Registry         | MUTABLE tags, AKS pull access granted                                   |
| Managed Identity                 | Created with Blob Storage Contributor role for Workload Identity        |
| Federated credential             | Configured for the `crewai-sa` ServiceAccount in the `crewai` namespace |

<Warning>
  CrewAI Platform only supports AMD64 (x86\_64) worker nodes. ARM64 worker nodes are not supported.
</Warning>

### Microsoft Entra ID

| Step                                                                    | Status |
| ----------------------------------------------------------------------- | ------ |
| App registration created in Azure portal                                |        |
| Redirect URI configured: `https://<YOUR_DOMAIN>/auth/entra_id/callback` |        |
| Application (client) ID collected                                       |        |
| Directory (tenant) ID collected                                         |        |
| Client secret created and value copied                                  |        |
| Admin consent granted for Microsoft Graph `User.Read`                   |        |
| App Roles created: `member` and `factory-admin`                         |        |
| Admin user assigned `factory-admin` App Role                            |        |

<Warning>
  The redirect URI **must** be configured in Azure before running `helm install`. Authentication will fail silently if it is missing or uses the wrong path.
</Warning>

### Tools

* `kubectl` connected to your AKS cluster
* `helm` 3.10+
* Azure CLI (`az`) authenticated to your subscription

***

## Infrastructure Setup

### PostgreSQL: Pre-Create All Four Databases

Create the Azure PostgreSQL Flexible Server if it does not already exist:

```bash theme={null}
az postgres flexible-server create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --name crewai-postgres \
  --location <AZURE_REGION> \
  --admin-user crewai \
  --admin-password <DB_PASSWORD> \
  --sku-name Standard_D4s_v3 \
  --tier GeneralPurpose \
  --version 16 \
  --storage-size 128
```

Create all four required databases:

```bash theme={null}
az postgres flexible-server db create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --server-name crewai-postgres \
  --database-name crewai_plus_production

az postgres flexible-server db create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --server-name crewai-postgres \
  --database-name crewai_plus_cable_production

az postgres flexible-server db create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --server-name crewai-postgres \
  --database-name crewai_plus_oauth_production

az postgres flexible-server db create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --server-name crewai-postgres \
  --database-name wharf
```

Then connect as the admin user and grant privileges:

```sql theme={null}
-- Grant full access to the crewai user on all databases
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_production TO crewai;
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_cable_production TO crewai;
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_oauth_production TO crewai;
GRANT ALL PRIVILEGES ON DATABASE wharf TO crewai;
```

<Warning>
  The chart default for `POSTGRES_OAUTH_DB` is `oauth_db`. You must override it to `crewai_plus_oauth_production` to match the database you created above. A mismatch causes the OAuth service to fail on startup.
</Warning>

<Note>
  When `postgres.enabled: false`, the Helm chart does not create databases automatically. All four databases must exist before `helm install` runs.
</Note>

### Azure Blob Storage: Create Container

```bash theme={null}
# Create a storage account
az storage account create \
  --name <YOUR_STORAGE_ACCOUNT> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --location <AZURE_REGION> \
  --sku Standard_LRS \
  --kind StorageV2

# Create the blob container
az storage container create \
  --name <YOUR_CONTAINER_NAME> \
  --account-name <YOUR_STORAGE_ACCOUNT>
```

### ACR: Create Repository and Grant AKS Access

```bash theme={null}
# Create the registry
az acr create \
  --name <YOUR_ACR_NAME> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --sku Standard

# Grant AKS pull access to ACR
az aks update \
  --name <YOUR_AKS_CLUSTER> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --attach-acr <YOUR_ACR_NAME>
```

<Warning>
  ACR image tags must be mutable. CrewAI overwrites tags for crew versions — immutable tags cause build failures. The default ACR configuration allows mutable tags.
</Warning>

<Note>
  `CREW_IMAGE_REGISTRY_OVERRIDE` must be set to `<YOUR_ACR_NAME>.azurecr.io/<YOUR_ORG>` — without the `/crewai-enterprise` suffix. The platform appends `/crewai-enterprise` automatically. If your ACR image path is `myregistry.azurecr.io/production/crewai-enterprise`, set only `myregistry.azurecr.io/production`.
</Note>

### AKS Workload Identity: Managed Identity for Blob Storage

Using AKS Workload Identity is the recommended approach. It avoids storing storage account keys in Kubernetes secrets.

```bash theme={null}
# Create a Managed Identity
az identity create \
  --name crewai-workload-identity \
  --resource-group <YOUR_RESOURCE_GROUP>

# Capture the client ID and principal ID
MANAGED_IDENTITY_CLIENT_ID=$(az identity show \
  --name crewai-workload-identity \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --query clientId -o tsv)

MANAGED_IDENTITY_PRINCIPAL_ID=$(az identity show \
  --name crewai-workload-identity \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --query principalId -o tsv)

# Grant Storage Blob Data Contributor on the container
STORAGE_ACCOUNT_ID=$(az storage account show \
  --name <YOUR_STORAGE_ACCOUNT> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --query id -o tsv)

az role assignment create \
  --assignee $MANAGED_IDENTITY_PRINCIPAL_ID \
  --role "Storage Blob Data Contributor" \
  --scope "$STORAGE_ACCOUNT_ID/blobServices/default/containers/<YOUR_CONTAINER_NAME>"

# Get the AKS OIDC issuer URL
AKS_OIDC_ISSUER=$(az aks show \
  --name <YOUR_AKS_CLUSTER> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --query oidcIssuerProfile.issuerUrl -o tsv)

# Create federated credential — must reference namespace and service account name exactly
az identity federated-credential create \
  --name crewai-federated-credential \
  --identity-name crewai-workload-identity \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --issuer $AKS_OIDC_ISSUER \
  --subject "system:serviceaccount:crewai:crewai-sa" \
  --audiences api://AzureADTokenExchange
```

<Note>
  The federated credential subject must match the ServiceAccount that the Helm chart creates. With `rbac.create: true` (the chart default), the chart creates a ServiceAccount named `crewai-sa` in the namespace you deploy into. If you deploy to a namespace other than `crewai`, update the subject accordingly.
</Note>

### NGINX Ingress and cert-manager

```bash theme={null}
# Install NGINX Ingress Controller
helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace

# Install cert-manager
helm upgrade --install cert-manager cert-manager \
  --repo https://charts.jetstack.io \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true
```

Create a ClusterIssuer for Let's Encrypt:

```yaml theme={null}
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <YOUR_EMAIL>
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
```

```bash theme={null}
kubectl apply -f cluster-issuer.yaml
```

***

## Entra ID: Azure Portal Setup

### Step 1: App Registration

1. Go to [portal.azure.com](https://portal.azure.com) > **Microsoft Entra ID** > **App registrations** > **New registration**
2. **Name**: `CrewAI` (or your preferred name)
3. **Supported account types**: Accounts in this organizational directory only
4. **Redirect URI**: Web platform — `https://<YOUR_DOMAIN>/auth/entra_id/callback`
5. Click **Register**

<Warning>
  The redirect URI path must be exactly `/auth/entra_id/callback` (lowercase, underscore). Configure this before running `helm install` — a missing or incorrect URI causes authentication to fail with no clear error.
</Warning>

### Step 2: Collect Credentials

From the app overview page, copy:

* **Application (client) ID** → `ENTRA_ID_CLIENT_ID`
* **Directory (tenant) ID** → `ENTRA_ID_TENANT_ID`

### Step 3: Create Client Secret

1. Left sidebar > **Manage** > **Certificates & secrets**
2. **New client secret** — enter a description, choose expiration
3. Copy the **Value** immediately — it is not shown again
   → `ENTRA_ID_CLIENT_SECRET`

### Step 4: Grant Admin Consent

1. **Enterprise applications** > select your app
2. **Security** > **Permissions** > **Grant admin consent**
3. Confirm consent for Microsoft Graph **User.Read**

### Step 5: Create App Roles

1. Back in **App registrations** > your app > **Manage** > **App roles**
2. Create two roles:

| Display Name  | Value           | Allowed Member Types |
| ------------- | --------------- | -------------------- |
| Member        | `member`        | Users/Groups         |
| Factory Admin | `factory-admin` | Users/Groups         |

Ensure **"Do you want to enable this app role?"** is checked for each.

### Step 6: Assign Users

1. **Enterprise applications** > your app > **Manage** > **Properties**
2. Set **Assignment required?** to **Yes**, then **Save**
3. **Manage** > **Users and groups** > **Add user/group**
   * Regular users: assign **Member** role
   * Admin users: assign **Factory Admin** role

<Warning>
  For Entra ID deployments, admin access is granted exclusively through the **Factory Admin** App Role in Azure portal. Do **NOT** run `factory:grant_admin` — it writes to a database table that is not consulted for Entra ID users. The platform reads admin status from the JWT `roles` claim.
</Warning>

***

## Complete values.yaml

Replace all `<PLACEHOLDER>` values before running `helm install`.

```yaml values.yaml theme={null}
# ──────────────────────────────────────────────
# Disable bundled PostgreSQL — using Azure PostgreSQL Flexible Server
# ──────────────────────────────────────────────
postgres:
  enabled: false

# ──────────────────────────────────────────────
# Disable bundled MinIO — using Azure Blob Storage
# ──────────────────────────────────────────────
minio:
  enabled: false

# ──────────────────────────────────────────────
# Database: Azure PostgreSQL Flexible Server
# ──────────────────────────────────────────────
envVars:
  DB_HOST: "<YOUR_SERVER_NAME>.postgres.database.azure.com"  # e.g. crewai-postgres.postgres.database.azure.com
  DB_PORT: "5432"
  DB_USER: "crewai"
  POSTGRES_DB: "crewai_plus_production"
  POSTGRES_CABLE_DB: "crewai_plus_cable_production"
  POSTGRES_OAUTH_DB: "crewai_plus_oauth_production"  # Chart default is "oauth_db" — must match what you created above

  # ──────────────────────────────────────────
  # Object storage: Azure Blob Storage
  # Using AKS Workload Identity — no static credentials needed
  # ──────────────────────────────────────────
  STORAGE_SERVICE: "microsoft"
  AZURE_STORAGE_ACCOUNT_NAME: "<YOUR_STORAGE_ACCOUNT>"   # e.g. crewaistorage
  AZURE_CONTAINER_NAME: "<YOUR_CONTAINER_NAME>"          # e.g. crewai-assets

  # ──────────────────────────────────────────
  # Crew image registry: Azure Container Registry
  # Value is the registry prefix WITHOUT /crewai-enterprise
  # The platform appends /crewai-enterprise automatically
  # ──────────────────────────────────────────
  CREW_IMAGE_REGISTRY_OVERRIDE: "<YOUR_ACR_NAME>.azurecr.io/<YOUR_ORG>"
  # Example: myregistry.azurecr.io/production
  # Do NOT include /crewai-enterprise here — the platform appends it

  # ──────────────────────────────────────────
  # Authentication: Microsoft Entra ID
  # CLIENT_ID and TENANT_ID are non-sensitive identifiers — they belong under envVars
  # CLIENT_SECRET is a credential — it belongs under secrets (below)
  # ──────────────────────────────────────────
  AUTH_PROVIDER: "entra_id"                              # exact value — lowercase with underscore
  ENTRA_ID_CLIENT_ID: "<APPLICATION_CLIENT_ID>"          # from App Registration overview
  ENTRA_ID_TENANT_ID: "<DIRECTORY_TENANT_ID>"            # from App Registration overview

  # ──────────────────────────────────────────
  # Application
  # ──────────────────────────────────────────
  APPLICATION_HOST: "<YOUR_DOMAIN>"                      # e.g. crewai.company.com — no https:// prefix
  RAILS_LOG_LEVEL: "info"
  RAILS_MAX_THREADS: "5"
  DB_POOL: "5"

# ──────────────────────────────────────────────
# Secrets
# ──────────────────────────────────────────────
secrets:
  DB_PASSWORD: "<YOUR_DB_PASSWORD>"
  SECRET_KEY_BASE: "<64-char-hex-string>"                # openssl rand -hex 64
  ENTRA_ID_CLIENT_SECRET: "<CLIENT_SECRET_VALUE>"        # from Azure Certificates & Secrets

# ──────────────────────────────────────────────
# Wharf: OTLP trace and span collection
# wharf.enabled is the chart default (true) — included here for clarity
# Wharf shares the same DB host/port/user/password as the main app
# The wharf database must be pre-created in Azure PostgreSQL (see commands above)
# ──────────────────────────────────────────────
wharf:
  enabled: true
  postgres:
    wharfDatabase: "wharf"                               # must match the database you created above

# ──────────────────────────────────────────────
# Web: application pods
# ──────────────────────────────────────────────
web:
  replicaCount: 2
  enableSslFromPuma: false                               # REQUIRED when NGINX handles TLS termination
                                                         # Chart default is true — omitting causes NGINX to use
                                                         # the wrong backend protocol and return 502 errors
  resources:
    requests:
      cpu: "1000m"
      memory: "6Gi"
    limits:
      cpu: "6"
      memory: "12Gi"

  ingress:
    enabled: true
    className: "nginx"
    host: "<YOUR_DOMAIN>"                                # must match APPLICATION_HOST and TLS certificate

    annotations:
      cert-manager.io/cluster-issuer: "letsencrypt-prod"  # references the ClusterIssuer created above

    tls:
      - secretName: crewai-tls                          # cert-manager creates and manages this secret
        hosts:
          - "<YOUR_DOMAIN>"

# ──────────────────────────────────────────────
# Worker: background job processing
# ──────────────────────────────────────────────
worker:
  replicaCount: 2
  resources:
    requests:
      cpu: "1000m"
      memory: "6Gi"
    limits:
      cpu: "6"
      memory: "12Gi"

# ──────────────────────────────────────────────
# BuildKit: crew container image builds
# ──────────────────────────────────────────────
buildkit:
  enabled: true
  replicaCount: 1
  resources:
    requests:
      cpu: "500m"
      memory: "2Gi"
    limits:
      cpu: "4"
      memory: "8Gi"

# ──────────────────────────────────────────────
# RBAC: auto-creates ServiceAccount crewai-sa
# Required for Workload Identity — name must match the federated credential subject created above
# ──────────────────────────────────────────────
rbac:
  create: true

# ──────────────────────────────────────────────
# ServiceAccount: annotate with Managed Identity client ID
# Required for AKS Workload Identity to function
# ──────────────────────────────────────────────
serviceAccount:
  annotations:
    azure.workload.identity/client-id: "<MANAGED_IDENTITY_CLIENT_ID>"  # from az identity show --query clientId
```

<Warning>
  **`web.enableSslFromPuma: false` is required.** The chart default is `true`. NGINX terminates TLS and forwards plain HTTP to backend pods. With the default `true`, Puma expects HTTPS connections but receives HTTP from NGINX, causing 502 errors on every request.
</Warning>

<Warning>
  **`CREW_IMAGE_REGISTRY_OVERRIDE` must not include `/crewai-enterprise`.** The platform appends this suffix automatically. If your ACR image path is `myregistry.azurecr.io/production/crewai-enterprise`, set only `myregistry.azurecr.io/production`. Including the suffix causes push failures to a double-suffixed path.
</Warning>

<Warning>
  **`POSTGRES_OAUTH_DB` must be set explicitly.** The chart default is `oauth_db`. If you do not override it to `crewai_plus_oauth_production`, the OAuth service will attempt to connect to a database that does not exist and fail on startup.
</Warning>

<Note>
  If you prefer to authenticate to Azure Blob Storage using a static storage account key instead of Workload Identity, remove the `serviceAccount.annotations` block and add `AZURE_STORAGE_ACCESS_KEY: "<storage-account-key>"` under `secrets:` instead. Workload Identity is strongly preferred for production deployments.
</Note>

***

## Install

```bash theme={null}
helm install crewai-platform \
  oci://registry.crewai.com/crewai/stable/crewai-platform \
  --values values.yaml \
  --namespace crewai \
  --create-namespace
```

Wait for all pods to reach `Running`:

```bash theme={null}
kubectl get pods -n crewai -w
```

***

## Post-Install

### Required Initialization

These commands must be completed before any user can log in. Run them in the order shown.

```bash theme={null}
# 1. Initialize the internal CrewAI organization
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails studio:install_internal_organization

# 2. Set up default permission roles
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails factory:setup_permissions_defaults

# 3. Add your admin user as owner of org 2 (the first customer-facing org)
#    Replace admin@company.com with the email address assigned the factory-admin App Role in Azure
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails 'factory:add_owner[2,admin@company.com]'
```

<Warning>
  **Do NOT run `factory:grant_admin` for Entra ID deployments.** Admin panel access is controlled by the `factory-admin` App Role in Azure portal. The `factory:grant_admin` command writes to a database table that Entra ID authentication does not consult — it has no effect on Entra ID users and will not grant admin access.
</Warning>

<Note>
  For Entra ID, the user record is created in the database automatically on first login. The `factory:add_owner` command above can be run before or after the user's first login.
</Note>

### Studio V2 Setup

Studio V2 cannot be configured in `values.yaml`. Adding `studioV2.enabled` or `STUDIO_V2_ENABLED` has no effect — Helm silently ignores unknown keys. Setup requires the platform to be fully running and accessible.

**Step 1: Create the LLM Connection (UI)**

1. Log in to the CrewAI web UI as an admin
2. Navigate to **Settings → LLM Connections**
3. Click **New Connection**
4. Set the name to exactly `studio-v2` (lowercase, no spaces)
5. Select your LLM provider, enter the model name and API key
6. Click **Save**

<Warning>
  The connection name must be exactly `studio-v2`. The install commands in Step 3 look up this name specifically — a different name or capitalization causes them to fail silently.
</Warning>

**Step 2: Set as Default Connection (UI)**

1. Navigate to **Settings → Crew Studio**
2. Under **Default Connection**, select `studio-v2`
3. Click **Save**

**Step 3: Run Install Commands (kubectl)**

Run these commands in order. Each must complete successfully before running the next.

```bash theme={null}
# 1. Install the Studio agent
#    This command FAILS if the studio-v2 LLM Connection does not exist yet
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails studio:agent:install

# 2. Sync and index tools
kubectl exec -it deploy/crewai-web -n crewai -- \
  bin/rails studio:tools:sync_crewai_tools \
       studio:tools:sync_enterprise_tools

# 3. Install the Studio runner
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails studio:runner:install
```

<Warning>
  `studio:agent:install` will fail if the `studio-v2` LLM Connection does not already exist in the UI. Complete Steps 1 and 2 before running any of these commands.
</Warning>

***

## Verify

### Platform Health

```bash theme={null}
# All pods should be Running
kubectl get pods -n crewai

# Check web pod logs for errors
kubectl logs -l app.kubernetes.io/component=web --tail=50 -n crewai

# Check worker pod logs
kubectl logs -l app.kubernetes.io/component=worker --tail=50 -n crewai
```

### NGINX Ingress

```bash theme={null}
# ADDRESS should show an IP or hostname within 2–3 minutes
kubectl get ingress -n crewai
```

If ADDRESS is empty after 5 minutes, check the NGINX Ingress Controller logs:

```bash theme={null}
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller | tail -50
```

Check cert-manager certificate status:

```bash theme={null}
kubectl get certificate -n crewai
kubectl describe certificate crewai-tls -n crewai
```

If the certificate is stuck in a non-Ready state, verify the ClusterIssuer is configured correctly and that the NGINX controller has a public IP (Let's Encrypt requires HTTP-01 challenge access).

### Authentication

1. Navigate to `https://<YOUR_DOMAIN>`
2. Click **Sign in with Microsoft**
3. Authenticate with a user assigned a role in Azure portal
4. Verify the user lands on the dashboard without error

If login fails with a redirect URI mismatch or authentication error, verify the redirect URI in Azure matches `https://<YOUR_DOMAIN>/auth/entra_id/callback` exactly.

### Wharf Trace Collection

```bash theme={null}
# Wharf pod should be Running
kubectl get pods -l app.kubernetes.io/component=wharf -n crewai

# Check Wharf logs
kubectl logs -l app.kubernetes.io/component=wharf --tail=30 -n crewai
```

Wharf connects to the `wharf` database on the same PostgreSQL Flexible Server as the main application. If the pod is in CrashLoopBackOff, verify the `wharf` database exists and the `crewai` user has access.

### Studio V2

```bash theme={null}
# Both deployments should show Running
kubectl get deploy -n crewai | grep -E "studio-assistant|studio-runner"

# Check Studio assistant logs
kubectl logs -l app=studio-assistant --tail=20 -n crewai
```

### Factory Health Endpoint

```bash theme={null}
TOKEN=$(kubectl get secret crewai-platform-secrets -n crewai \
  -o jsonpath='{.data.FACTORY_DEBUG_TOKEN}' | base64 -d)

curl -H "X-Factory-Debug-Token: $TOKEN" \
  https://<YOUR_DOMAIN>/health/debug
```

All components should report `"status": "ok"`.
