DEMO/Infrastructure

← Back to dashboard

Storage Management

Audience: operator · Profile: both · Read this when: you are enabling a feature that needs a storage backend, which several of them do.

Part of Operations.

This document explains how the dashboard stores playbooks, scripts, and other large assets that don't fit in the encrypted credentials database — and how to choose, configure, and migrate between cloud object stores.

The companion to Secrets Management: secrets are small, sensitive, and live in a per-key encrypted store. Storage holds bigger, mostly-non-sensitive payloads (playbooks, shell scripts, package files, image artefacts) that need to be readable by Ansible runners across hosts and by cloud VM-import APIs. For the philosophy and best-practice side of running playbooks against your fleet, see Config Management. For the IaC layer that stood the targets up in the first place, see Infrastructure as Code. For the image build → promote lifecycle that produces the binaries the IaC layer deploys, see Image Management.


Philosophy

The dashboard talks to one active storage backend at a time. You pick from the list below, configure as many as you like, and choose one to be active. Backends not currently active stay reachable through the migration UI so you can copy assets between them without downtime.

Backend Underlying service Best for
AWS S3 S3 bucket + key prefix Teams already on AWS; cheapest at scale
Azure Blob Storage Storage account + container + blob prefix Teams on Azure; integrates with Azure SP creds
Google Cloud Storage GCS bucket + object prefix Teams on GCP; same SA creds as Compute Engine
Local Filesystem / UNC Filesystem path inside the dashboard container, or a corporate \\server\share UNC accessed via SMB On-prem hypervisor targets when a corporate file share is the source of truth — see the constraint below
Remote Filesystem / UNC (via agent) The same kind of target, reached by a remote agent instead of by the dashboard container A cloud-hosted dashboard — or a POV instance — that needs an on-prem share it has no network route to. See below

They are interchangeable from the dashboard's perspective. Switching backends does not move data; the Migrate panel does that explicitly, and only deletes from the source if you ask it to (today: never — see "Migration semantics" below).

The active backend holds two kinds of data: user assets (playbooks, packages, Packer manifests, the image-registry hub) and Terraform remote state for cloud deploys (see What counts as an asset). That second kind is why switching backends is guarded — if live deployments have state on the current backend, the dashboard won't let you flip the active backend out from under it without migrating that state too (see "Migration semantics").


Why storage is its own page

Storage was originally configured inside the Ansible feature panel. It lives on its own page because:


What counts as an asset

