microsoft / microsoft/aspire

Track first-class Kubernetes persistent volume support

Open
#16,999 1 comment 2 reactions 0 assignees View on GitHub
area-deployment kubernetes
Dominant language
C#
Stars
6.3k
Forks
991
Avg merge
2d 12h
Merged PRs (30d)
201

Description

> [!NOTE]
> Retroactive tracking issue — created after the fact to record the decisions, design
> rationale, and context behind PR #16929. Use this issue (plus the linked PR) as the
> entry point for any follow-up work or for an agent picking the work up on a different
> machine. The full design write-up is reproduced below.

## Goal

Introduce **first-class Kubernetes persistent volume support** in Aspire so that users can
deploy stateful production workloads (databases, message brokers, etc.) to Kubernetes via
the Aspire publisher. Today the only knobs are environment-wide defaults
(`DefaultStorageType` / `DefaultStorageClassName` / `DefaultStorageSize` /
`DefaultStorageReadWritePolicy`) on `KubernetesEnvironmentResource` — every volume in the
environment shares one shape, projects can''t mount volumes at all, and the generated
PV/PVC YAML has known correctness bugs.

## Implemented (PR #16929 — merged into this issue''s scope)

- Bug fixes for #14096 and #16504 (and an extension of the same fix to `StatefulSetSpecV1` /
`StatefulSetUpdateStrategyV1` triggered by the new auto-promote behaviour).
- New first-class app-model resource `KubernetesPersistentVolumeResource`, sibling of
`KubernetesIngressResource` / `KubernetesGatewayResource`.
- New public API on `Aspire.Hosting.Kubernetes`:
- `AddPersistentVolume(name)` factory on `IResourceBuilder`.
- `WithStorageClass`, `WithCapacity`, `WithAccessMode`, `WithVolumeAnnotation` modifiers
on `IResourceBuilder`.
- Two `WithPersistentVolume(...)` overloads on `IResourceBuilder where T : IComputeResource`:
a name-match overload (binds to an existing `ContainerMountAnnotation` by source name,
so integrations like `AddPostgres(...).WithDataVolume()` keep working unchanged) and a
mount-path overload (works for `ProjectResource` and any other compute resource — closes
#9430).
- `PersistentVolumeAccessMode` enum.
- Workload-side annotation `KubernetesPersistentVolumeBindingAnnotation` (internal).
- Publisher pipeline:
- `KubernetesEnvironmentResource.ProcessPersistentVolumeResources` builds the standalone
PVC from the volume resource''s configuration.
- `KubernetesPublishingContext` writes each `KubernetesPersistentVolumeResource` to its own
`templates//.yaml`.
- `KubernetesResource.CreateApplication` auto-promotes the workload to `StatefulSet` when
a `KubernetesPersistentVolumeBindingAnnotation` is present (no opt-out).
- `Extensions/ResourceExtensions.WithPodSpecVolumes` routes pod `volumes[]` entries through
the generated PVC `claimName` when bound; falls back to the env-default switch for
unbound volumes.
- All new public APIs marked `[Experimental("ASPIRECOMPUTE002", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]`.
- Tests (3 new) + Verify snapshots (6) + bug-fix regression tests.

### Example

```csharp
var k8s = builder.AddKubernetesEnvironment("k8s");

var pgData = k8s.AddPersistentVolume("pg-data")
.WithStorageClass("managed-csi")
.WithCapacity("20Gi")
.WithAccessMode(PersistentVolumeAccessMode.ReadWriteOnce)
.WithVolumeAnnotation("disk.csi.azure.com/skuName", "Premium_LRS");

// Name-match overload — Postgres'' WithDataVolume() emits a ContainerMountAnnotation
// with source "pg-data"; the binding picks it up and routes the pod''s volumes[]
// entry through the generated PVC.
builder.AddPostgres("pg")
.WithDataVolume()
.WithPersistentVolume(pgData);

// Mount-path overload — works for projects (closes #9430) and any IComputeResource.
builder.AddProject("api")
.WithPersistentVolume(pgData, "/srv/data");
```

## Locked-in design decisions

These are reproduced from the session design doc to make the issue self-contained.

### Public surface

- **K8s-only abstraction.** Lives in `Aspire.Hosting.Kubernetes`. No Compose / ACA
cross-pollination in this work.
- **Volume factory:** `AddPersistentVolume(name)` on
`IResourceBuilder` — K8s-ness is implied by the receiver
type, so no `AddKubernetesPersistentVolume` prefix on the method.
- **Workload binding:** `WithPersistentVolume(volumeResource)` on
`IResourceBuilder where T : IComputeResource` — same naming reasoning, K8s-ness implied
by the volume resource parameter.
- **`metadata.annotations` on the volume resource** so CSI drivers / dynamic provisioners /
external operators (external-secrets, cert-manager, etc.) can pick them up. Direct
precedent: `KubernetesIngressResource.WithIngressAnnotation` and
`KubernetesGatewayResource.WithGatewayAnnotation`.
- **`ProjectResource` support is in scope.** The workload-side bind API targets
`IComputeResource`, so deployed projects can mount persistent volumes too. Closes #9430.
- **Run-mode behaviour: no-op.** `AddPersistentVolume` is publish-only. No DCP realization,
no dashboard surface for v1.
- **Workload kind:** auto-promote `Deployment` → `StatefulSet` whenever the workload has a
`WithPersistentVolume` binding. **No `.AsDeployment()` opt-out.** Persistent volumes
require StatefulSet semantics; letting users opt out would re-introduce the very foot-gun
the abstraction exists to prevent (`Deployment` + RWO PVC + replicas is broken-by-design).
- **All new public APIs marked `[Experimental("ASPIRECOMPUTE002")]`** — matches existing K8s
surface like `KubernetesEnvironmentResource.GetHostAddressExpression`.

### Bug fixes #14096 / #16504

- Root cause: complex sub-properties on V1 spec types were eagerly initialized to `new()`,
so the YAML serializer''s `OmitNull` configuration couldn''t drop them. Result was invalid
empty mappings like `dataSource: {}`, `selector: {}`, `nodeAffinity.required: {}`,
`volumeClaimRetentionPolicy: {}`, `updateStrategy: {}`, `rollingUpdate: {}`.
- Fix: make the affected complex properties nullable, drop the eager `= new()` initializers.
The YAML serializer''s existing `OmitNull` handling does the rest.
- Affected types: `PersistentVolumeSpecV1`, `PersistentVolumeClaimSpecV1`,
`VolumeNodeAffinityV1`, `StatefulSetSpecV1`, `StatefulSetUpdateStrategyV1`.

### PR #16503 (community PR by @cdbrown2018)

Inspiration only, not a merge candidate. Their PVC-only-emission scenario (per-workload
`WithDynamicProvisioning(bool)`) is naturally covered by the new design: a volume with a
StorageClass and no static-PV details emits a PVC and lets the cluster handle dynamic
provisioning. The per-workload API in #16503 doesn''t survive this redesign, but the
underlying scenario is.

## Deferred / follow-up work

> The session implemented v1; these are explicitly out of scope for PR #16929 and should be
> follow-up issues / PRs.

- **Per-replica PVCs (`volumeClaimTemplates`)** — recommended landing shape =
`WithPersistentVolumeTemplate(name, configure)` on the workload (not a standalone
resource), mirroring how templates live inline in `StatefulSet.spec` rather than as
cluster-scoped resources. v1 guardrails to keep this door open are documented in the
session''s design doc (`plan.md` §"Deep dive: `volumeClaimTemplates`").
- **Headless-Service generation (`spec.serviceName`)** for stable per-replica DNS — needed
alongside per-replica PVCs.
- **Helm `pre-upgrade` hook** to delete any pre-existing Deployment with the same release
name before installing the StatefulSet. Defuses the in-place kind-change foot-gun (a
workload that previously rendered as `Deployment` and is now bound to a persistent volume
will render as `StatefulSet`; `helm upgrade` cannot mutate kind in place). Currently
documented as acceptable to defer; should be a prominent doc note in the integration
README until landed.
- **Azure Files / Azure Disks bridge** in a new `Aspire.Hosting.Azure.Kubernetes` layer:
`WithAzureFiles(...)`, `WithAzureDisk(...)` extensions that produce a
`KubernetesPersistentVolumeResource` pre-configured with the right `metadata.annotations`
for the Azure CSI drivers.
- **Coordinate with @cdbrown2018 on closing #16503** once PR #16929 is merged.

## Linked PR

- PR #16929 — *Add first-class KubernetesPersistentVolumeResource (and fix related YAML serializer bugs)*

## Issues closed by PR #16929

- Fixes #14096
- Fixes #16504
- Closes #9430

## Related issues

- #1521 — original 2024 `volume.v0` discussion (this work delivers on the K8s slice).

## Context for agents picking this up on another machine

The full design write-up — including the deep-dive sections on `volumeClaimTemplates`,
StatefulSet vs Deployment trade-offs, the dual-binding problem, options considered and
rejected, and the alternative API shapes that were explored — lives in this session''s
`plan.md`:

```
~/.copilot/session-state//plan.md
```

For a fresh agent on a different machine, the canonical sources are this issue + PR #16929
+ the commits on the PR branch:

- Branch: `mitchdenny/k8s-persistent-volumes-research`
- Commits:
- `d46ec4026` — bug fixes for #14096 / #16504.
- `114a3cf14` — first-class `KubernetesPersistentVolumeResource` + workload binding +
StatefulSet auto-promotion + extra StatefulSet serializer fixes.
- `7d0624de7` — fix Verify temp-path scrubber collision in the fallthrough test
(`/tmp/scratch` → `/srv/scratch`; Verify auto-scrubs `Path.GetTempPath()` to `{TempPath}`,
so any literal `/tmp/...` path in tests will diverge between Windows-recorded snapshots
and Linux CI — use `/srv/...` or `/var/lib/...` for test mount paths).

Key files to read for an agent picking up follow-up work:

- `src/Aspire.Hosting.Kubernetes/KubernetesPersistentVolumeResource.cs` — the new resource type.
- `src/Aspire.Hosting.Kubernetes/KubernetesPersistentVolumeExtensions.cs` — public API surface.
- `src/Aspire.Hosting.Kubernetes/Annotations/KubernetesPersistentVolumeBindingAnnotation.cs` — binding annotation.
- `src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs` — `ProcessPersistentVolumeResources` / `BuildPersistentVolumeClaim`.
- `src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs` — standalone-template loop for volumes.
- `src/Aspire.Hosting.Kubernetes/KubernetesResource.cs` — StatefulSet promotion logic.
- `src/Aspire.Hosting.Kubernetes/Extensions/ResourceExtensions.cs` — `WithPodSpecVolumes` binding lookup.
- `tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs` — three new tests at the bottom of the file.
- `tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_WithFirstClassPersistentVolume_*` — verified snapshots.

Contributor guide

Open the contributing guide

Research direction

This tracking issue documents work already implemented by PR #16929. For context, read KubernetesPersistentVolumeExtensions.cs, KubernetesResource.cs, and KubernetesPublisherTests.cs with the verified snapshots; no specific remaining change or completion condition is defined, since follow-up items are only listed as deferred.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, kubernetes
Domain
infrastructure
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.