---
icon: material/microsoft-azure
---

Deploy the [Retriever](../) to AKS and query blobs that already sit in an Azure Storage account. The retriever reads what your forwarder or analyzer lands in Blob Storage, indexes it in place, and serves queries over it.

Indexing is event driven. An Event Grid subscription raises `BlobCreated` on the input container, and four Azure Storage Queues carry index, query, sub-query and stream work between the retriever's roles, so a new blob is indexed as it arrives.

Pods authenticate with [workload identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview){target="\_blank"}. An Entra identity federated to the pod's Kubernetes service account issues short-lived tokens at run time, which is why the install below grants roles to an identity rather than configuring a key.

The storage account must have hierarchical namespace off. ADLS Gen2 sorts `/` lowest, which breaks the byte-lexicographic ordering the index scan depends on, and the accessor rejects such an account at construction.

???+ tenx-bootstrap "Step 1: Prerequisites"

    | Requirement | Description |
    |-------------|-------------|
    | 10x License | Optional. With none set, the engine runs the built-in [evaluation license](../../../manage/license.md#no-license-the-evaluation-license) |
    | Azure subscription | Permission to create resource groups, storage accounts, managed identities, role assignments, and Event Grid system topics |
    | CLI tools | `az`, `kubectl`, `helm` |
    | Storage account | `StorageV2` with hierarchical namespace off, or let the provisioning script create one |
    | AKS cluster | With the OIDC issuer and workload identity enabled, or let the provisioning script create one |

??? tenx-cloud "Step 2: Provision"

    The provisioning script ships inside the [chart](https://github.com/log-10x/helm-charts){target="\_blank"}. Pull and unpack it:

    ``` { .console .copy }
    helm repo add log10x https://log-10x.github.io/helm-charts

    helm pull log10x/retriever-10x --version 1.0.24 --untar
    ```

    One run creates the Azure side and writes the Helm values file:

    The chart archive stores every file without the executable bit, so run the script through `bash`.

    ``` { .console .copy }
    bash retriever-10x/scripts/azure/provision-retriever.sh \
      --resource-group tenx-rg \
      --location eastus \
      --account tenxlogs \
      --create-aks tenx-aks \
      --namespace log10x-retriever \
      --release tenx-retriever \
      --values-out values/azure.yaml
    ```

    Pass `--aks NAME` instead of `--create-aks NAME` to use a cluster that already has the OIDC issuer and the workload identity webhook. See the [Components](#components) section below for the resources the script creates.

    Subscriptions differ in which VM sizes they are allowed to create. `--node-size` defaults to `Standard_D2s_v7`, and a subscription that does not allow that size fails the cluster create with a message from Azure rather than from the script:

    ```
    The VM size of Standard_D2s_v7 is not allowed in your subscription in location 'eastus'
    ```

    On that failure the script prints the sizes the subscription does allow in that location. Re-run with `--node-size` set to one of them.

    The script writes the values file to `--values-out`, creating the directory for it, and prints the `helm install`, kubeconfig, first-query and result-download commands.

    Optional flags:

    | Flag | Default |
    |---|---|
    | `--input-container` | `logs` |
    | `--index-container` | `tenx-index` |
    | `--index-path` | `tenx` |
    | `--queue-prefix` | `tenx` |
    | `--image-tag` | `1.1.78` |
    | `--node-size` | `Standard_D2s_v7` |

??? tenx-mainconfig "Step 3: Install"

    Point kubectl at the cluster first, so the release lands on the AKS cluster rather than on whatever context kubectl already has:

    ``` { .console .copy }
    az aks get-credentials --resource-group tenx-rg --name tenx-aks \
      --file values/tenx-aks.kubeconfig --overwrite-existing
    export KUBECONFIG=values/tenx-aks.kubeconfig
    ```

    The emitted values file, with the defaults above:

    ``` { .yaml title="values/azure.yaml"}
    # The service account the chart creates is named for the release, which is
    # what the federated credential subject binds to.
    fullnameOverride: tenx-retriever

    image:
      tag: "1.1.78"

    storage:
      provider: azure
      azure:
        account: tenxlogs
        indexContainer: "tenxlogs/tenx-index/tenx"
        inputContainer: logs
        invoke: queue
        queues:
          index: tenx-index
          query: tenx-query
          subquery: tenx-subquery
          stream: tenx-stream
        auth:
          method: workloadIdentity
          clientId: "<managed identity client id>"
          tenantId: "<entra tenant id>"

    scheduledQueries:
      enabled: false
    ```

    Install:

    ``` { .console .copy }
    helm install tenx-retriever log10x/retriever-10x --version 1.0.24 \
      --namespace log10x-retriever --create-namespace \
      -f values/azure.yaml
    ```

    `log10xApiKey` is optional, and empty runs the built-in [evaluation license](../../../manage/license.md#no-license-the-evaluation-license). Add `--set-string log10xApiKey="$LOG10X_API_KEY"` for a production license, which keeps the key out of the values file.

    The emitted file pins `image.tag` to an engine release. Never set it to `latest`, and pin the same field in a values file written by hand, because an unset tag falls back to the chart's `appVersion`, an engine release that predates Azure support. On `storage.provider: azure` the chart adds the `azure.workload.identity/use` pod label and the `azure.workload.identity/client-id` service account annotation, and the webhook injects `AZURE_CLIENT_ID`, `AZURE_TENANT_ID` and `AZURE_FEDERATED_TOKEN_FILE`. `queryLogGroup` is an AWS CloudWatch Logs setting. Leave it unset on Azure.

??? tenx-objectstoragequery "Step 4: Verify Indexing and Querying"

    Wait for the pod:

    ```bash
    kubectl -n log10x-retriever get pods -w
    ```

    The image is a JVM build, so the pod takes tens of seconds to become ready. The script granted the signed-in operator `Storage Blob Data Contributor` and `Storage Queue Data Contributor` on the account, so `--auth-mode login` works below unless the run passed `--no-operator-roles`.

    Upload one log file to the input container:

    ```bash
    echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"level\":\"ERROR\",\"message\":\"Test error\"}" > test.log

    az storage blob upload --auth-mode login \
      --account-name tenxlogs --container-name logs \
      --name app/test.log --file test.log
    ```

    Plain text lines and JSON lines both index. A plain text line carries its own timestamp first:

    ```
    2026-09-13T15:32:25Z ERROR OrderService failed to settle order ORD-8842317 after 3 attempts
    ```

    The ISO-8601 UTC timestamp at the start of the line is what the query window is evaluated against. A level word in the event text, `ERROR`, `WARN` or `INFO`, sets `severity_level`, which is the field the query below filters on.

    Event Grid puts a `BlobCreated` event on the `tenx-index` queue and an index pod picks it up. `IndexFilterStats - index complete` carries the bytes read and the filter count, and `IndexFilterWriter - index written` follows it with one line per blob indexed:

    ```bash
    kubectl logs -n log10x-retriever -l app=retriever-10x \
      -c retriever-10x-all-in-one --tail=50
    ```

    ```
    [INFO ] 2026-09-13 14:31:50.112 [executor-thread-1] IndexFilterStats - index complete. Bytes: 354, filters.size: 1, ...
    [INFO ] 2026-09-13 14:31:50.128 [executor-thread-1] IndexFilterWriter - index written: object=app/test.log, byteRanges=1, filters=1, values=34, minTimestamp=..., maxTimestamp=...
    ```

    Then enqueue a query on the query queue as raw JSON. The `name` field must equal the first path segment of the blob name, so `app/test.log` is queried as `"name":"app"`, and any other value matches nothing and reports nothing. `writeResults` tells the stream role to write matched events back to the index container as JSONL:

    ```bash
    az storage message put --auth-mode login \
      --account-name tenxlogs --queue-name tenx-query \
      --content '{"name":"app","from":"now(\"-1h\")","to":"now()","search":"severity_level==\"ERROR\"","writeResults":true}'
    ```

    The query window is evaluated against the timestamps inside the log lines, so lines older than the window return nothing. Upload a log with current timestamps, or widen `from`.

    Results land in the index container at `<index-path>/tenx/<name>/qr/<queryId>/<fromEpochMs>_<toEpochMs>/<id>.jsonl`. With the defaults above the listing prefix is `tenx/tenx/app/qr/`:

    ```bash
    az storage blob list --auth-mode login \
      --account-name tenxlogs --container-name tenx-index \
      --prefix tenx/tenx/app/qr/ --output table
    ```

    Download one of the listed `.jsonl` blobs to read the matches:

    ```bash
    az storage blob download --auth-mode login \
      --account-name tenxlogs --container-name tenx-index \
      --name <blob> --file results.jsonl
    ```

    Each line is one matched event, carrying `text` with the event as read, `severity_level` with the level the indexer inferred, and `message_pattern` with the pattern the event belongs to.

    A `_DONE.json` marker sits under the same prefix. The coordinator writes that marker once it has dispatched the sub-queries, ahead of the result blobs, and its counters do not describe what the stream role went on to write. Poll the prefix for `.jsonl` files rather than reading the marker as a completion signal.

??? tenx-auth "Step 5: Other Credentials"

    Workload identity is the default. Exactly one credential may be set, and it must match `storage.azure.auth.method`. Pass the value with `--set-string` so it stays out of the values file.

    === "Service principal"

        An Entra application rather than a managed identity. Set `auth.method: servicePrincipal` with `clientId`, `tenantId` and `clientSecret`, which the chart mounts as `AZURE_CLIENT_SECRET`.

    === "Account key"

        Set `auth.method: accountKey` with `accountKey`, which the chart mounts as `AZURE_STORAGE_KEY`. An account key carries full control of the account. Use it for short-lived tests, and workload identity or a service principal for standing deployments.

    === "SAS"

        Set `auth.method: sasToken` with `sasToken`, which the chart mounts as `AZURE_STORAGE_SAS_TOKEN`. A SAS token cannot read account properties, so the namespace check is skipped. Issue the token against a flat-namespace account.

    Point `storage.azure.auth.secret.existingSecret` at a secret you manage to keep the credential outside the release.

??? tenx-monitoring "Step 6: Expected Log Lines"

    The namespace check calls `Get Account Information`, an account-level operation covered by control-plane roles rather than by data-plane roles such as `Storage Blob Data Contributor`. The call answers 403, the check treats the account as flat-namespace, and each accessor construction logs one line. One line per pipeline run is expected.

    ```
    [WARN ] 2026-09-13 14:31:49.897 [executor-thread-2] AzureBlobClient - could not read account information for tenxlogs, status 403; assuming a flat namespace
    ```

    The caller logs a second line for the same reason:

    ```
    [WARN ] 2026-09-13 14:31:49.903 [executor-thread-2] AzureIndexAccess - could not determine the namespace of storage account tenxlogs; the index requires a flat namespace
    ```

??? tenx-delete "Step 7: Teardown"

    ```bash
    helm uninstall tenx-retriever -n log10x-retriever

    bash retriever-10x/scripts/azure/provision-retriever.sh \
      --resource-group tenx-rg --destroy
    ```

    `--destroy` removes what the script created, including the blobs in both containers. A cluster passed with `--aks` is left alone.

## :material-cube-outline: Components

What the provisioning script creates. Tabs describe each component and what it's for.

=== ":material-database-outline: Storage account"

    A `StorageV2` account in the resource group, hierarchical namespace off, with a TLS 1.2 floor. Pass `--account` to name it. The index scan reads listings in byte-lexicographic order, which is what a flat namespace gives.

=== ":material-folder-outline: Containers"

    Two blob containers on that account. `logs` holds the source logs the forwarder or analyzer lands. `tenx-index` holds the Bloom filter and reverse-index artifacts, and the JSONL results of queries that set `writeResults`. Rename either with `--input-container` and `--index-container`.

=== ":material-inbox-multiple-outline: Storage Queues"

    Four queues, `tenx-index`, `tenx-query`, `tenx-subquery` and `tenx-stream`, buffer work between the index, query and stream roles. A pod crash does not drop work in flight. The next poller picks it up. Change the prefix with `--queue-prefix`.

=== ":material-shield-lock-outline: Managed identity"

    The pod identity. A user-assigned managed identity holding `Storage Blob Data Contributor` and `Storage Queue Data Contributor` on the account, with a federated credential for `system:serviceaccount:<namespace>:<release>` so the service account the chart creates can exchange its projected token for an Azure token. The script grants the same two roles to the signed-in operator, which `--no-operator-roles` skips. `--create-aks` also creates the cluster with the OIDC issuer and the workload identity webhook that make the exchange work.

=== ":material-flash-outline: Event Grid system topic"

    Triggers indexing. A system topic on the storage account with a `Microsoft.Storage.BlobCreated` subscription, scoped to the input container and delivering to the `tenx-index` queue, so the indexer's own writes to the index container cannot re-trigger it.

=== ":simple-helm: Helm release"

    Installs the `retriever-10x` chart from the emitted values file, which in turn creates pods, HPAs, Services, the service account the federated credential binds to, and optional CronJobs for scheduled queries.
