
When one of the “Big Three” cloud services — compute, networking and storage — fundamentally changes, it’s big, big news.
So I was very interested to hear in a recent episode of the Microsoft Cloud IT Pro Podcast, hosted by Ben Stegnik and Scott Hoag, about a new feature of Azure Files: NFS shares that do not require first defining an Azure Storage account. Yup, you can now (in preview) just define an NFS share and never have to create or see an Azure Storage account.
For as long as I can remember, to use any Azure storage you had to first create a storage account. Then, you had to create the actual resource you wanted — be it blobs, tables, shares or whatever — inside the storage account. That indirection can be confusing because the resource you really care about may have networking, performance and/or security properties that you need to configure but which are not directly properties of the storage resource itself. The property you need might be in the storage account properties. That indirection can lead to misconfiguration, lax cost control or performance issues — or unhappy combinations of all of these.
Scott and Ben and their guest go into great detail on how the issues of storage account indirection make life harder and how, over time, Microsoft will expand the types of Azure storage that are available without being contained in a storage account.
NFS isn’t really my jam but Azure Files being independent of a storage account is such a monumental change, I wanted to try it out.
One cannot understand an Azure resource by just creating one in the portal. You need to get your fingernails dirty and deploy the resource from code. That’s what enterprises do at scale and is the very best way to understand the underlying resource’s capabilities and properties.
The rest of this post describes a Bicep template I came up with to deploy an independent Azure Files NFS share and what I learned while developing it. I hope you find it useful for your own experimentation. My intent is that all you will need to deploy the Bicep template is a public key to allow you to ssh to a VM so you access the NFS share. (Be sure to ssh-keygen a new, temporary keypair for your exploration.) Also, be sure to create a new resource group to deploy the template in so you can easily delete everything all at once.
Microsoft has chosen to expose the new top-level Azure Files resource provider precisely as it should: by exposing a new ARM namespace: Microsoft.FileShares. This is important because Azure resources and systems that do not strictly adhere to the ARM namespace conventions and the ARM resource provider model are nightmares to manage at scale (Sentinel, Diagnostics and Log Analytics are the poster children for doing it poorly).
There is a formal definition for the filesShares resource in the new Microsoft.FileShares namespace. That doc is incomplete at the moment, which is to be expected in preview. For example, some parameters are required and are not marked as such and the parameter explanations are thin. But this will likely be corrected over time.
Here are some key points I took away from my experiments:
- If you know NFS, you already know that it doesn’t permit user authentication. Therefore, what Microsoft describes as the “public endpoint” for the NFS shares really means access only via a Vnet. See this description from the troubleshooting page. This means you must access the NFS share from a VM in an authorized Vnet. The example Bicep template sets all this up for you.
- It’s better, of course, to use private endpoints to permit access to anything in Azure that supports a private endpoint. But in this template for convenience, I define an OG service endpoint. But there is no service endpoint for the new ARM namespace, so guess what? You use
Microsoft.Storagein the subnet definition of a service endpoint. - Finally, if you change the NFS share’s properties in the Bicep template and they’re invalid, you’re likely to get an unhelpful return message like the one in the screenshot below. And, Bicep IntelliSense/CoPilot for
Microsoft.FileSharesin VS Code will send you down the wrong path about 90% of the time. These are all, I suspect, issues of being in preview.

Here are some screenshots with example results from deploying the Bicep template.
It took me a while to find where the scripts to configure the NFS share for use in a VM were buried in the portal. They’re in the resources Overview page in an oddly-worded “Guided set-up” pane which is not the default pane in Overview. This screenshot shows where the configuration scripts are.

Here are the networking properties of the share that was created by the template. Note especially the text that says access is only available in a Vnet.

This screenshot shows use of the configuration scripts (actually, only the mount script; the setup script was already executed). Note that once mounted, the share is available just like the local filesystem.