Type Extensions Used by
Ansible playbook .yml, .yaml Ansible runner — executed as-is
Shell script .sh Ansible runner — auto-wrapped: ansible.builtin.script
PowerShell script .ps1 Ansible runner — auto-wrapped: ansible.windows.win_script (Windows targets only; the host's inventory must set ansible_connection=winrm)
RPM package .rpm Ansible runner — auto-wrapped: copy + dnf install
DEB package .deb Ansible runner — auto-wrapped: copy + apt install

Files outside this set are rejected at upload. Assets are stored under a configurable key prefix (default config-mgmt/), so multiple deployments can share a bucket if the prefix differs.

The table above governs the ASSET UPLOAD path only — files a user uploads for the Ansible runner to execute. It is not a restriction on what the dashboard itself can store. In particular, Cloud Functions are unaffected by it: their Python handlers are not uploaded at all. They ship inside the dashboard image, the packager builds the deployable zip in memory, and it goes to a dedicated per-cloud bucket (function_package_s3_bucket / function_package_gcs_bucket / a container on storage_azure_account) rather than to the active storage backend. See Cloud Functions → package stores.

Beyond user-uploaded assets, the active backend also holds Terraform remote state. Cloud VM, cloud-database, and Kubernetes-cluster deploys write their state to the same active backend (keyed per job under terraform-state/, with backend-native locking), so the backend you pick here is load-bearing for infrastructure teardown, not just playbooks. The BeyondTrust PRA tunnel state is the one exception — it's scrubbed of credentials and kept in the database instead. See Infrastructure as Code → State.

Pages

The larger parts have a page each:

Page Read it when
Uploading assets you are uploading playbooks, scripts, packages or installers, including ones too large for a browser upload
Configuring a storage backend you are setting up S3, Azure Blob, Google Cloud Storage, a local or UNC path, or a remote share through an agent
The image-registry hub you want container images mirrored into each cloud's registry from one place


Migration semantics

The Migrate panel copies every asset from a source backend to a target backend. Operating principles:

The migrate result block on the page summarises three lists: Copied, Skipped, Failed. Save the page or screenshot before navigating away if you need a record.


Scoping an asset to a workstream

Every asset is global by default: anyone who can open the Storage page sees it, and anyone who can run Config Management can run it. An administrator can limit an asset to one workstream with the Workstream column on the asset table. From then on only that workstream's members, and administrators, can:

Other things to know:

Changing a scope is administrator-only and audited (storage_asset_scope_update). The API is PUT /api/storage/scope with {"backend": "s3", "name": "baseline.yml", "workgroup": "team-a"}; "workgroup": null makes the asset global again. A blank backend means the active backend.


API reference

Method + path Auth Purpose
GET /api/storage/backends logged-in user Backend metadata + active state. Used by /storage and the Ansible flag prereq gate.
GET /api/storage/config admin All per-backend config values.
PATCH /api/storage/config admin Update fields + active selection. Validates active is configured before flipping.
POST /api/storage/test admin Reachability probe (lists assets in the named backend).
GET /api/storage/list logged-in user Assets in the active backend.
GET /api/storage/list/{backend} admin Assets in a specific backend (used by the migrate UI's source picker).
POST /api/storage/upload logged-in user Upload {filename, content_b64} to the active backend. Inline lane; 413 above 64 MB.
POST /api/storage/upload/begin logged-in user Open a chunked upload: {filename, size} → {handle, part_bytes, parts}. Object stores only.
PUT /api/storage/upload/part/{n} logged-in user Stage one raw part. Body is the bytes; handle in X-Upload-Handle.
POST /api/storage/upload/commit logged-in user Commit {handle, parts} into one object.
POST /api/storage/upload/abort logged-in user Discard {handle}'s staged parts. Never fails loudly.
POST /api/storage/migrate admin Copy {source, target, overwrite} → returns {copied, skipped, failed}.
DELETE /api/storage/asset/{name} admin Remove a single asset from the active backend.
PUT /api/storage/scope admin Limit {backend, name} to workgroup, or make it global with null. See Scoping an asset to a workstream.

Storage credentials live in the encrypted DB exactly the same way as other config values; nothing on this page reads or writes .jwt_secret_key.


Cost

Idle cost is roughly the cost of the underlying object store, which is near-zero for the playbook/script asset profile (KB-MB files, low PUT/GET volume).

Backend Storage class Typical monthly cost for ~100 MB of assets
S3 Standard ~$0.0023
Azure Blob Hot LRS ~$0.0018
GCS Standard ~$0.0020

Network egress during runs (the runner downloads the playbook bytes once per job) is the same per-byte rate as any cross-AZ traffic in the respective cloud.


Backup and lifecycle

The dashboard does not manage backup or lifecycle for storage backends. Use each provider's native primitives:

Recommended baseline for any production-style deployment: enable versioning so an accidental overwrite or migration can be reverted. This matters doubly for the terraform-state/ prefix — versioning there is your recovery path if a state object is corrupted or deleted, since losing it orphans the resources that state tracks.


Troubleshooting

"No active storage backend" error on the Config Management page. You have a backend configured but didn't activate it. Open /storage, pick the radio button for the backend you intended, and Save.

Test connection fails with a 403 / AccessDenied. The cloud credentials configured in Setup → AWS/Azure/GCP don't include read+write access to the bucket. For S3 this typically means s3:ListBucket, s3:GetObject, s3:PutObject, s3:DeleteObject on the bucket and arn:aws:s3:::bucket/prefix/*. For Azure Blob, the service principal needs Storage Blob Data Contributor on the storage account or the specific container.

Migrate finishes, but the new uploads still go to the old backend. Switching the active backend is a separate step. After migration, change the radio button in Backend and click Save configuration.

An asset shows up in GET /api/storage/list/{backend} but not in the active list view. Different backends maintain different prefixes. The active backend's prefix may be excluding it. Either reuse the same prefix everywhere, or include the prefix when uploading.

The Ansible feature flag toggle is greyed out. Working as intended — the dashboard requires storage to be configured and active before Ansible can be enabled. Open /storage, pick a backend, save, then come back to Settings → Integrations.