RFC: Baked Image Strategy — pre-built VM images for DBInstance provisioning (feature-imagebaking) #168
Replies: 3 comments 1 reply
-
|
Overall, this is a very solid idea to have baked images. Regarding the design decisions, please find my suggestions below. 1. Image catalog structure 2. Multi-OS stream var LatestBakedImages = map[string]BakedImage{ 3. Repave trigger 4. engineVersion validation 5. BackingImage replica count In my opinion, osVersion does not need to be exposed |
Beta Was this translation helpful? Give feedback.
-
EOL handling — two separate pathsVersion EOL splits into two completely different flows depending on whether it's a PostgreSQL major version or an Ubuntu LTS. The key difference: PG stores data in a version-specific on-disk format — a repave alone cannot change the PG major version without the data directory becoming unreadable. Ubuntu has no such constraint. %%{init: {'themeVariables': {'fontSize': '12px'}}}%%
flowchart TD
EOL(["Version reaches EOL"])
EOL --> PG_EOL["PG Major Version EOL\ne.g. PG 15 — Nov 2027"]
EOL --> OS_EOL["Ubuntu LTS EOL\ne.g. 22.04 — Apr 2027"]
subgraph PG_PATH["PG EOL — Customer action required"]
direction TB
PG1["Ops: remove from build\nOperator: PGVersionEOL = True"]
PG2["Customer: pg_dump old VM\nrestore to new DBInstance\nwith engineVersion: 16"]
PG3["Cutover + delete old VM\nor force-deleted at T+3 months"]
PG1 --> PG2 --> PG3
end
subgraph OS_PATH["Ubuntu EOL — Transparent to customer"]
direction TB
OS1["Ops: flip BACKING_IMAGE_OS_VERSION\nto 24.04 stream"]
OS2["Operator: OSUpdateAvailable = True\non existing 22.04 VMs"]
OS3["Repave: OS disk swapped\npgdata untouched\nsame IP, same data"]
OS1 --> OS2 --> OS3
end
PG_EOL --> PG1
OS_EOL --> OS1
|
Beta Was this translation helpful? Give feedback.
-
Update — Implementation & Testing Findings1. Overview of Image Baking Feature M12. High Level Architecture - VM Provisioning and Teardown Flowflowchart LR
subgraph Provision
P1["VM annotation:<br/>volumeClaimTemplates"]
P2["Harvester reads annotation"]
P3[("OS PVC<br/>no owner")]
P4[("pgdata PVC<br/>no owner")]
P1 --> P2
P2 --> P3
P2 --> P4
end
subgraph Repave
R1["New image"]
R2["New PVC name"]
R3["Patch annotation"]
R4[("New OS PVC")]
R5["Delete old PVC"]
R1 --> R2 --> R3 --> R4 --> R5
end
subgraph Teardown
T1["Delete instance"]
T2["Delete VM"]
T3[("PVCs remain<br/>no cascade")]
T4["Explicit PVC delete"]
T1 --> T2 --> T3 --> T4
end
P3 --> R1
R5 --> T1
Attached for the discussion — this is the visual counterpart to point 1 above: the actual Harvester-native volume path (annotation-declared PVC, no 3. Ownerless PVCsEach repave gives the OS disk a revision-suffixed name ( Harvester creates annotation-declared PVCs with no ownerReference. Deleting the VM does not cascade-delete its disks. 4. Blocked-repave failures needed an unplanned annotation-clearing fix
5. Engine version validation
6. PG major-version EOL guardsDrift detection ended up needing two mutually-exclusive conditions, not one: |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
Problem
Today every
DBInstanceprovisions a PostgreSQL database by runningapt-get install postgresqlat first VM boot. This creates four compounding issues:
apt-get install postgresqlis the dominant cost. Customers wait.archive.ubuntu.comorapt.postgresql.orgis slow during a burst of deployments, VMs hang silently and DBInstances enterFailed.engineVersionis completely ignored — the field isomitemptyand never validated.spec.engineVersion: "17"silently gives whatever version apt resolves that day.apt-get install postgresqlwrites 1–2 GB of packages into every VM's delta at first boot. At 1000 VMs that is ~1–2 TB in deltas vs ~200 GB with baked images.Proposed Solution — Baked Image + Repave
Pre-build a qcow2 image containing all supported PostgreSQL versions (15, 16, 17) using Packer.
Upload to Harvester as a BackingImage. All VMs share this base via Longhorn COW — each VM holds
only a ~200 MB config delta.
Boot-to-ready drops from ~3 min to 30–60 seconds. No network calls at boot.
How Packer Builds the Image
%%{init: {'themeVariables': {'fontSize': '18px'}}}%% flowchart LR A(["Ubuntu 22.04\ncloud image"]) subgraph PACKER["Packer Build runs once per patch cycle"] direction LR B["Boot temporary\nQEMU VM"] subgraph PROVISION["provision.sh runs inside VM over SSH"] direction TB P1["apt-get upgrade\nsecurity patches"] P2["Install PG 15 / 16 / 17\ndisable all PG clusters"] P3["Disable unattended-upgrades\nand apt daily timers"] P4["cloud-init clean\ntruncate machine-id\nremove Packer SSH key"] P1 --> P2 --> P3 --> P4 end D["Shut down VM\nexport disk as qcow2"] B --> P1 P4 --> D end E(["ubuntu-2204-postgres-vYYYYMMDD.qcow2\nready to upload to Harvester"]) A --> B D --> EWhy seal the image?
cloud-init cleanwipes all first-boot state so every VM booted from this qcow2 runsbootstrap.shfresh. Without it VMs look like the already-booted Packer VM and skipinitialization entirely — PostgreSQL never starts.
truncate -s 0 /etc/machine-idforces systemd to generate a unique UUID per VM at boot,preventing DHCP IP conflicts when many VMs come up from the same base image.
Why disable unattended-upgrades?
Ubuntu's background apt updater would silently pull packages into each VM's COW delta after
provisioning — erasing the storage savings and causing PostgreSQL minor-version drift between
VMs provisioned at different times. Security patches are instead delivered via a controlled
image rebuild and repave .
End-to-End Lifecycle
%%{init: {'themeVariables': {'fontSize': '18px'}}}%% flowchart TD subgraph OPS["Ops Team once per patch cycle"] B1["Build\n./build.sh YYYYMMDD\nPacker produces qcow2"] B2["Publish\nUpload to Harvester\nBackingImage + StorageClass auto-created"] B3["Catalog update + redeploy\nLatestBakedImages bumped\noperator redeployed"] B1 --> B2 --> B3 end B3 --> NEW_PATH B3 --> EXIST_PATH subgraph NEW_PATH["New DBInstance customer applies CR"] N1{"engineVersion\nin catalog?"} N_REJECT["Rejected\nclear error returned"] N2["OS disk provisioned\nCOW delta on new BackingImage\n30-60 sec ready, no apt at boot"] N3["status.phase = Available\nimageRevision = latest"] N1 -- No --> N_REJECT N1 -- Yes --> N2 --> N3 end subgraph EXIST_PATH["Existing Running VMs operator reconcile"] E1["imageRevision != LatestBakedImage\nOSUpdateAvailable = True"] E1 --> REPAVE_TYPE REPAVE_TYPE{"What changed?"} end subgraph PATCH["Security Patch or new PG minor\ne.g. 22.04.5 or PG 16.4"] R1["Repave same OS stream\nclear pgdata ownerRef\nstop VM, swap OS disk\nstart VM, cloud-init re-runs"] R2["pgdata untouched\nPG version unchanged\n30-60 sec downtime"] R1 --> R2 end subgraph OS_EOL["Ubuntu LTS EOL e.g. 22.04 reaches Apr 2027"] OE1["DefaultOSVersion flipped to 24.04\nnew VMs get 24.04 automatically"] OE2["Repave each 22.04 VM to 24.04 stream\nOS disk replaced, pgdata untouched\nPG version stays the same"] OE1 --> OE2 end subgraph PG_EOL["PG Major Version EOL e.g. PG 15 reaches Nov 2027"] PE1["Removed from new image builds\nnew DBInstances with engineVersion 15 rejected"] PE2["Existing PG 15 VMs keep running\nPGVersionEOL = True flagged on CR"] PE3["Customer must migrate\npg_dump and restore to PG 16+\nno in-place major upgrade path"] PE1 --> PE2 --> PE3 end REPAVE_TYPE -->|"OS or PG patch"| PATCH REPAVE_TYPE -->|"Ubuntu EOL migration"| OS_EOL REPAVE_TYPE -->|"PG version EOL"| PG_EOL R2 --> E1 OE2 -.->|"next patch cycle"| B1Release Cadence
PG_VERSIONS, new enum valueSecurity patches are not applied by monthly scheduled builds — that would rebuild without
any new patches half the time. The build is triggered by an actual release event.
Key Design Decisions — Input Wanted
1. Image catalog structure
One image per OS version containing all PG versions vs one image per OS+PG pair.
cloud-initactivates only the requested PG version at boot. Catalog is simple — one entry per build date.2. Multi-OS stream
Two parallel streams keyed by OS version:
DefaultOSVersion = "22.04"until the 24.04 stream is validated.3. Repave trigger
Currently scoped to manual
kubectl annotate dbinstance ... repave=true. Should we support:OSUpdateAvailable=True, human triggers repavemaxUnavailable4.
engineVersionvalidationMake
engineVersionrequired with a kubebuilder enum ("15","16","17"). This is abreaking API change — existing CRs without a valid value will fail admission. Migration path needed.
5. BackingImage replica count —
numberOfReplicason uploadOption 1 — Set
numberOfReplicas: "4"on VirtualMachineImage uploadharvester-longhorntemplate"2"viamergeStorageClassParams()in Harvester's mutatorlonghorn-ubuntu-postgres-vXStorageClass inheritsnumberOfReplicas: "4"Option 2 — Keep
"2"(inherit template StorageClass)storageClassParameters.numberOfReplicasin VirtualMachineImage specQuestion: Should we enforce
numberOfReplicas: "4"in the upload spec (Option 1) and document itas a required step in the image publishing playbook?
Major Operator Changes
api/v1alpha1/dbinstance_types.goBakedImagetype +BakedImagescatalog +LatestBakedImagesmap;engineVersionrequired+enum;ImageRevisioninAppliedSpec;internal/harvester/client.gop.OSImagelookup with catalog entryinternal/harvester/cloudinit.goapt-get installblock; addENGINE_VERSIONenv; add PG cluster activationinternal/controller/dbinstance_controller.goImageRevisionsnapshot; drift detection;phaseRepave()images/packer/scripts/provision.shunattended-upgrades+ apt timers before sealingOpen Questions
osVersionbe exposed in theDBInstancespec, or kept as an operator-internal concern?pg_dump+ restore). What notice period should we mentioned to?Beta Was this translation helpful? Give feedback.
All reactions