And here is the Bicep template:
// This simple Bicep template demonstrates deployment of a preview version of the Microsoft.FileShares/fileShares resource provider
// Preview version of namespace Bicep definition: https://learn.microsoft.com/en-us/azure/templates/microsoft.fileshares/fileshares?pivots=deployment-language-bicep
@description('Location for all resources.')
param location string = (resourceGroup().location)
@description('Generate a unique name for the file share. Must be between 3 and 63 characters long.')
param fileShareName string = 'nfsfileshare-${uniqueString(resourceGroup().id)}'
// This is the very interesting new resource type in preview. This portends a mega-change in Azure storage over time. See https://techcommunity.microsoft.com/blog/azurestorageblog/simplifying-file-share-management-and-control-for-azure-files/4452634
resource fileShare 'Microsoft.FileShares/fileShares@2025-06-01-preview' = {
name: fileShareName
location: location
properties: {
mountName: fileShareName // Shown for clarity; this property is optional and defaults to the resource name if not provided
mediaTier: 'SSD' // Default and only choice
protocol: 'NFS' // Preview supports NFS only
redundancy: 'Zone' // Will fail in preview with misleading error message if not specified as either 'Local' or 'Zone'
// For limits for fileShares, see https://learn.microsoft.com/en-us/azure/storage/files/storage-files-scale-targets#microsoftfileshares-control-plane-limits. Specifications below are from portal deployment defaults
provisionedStorageGiB: 1024
provisionedIOPerSec: 4024
provisionedThroughputMiBPerSec: 228
// Network access configuration. This template uses service endpoints to allow public access to the share into a specific Vnet subnet. You really want to use private endpoints in production.
publicAccessProperties: {
allowedSubnets: [
virtualNetwork.properties.subnets[0].id
]
}
publicNetworkAccess: 'Enabled' // This does NOT mean that this share will be publicly accessible over the internet. Instead it means that a trusted network subnet will be able to access. See the drawing at https://learn.microsoft.com/en-us/troubleshoot/azure/azure-storage/files/security/files-troubleshoot-linux-nfs?toc=%2Fazure%2Fstorage%2Ffiles%2Ftoc.json&tabs=Ubuntu#cause-1-request-originates-from-a-client-in-an-untrusted-networkuntrusted-ip and the screenshot in my blog post. This is unique to NFS and due to the lack of user authentication in NFSv4.1.
}
}
// Resources for a Vnet, subnet, NSG, and a VM to demonstrate access to the file share from within the Vnet follow. These are pretty much standard resources included in this template for convenience in experimenting with the new fileShares namespace.
@description('Vnet name.')
param virtualNetworkName string = 'nfsVnet-${uniqueString(resourceGroup().id)}'
@description('Subnet name.')
param subnetName string = 'nfsSubnet-${uniqueString(resourceGroup().id)}'
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-03-01' = {
name: virtualNetworkName
location: location
properties: {
addressSpace: {
addressPrefixes: [
'10.17.0.0/16'
]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: '10.17.0.0/24'
serviceEndpoints: [
{
service: 'Microsoft.Storage' // Note that there is no 'Microsoft.FileShares' service endpoint at least in preview, so the required access is via the storage service endpoint
}
]
networkSecurityGroup: {
id: nsg.id
}
}
}
]
}
}
// Network Security Group allowing inbound SSH (TCP 22) from anywhere at subnet level
// This is OH SO insecure. Don't do this other than in a throw-away demo environment.
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-04-01' = {
name: '${virtualNetworkName}-nsg'
location: location
properties: {
securityRules: [
{
name: 'AllowInboundSSH'
properties: {
priority: 1000
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '22'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
}
}
]
}
}
@description('Admin username for the VM.')
param adminUsername string = 'azureuser'
@description('SSH public key for the VM.')
// This is OH SO insecure. Don't do this other than in a throw-away demo environment.
param adminSshPublicKey string = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQClok3EN1QkcYYWmOS/HWobOQmO+a0W043oOHo8Tzv7jj+.......= filesharesdemo' //Replace with your own SSH public key
@description('Virtual machine name.')
param vmName string = 'ubuntuVm-${uniqueString(resourceGroup().id)}'
@description('Network interface name.')
param nicName string = 'ubuntuVmNic-${uniqueString(resourceGroup().id)}'
// Public IP for the VM's NIC
resource publicIp 'Microsoft.Network/publicIPAddresses@2023-04-01' = {
name: '${vmName}-pip'
location: location
sku: {
name: 'Standard'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
dnsSettings: {
domainNameLabel: 'filesharedemo-${uniqueString(resourceGroup().id)}'
domainNameLabelScope: 'TenantReuse'
}
}
}
resource nic 'Microsoft.Network/networkInterfaces@2023-04-01' = {
name: nicName
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
privateIPAllocationMethod: 'Dynamic'
subnet: {
id: virtualNetwork.properties.subnets[0].id
}
publicIPAddress: {
id: publicIp.id
}
}
}
]
}
}
resource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: vmName
location: location
properties: {
hardwareProfile: {
vmSize: 'Standard_B1s'
}
osProfile: {
computerName: vmName
adminUsername: adminUsername
linuxConfiguration: {
disablePasswordAuthentication: true
ssh: {
publicKeys: [
{
path: '/home/${adminUsername}/.ssh/authorized_keys'
keyData: adminSshPublicKey
}
]
}
}
}
storageProfile: {
imageReference: {
publisher: 'Canonical'
offer: '0001-com-ubuntu-server-jammy'
sku: '22_04-lts'
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'Standard_LRS'
}
}
}
networkProfile: {
networkInterfaces: [
{
id: nic.id
}
]
}
}
}
output location string = location
output virtualNetwork string = (virtualNetwork.id)
output subnet string = (virtualNetwork.properties.subnets[0].id)
output vm string = (vm.id)
output publicIp string = (publicIp.id)
output vmFqdn string = publicIp.properties.dnsSettings.fqdn
output nsg string = (nsg.id)
output fileShareName string = fileShareName
output fileShare string = (fileShare.id)
Leave a Reply