Skip to content

Add the Azure Files sample and make the repository public-ready - #23

Merged
paolosalvatori merged 6 commits into
mainfrom
file-csi-driver
Aug 6, 2026
Merged

Add the Azure Files sample and make the repository public-ready#23
paolosalvatori merged 6 commits into
mainfrom
file-csi-driver

Conversation

@paolosalvatori

@paolosalvatori paolosalvatori commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Two things, both prerequisites for making this repository public.

A sample for Azure Files. LocalStack for Azure now installs the Azure Files CSI driver on an emulated AKS cluster (localstack/localstack-pro#8194), and nothing here exercised it: all eight Vacation Planner samples persist to a database, Cosmos DB or blob storage, and the repository had exactly one PersistentVolumeClaim anywhere.

Public-repo readiness. The repository had no license, no security policy and no CI at all, and it never actually said how to point the Azure CLI at the emulator: the README only offered "point the Azure CLI at the emulator endpoint (typically http://localhost:4566)". The wrapper that used to do that, azlocal, is deprecated in favour of lstk az.

Fixes SMF-841

Changes

A new sample, samples/web-app-file-storage. The same Vacation Planner app, storing each activity as a text file on an Azure Files share mounted into every pod. Its point is that the app uses no Azure SDK at all: where the blob sibling imports azure-storage-blob and threads a connection string through a Secret, this one calls open(), os.listdir() and os.remove() on a directory, and the CSI driver turns that directory into an Azure file share. Three replicas mount the same share (ReadWriteMany) and the page header names the pod that served it, so that is visible in the UI.

One set of scripts covers four combinations, chosen with two menus in 01-deploy-resources.sh and persisted to scripts/.deploy-options.env so 05-deploy-app.sh never asks twice. Exporting PROVISIONING_MODE and FILE_SHARE_PROTOCOL skips the menus for an unattended run:

static dynamic
SMB a StorageV2 account and share created by the script, a PersistentVolume with the documented uid=1000,gid=1000 mount options, and a Secret with the account key (the driver mounts SMB with the key only) a claim on the built-in azurefile-csi class
NFS a premium FileStorage account restricted to the node subnets, --https-only false, protocol: nfs, no Secret a claim on a class of its own, since none of the built-in ones sets protocol: nfs

Seven of the eleven manifests are shared by all four combinations. The two PersistentVolume files are deliberately separate rather than one file plus yq surgery, because yq's |= creates a missing path and would graft a half-formed nodeStageSecretRef onto the NFS volume.

An init container prepares and seeds the share for all four combinations. On NFS it takes ownership of the mount point, because an NFS share carries real POSIX ownership, arrives owned by root, and ignores the uid/gid mount options that solve this for SMB. securityContext.fsGroup is not an alternative: the file.csi.azure.com CSIDriver declares fsGroupPolicy: ReadWriteOnceWithFSType and this volume is ReadWriteMany, so the kubelet skips the fsGroup ownership change entirely (verified on a live cluster, and called out in a manifest comment).

Both cluster provisioning scripts now check that the Azure Files CSI driver and the CSI snapshot controller are enabled (az aks create only has --disable-* flags for them, so --enable-file-driver exists solely on az aks update, and the update runs only when needed), enable the Microsoft.Storage service endpoint on the node subnets that an NFS share requires, and print the cluster's storage profile and storage classes when they finish. Every failure path now exits non-zero: exit with no status exits with the status of the preceding echo, so these scripts used to announce a failure and then report success.

lstk az instead of azlocal. The root README gains a "Run against LocalStack" section (install lstk, start the emulator, lstk az start-interception, lstk az stop-interception), and each of the other 22 READMEs gains a one-line pointer to it. The single literal azlocal reference, a comment in the Terraform tutorial's providers.tf, now names lstk az.

Public-repo files and CI. MIT LICENSE and SECURITY.md, plus .github/workflows/lint_workflows.yml (actionlint + zizmor) and .github/workflows/pre-commit.yml with .pre-commit-config.yaml (gitleaks + zizmor), all matching the public localstack-samples/localstack-azure-samples repository including its SHA-pinned actions.

Renames web-app-storage-account to web-app-blob-storage, so the two blob-backed samples are told apart by what they store. Internal names are unchanged. New architecture diagrams for the blob, file and managed-identity samples. The root README sample list was missing the MySQL sample and read "Azure Database DB for PostgreSQL". A duplicate architecture.png committed under a malformed path in the PostgreSQL sample is removed.

Testing

All four combinations, end to end against the emulator, on one cluster created by 01-user-assigned-managed-identity.sh, each with 01020405 and a full teardown in between. Per combination: the claim is Bound, 3/3 replicas ready with no restarts, /data is a real cifs vers=3.1.1 or nfs4 vers=4.1 mount carrying the documented options, the seeded activities render, an activity added over HTTP through one pod lands on the share as a file, and both other replicas serve it. Static SMB is cross-checked with az storage file list, which sees the same files over the file data plane, and both NFS combinations show /data as 1000:1000 thanks to the init container.

The ACR pull path works: the k3d nodes resolve localacrtest.azurecr.azure.localhost.localstack.cloud:4566 through CoreDNS and validate it against the public *.azurecr.azure.localhost.localstack.cloud certificate. No insecure-registry configuration and no k3d image import were needed.

Review fixes re-verified after the Copilot review: the four provisioning combinations again, the delete-by-name route in-cluster, a subdirectory ACTIVITIES_DIR (which the init container now creates), the write and delete failure paths against a read-only share, and the rejected activity names. pre-commit run --all-files (gitleaks + zizmor) and actionlint are clean.

Known emulator issue, not caused by this PR: on a cluster whose node pool is added after creation, which is what these scripts do, the emulator patches the new node's /etc/resolv.conf after the kubelet has already started csi-azurefile-node on it. That pod is hostNetwork, so it keeps the pre-patch resolver and Azure Files mounts on that node fail with could not resolve address until kubectl rollout restart daemonset/csi-azurefile-node -n kube-system. Fixing it belongs in the emulator, and it is written up there; no workaround is baked into this sample.

paolosalvatori and others added 2 commits August 6, 2026 09:12
Adds samples/web-app-file-storage, a Vacation Planner variant that stores each activity as a text file on an Azure Files share mounted by the Azure Files CSI driver.
The app carries no Azure SDK: it calls open(), os.listdir() and os.remove() on a directory, and the driver turns that directory into an Azure file share.

One set of scripts covers four combinations, chosen with two menus in 01-deploy-resources.sh and persisted to .deploy-options.env so 05-deploy-app.sh never asks twice (exporting PROVISIONING_MODE and FILE_SHARE_PROTOCOL skips the menus for an unattended run):

* static SMB: a StorageV2 account, a PersistentVolume with uid/gid mount options, and a Secret with the account key the driver mounts with
* static NFS: a premium FileStorage account restricted to the node subnets, with protocol: nfs and no Secret
* dynamic SMB: a claim on the built-in azurefile-csi storage class
* dynamic NFS: a claim on a storage class of its own, since none of the built-in ones sets protocol: nfs

An init container prepares and seeds the share for all four: it chowns the mount point on NFS, where the share carries real POSIX ownership and fsGroup cannot help (the file.csi.azure.com CSIDriver declares fsGroupPolicy: ReadWriteOnceWithFSType and the volume is ReadWriteMany), and copies the sample activities that are missing.

Both cluster provisioning scripts now check that the Azure Files CSI driver and the CSI snapshot controller are enabled, enable the Microsoft.Storage service endpoint on the node subnets that an NFS share requires, and print the cluster's storage profile and storage classes.

Also renames web-app-storage-account to web-app-blob-storage, so the two blob-backed samples are told apart by what they store rather than by an ambiguous folder name, fixes the sample list in the root README (the MySQL sample was missing and PostgreSQL read "Azure Database DB for"), and removes a duplicate architecture.png committed under a malformed path in the PostgreSQL sample.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The local smoke test mounted its activities directory from /tmp, which the app then rejected as not writable: the container runs as uid 1000 and the directory has to be world-writable, which is a poor fit for a directory shared with every other user on the machine (and not shared with the Docker daemon on every setup). It now lives under the user's cache directory.

Also records what the teardown leaves behind: the reclaim policy deletes the file share the CSI driver created, but not the storage account it created for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 6, 2026 08:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new Vacation Planner sample that persists activities as plain files on an Azure Files share mounted into pods via the Azure Files CSI driver, and updates the repo to better cover/storage-driver scenarios (including provisioning-script checks for Azure Files CSI + snapshot controller). It also renames the blob-storage sample directory for clarity and updates top-level documentation accordingly.

Changes:

  • Add samples/web-app-file-storage, supporting SMB/NFS and static/dynamic provisioning paths, with Kubernetes manifests and deployment scripts.
  • Update AKS provisioning scripts to ensure Azure Files CSI + snapshot controller are enabled and to enable Microsoft.Storage service endpoints on node subnets.
  • Rename/introduce samples/web-app-blob-storage and update README references.

Reviewed changes

Copilot reviewed 36 out of 139 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
scripts/01-user-assigned-managed-identity.sh Ensures Microsoft.Storage endpoint on subnets; enables Azure Files CSI + snapshot controller; prints storage profile/classes
scripts/01-system-assigned-managed-identity.sh Same as user-assigned script updates for storage endpoint + drivers + diagnostics
samples/web-app-managed-identity/README.md Updates sample cross-reference to renamed blob sample path
samples/web-app-file-storage/src/app.py Flask app persisting activities as files on a mounted share
samples/web-app-file-storage/src/gunicorn.conf.py Gunicorn worker signal hooks to avoid noisy tracebacks
samples/web-app-file-storage/src/requirements.txt Python dependencies for the file-storage app
samples/web-app-file-storage/src/templates/index.html UI template (pod name header, add/edit/delete)
samples/web-app-file-storage/src/static/style.css UI styling for the file-storage sample
samples/web-app-file-storage/src/static/bootstrap/css/bootstrap-reboot.css Vendor bootstrap reboot CSS asset
samples/web-app-file-storage/src/static/bootstrap/css/bootstrap-reboot.min.css Vendor bootstrap reboot CSS asset (minified)
samples/web-app-file-storage/src/static/bootstrap/css/bootstrap-reboot.rtl.css Vendor bootstrap reboot CSS asset (RTL)
samples/web-app-file-storage/src/static/bootstrap/css/bootstrap-reboot.rtl.min.css Vendor bootstrap reboot CSS asset (RTL minified)
samples/web-app-file-storage/scripts/00-variables.sh Shared variables, including persisted deploy option file
samples/web-app-file-storage/scripts/01-deploy-resources.sh Interactive/static-vs-dynamic and SMB-vs-NFS provisioning logic; creates required Azure resources
samples/web-app-file-storage/scripts/02-build-docker-image.sh Builds the sample image
samples/web-app-file-storage/scripts/03-run-docker-container.sh Local smoke test using host-mounted directory
samples/web-app-file-storage/scripts/04-push-docker-image.sh Tags/pushes image to ACR
samples/web-app-file-storage/scripts/05-deploy-app.sh Applies manifests with yq, selects static/dynamic + SMB/NFS paths, waits for rollout
samples/web-app-file-storage/scripts/Dockerfile Builds runtime image pinned to uid/gid 1000 for share permissions
samples/web-app-file-storage/scripts/namespace.yml Namespace manifest
samples/web-app-file-storage/scripts/configmap.yml ConfigMap for mount path env var
samples/web-app-file-storage/scripts/seed-configmap.yml Seed activities for init-container copy
samples/web-app-file-storage/scripts/secret.yml Secret placeholder for Flask SECRET_KEY
samples/web-app-file-storage/scripts/storage-secret.yml Secret placeholder for SMB account key (static SMB only)
samples/web-app-file-storage/scripts/storageclass-nfs.yml StorageClass for dynamic NFS provisioning (protocol: nfs)
samples/web-app-file-storage/scripts/persistentvolumeclaim.yml RWX PVC (static shape, rewritten for dynamic)
samples/web-app-file-storage/scripts/persistentvolume-smb.yml Static SMB PV with mount options + secret ref
samples/web-app-file-storage/scripts/persistentvolume-nfs.yml Static NFS PV with protocol attribute + mount options
samples/web-app-file-storage/scripts/deployment.yml Deployment with init container that prepares/seeds the share
samples/web-app-file-storage/scripts/service.yml ClusterIP Service for the app
samples/web-app-file-storage/README.md Sample documentation covering 4-mode matrix + operational notes
samples/web-app-blob-storage/src/app.py Blob-backed Flask app (renamed sample)
samples/web-app-blob-storage/src/gunicorn.conf.py Gunicorn worker signal hooks
samples/web-app-blob-storage/src/requirements.txt Python dependencies for blob sample
samples/web-app-blob-storage/src/templates/index.html UI template for blob sample
samples/web-app-blob-storage/src/static/style.css UI styling for blob sample
samples/web-app-blob-storage/src/static/bootstrap/css/bootstrap-reboot.css Vendor bootstrap reboot CSS asset
samples/web-app-blob-storage/src/static/bootstrap/css/bootstrap-reboot.min.css Vendor bootstrap reboot CSS asset (minified)
samples/web-app-blob-storage/src/static/bootstrap/css/bootstrap-reboot.rtl.css Vendor bootstrap reboot CSS asset (RTL)
samples/web-app-blob-storage/src/static/bootstrap/css/bootstrap-reboot.rtl.min.css Vendor bootstrap reboot CSS asset (RTL minified)
samples/web-app-blob-storage/scripts/00-variables.sh Shared variables for blob sample scripts
samples/web-app-blob-storage/scripts/01-deploy-resources.sh Creates RG/ACR/storage account/container and exports connection string
samples/web-app-blob-storage/scripts/02-build-docker-image.sh Builds the blob sample image
samples/web-app-blob-storage/scripts/03-run-docker-container.sh Local run wiring via env vars
samples/web-app-blob-storage/scripts/04-push-docker-image.sh Tags/pushes blob sample image to ACR
samples/web-app-blob-storage/scripts/05-deploy-app.sh Deploys blob sample manifests with yq
samples/web-app-blob-storage/scripts/Dockerfile Builds blob sample runtime image
samples/web-app-blob-storage/scripts/namespace.yml Namespace manifest
samples/web-app-blob-storage/scripts/configmap.yml ConfigMap for container name
samples/web-app-blob-storage/scripts/secret.yml Secret for connection string + Flask SECRET_KEY
samples/web-app-blob-storage/scripts/deployment.yml Deployment wiring env vars/secrets
samples/web-app-blob-storage/scripts/service.yml ClusterIP Service for blob sample
samples/web-app-blob-storage/README.md Blob sample documentation under new directory name
README.md Updates sample list/table to include MySQL + new Azure Files sample and rename blob sample
.gitignore Ignores generated .deploy-options.env file
Suppressed comments (4)

scripts/01-user-assigned-managed-identity.sh:703

  • This exit is reached after a failure path but does not specify a non-zero status, so the script may exit successfully despite the AKS update failing. Use exit 1 for error termination.
    scripts/01-system-assigned-managed-identity.sh:643
  • This failure path uses exit without an explicit non-zero status, which can make the script succeed even when az aks update fails. Use exit 1.
    samples/web-app-file-storage/src/app.py:159
  • Same issue as write_activity: delete_activity allows . / .. which can escape the activities directory and delete unintended files. Tighten validation to reject dot-paths and alternate separators.
    samples/web-app-file-storage/src/app.py:172
  • delete_activity swallows most exceptions, so the handler can still remove the entry from activities and flash success even when the delete failed. Re-raise unexpected errors after logging (keeping FileNotFound as a harmless no-op if desired).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/01-system-assigned-managed-identity.sh Outdated
Comment thread samples/web-app-file-storage/src/app.py Outdated
Comment thread samples/web-app-file-storage/src/app.py
Comment thread samples/web-app-file-storage/src/app.py Outdated
Comment thread scripts/01-user-assigned-managed-identity.sh Outdated
Comment thread samples/web-app-file-storage/scripts/deployment.yml
paolosalvatori and others added 4 commits August 6, 2026 11:37
New diagrams for the Azure Files sample and for the two blob-backed samples, which until now shared a single generic one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository is about to be made public and had no LICENSE, no security policy and no CI at all.

* LICENSE: MIT, matching the sibling localstack-samples/localstack-azure-samples repository.
* SECURITY.md: what these samples are (demo code with throwaway demo credentials, security trade-offs chosen for clarity) and how to report a vulnerability privately through GitHub. Enabling private vulnerability reporting in the repository settings is a prerequisite for that link to work.
* .github/workflows/lint_workflows.yml: actionlint and zizmor over the repository's own workflows.
* .github/workflows/pre-commit.yml plus .pre-commit-config.yaml: gitleaks for secret scanning and zizmor, the same two hooks the sibling repository uses.

All three are copied from that sibling repository, including its SHA-pinned action references. Verified locally: pre-commit run --all-files passes (gitleaks reports no hardcoded secrets across the repository, so no allowlist is needed) and actionlint accepts both workflows.

shellcheck, markdownlint and terraform fmt hooks are deliberately left out: the ~60 shell scripts here are written in a house style that those tools flag in the hundreds, which would turn CI red on day one. Bringing that corpus into line is separate work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository never said how to point the Azure CLI at the emulator: the root README only offered "point the Azure CLI at the emulator endpoint (typically http://localhost:4566)", which is not something a reader can act on. The wrapper that used to do it, azlocal, is deprecated in favour of `lstk az`.

* Root README: a "Run against LocalStack" section with the lstk install options, `localstack start`, `lstk az start-interception` and `lstk az stop-interception`, plus lstk in the prerequisites. Links point at the Azure CLI interception docs, the lstk CLI docs and the lstk repository.
* Every other README (9 samples, 13 tutorials): a one-line pointer to that section, with the relative path computed per file. All 22 links were resolved against the file system, and the anchor matches the heading.
* tutorials/terraform/tags-labels-taints/providers.tf: the one remaining literal azlocal reference, in a comment about instance discovery, now names `lstk az start-interception`.

Also unifies the author email on paolo.salvatori@localstack.cloud: the cert-manager issuer of the managed-identity sample registered its Let's Encrypt account with a personal gmail address.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six findings from the Copilot review of #23, all verified.

Failure paths reported success. `exit` with no status exits with the status of the last command, which on these paths is the `echo` that just announced the failure. Both provisioning scripts now use `exit 1` on every failure path: the two spots the review flagged in each file, and the 21 and 18 pre-existing ones, which have the same defect. The only bare `exit` left in each is the menu's `"Quit")` branch, where exiting 0 is what is meant.

The app reported successes that never happened. `write_activity` and `delete_activity` now return whether the share actually changed, and the routes only touch the in-memory list and flash success when it did. Against a read-only share the app now says "Failed to add the activity to the file share." instead of claiming success and showing a row that is not on the share. `delete_activity` separates a file that is already gone (success: another replica may have deleted it, and the UI should converge) from a missing activities directory (failure: the share is not mounted).

Deleting by row position could delete the wrong activity. Three replicas share one file share and the list is reloaded on every request, so the position is not stable between rendering a page and submitting a delete from it. `POST /delete` now takes the activity's file name, which the template already carried in `data-id` and now posts as a hidden field.

Activity names were only implicitly validated. `os.path.basename(name) != name` did reject every name with a separator, and `..` resolved to a directory that both `open(..., "w")` and `os.remove` refuse, so nothing could escape the share. It was still an implicit guard: `is_activity_name()` now requires the shape this app creates, which rejects the empty name, `.`, `..`, both separators, and any file the app did not write.

The init container assumed the activities directory existed. It chowned and seeded it directly, so pointing `ACTIVITIES_DIR` at a subdirectory of the mount, which the app documents as supported, aborted the init container before the app could start. It now runs `mkdir -p` first. Verified by running the manifest's own init script against a root-owned share with `ACTIVITIES_DIR=/data/activities`: it creates the directory, chowns it and the seeded files to 1000:1000, and is idempotent on re-run; the same script without the `mkdir` fails with `chown: cannot access '/data/activities': No such file or directory` and exits 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@paolosalvatori paolosalvatori changed the title Add the Azure Files Vacation Planner sample Add the Azure Files sample and make the repository public-ready Aug 6, 2026
@paolosalvatori
paolosalvatori merged commit f16ebf7 into main Aug 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants