← Back to dashboard

Storage Management

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 object-storage backend at a time. You pick from three providers, 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

All four 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.

Uploading

Two equivalent paths to put an asset in storage:

  1. /storage page — drag-and-drop or pick a file in the Upload card, click Upload. Goes straight to the active backend. Available to any logged-in user.
  2. /config-mgmt page — same upload form, plus inline run controls for executing the asset against your hypervisor inventory or cloud instances. Available to any logged-in user.

Either path uses the same POST /api/storage/upload endpoint behind the scenes. The /config-mgmt-side POST /api/config-mgmt/upload endpoint also still works (delegates to the same service).


Configuring storage

Open /storage (admin only). The page has four sections:

  1. Backend — pick the active backend with a radio button. Each backend's configuration card shows below; fill in the fields for the one(s) you want to use. A backend appears as configured when its primary identifier (bucket / storage-account / bucket name) is set, regardless of whether it's the active one.
  2. Image-registry hub — pick the backend that holds the canonical VHD/raw artefact for every registered image. Leave on "Same as active backend" for single-backend installs; only change it if you want the image hub on a different cloud than your day-to-day asset uploads. See Image-registry hub below.
  3. Stored assets — once a backend is active, the list shows what's in it. Use Config Management's upload form (/config-mgmt) to add playbooks; this page is read-mostly except for delete.
  4. Migrate — covered below.

Required cloud credentials

Each backend reuses cloud credentials configured elsewhere in the dashboard:

Backend Reads creds from
S3 aws_access_key_id / aws_secret_access_key (Setup → AWS)
Azure Blob Azure service principal (Setup → Azure)
GCS GCP service account JSON (Setup → GCP)

If the cloud you want to use for storage isn't already configured for VM deploys, set those creds first in /setup or the matching Settings panel.

S3

Field Notes
Bucket Required. The bucket must already exist (the dashboard does not auto-create).
Region Optional. Defaults to your AWS region from Setup → AWS.
Key prefix Optional. Defaults to config-mgmt. Useful for sharing one bucket across deployments.

Azure Blob Storage

Field Notes
Storage account Required. The account must already exist.
Container Defaults to playbooks. Created on first upload if missing.
Blob prefix Defaults to config-mgmt.

Google Cloud Storage

Field Notes
Bucket Required. The bucket must already exist.
Object prefix Defaults to config-mgmt.

Local Filesystem / UNC

Field Notes
Path Required. Either a path inside the dashboard container (typically a bind-mounted host directory like /srv/playbooks) or a UNC \\server\share[\subpath]. UNC paths use the SMB protocol via the smbprotocol Python library — no host-side mount or cifs-utils required.
Username / Password / Domain Optional, used only for UNC. Username may be bare or DOMAIN\user; the Domain field is convenience for the latter. Password is encrypted at rest in the dashboard's config DB.

Constraint: local backend only works with the local Ansible runner

The Local backend is only selectable when Settings → Ansible → Runner is set to Local Docker (default). Cloud Ansible runners (AWS ECS, Azure ACI, GCP Cloud Run) live in cloud-only VPCs/VNets and have no network path back to a corporate file server. If you tried to use a UNC path from a Fargate task, the SMB connection would fail at TCP 445 and the run would error before the playbook ran.

The dashboard enforces this in two places:

Concrete fit: if your contributors test on-prem hypervisor targets (Proxmox VE, vSphere/ESXi, Nutanix AHV, XCP-ng, Hyper-V) with playbooks hosted on a corporate share, the Local backend is the right choice.

After filling in fields, click Test connection to probe the backend — it lists the bucket/container as a quick reachability check. Save with Save configuration; activation flips the moment the save succeeds.


Image-registry hub

The hub backend is the single storage backend that holds the canonical VHD/raw artefact for every registered image, regardless of which cloud built it. It's the source the cross-cloud promote flow reads from when it kicks off a per-target runner — see Image Management for the full lifecycle and runners/promote/README.md for the runner internals.

Configuration. On /storage, the Image-registry hub picker has four options:

Local / SMB backends can't be the hub. The promote runners need an HTTPS-reachable URL for the source artefact, which the local filesystem doesn't offer. The page's picker doesn't list local; posting it via the API returns a 400 with a pointer to this section.

What the hub does. After every successful Packer build the dashboard exports a portable VHD via the cloud's native export API into same-cloud storage. If that same-cloud storage is the hub (e.g. build cloud = AWS, hub = S3), no extra hop. If it isn't (build cloud = AWS, hub = Azure Blob), the dashboard runs storage_service.copy(build_backend, build_key, hub, hub_key) to move the VHD into the hub and deletes the build-side staging copy. Into an Azure Blob hub this copy is fully server-side (the staging object is presigned and Azure pulls each block itself — no bytes through the dashboard container); into an S3/GCS/OCI hub from a different build cloud it stages through the container's ephemeral disk, which multi-GB VHDs can overflow — pick an Azure Blob hub or the build cloud's own backend if you build cross-cloud.

The same export-and-land-on-hub path runs when an operator clicks Export VHD on a cloud-native image in the per-cloud Images tab (AWS Private AMIs / Azure Managed Images / GCP Custom Images). This is the recovery path for builds whose auto-export was skipped (e.g. the storage prerequisite was missing at build time). See Image Management → Manual export.

What it doesn't do. The hub is not where the promote runner uploads to. Each target cloud has its own staging container the runner writes into (promote_runner_aws_staging_bucket, promote_runner_azure_staging_container, promote_runner_gcp_staging_bucket) so the cloud's import API reads from local storage. The hub stays the read-only source-of-truth.

Promote-runner config. The promote_runner_* keys (image override, ECS/ACI/Cloud Run/Container Instances plumbing, target-side staging, IAM role ARNs) are edited in Settings → Remote Worker → Image-promote runner, one sub-card per target cloud. PATCH /api/storage/config also accepts them — the promote runner shares the hub backend's lifecycle, so the storage API round-trips them for scripted setups. The runner README has the full table.


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.


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.
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.

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.