Imported from Nano-Core/Nano.Templates (
Svc.Locations/.claude/skills/nano-add-storage-provider/SKILL.md). Install upstream withnpx skills add Nano-Core/Nano.Templates --skill nano-add-storage-provider. Copyright stays with the author.
Nano add storage provider
Wires a Nano storage provider into an existing Nano API, Web, or Console application. Read
AGENTS.md in the target repo root first — its ## Nano.Storage section documents the
Configuration table, the provider/package table, and IPathProvider in full; this skill does
not repeat any of that, only how to apply it and wire the surrounding infrastructure
(docker-compose, K8s, and for Azure, CI) without breaking what's already there.
Both providers are simpler at the code level than a data or eventing provider — per AGENTS.md,
Local and Azure both represent storage already mounted into the container's filesystem and
are accessed identically through IPathProvider; there's no provider-specific client/SDK to
wire into the app itself. Everything that differs between them is infrastructure —
docker-compose is identical either way; only the Kubernetes manifests and (for Azure) the CI
provisioning step differ.
Before making any change, determine
- Which provider.
LocalorAzure(see AGENTS.md's provider table for package/type names). Ask the user if not already given. - Is a storage provider already registered? Check
Program.csfor an existing.AddNanoStorage<...>()call — like eventing, there's oneIPathProviderimplementation per app, not a multi-provider case. If one exists, treat this as a replace and say so. - Is a package reference even needed? Same check as the other add-provider skills: look for
NanoCore/Nano.All(directly, or transitively via a.Modelsproject). If found, skip the package step. Otherwise add<PackageReference Include="Nano.Storage.<Provider>" Version="X.Y.Z" />to the application project, matching the version of the project's existing Nano application-type package. Never aProjectReferenceto Nano.Library source.
Program.cs
using Nano.Storage.Extensions;
using Nano.Storage.<Provider>;
x.AddNanoStorage<<Provider>Provider>();
<Provider>Provider is LocalFileShareProvider for Local, AzureFileshareProvider for
Azure (see AGENTS.md's provider table for the exact names). Same .ConfigureServices(...)
lambda placement and _ → x rename rule as the other add-provider skills. No other C# files
are needed — no context/factory equivalent, unlike the data-provider skill.
appsettings.json
Add the Storage section from AGENTS.md's ### Configuration example to the base
appsettings.json (sibling of App). ShareName isn't sensitive (it's just a name, not a
credential) — it can stay set in the base file for both providers, no Development-specific
override needed for it.
Include HealthCheck only if App:HealthCheck is also enabled — AGENTS.md is explicit that
storage health checks do nothing without it (⚠ under #### Health Checks), so adding one
without the other is dead configuration.
docker-compose.yml (local Development)
Identical for both providers — a bind-mounted local directory standing in for whatever the real
provider mounts in Staging/Production. Add to the app's own service in
.docker/docker-compose.yml:
volumes:
- ./bin/<share-name>:/mnt/<share-name>
matching Storage:ShareName. No separate service container needed (unlike a data or eventing
provider) — there's nothing to run, just a directory.
Kubernetes — Local
.kubernetes/storage-storageclass.yaml(new file):apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: %SERVICE_NAME%-storage-class provisioner: disk.csi.azure.com parameters: storageaccounttype: Standard_LRS kind: Managed reclaimPolicy: Retain volumeBindingMode: WaitForFirstConsumerReadWriteOnce, one volume per pod, not one shared volume. A local disk-backed volume can only attach to a single pod — so if the app runs more than one replica (deployment.yaml'skind: Deployment, all replicas sharing one pod template), every replica referencing the same static PVC name would race for the same single-attach disk; only the first pod to schedule would mount successfully; the rest fail with aMulti-Attacherror and never become ready. SoLocalstorage's Deployment must be aStatefulSet, usingvolumeClaimTemplatesinstead of a single staticPersistentVolumeClaimfile — that gives each replica pod its own separate, uniquely-named PVC/disk automatically. (This does mean each pod's files are isolated from the others — a file written via one replica isn't visible from another. If the app instead needs one shared volume across replicas, that's whatAzurestorage is for, below — its file-share CSI mount supports concurrent multi-pod access.).kubernetes/deployment.yaml: changekind: Deployment→kind: StatefulSet, and addserviceName: %SERVICE_NAME%-stateful-headlessalongsidereplicas/selector(aStatefulSetfield, required — see the headless service below). Mount the volume, plus the standardtmpemptyDirvolume that backsIPathProvider's temporary directory (AGENTS.md: registering a provider "also registersIPathProvider... exposing the storage root and a temporary (tmp) directory") — includetmpfor both providers, it's provider-agnostic:volumeMounts: - name: %SERVICE_NAME%-volume mountPath: /mnt/%STORAGE_SHARE_NAME% - name: tmp mountPath: /tmp
and, as a top-level sibling ofvolumes: - name: tmp emptyDir: {}template:(not nested insidetemplate.spec) —volumeClaimTemplatesreplaces thePersistentVolumeClaimfile entirely:volumeClaimTemplates: - metadata: name: %SERVICE_NAME%-volume spec: accessModes: - ReadWriteOnce storageClassName: %SERVICE_NAME%-storage-class resources: requests: storage: %STORAGE_SIZE%Gi.kubernetes/service-headless.yaml(new file) — aStatefulSetrequires a governing headless service for pod network identity, separate from the app's normalClusterIPservice:apiVersion: v1 kind: Service metadata: name: %SERVICE_NAME%-stateful-headless namespace: %KUBERNETES_NAMESPACE% spec: clusterIP: None ports: - name: http port: 8080 selector: app: %SERVICE_NAME%.kubernetes/autoscaler.yaml— always present on an API/Web app (per AGENTS.md's Solution Structure), the only app types thisStatefulSetconversion ever applies to: change itsscaleTargetRef.kindfromDeploymenttoStatefulSettoo — otherwise it silently targets a resource kind that no longer exists.- Workflow: add
STORAGE_SIZE(a bare number of GB, e.g.1000— the template above appendsGi) andSTORAGE_SHARE_NAMEenv vars, and applystorage-storageclass.yaml(still needed — referenced by name fromvolumeClaimTemplates) andservice-headless.yaml(sameGet-Content | ExpandEnvironmentVariables | kubectl applypattern as every other manifest) inKubernetes Deploy, beforedeployment.yaml. There's no separate PVC file to apply —volumeClaimTemplatescreates one per pod automatically as theStatefulSetitself is applied. Also add.kubernetes\storage-storageclass.yaml = .kubernetes\storage-storageclass.yamland.kubernetes\service-headless.yaml = .kubernetes\service-headless.yamlto{name}.sln's.kubernetesSolutionItemsblock (see AGENTS.md's Solution Structure note) — new files under.kubernetes/don't show up in Visual Studio's Solution Explorer otherwise.
Kubernetes — Azure
This provider requires Managed Identity (nano-add-azure-managed-identity — service-account.yaml,
workload-identity annotations, the CI "Managed Identity" step that produces
$env:IDENTITY_NAME/$env:IDENTITY_CLIENT_ID/$env:IDENTITY_PRINCIPAL_ID) — the fileshare mount
authenticates via that identity, not a stored credential, and unlike Data's AuthenticationType,
Azure storage has no credentials-based fallback at all (per AGENTS.md's Configuration table,
Storage has no AuthenticationType setting). This isn't an adjacent, optional feature to ask
about before pulling in — it's a hard technical dependency of Azure storage itself: the
"Storage Role Permissions" step below directly references $env:IDENTITY_PRINCIPAL_ID, which is
simply undefined without it, so the generated workflow would fail the first time it runs. If the
project doesn't have Managed Identity yet, apply nano-add-azure-managed-identity as part of this
same change rather than stopping to ask or leaving it as a dangling prerequisite — then note in
the final summary that it was added alongside storage, so the user isn't surprised by the extra
files. It also assumes the target Azure Storage account already exists — provisioning the
account itself is out of this skill's scope (a real external resource to ask about, unlike
Managed Identity, which is just configuration this skill can apply itself), only the fileshare
on it is provisioned below.
- Workflow env vars:
AZURE_GROUP_STORAGE: ${{ vars.AZURE_RESOURCE_GROUP_STORAGE }} AZURE_GROUP_BACKUP: ${{ vars.AZURE_RESOURCE_GROUP_BACKUP }} STORAGE_SIZE: 25 STORAGE_SHARE_NAME: <share name> - Fileshare provisioning — two steps, placed after
Managed Identityand beforeKubernetes Deploy. Both are idempotent (existence-checked), safe to always include:- name: Storage Role Permissions shell: pwsh run: | $env:STORAGE_ACCOUNT_ID = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].id -o tsv; az role assignment create ` --assignee-object-id $env:IDENTITY_PRINCIPAL_ID ` --assignee-principal-type ServicePrincipal ` --role "Storage File Data SMB MI Admin" ` --scope $env:STORAGE_ACCOUNT_ID if ($LastExitCode -ne 0) { throw "error"; }; - name: Create Fileshare shell: pwsh run: | $env:STORAGE_ACCOUNT_NAME = az storage account list -g $env:AZURE_GROUP_STORAGE --query [0].name -o tsv; $env:FILE_SHARE_EXISTS = az storage share-rm exists -g $env:AZURE_GROUP_STORAGE -n $env:STORAGE_SHARE_NAME --storage-account $env:STORAGE_ACCOUNT_NAME --query exists; if ($env:FILE_SHARE_EXISTS -eq "false") { az storage share-rm create -g $env:AZURE_GROUP_STORAGE -n $env:STORAGE_SHARE_NAME --storage-account $env:STORAGE_ACCOUNT_NAME --access-tier TransactionOptimized --quota $env:STORAGE_SIZE; } else { az storage share-rm update -g $env:AZURE_GROUP_STORAGE -n $env:STORAGE_SHARE_NAME --storage-account $env:STORAGE_ACCOUNT_NAME --access-tier TransactionOptimized --quota $env:STORAGE_SIZE; } if ($LastExitCode -ne 0) { throw "error"; }; $env:BACKUP_VAULT_NAME = az backup vault list -g $env:AZURE_GROUP_BACKUP --query [0].name -o tsv; az backup protection enable-for-azurefileshare -g $env:AZURE_GROUP_BACKUP -v $env:BACKUP_VAULT_NAME -p $env:STORAGE_ACCOUNT_NAME-fileshare-backup-policy --storage-account $env:STORAGE_ACCOUNT_NAME --azure-file-share $env:STORAGE_SHARE_NAME; if ($LastExitCode -ne 0) { throw "error"; }; echo "STORAGE_ACCOUNT_NAME=$env:STORAGE_ACCOUNT_NAME" >> $env:GITHUB_ENV; .kubernetes/storage-pv.yaml(new file):apiVersion: v1 kind: PersistentVolume metadata: name: %SERVICE_NAME%-azurefile-pv-%VOLUME_NAME_SUFFIX% spec: capacity: storage: %STORAGE_SIZE%Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: azurefile-static mountOptions: - dir_mode=0777 - file_mode=0777 - uid=0 - gid=0 claimRef: name: %SERVICE_NAME%-azurefile-pvc-%VOLUME_NAME_SUFFIX% namespace: %KUBERNETES_NAMESPACE% csi: driver: file.csi.azure.com volumeHandle: %AZURE_GROUP_STORAGE%#%STORAGE_ACCOUNT_NAME%#%STORAGE_SHARE_NAME%-%VOLUME_NAME_SUFFIX% volumeAttributes: shareName: %STORAGE_SHARE_NAME% storageAccount: %STORAGE_ACCOUNT_NAME% resourceGroup: %AZURE_GROUP_STORAGE% clientID: %IDENTITY_CLIENT_ID% mountWithWorkloadIdentityToken: "true".kubernetes/storage-pvc.yaml(new file):apiVersion: v1 kind: PersistentVolumeClaim metadata: name: %SERVICE_NAME%-azurefile-pvc-%VOLUME_NAME_SUFFIX% namespace: %KUBERNETES_NAMESPACE% spec: accessModes: - ReadWriteMany storageClassName: azurefile-static resources: requests: storage: %STORAGE_SIZE%Gi volumeName: %SERVICE_NAME%-azurefile-pv-%VOLUME_NAME_SUFFIX%%VOLUME_NAME_SUFFIX%— derived in theKubernetes Deploystep, not a static env var:
placed before the$env:VOLUME_NAME_SUFFIX = $env:IDENTITY_CLIENT_ID.Substring(0, 5);storage-pv.yaml/storage-pvc.yamlapply block (which come beforedeployment.yaml, same order as every other manifest). The suffix keeps the PV/PVC name unique per identity, avoiding collisions across redeploys. Also add.kubernetes\storage-pv.yaml = .kubernetes\storage-pv.yamland.kubernetes\storage-pvc.yaml = .kubernetes\storage-pvc.yamlto{name}.sln's.kubernetesSolutionItemsblock (see AGENTS.md's Solution Structure note) — new files under.kubernetes/don't show up in Visual Studio's Solution Explorer otherwise..kubernetes/deployment.yaml— mount it (ReadWriteMany, so multiple replicas can share it, unlikeLocal), plus the sametmpemptyDirvolume noted in the Local section above:volumeMounts: - name: %SERVICE_NAME%-volume mountPath: /mnt/%STORAGE_SHARE_NAME% - name: tmp mountPath: /tmpvolumes: - name: %SERVICE_NAME%-volume persistentVolumeClaim: claimName: %SERVICE_NAME%-azurefile-pvc-%VOLUME_NAME_SUFFIX% - name: tmp emptyDir: {}
After making the change
- Show the user every file touched, grouped by concern (app code, local docker-compose, Kubernetes, and for Azure, CI) — too many files for a flat list to be easy to sanity-check.
- If the package step was skipped (
NanoCore/Nano.Allalready covering it), say so explicitly. - For
Azure, if Managed Identity wasn't already wired, say explicitly that it was added as part of this change (list its files alongside storage's own) — don't let it pass as an unremarked side effect. If the target Storage account doesn't exist yet, that's still an external prerequisite outside this skill's scope — flag it rather than silently doing only the app-code half of the job.