Operations
Storage account configuration, Service Bus topology, Helm releases, and the pipeline for the documents service.
The documents service is in DEPLOYED_SERVICES, so its paths are part of the merged platform
OpenAPI document and its .azure/ package is the one that runs.
Storage accounts
The catalog is non-secret and lives in services/documents/.azure/env/config.dev.yaml:
| Key | Dev value |
|---|---|
DOCUMENTS_STORAGE_ACCOUNTS | flowintelprivatedev01 |
DOCUMENTS_STORAGE_FLOWINTELPRIVATEDEV01_VISIBILITY | private |
DOCUMENTS_STORAGE_FLOWINTELPRIVATEDEV01_CONTAINER | knowledge-storage,project-storage,agent-storage |
DOCUMENTS_STORAGE_FLOWINTELPRIVATEDEV01_SAS_TTL | 15m |
_CONTAINER is a comma-separated list, so one Azure storage account can serve several containers
under a single catalog entry. The list is an allow-list rather than a default: a request names one of
the containers on it, and a request naming anything else gets 422. The set of containers a caller
can reach is therefore exactly the set written in configuration.
Where an account owns a single container, a request may leave container out and gets that one.
Where it owns several, a request must name one — there is no safe default, and picking one would put
a document somewhere the caller did not ask for.
The per-account key name is the account name upper-cased with every non-alphanumeric character
replaced by _, so adding archive-cold means DOCUMENTS_STORAGE_ARCHIVE_COLD_* and appending the
name to DOCUMENTS_STORAGE_ACCOUNTS. Visibility, container, and connection string are required: an
account listed without them fails startup rather than serving requests it cannot sign for. Container
names are checked against Azure's naming rules at startup too, so a typo is a startup failure that
names the setting rather than an error on first upload. _SAS_TTL is optional, defaults to 15
minutes, and is capped at 24 hours.
DOCUMENTS_STORAGE_<ACCOUNT>_CONNECTION_STRING is the one secret per account and belongs in the
SOPS secrets file, never in config.*.yaml. Layout:
services/documents/.azure/env/readme.md.
Changing a SAS lifetime
*_SAS_TTL is read at startup and applied when a URL is signed, so a change takes effect for
new SAS URLs after a restart. Already-issued URLs keep the lifetime they were signed with — there is
no revocation short of rotating the account key.
Adding an account
- Create the storage account and its containers in Azure.
- Add the connection string to the SOPS secrets file for that environment.
- Add the visibility, container, and TTL keys to
config.<env>.yaml, and the name toDOCUMENTS_STORAGE_ACCOUNTS. - Deploy.
cmd/apicallsCreateIfNotExistsfor every container in the list at startup, so step 1's containers are belt-and-braces.
Adding a container to an existing account is steps 3 and 4 alone: append it to that account's
_CONTAINER list and deploy.
Removing an account is not symmetric: documents that name it keep naming it, and reads of those
documents return 503 until the account is reachable again. Migrate the rows first.
Upload cap
DOCUMENTS_MAX_UPLOAD_BYTES sets the largest blob that can be registered. The default is 256 MiB.
The setting is optional, but if it is present and is not a positive whole number, the service
refuses to start rather than ignoring it. Someone who sets a limit should be able to assume it took
effect.
The limit is checked when the document is registered, not while the bytes are being written, because
a SAS URL cannot carry a size limit. So a caller can upload something too big and only find out
afterwards. That upload is refused with 413, never becomes a document, and its bytes are removed
by the reaper below.
Orphan reaper
Every upload takes two calls. Between handing out a SAS URL and registering the document, there is a blob in storage with no database row. A caller who gives up halfway leaves that blob behind forever.
POST /internal/jobs/reap-orphans deletes blobs in the configured containers that have no document
row and have not been touched for DOCUMENTS_ORPHAN_AGE, which defaults to six hours. It makes one
pass per container and reports a result for each, because a container's listing is only meaningful
against the document rows for that same container.
The window is that long on purpose. Deleting a blob while its registration call is still in flight would break a caller doing exactly what the API told it to do. Each run deletes at most 500 blobs and is safe to repeat, so a backlog clears over several runs.
Deletion only goes one way. A blob with no row is rubbish and gets removed. A row with no blob is a bug, and the reaper leaves it alone rather than tidying the evidence away.
The route uses the shared scheduler secret as Authorization: Bearer
(ADR 0026) and is not in the
OpenAPI document. If SCHEDULER_TOKEN is unset, the route is never registered and returns 404. A
process that never received the secret has no reaper at all, rather than one anybody can trigger.
Service Bus topology
services/documents/.azure/bus/topology.yaml declares one domain:
| Domain | Topic | Subscriber queues |
|---|---|---|
documents | documents-documents-fanout | documents-documents-queue-listener |
documentsListener is the only consumer. Only name a subscriber that has a reader — a queue
nobody consumes fills until its messages dead-letter. Every subscriber receives every core event
from the documents outbox — documents.created and
documents.updated. Append another name under subscribers to add a queue. There is no PII topic:
neither event type declares PII, and payloads carry filenames and reference ids rather than file
contents. See ADR 0005.
Environment namespace and per-domain fanout/queue settings:
services/documents/.azure/bus/bus.dev.yaml. The publisher embeds the same
definition via services/documents/messageBus/topology_gen.go, generated by
make generate.
One image, several releases
| Helm release | Binary | Chart |
|---|---|---|
documents-api | /service (default) | k8s/ |
documents-documentslistener | /documentsListener | k8s/ |
Dev API ingress: documents.dev.apps-and-services.com
(services/documents/.azure/k8s/values/api.dev.yaml); prod is
documents.apps-and-services.com.
Pipeline
services/documents/.azure/azure-pipelines.yml triggers on develop /
master changes under services/documents. Stages match template's: decrypt SOPS secrets, build
the image from the repository root, busctl plan (exit 2 additive, exit 3 destructive and gated by
a manual approval), apply, then helm upgrade --install per chart.
Migrations
A Helm pre-upgrade Job runs the API binary with -migrate. Secrets arrive as a ConfigMap at deploy
time and are never baked into the image.
0002_documents.sql has no down: forward-only, like every migration on the platform.
What to look at when uploads fail
| Symptom | Where to look |
|---|---|
422 on create, "blob is not present" | The caller skipped the SAS PUT, or the SAS expired before the upload finished — check *_SAS_TTL against the file sizes callers send |
422 "unknown storage account" | DOCUMENTS_STORAGE_ACCOUNTS and the per-account keys for that environment |
503 "storage is not configured" | The process started without any DOCUMENTS_STORAGE_* keys |
503 naming a specific account | That account was removed from config while documents still reference it |
| Events not arriving | The outbox: unpublished rows in documents_events, and the publisher's advisory lock |
Unpublished outbox rows and queue depths are readable from the DevOps console once this service is deployed.