Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/frontend/config/sidebar/deployment.topics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ export const deploymentTopics: StarlightSidebarTopicsUserConfig = {
label: 'External Helm charts',
slug: 'deployment/kubernetes/helm-charts',
},
{
label: 'Persistent volumes',
slug: 'deployment/kubernetes/persistent-volumes',
},
{
label: 'Ingress & Gateway API',
slug: 'deployment/kubernetes-ingress',
Expand Down
7 changes: 7 additions & 0 deletions src/frontend/src/content/docs/deployment/kubernetes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ When you publish or deploy, Aspire translates your application model into Kubern
| Endpoints | Services |
| Volumes | PersistentVolumes and PersistentVolumeClaims |

For durable storage that you model as first-class resources and bind to workloads, see [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/).

## Publish vs deploy

Both Kubernetes targets support `aspire publish`. The key difference is whether the target also supports `aspire deploy`:
Expand Down Expand Up @@ -91,4 +93,9 @@ For more on the pipeline model and how publish and deploy relate, see [Pipelines
description="Install pre-existing Helm charts alongside your application using AddHelmChart."
href="/deployment/kubernetes/helm-charts/"
/>
<LinkCard
title="Persistent volumes"
description="Model durable storage as first-class resources and bind them to workloads."
href="/deployment/kubernetes/persistent-volumes/"
/>
</CardGrid>
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ See [Install external Helm charts](/deployment/kubernetes/helm-charts/) for a fu
- [Kubernetes integration](/integrations/compute/kubernetes/)
- [Deploy to AKS](/deployment/kubernetes/aks/)
- [Install external Helm charts](/deployment/kubernetes/helm-charts/)
- [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/)
- [Pipelines and app topology](/deployment/pipelines/)
- [Publishing and deployment overview](/deployment/deploy-with-aspire/)
- [Kubernetes documentation](https://kubernetes.io/docs/)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
---
title: Persistent volumes on Kubernetes
seoTitle: 'Persistent volumes for Aspire on Kubernetes clusters'
description: Model durable storage as first-class persistent volume resources in your Aspire AppHost, then bind them to Kubernetes workloads with a storage class, capacity, and access modes.
---

import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
import LearnMore from '@components/LearnMore.astro';

Stateful workloads — databases, message brokers, or any service that writes durable data — need storage that survives pod restarts and rescheduling. Call `AddPersistentVolume` on a Kubernetes environment to describe a durable disk once in your AppHost, configure its storage class, capacity, access modes, and annotations with fluent methods, then bind it to one or more workloads:

<Tabs syncKey="aspire-lang">
<TabItem id="csharp" label="C#">

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

var pgData = k8s.AddPersistentVolume("pg-data")
.WithStorageClass("managed-csi")
.WithCapacity("20Gi");
```

</TabItem>
<TabItem id="typescript" label="TypeScript">

```typescript title="apphost.mts"
const k8s = await builder.addKubernetesEnvironment('k8s');

const pgData = await k8s.addPersistentVolume('pg-data');
await pgData.withStorageClass('managed-csi');
await pgData.withCapacity('20Gi');
```

</TabItem>
</Tabs>

At publish time, the volume renders as a `v1.PersistentVolumeClaim` in the generated Helm chart. Like [ingress and gateway resources](/deployment/kubernetes-ingress/), you configure the volume once and reference it from workloads, rather than relying on a single environment-wide storage shape for every mount.

<Aside type="note">
The persistent volume APIs are experimental. In C#, suppress the `ASPIRECOMPUTE002` diagnostic to use them.
</Aside>

## Prerequisites

- The [Aspire.Hosting.Kubernetes](/integrations/compute/kubernetes/) hosting integration installed in your AppHost.
- A [Kubernetes environment](/integrations/compute/kubernetes/#add-kubernetes-environment) added to your AppHost.
- A cluster with a storage class that can provision the volumes you request. Most managed clusters ship a default storage class.

## Add a persistent volume

The full set of configuration methods lets you pin the storage class, capacity, access modes, and provisioner annotations. A complete AppHost that defines a volume looks like this:

<Tabs syncKey="aspire-lang">
<TabItem id="csharp" label="C#">

```csharp title="AppHost.cs"
using Aspire.Hosting.Kubernetes;

var builder = DistributedApplication.CreateBuilder(args);

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");

builder.Build().Run();
```

</TabItem>
<TabItem id="typescript" label="TypeScript">

```typescript title="apphost.mts"
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const k8s = await builder.addKubernetesEnvironment('k8s');

const pgData = await k8s.addPersistentVolume('pg-data');
await pgData.withStorageClass('managed-csi');
await pgData.withCapacity('20Gi');
await pgData.withVolumeAnnotation('disk.csi.azure.com/skuName', 'Premium_LRS');
Comment thread
mitchdenny marked this conversation as resolved.

await builder.build().run();
```

</TabItem>
</Tabs>

Every configuration method is optional. When you don't set a storage class, the cluster's default storage class provisions the backing disk. When you don't set a capacity or access mode, the environment's default storage size and read-write policy apply.

The available configuration methods are:

| C# | TypeScript | Description |
|---|---|---|
| `WithStorageClass(string)` | `withStorageClass` / `withPvStorageClassParam` | Sets `spec.storageClassName` on the PVC. In C#, the single method accepts a literal string or an Aspire parameter; in TypeScript, use `withStorageClass` for a literal and `withPvStorageClassParam` for a parameter. |
| `WithCapacity(string)` | `withCapacity` / `withPvCapacityParam` | Sets the requested storage on `spec.resources.requests.storage` (for example, `"20Gi"`). In TypeScript, use `withCapacity` for a literal and `withPvCapacityParam` for a parameter. |
| `WithAccessMode(PersistentVolumeAccessMode)` | `withAccessMode` | Adds an entry to `spec.accessModes`. Call multiple times to declare more than one mode. |
| `WithVolumeAnnotation(string, string)` | `withVolumeAnnotation` / `withVolumeAnnotationParam` | Adds a key-value pair to the PVC's `metadata.annotations`. Useful for CSI driver hints, dynamic provisioner parameters, or backup tooling tags. In TypeScript, use `withVolumeAnnotation` for a literal value and `withVolumeAnnotationParam` for a parameter. |

The `PersistentVolumeAccessMode` values map directly to the Kubernetes access modes:

| Access mode | Description |
|---|---|
| `ReadWriteOnce` | Mounted as read-write by a single node. Most common for block-storage-backed databases. |
| `ReadOnlyMany` | Mounted as read-only by many nodes simultaneously. |
| `ReadWriteMany` | Mounted as read-write by many nodes simultaneously. Typically used for shared file stores such as Azure Files or NFS. |
| `ReadWriteOncePod` | Mounted as read-write by a single pod. Requires Kubernetes 1.27 or later. |

## Bind a volume to a workload

After defining a volume, bind it to the workloads that mount it. There are two overloads, depending on whether the workload already declares a named volume.

### Bind by name

Use the name-match overload when the workload already declares a volume — for example, through `WithVolume("name", "/path")` or an integration helper such as Postgres' `WithDataVolume()`. The persistent volume's name must match the workload volume's name so the publisher can route the pod's `volumes[]` entry through the generated PVC:

<Tabs syncKey="aspire-lang">
<TabItem id="csharp" label="C#">

```csharp title="AppHost.cs"
var pgData = k8s.AddPersistentVolume("pg-data")
.WithStorageClass("managed-csi")
.WithCapacity("20Gi");

builder.AddPostgres("pg")
.WithDataVolume("pg-data")
.WithPersistentVolume(pgData);
```

</TabItem>
<TabItem id="typescript" label="TypeScript">

```typescript title="apphost.mts"
const pgData = await k8s.addPersistentVolume('pg-data');
await pgData.withStorageClass('managed-csi');
await pgData.withCapacity('20Gi');

const pg = await builder.addPostgres('pg');
await pg.withDataVolume({ name: 'pg-data' });
await pg.withKubernetesPersistentVolume(pgData);
```

</TabItem>
</Tabs>

### Bind with a mount path

Use the mount-path overload when the workload doesn't already declare a named volume. It creates the mount itself, so it works for projects and any compute resource. Pass the mount path inside the container, and optionally mount read-only:

<Tabs syncKey="aspire-lang">
<TabItem id="csharp" label="C#">

```csharp title="AppHost.cs"
var media = k8s.AddPersistentVolume("media")
.WithStorageClass("azurefile-csi")
.WithCapacity("100Gi")
.WithAccessMode(PersistentVolumeAccessMode.ReadWriteMany);

builder.AddProject<Projects.Api>("api")
.WithPersistentVolume(media, "/srv/media");
```

</TabItem>
<TabItem id="typescript" label="TypeScript">

```typescript title="apphost.mts"
const media = await k8s.addPersistentVolume('media');
await media.withStorageClass('azurefile-csi');
await media.withCapacity('100Gi');

const api = await builder.addProject('api');
await api.withKubernetesPersistentVolumeMount(media, '/srv/media');
```

</TabItem>
</Tabs>

<Aside type="caution" title="Workloads are promoted to StatefulSet">
Any workload bound to a persistent volume is rendered as a `StatefulSet` instead of a `Deployment`, regardless of its default workload kind. There is no opt-out. A `Deployment` with a `ReadWriteOnce` claim and more than one replica is broken by design — the second pod can't mount the same volume — so binding to a durable volume automatically selects the `StatefulSet` path, which gives pods stable identity and ordered rollout.
</Aside>

## Unbound volumes and default storage

Volumes on a workload that aren't bound to a `KubernetesPersistentVolumeResource` continue to use the environment's default storage type. You can mix a first-class persistent volume and an unbound ephemeral volume on the same workload — each is resolved independently at publish time.

## Generated output

Binding a container to a persistent volume by name produces a `StatefulSet` whose pod spec references the generated claim, alongside the `PersistentVolumeClaim` itself:

```yaml title="StatefulSet (excerpt)"
apiVersion: "apps/v1"
kind: "StatefulSet"
metadata:
name: "service-statefulset"
spec:
template:
spec:
containers:
- image: "nginx:latest"
name: "service"
volumeMounts:
- name: "data"
mountPath: "/var/lib/data"
volumes:
- name: "data"
persistentVolumeClaim:
claimName: "data"
replicas: 1
```

```yaml title="PersistentVolumeClaim"
apiVersion: "v1"
kind: "PersistentVolumeClaim"
metadata:
name: "data"
annotations:
volume.beta.kubernetes.io/storage-provisioner: "disk.csi.azure.com"
spec:
storageClassName: "managed-csi"
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: "20Gi"
```

<LearnMore>
To generate and inspect the Helm chart for yourself, see [Deploy to Kubernetes clusters](/deployment/kubernetes/clusters/).
</LearnMore>

## See also

- [Kubernetes integration](/integrations/compute/kubernetes/)
- [Deploy to Kubernetes clusters](/deployment/kubernetes/clusters/)
- [Deploy to AKS](/deployment/kubernetes/aks/)
- [Persist data with volumes](/fundamentals/persist-data-volumes/)
- [Kubernetes persistent volumes documentation](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,4 @@ You can apply the volume concepts in the preceding code to a variety of services

- [Tutorial: Connect an ASP.NET Core app to SQL Server using Aspire and Entity Framework Core](/integrations/databases/efcore/sql-server/sql-server-get-started/)
- [Aspire orchestration overview](/get-started/app-host/)
- [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/)
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,49 @@ await k8s.withContainerRegistry(registry);
The container registry APIs are currently in preview. In C#, suppress the `ASPIRECOMPUTE003` diagnostic to use them.
</Aside>

## Persistent volumes

Model durable storage as a first-class `KubernetesPersistentVolumeResource` with `AddPersistentVolume`, configure its storage class, capacity, and access modes, then bind it to workloads. At publish time the volume renders as a `v1.PersistentVolumeClaim`, and any workload bound to it is promoted to a `StatefulSet`.

<Tabs syncKey="aspire-lang">
<TabItem id="csharp" label="C#">

```csharp title="AppHost.cs"
using Aspire.Hosting.Kubernetes;

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

var pgData = k8s.AddPersistentVolume("pg-data")
.WithStorageClass("managed-csi")
.WithCapacity("20Gi");

builder.AddPostgres("pg")
.WithDataVolume("pg-data")
.WithPersistentVolume(pgData);
```

</TabItem>
<TabItem id="typescript" label="TypeScript">

```typescript title="apphost.mts"
const k8s = await builder.addKubernetesEnvironment('k8s');

const pgData = await k8s.addPersistentVolume('pg-data');
await pgData.withStorageClass('managed-csi');
await pgData.withCapacity('20Gi');

const pg = await builder.addPostgres('pg');
await pg.withDataVolume({ name: 'pg-data' });
await pg.withKubernetesPersistentVolume(pgData);
Comment thread
mitchdenny marked this conversation as resolved.
```

</TabItem>
</Tabs>

<LearnMore>
For the full configuration surface, binding overloads, access modes, and generated output, see [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/).
</LearnMore>

## Customize individual resources

Use `PublishAsKubernetesService` to modify the generated Kubernetes resources for individual services:
Expand Down Expand Up @@ -356,6 +399,7 @@ For complete walkthroughs, see [Deploy to Kubernetes clusters](/deployment/kuber
## See also

- [Deploy to Kubernetes](/deployment/kubernetes/)
- [Persistent volumes on Kubernetes](/deployment/kubernetes/persistent-volumes/)
- [Expose services with Ingress and Gateway API](/deployment/kubernetes-ingress/)
- [Configure Ingress on AKS](/deployment/kubernetes-ingress-aks/)
- [Configure Gateway API on AKS](/deployment/kubernetes-gateway-aks/)
Expand Down
Loading