Managed image lifecycle
Golden images, built the same way every time.
ImageFactory defines an image as data, builds it on infrastructure you choose, publishes it to your own galleries, and keeps a permanent answer to what went into every version.
Platform
One pipeline, whatever you build on.
The provisioning chain is platform-agnostic, so Azure, vSphere and Hyper-V are source templates rather than a second product.
- Microsoft Azure
- Google Cloud Platform — planned
- Amazon Web Services — planned
- Red Hat OpenShift — planned
- VMware vSphere — planned
- Hyper-V — planned
The lifecycle
Three stages, one record.
Define it, build it, and retire it without breaking the catalogs that depend on it. The third one is where image tooling usually stops.
01 — Define
An image is a document
One validated manifest describes the whole image. Nothing important lives in a file somebody forgot to commit.
One schema, everywhere
The same schema validates the form you type into and the job a runner executes.
Every change is a revision
The complete manifest is appended, never a delta — so any two can be diffed and any one restored.
Review before it builds
A change request carries the revision it was based on, so a concurrent edit is caught rather than silently overwritten.
Steps disable, they do not disappear
Taking a step out to isolate a failure should not mean losing how it was configured.
02 — Build
Packer, driven properly
The build is HashiCorp Packer. What we add is everything around it that decides whether a build is repeatable.
One parameterised source
The build block is generated from the image's own pipeline, by the same function the console previews.
Runners where the work is
One ships with the install so a fresh deployment can build immediately. More can sit in your network, on your side of the firewall.
Leases, not hope
A runner that dies mid-build returns its job to the queue marked as retried — because a run that created a VM may have left one behind.
Logs while it runs
Append-only chunks streamed back from the runner, not a file you go looking for after the fact.
03 — Operate
Retire without breaking anything
Most image tooling is good at creating and careless about everything after. The dangerous operations are the ones worth designing.
Archiving a version is instant
Catalogs already provisioned from it keep working; new ones will not pick it up.
Deleting has the brakes on
Deletion refuses while any version still exists, and never removes the record of what the image contained.
Nothing flips optimistically
A pending change shows as pending. An operator who believes an image is archived would otherwise go on building it.
Append-only audit
Written by nothing that could also delete it, which is what makes it worth reading.
Definitions
The image is a document,
not tribal memory.
Source, target, hardening and the ordered build pipeline all live in one validated manifest.
- Validated before it is saved, not when it failsThe schema is the definition. A manifest that would not build is rejected in the form, not thirty minutes into a run.
- Ordered, and not yours to reorderThe image always turn out the same, regardless of who is looking at it.
- YAML in, YAML outImport what you already have, export whenever you want. Portability is a feature, not a retention strategy.
apiVersion: image-factory/v1
name: windows-11-corporate
lifecycle: active
source:
publisher: MicrosoftWindowsDesktop
offer: Windows-11
sku: win11-25h2-avd
target:
gallery: acg_ctx_automation
imageDefinition: windows-11-corporate
hardening:
acsc: true
wdot: { enabled: true, profile: Corporate-AVD }
aaronLocker: { enabled: true, mode: AppLocker }
provisioning:
- id: applications
kind: packages
packages:
- id: 7zip
- id: googlechrome
- id: settle
kind: reboot
timeout: 15m
- id: baseline
kind: ansible
playbook: windows/baseline.ymlCredential custody
Your vCenter password never leaves your network.
Buy the managed service and keep your platform credentials entirely inside your own perimeter. A runner you install resolves build environments against its own local configuration; the control plane dispatches the environment's name and never holds the secret. That is a property of the design, not a promise about how we operate.
- Outbound only, alwaysThe runner opens connections and never accepts them. No inbound rule, no VPN, no public IP, no reverse tunnel — just HTTPS out, which is what already survives a corporate egress proxy.
- Or hand them to us, knowinglyHosted-custody environments are encrypted at rest and injected at dispatch. It is a genuinely weaker position, and we sell it as the convenience option it is.
- Fails fast, not forty minutes inA job naming an environment its runner cannot resolve is rejected at dispatch with a message you can act on.
Where it runs
Your subscription. Your network. Your images.
We hold the definitions, the queue and the history. Everything that touches a machine runs in your own account: the runner you install, the build VM it creates and destroys for each image, the package repository your applications come from, and the gallery the finished image lands in.
No black boxes
See the template it is actually going to run.
Edit the build pipeline and the console shows you the exact Packer template it renders to, produced by the same function a runner calls. The preview cannot drift from what builds, because it is not a preview of something else.
- One code pathA workstation build and a CI build take an identical route through the same generator.
- Nothing appended behind your backThe steps the generator adds are visible in the preview, in position, with the reason they are there.
- Read the sourceEvery decision on this page is one you can go and check.
# from provisioning[] — yours
provisioner "powershell" {
scripts = ["scripts/install-packages.ps1"]
}
provisioner "ansible" {
playbook_file = "windows/baseline.yml"
}
# appended by the generator — always, in this order
provisioner "powershell" {
scripts = ["scripts/harden-acsc.ps1"]
}
provisioner "powershell" {
scripts = ["scripts/aaronlocker.ps1"]
# needs a settled file system
}
provisioner "powershell" {
scripts = ["scripts/sysprep.ps1"]
# last, or the disk is half-configured
}Build versus buy
You could build this. Here is the bill.
None of the pieces are exotic. Your team could write any one of them in an afternoon. It is the eightieth afternoon that gets you — and the fact that the afternoons keep arriving after the project is declared done.
- A schema for what an image isPlus the codec that renders it to Packer variables, shared by the console and CI — so a preview cannot drift from a build.
- Hardening that runs in the right orderAaronLocker needs a settled file system. Sysprep has to be last. Get the order wrong and you publish a half-configured disk that looks fine until it does not.
- Version history you can answer questions fromComplete manifests, appended, with every build pinned to the revision it was produced from. “What was in 0.0.2?” is the question that starts all of this.
- A job queue that survives a dead runnerLeases, heartbeats, and a retry that is marked as a retry rather than quietly re-run.
- Sign-in you would let an auditor look atPasskeys, verified against the origin and relying-party ID this instance is configured with rather than a header the client controls; a one-time code that can register a credential and authorise nothing; opaque server-verified sessions; and no password stored anywhere.
- Exports that cannot quietly become an exposureA one-time link, a hard retention ceiling, revocation that keeps the disk, and a sweep that cleans up what everyone forgot.
- The parts nobody budgets forReconciling orphaned Key Vault secrets. Verifying that AppIDSvc actually took — Set-Service fails on it with “Access is denied” even as an administrator, and swallowing that failure ships images where AppLocker was never armed.
We are not claiming any of it is hard. We are claiming it is finished, in production, and somebody else's job to keep working.
Where it runs
Hosted, self-hosted, or nowhere near the internet.
One codebase and one set of container images. The profiles differ by configuration and optional services — never by fork, because a second codebase for airgapped customers would be dead within two releases.
Hosted
We run the control plane. You point it at a gallery and start building. Add your own runner and your platform credentials still never leave your network.
Self-hosted
The same container images in your cloud or on your own hardware, signing in against your identity provider, entitled by a signed licence.
Airgapped
No egress in the critical path. The runner artifact carries Packer, its pinned plugins and the vendored hardening toolkits, so a build resolves nothing from the internet.
Colophon
We are loud about our tools.
There is no secret sauce here, and pretending otherwise would insult the people we sell to. The interesting work was in the decisions — so we publish those too, including the things we deliberately did not build.
Packer, Ansible, Chocolatey, PowerShell
Azure Compute Gallery, PostgreSQL, Docker
Next.js, React, TypeScript, Zod, Tailwind, HeroUI
GitHub Actions, GHCR, Portainer
Start with one image.
Point us at a gallery and a base SKU. The first build is the demo — there is nothing to evaluate that is more convincing than a version appearing in your own gallery.