|
1 | | -# Azure NFS Storage Module |
2 | | - |
3 | | -Terraform module to deploy Azure File Storage with NFS 4.1 support, secured with Private Endpoint and network security rules. |
4 | | - |
5 | | -## Features |
6 | | - |
7 | | -- **Azure Files Premium (NFS 4.1)**: High-performance file storage with native Linux NFS support |
8 | | -- **Private Endpoint**: Secure private connectivity from VNet without public internet exposure |
9 | | -- **Network Security Rules**: Default deny policy with explicit subnet and IP allowlists |
10 | | -- **Auto DNS Zone Creation**: Automatically creates and links Private DNS Zone if not provided |
11 | | -- **Multiple NFS Shares**: Support for creating multiple file shares with different quotas |
12 | | - |
13 | | -## Architecture |
14 | | - |
15 | | -This module deploys: |
16 | | - |
17 | | -1. **Storage Account** (Premium FileStorage, LRS) - Optimized for NFS with low latency |
18 | | -2. **Network Security Rules** - Default deny with explicit allowlist for subnets and IPs |
19 | | -3. **Private Endpoint** - Maps storage to private IP inside VNet |
20 | | -4. **Private DNS Zone** (optional) - Auto-created `privatelink.file.core.windows.net` for easy DNS resolution |
21 | | -5. **NFS File Shares** - One or more NFS 4.1 shares for mounting |
22 | | - |
23 | | -## Usage |
24 | | - |
25 | | -### Basic Example |
26 | | - |
27 | | -```hcl |
28 | | -module "nfs_storage" { |
29 | | - source = "../../modules/nfs-storage" |
30 | | -
|
31 | | - resource_group_name = "rg-myapp" |
32 | | - location = "westeurope" |
33 | | - storage_account_name = "mystorageaccount01" |
34 | | -
|
35 | | - # Private Endpoint configuration |
36 | | - private_endpoint_subnet_id = azurerm_subnet.private_endpoints.id |
37 | | -
|
38 | | - # VNet for auto DNS zone creation |
39 | | - vnet_id = azurerm_virtual_network.main.id |
40 | | -
|
41 | | - # Network Security Rules - Default deny policy |
42 | | - allowed_subnet_ids = [azurerm_subnet.aks_nodes.id] |
43 | | - allowed_ip_addresses = ["1.2.3.4"] # Your public IP for Terraform operations |
44 | | -
|
45 | | - # Create NFS shares |
46 | | - nfs_shares = [ |
47 | | - { |
48 | | - name = "shared-data" |
49 | | - quota_gb = 100 |
50 | | - } |
51 | | - ] |
52 | | -
|
53 | | - tags = { |
54 | | - environment = "production" |
55 | | - } |
56 | | -} |
57 | | -``` |
58 | | - |
59 | | -### With Automatic Public IP Detection |
60 | | - |
61 | | -```hcl |
62 | | -data "http" "my_public_ip" { |
63 | | - url = "https://api.ipify.org?format=text" |
64 | | -} |
65 | | -
|
66 | | -module "nfs_storage" { |
67 | | - source = "../../modules/nfs-storage" |
68 | | -
|
69 | | - resource_group_name = "rg-myapp" |
70 | | - location = "westeurope" |
71 | | - storage_account_name = "mystorageaccount01" |
72 | | -
|
73 | | - private_endpoint_subnet_id = azurerm_subnet.private_endpoints.id |
74 | | - vnet_id = azurerm_virtual_network.main.id |
75 | | -
|
76 | | - allowed_subnet_ids = [azurerm_subnet.aks_nodes.id] |
77 | | - allowed_ip_addresses = [trimspace(data.http.my_public_ip.response_body)] |
78 | | -
|
79 | | - nfs_shares = [ |
80 | | - { |
81 | | - name = "shared-data" |
82 | | - quota_gb = 100 |
83 | | - metadata = { |
84 | | - purpose = "application-data" |
85 | | - } |
86 | | - } |
87 | | - ] |
88 | | -} |
89 | | -``` |
90 | | - |
91 | | -### Using Existing Private DNS Zone |
92 | | - |
93 | | -```hcl |
94 | | -module "nfs_storage" { |
95 | | - source = "../../modules/nfs-storage" |
96 | | -
|
97 | | - resource_group_name = "rg-myapp" |
98 | | - location = "westeurope" |
99 | | - storage_account_name = "mystorageaccount01" |
100 | | -
|
101 | | - private_endpoint_subnet_id = azurerm_subnet.private_endpoints.id |
102 | | -
|
103 | | - # Use existing DNS zone instead of auto-creating |
104 | | - private_dns_zone_ids = [azurerm_private_dns_zone.storage.id] |
105 | | -
|
106 | | - allowed_subnet_ids = [azurerm_subnet.aks_nodes.id] |
107 | | - allowed_ip_addresses = ["1.2.3.4"] |
108 | | -
|
109 | | - nfs_shares = [ |
110 | | - { |
111 | | - name = "shared-data" |
112 | | - quota_gb = 100 |
113 | | - } |
114 | | - ] |
115 | | -} |
116 | | -``` |
117 | | - |
118 | | -## Mounting NFS Shares |
119 | | - |
120 | | -From a Linux VM inside the allowed subnet: |
121 | | - |
122 | | -```bash |
123 | | -# Create mount point |
124 | | -sudo mkdir -p /mnt/shared-data |
125 | | - |
126 | | -# Mount the NFS share |
127 | | -sudo mount -t nfs -o vers=4.1,sec=sys \ |
128 | | - mystorageaccount01.privatelink.file.core.windows.net:/mystorageaccount01/shared-data \ |
129 | | - /mnt/shared-data |
130 | | - |
131 | | -# Verify mount |
132 | | -df -h | grep shared-data |
133 | | -``` |
134 | | - |
135 | | -### Persistent Mount (fstab) |
136 | | - |
137 | | -Add to `/etc/fstab`: |
| 1 | +<!-- BEGIN_TF_DOCS --> |
| 2 | +## Requirements |
138 | 3 |
|
139 | | -``` |
140 | | -mystorageaccount01.privatelink.file.core.windows.net:/mystorageaccount01/shared-data /mnt/shared-data nfs vers=4.1,sec=sys 0 0 |
141 | | -``` |
| 4 | +| Name | Version | |
| 5 | +|------|---------| |
| 6 | +| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.5.0 | |
| 7 | +| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | >= 3.112.0, < 4.0.0 | |
142 | 8 |
|
143 | | -## Network Security |
| 9 | +## Providers |
144 | 10 |
|
145 | | -The module implements a **default deny policy**: |
| 11 | +| Name | Version | |
| 12 | +|------|---------| |
| 13 | +| <a name="provider_azurerm"></a> [azurerm](#provider\_azurerm) | >= 3.112.0, < 4.0.0 | |
146 | 14 |
|
147 | | -- All public access is denied by default |
148 | | -- Access is only allowed from: |
149 | | - - **Allowed subnets** (e.g., AKS nodes subnet) |
150 | | - - **Allowed IP addresses** (e.g., your public IP for Terraform operations) |
151 | | - - **Azure Services** (if `network_bypass` includes "AzureServices") |
| 15 | +## Modules |
152 | 16 |
|
153 | | -### Why Allow Public IP? |
| 17 | +No modules. |
154 | 18 |
|
155 | | -When you run `terraform apply` or `terraform destroy` from outside the VNet, Terraform needs to make data-plane calls to create/delete NFS shares. Adding your public IP to `allowed_ip_addresses` enables these operations. |
| 19 | +## Resources |
156 | 20 |
|
157 | | -**Options:** |
158 | | -1. Add your public IP temporarily (manually or via `data "http"`) |
159 | | -2. Run Terraform from inside the VNet (e.g., from a VM or Azure DevOps agent) |
160 | | -3. Use Azure Bastion or VPN to access resources |
| 21 | +| Name | Type | |
| 22 | +|------|------| |
| 23 | +| [azurerm_private_dns_zone.storage_file](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_dns_zone) | resource | |
| 24 | +| [azurerm_private_dns_zone_virtual_network_link.storage_file](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_dns_zone_virtual_network_link) | resource | |
| 25 | +| [azurerm_private_endpoint.nfs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) | resource | |
| 26 | +| [azurerm_storage_account.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account) | resource | |
| 27 | +| [azurerm_storage_account_network_rules.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account_network_rules) | resource | |
| 28 | +| [azurerm_storage_share.nfs_shares](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_share) | resource | |
161 | 29 |
|
162 | 30 | ## Inputs |
163 | 31 |
|
164 | 32 | | Name | Description | Type | Default | Required | |
165 | 33 | |------|-------------|------|---------|:--------:| |
166 | | -| `resource_group_name` | Resource group name | `string` | n/a | yes | |
167 | | -| `location` | Azure region | `string` | n/a | yes | |
168 | | -| `storage_account_name` | Storage account name (globally unique, 3-24 lowercase alphanumeric chars) | `string` | n/a | yes | |
169 | | -| `private_endpoint_subnet_id` | Subnet ID for Private Endpoint | `string` | n/a | yes | |
170 | | -| `vnet_id` | VNet ID for auto DNS zone creation (required if `private_dns_zone_ids` not provided) | `string` | `null` | no | |
171 | | -| `private_dns_zone_ids` | List of Private DNS Zone IDs. If not provided, module creates one automatically | `list(string)` | `[]` | no | |
172 | | -| `allowed_subnet_ids` | List of subnet IDs allowed to access storage (e.g., AKS nodes) | `list(string)` | `[]` | no | |
173 | | -| `allowed_ip_addresses` | List of IP addresses allowed to access storage (without CIDR, e.g., "1.2.3.4") | `list(string)` | `[]` | no | |
174 | | -| `network_bypass` | Services that can bypass network rules | `list(string)` | `["AzureServices"]` | no | |
175 | | -| `nfs_shares` | List of NFS file shares to create | `list(object)` | `[]` | no | |
176 | | -| `tags` | Tags to apply to resources | `map(string)` | `{}` | no | |
177 | | - |
178 | | -### NFS Shares Object |
179 | | - |
180 | | -```hcl |
181 | | -nfs_shares = [ |
182 | | - { |
183 | | - name = string # Share name |
184 | | - quota_gb = number # Quota in GB |
185 | | - access_tier = string # Optional: "Hot", "Cool", "TransactionOptimized" |
186 | | - metadata = map(string) # Optional: metadata tags |
187 | | - } |
188 | | -] |
189 | | -``` |
| 34 | +| <a name="input_allowed_ip_addresses"></a> [allowed\_ip\_addresses](#input\_allowed\_ip\_addresses) | List of IP addresses or CIDR ranges that are allowed to access the storage account (e.g., admin IP for Terraform operations to create file shares) | `list(string)` | `[]` | no | |
| 35 | +| <a name="input_allowed_subnet_ids"></a> [allowed\_subnet\_ids](#input\_allowed\_subnet\_ids) | List of subnet IDs that are allowed to access the storage account (e.g., AKS nodes subnet for NFS access) | `list(string)` | `[]` | no | |
| 36 | +| <a name="input_location"></a> [location](#input\_location) | Azure region | `string` | n/a | yes | |
| 37 | +| <a name="input_network_bypass"></a> [network\_bypass](#input\_network\_bypass) | List of services that can bypass network rules (AzureServices, Logging, Metrics, None) | `list(string)` | <pre>[<br/> "AzureServices"<br/>]</pre> | no | |
| 38 | +| <a name="input_nfs_shares"></a> [nfs\_shares](#input\_nfs\_shares) | List of NFS file shares to create | <pre>list(object({<br/> name = string<br/> quota_gb = number<br/> access_tier = optional(string)<br/> metadata = optional(map(string))<br/> }))</pre> | `[]` | no | |
| 39 | +| <a name="input_private_dns_zone_ids"></a> [private\_dns\_zone\_ids](#input\_private\_dns\_zone\_ids) | List of Private DNS Zone IDs for privatelink.file.core.windows.net. If not provided, a new Private DNS Zone will be created automatically. | `list(string)` | `[]` | no | |
| 40 | +| <a name="input_private_endpoint_subnet_id"></a> [private\_endpoint\_subnet\_id](#input\_private\_endpoint\_subnet\_id) | Subnet ID where the Private Endpoint will be created (typically a dedicated subnet for private endpoints) | `string` | n/a | yes | |
| 41 | +| <a name="input_resource_group_name"></a> [resource\_group\_name](#input\_resource\_group\_name) | Resource group name (same RG as AKS) | `string` | n/a | yes | |
| 42 | +| <a name="input_storage_account_name"></a> [storage\_account\_name](#input\_storage\_account\_name) | Storage account name (must be globally unique, 3-24 chars, lowercase alphanumeric) | `string` | n/a | yes | |
| 43 | +| <a name="input_tags"></a> [tags](#input\_tags) | Tags to apply to resources | `map(string)` | `{}` | no | |
| 44 | +| <a name="input_vnet_id"></a> [vnet\_id](#input\_vnet\_id) | VNet ID to link the Private DNS Zone to (required if private\_dns\_zone\_ids is not provided) | `string` | `null` | no | |
190 | 45 |
|
191 | 46 | ## Outputs |
192 | 47 |
|
193 | 48 | | Name | Description | |
194 | 49 | |------|-------------| |
195 | | -| `storage_account_id` | ID of the NFS storage account | |
196 | | -| `storage_account_name` | Name of the NFS storage account | |
197 | | -| `storage_account_primary_location` | Primary location of the storage account | |
198 | | -| `storage_account_primary_file_endpoint` | Primary file endpoint for NFS access | |
199 | | -| `private_endpoint_id` | ID of the Private Endpoint | |
200 | | -| `private_endpoint_ip_address` | Private IP address of the Private Endpoint | |
201 | | -| `private_dns_zone_id` | ID of the Private DNS Zone (auto-created or provided) | |
202 | | -| `private_dns_zone_name` | Name of the Private DNS Zone | |
203 | | -| `nfs_shares` | Map of created NFS file shares with details | |
204 | | - |
205 | | -## Requirements |
206 | | - |
207 | | -| Name | Version | |
208 | | -|------|---------| |
209 | | -| terraform | >= 1.0 | |
210 | | -| azurerm | >= 3.0 | |
211 | | - |
212 | | -## Important Notes |
213 | | - |
214 | | -### NFS 4.1 vs NFS 3.0 |
215 | | - |
216 | | -- This module uses **NFS 4.1** via Azure Files Premium |
217 | | -- NFS 4.1 provides better security, performance, and POSIX compliance |
218 | | -- Different from NFS 3.0 available on Blob Storage with HNS enabled |
219 | | - |
220 | | -### HTTPS Traffic |
221 | | - |
222 | | -- `https_traffic_only_enabled` is always `false` |
223 | | -- NFS protocol does not use HTTPS |
224 | | -- This is a requirement for NFS support |
225 | | - |
226 | | -### Storage Account Naming |
227 | | - |
228 | | -- Must be globally unique across Azure |
229 | | -- 3-24 characters |
230 | | -- Only lowercase letters and numbers |
231 | | -- No hyphens or special characters |
232 | | - |
233 | | -### Network Rules Best Practices |
234 | | - |
235 | | -1. Start with restrictive rules (default deny) |
236 | | -2. Add only necessary subnets and IPs |
237 | | -3. Remove admin IPs after deployment if not needed |
238 | | -4. Use Azure Bastion or VPN for admin access instead of public IPs |
239 | | -5. Monitor access logs for unauthorized attempts |
240 | | - |
241 | | -## Examples |
242 | | - |
243 | | -See the [examples directory](../../examples/public-quix-infr-nfs-storage) for complete working examples. |
244 | | - |
245 | | -## References |
246 | | - |
247 | | -- [Azure Files NFS 4.1 Documentation](https://learn.microsoft.com/en-us/azure/storage/files/storage-files-how-to-create-nfs-shares) |
248 | | -- [Azure Private Endpoint](https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview) |
249 | | -- [FoggyKitchen Blog: Secure Azure File Storage (NFS) with Private Endpoint using Terraform](https://foggykitchen.com/2025/10/02/azure-file-storage-nfs-terraform/) |
250 | | - |
251 | | -## License |
252 | | - |
253 | | -This module is maintained by Quix. |
| 50 | +| <a name="output_nfs_shares"></a> [nfs\_shares](#output\_nfs\_shares) | Map of NFS mount paths for each share | |
| 51 | +| <a name="output_private_dns_zone_id"></a> [private\_dns\_zone\_id](#output\_private\_dns\_zone\_id) | ID of the Private DNS Zone (auto-created or provided) | |
| 52 | +| <a name="output_private_dns_zone_name"></a> [private\_dns\_zone\_name](#output\_private\_dns\_zone\_name) | Name of the Private DNS Zone | |
| 53 | +| <a name="output_private_endpoint_id"></a> [private\_endpoint\_id](#output\_private\_endpoint\_id) | ID of the Private Endpoint | |
| 54 | +| <a name="output_private_endpoint_ip_address"></a> [private\_endpoint\_ip\_address](#output\_private\_endpoint\_ip\_address) | Private IP address of the Private Endpoint | |
| 55 | +| <a name="output_storage_account_id"></a> [storage\_account\_id](#output\_storage\_account\_id) | ID of the NFS storage account | |
| 56 | +| <a name="output_storage_account_name"></a> [storage\_account\_name](#output\_storage\_account\_name) | Name of the NFS storage account | |
| 57 | +| <a name="output_storage_account_primary_file_endpoint"></a> [storage\_account\_primary\_file\_endpoint](#output\_storage\_account\_primary\_file\_endpoint) | Primary file endpoint for NFS access | |
| 58 | +| <a name="output_storage_account_primary_location"></a> [storage\_account\_primary\_location](#output\_storage\_account\_primary\_location) | Primary location of the NFS storage account | |
| 59 | +<!-- END_TF_DOCS --> |
0 commit comments