Azure- Storage account creation with specific Virtual Network ,private endpoint & private DNS using Terraform.
Recently I have been tasked to create new Infrastructure on Azure using Terraform. While looking at some of the available documentation had some challenges with the way terraform has made the changes in their modules. So my intention was to test and do further granular way to modularizing so that anyone can use it.
Why Terraform
Introduction: Terraform is a popular Infrastructure as Code (IaC) tool used for managing and provisioning infrastructure resources in a declarative manner. In this article, we will dive into the key Terraform configuration files and understand how they interact in a real-world scenario. We’ll use a sample Terraform project as an example.
Lets Start creating Infrastructure
Sample Project Structure: Let’s start by exploring the structure of our Terraform project. We have organized our project into several directories, including modules and the main configuration files.
- data.tf: This file contains locals, which are used for defining reusable values throughout the configuration. Locals can simplify the configuration by assigning values to variables.
- main.tf: The main configuration file defines the infrastructure resources and how they interact. It includes module declarations for various components of our infrastructure, such as storage accounts, virtual networks, and private endpoints.
- variable.tf: The variable file defines input variables used throughout the project. Variables allow us to parameterize our configuration and make it more flexible.
- versions.tf: In this file, we specify the required Terraform version and provider version. It ensures compatibility and allows us to take advantage of specific features.
Now, let’s delve deeper into these files and see how they interact:
data.tf
In this file, we define several local variables, such as shared
, tenant
, and environment
. These variables are used to construct other values and names throughout the configuration. For instance, tenant_domain
is derived from tenant_env
, which combines tenant_name
and environment
.
main.tf
This is the heart of our Terraform configuration, where we declare the infrastructure components and their dependencies. Let’s break down the key elements:
- Module Declarations: In this file, we declare and configure three modules:
storage_account
,virtual_network
, andprivate_endpoint
. Each module is sourced from a separate directory, and we provide input variables specific to each module. - Outputs: We define output variables that allow us to access information about the created resources, such as
storage_account_name
,virtual_network_name
, andprivate_endpoint_id
.
variable.tf
In the variable file, we define input variables that can be customized to fit different environments or scenarios. Variables like location
, storage_account
, and account_tier
are set to default values but can be easily overridden when running Terraform commands.
versions.tf
This file specifies the required Terraform version and provider version, ensuring that our configuration is compatible with the chosen Terraform version and provider plugins.
Modules
-modules/dns
-modules/private_endpoint
-modules/storage_account
-modules/virtualnetwork
Each module has its own set of Terraform configuration files: main.tf
, var.tf
, and sometimes output.tf
if outputs are needed. These modules encapsulate specific infrastructure resources and their configurations.
Lets start looking into individual Modules 1 by 1
- Modules/Storage_Account: This module manages Azure Storage Accounts. It includes resource definitions for a storage account and a resource group.
- Modules/Virtual_Network: Here, we define a virtual network with associated subnets. This module is responsible for creating the networking components of our infrastructure.
- Modules/Private_Endpoint: This module creates an Azure Private Endpoint that connects to the Azure Storage Account. It also defines a resource group.
- Modules/DNS: This module handles Private DNS Zone configurations. It creates a Private DNS Zone and associated zone groups and links.
Let’s dive deeper into each of the module files within your Terraform project to understand their purpose and functionality.
Module: modules/storage_account
main.tf: In the main.tf
file of the storage_account
module, the focus is on creating an Azure Storage Account. Key elements include:
- azurerm_resource_group: This resource defines the Azure resource group where the storage account will be created.
- azurerm_storage_account: This is the core resource, defining the Azure Storage Account itself. It specifies the account name, resource group, location, account tier (e.g., Standard), and replication type (e.g., LRS). This storage account will be used to store various types of data, such as blobs or files.
- azurerm_storage_container: This resource creates a container within the storage account. Containers are used to organize and manage data stored in the storage account. In this example, it creates a container named “examplecontainer.”
var.tf: The var.tf
file contains input variable definitions for the storage_account
module. These variables allow you to customize the behavior of the module, including specifying the resource group name, storage account name, account tier, replication type, and access tier.
output.tf: While there are no output variables defined in this module, it’s common to include outputs that provide information about the created resources. In this case, you might want to output the storage account’s name or ID for use in other parts of your configuration.
Here is the code block for Storage account :
resource "azurerm_resource_group" "acs" {
name = var.resource_group_name
location = var.location
}
resource "azurerm_storage_account" "acs" {
name = var.storage_account_name
resource_group_name = azurerm_resource_group.acs.name
location = azurerm_resource_group.acs.location
account_tier = var.account_tier
account_replication_type = var.account_replication_type
access_tier = var.access_tier
}
resource "azurerm_storage_container" "container" {
name = var.container_name
storage_account_name = azurerm_storage_account.acs.name
}
output "storage_account_id" {
description = "ID of the created Azure Storage Account."
value = azurerm_storage_account.acs.id
}
output "storage_account_name" {
value = azurerm_storage_account.acs.name
}
output "resource_group_name" {
value = azurerm_resource_group.acs.name
}
###var.tf content
variable "resource_group_name" {
description = "Name of the Azure resource group"
}
variable "location" {
description = "Location for the Azure resources"
}
variable "storage_account_name" {
description = "Name of the Azure Storage Account"
}
variable "account_tier" {
description = "Storage Account Tier (e.g., Standard)"
}
variable "account_replication_type" {
description = "Storage Account Replication Type (e.g., LRS)"
}
variable "access_tier" {
description = "Access Tier for Blob Storage (e.g., Hot)"
}
variable "container_name" {
description = "Name of the Storage Container"
}
Module: modules/virtual_network
main.tf: The virtual_network
module is responsible for creating an Azure Virtual Network along with its subnets. Key elements include:
- azurerm_resource_group: Similar to the
storage_account
module, this resource defines the Azure resource group where the virtual network and subnets will be created. - azurerm_virtual_network: This resource defines the Azure Virtual Network itself. It specifies the network’s name, resource group, location, and address space (e.g., 10.0.0.0/16).
- azurerm_subnet: This resource creates subnets within the virtual network. The subnets are defined by name and address prefix (e.g., “appsubnet” with the address prefix 10.0.1.0/24). The module creates both the primary subnet and an “endpoint_subnet.”
var.tf: In the var.tf
file, you'll find input variable definitions for the virtual_network
module. These variables allow you to customize aspects of the virtual network, such as the resource group name, virtual network name, address space, subnet names, and subnet address prefixes.
output.tf: The output.tf
file doesn't define any output variables in this module. However, you could include outputs to provide information about the created virtual network, subnets, or other relevant details.
###main.tf
resource "azurerm_resource_group" "acs" {
name = var.resource_group_name
location = var.location
}
resource "azurerm_virtual_network" "acs" {
name = var.virtual_network_name
resource_group_name = azurerm_resource_group.acs.name
location = azurerm_resource_group.acs.location
address_space = var.address_space
}
resource "azurerm_subnet" "subnet" {
count = length(var.subnet_names)
name = var.subnet_names[count.index]
resource_group_name = azurerm_resource_group.acs.name
virtual_network_name = azurerm_virtual_network.acs.name
address_prefixes = [var.subnet_address_prefixes[count.index]]
}
resource "azurerm_subnet" "endpoint_subnet" {
name = "endpoint_subnet"
resource_group_name = azurerm_resource_group.acs.name
virtual_network_name = azurerm_virtual_network.acs.name
address_prefixes = ["10.0.2.0/24"]
}
output "virtual_network_name" {
value = azurerm_virtual_network.acs.name
}
output "resource_group_name" {
value = azurerm_resource_group.acs.name
}
output "subnet_id" {
description = "ID of the endpoint subnet"
value = azurerm_subnet.endpoint_subnet.id
}
###var.tf
variable "resource_group_name" {
description = "Name of the Azure resource group"
}
variable "location" {
description = "Location for the Azure resources"
}
variable "virtual_network_name" {
description = "Name of the Azure Virtual Network"
}
variable "address_space" {
description = "Address space for the Virtual Network"
type = list(string)
}
variable "subnet_names" {
description = "Names of the Subnets"
type = list(string)
}
variable "subnet_address_prefixes" {
description = "Address prefixes for Subnets"
type = list(string)
}
Module: modules/private_endpoint
main.tf: The private_endpoint
module is responsible for creating an Azure Private Endpoint that connects to an Azure Storage Account. Key elements include:
- azurerm_resource_group: Similar to the previous modules, this resource defines the Azure resource group where the private endpoint will be created.
- azurerm_private_endpoint: This resource defines the Azure Private Endpoint itself. It specifies the endpoint’s name, location, resource group, subnet ID (where it will be deployed), and details of the private service connection.
var.tf: In the var.tf
file, you'll find input variable definitions for the private_endpoint
module. These variables allow you to customize the behavior of the private endpoint, including specifying the resource group name, endpoint name, subnet ID, Private DNS zone ID, and storage account name.
output.tf: The output.tf
file defines two output variables:
- private_endpoint_id: This output variable provides the ID of the created Azure Private Endpoint. It can be used to reference the private endpoint in other parts of your configuration.
- subnet_id: This output variable provides the ID of the subnet where the private endpoint is deployed. It can be useful for understanding the network topology of your infrastructure.
#main.tf
resource "azurerm_resource_group" "acs" {
name = var.resource_group_name
location = var.location
}
resource "azurerm_private_endpoint" "acs" {
name = var.private_endpoint_name
location = azurerm_resource_group.acs.location
resource_group_name = azurerm_resource_group.acs.name
subnet_id = var.subnet_id
private_service_connection {
name = "example-connection"
private_connection_resource_id = var.storage_account_id
subresource_names = ["blob"]
is_manual_connection = false
}
}
output "private_endpoint_id" {
description = "ID of the created Azure Private Endpoint."
value = azurerm_private_endpoint.acs.id
}
output "subnet_id" {
value = azurerm_private_endpoint.acs.subnet_id
}
##var.tf
variable "resource_group_name" {
description = "Name of the Azure resource group"
}
variable "location" {
description = "Location for the Azure resources"
}
variable "private_endpoint_name" {
description = "Name of the Azure Private Endpoint"
}
variable "subnet_id" {
description = "ID of the subnet where the Private Endpoint will be deployed"
}
variable "private_dns_zone_id" {
description = "ID of the Private DNS zone associated with the Private Endpoint"
}
variable "storage_account_name" {
description = "Name for the Azure Storage Account."
type = string
}
variable "storage_account_id" {
description = "ID of the Azure Storage Account to which the private endpoint will be connected."
}
Module: modules/dns
main.tf: The dns
module focuses on configuring Private DNS Zones in Azure. Key elements include:
- azurerm_private_dns_zone: This resource creates a Private DNS Zone. In this case, it creates a zone for “privatelink.blob.core.windows.net.”
- azurerm_private_dns_zone_group: This resource defines a DNS zone group and associates it with the created Private DNS Zone.
- azurerm_private_dns_zone_virtual_network_link: This resource establishes a link between the Private DNS Zone and a virtual network. It enables resolution of DNS names within the linked virtual network.
var.tf: The var.tf
file contains input variable definitions for the dns
module. These variables allow you to customize the DNS zone's name and specify the DNS zone group name.
output.tf: In the output.tf
file, two output variables are defined:
- storage_account_name: This output variable provides the name of the storage account created within the DNS module.
- resource_group_name: This output variable provides the name of the resource group associated with the DNS configurations.
resource "azurerm_resource_group" "acs" {
name = var.resource_group_name
location = var.location
}
resource "azurerm_private_dns_zone" "acs" {
name = "privatelink.blob.core.windows.net"
resource_group_name = azurerm_resource_group.acs.name
}
resource "azurerm_private_dns_a_record" "acs" {
name = var.storage_account_name
zone_name = azurerm_private_dns_zone.acs.name
resource_group_name = azurerm_resource_group.acs.name
ttl = 300
records = ["10.0.1.0"]
}
variable "resource_group_name" {
description = "Name of the Azure resource group"
}
variable "location" {
description = "Location for the Azure resources"
}
variable "private_dns_zone_name" {
description = "The name of the Private DNS Zone."
}
variable "record_name" {
description = "The name of the DNS record to create."
}
variable "record_type" {
description = "The type of DNS record to create (e.g., A, CNAME)."
}
variable "virtual_network_name" {
description = "The name of the virtual network."
type = string
}
variable "address_space" {
description = "The address space of the virtual network."
}
variable "subnet_names" {
description = "The names of the subnets in the virtual network."
}
variable "subnet_address_prefixes" {
description = "The address prefixes of the subnets in the virtual network."
}
variable "storage_account_name" {
description = "The name of the Storage Account."
type = string
}
variable "vnet_private_endpoint_id" {
description = "The ID of the Virtual Network Private Endpoint."
}
variable "private_dns_zone_group_name" {
description = "Name of the Private DNS Zone Group for Private Endpoint"
}
This is how my Terraform init shows
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.dns.azurerm_private_dns_a_record.acs will be created
+ resource "azurerm_private_dns_a_record" "acs" {
+ fqdn = (known after apply)
+ id = (known after apply)
+ name = "local.output_variables.storage_account_name"
+ records = [
+ "172.16.200.1",
]
+ resource_group_name = "tn0esbxeastus-storage01"
+ ttl = 300
+ zone_name = "privatelink.blob.core.windows.net"
}
# module.dns.azurerm_private_dns_zone.acs will be created
+ resource "azurerm_private_dns_zone" "acs" {
+ id = (known after apply)
+ max_number_of_record_sets = (known after apply)
+ max_number_of_virtual_network_links = (known after apply)
+ max_number_of_virtual_network_links_with_registration = (known after apply)
+ name = "privatelink.blob.core.windows.net"
+ number_of_record_sets = (known after apply)
+ resource_group_name = "tn0esbxeastus-storage01"
}
# module.dns.azurerm_resource_group.acs will be created
+ resource "azurerm_resource_group" "acs" {
+ id = (known after apply)
+ location = "eastus"
+ name = "tn0esbxeastus-storage01"
}
# module.private_endpoint.azurerm_private_endpoint.acs will be created
+ resource "azurerm_private_endpoint" "acs" {
+ custom_dns_configs = (known after apply)
+ id = (known after apply)
+ location = "eastus"
+ name = "tn0esbxeastusstorage-ep"
+ network_interface = (known after apply)
+ private_dns_zone_configs = (known after apply)
+ resource_group_name = "tn0esbxeastus-storage01"
+ subnet_id = (known after apply)
+ private_service_connection {
+ is_manual_connection = false
+ name = "example-connection"
+ private_connection_resource_id = (known after apply)
+ private_ip_address = (known after apply)
+ subresource_names = [
+ "blob",
]
}
}
# module.private_endpoint.azurerm_resource_group.acs will be created
+ resource "azurerm_resource_group" "acs" {
+ id = (known after apply)
+ location = "eastus"
+ name = "tn0esbxeastus-storage01"
}
# module.storage_account.azurerm_resource_group.acs will be created
+ resource "azurerm_resource_group" "acs" {
+ id = (known after apply)
+ location = "eastus"
+ name = "tn0esbxeastus-storage01"
}
# module.storage_account.azurerm_storage_account.acs will be created
+ resource "azurerm_storage_account" "acs" {
+ access_tier = "Hot"
+ account_kind = "StorageV2"
+ account_replication_type = "LRS"
+ account_tier = "Standard"
+ allow_nested_items_to_be_public = true
+ enable_https_traffic_only = true
+ id = (known after apply)
+ infrastructure_encryption_enabled = false
+ is_hns_enabled = false
+ large_file_share_enabled = (known after apply)
+ location = "eastus"
+ min_tls_version = "TLS1_2"
+ name = "tn0esbxeastusstorage"
+ nfsv3_enabled = false
+ primary_access_key = (sensitive value)
+ primary_blob_connection_string = (sensitive value)
+ primary_blob_endpoint = (known after apply)
+ primary_blob_host = (known after apply)
+ primary_connection_string = (sensitive value)
+ primary_dfs_endpoint = (known after apply)
+ primary_dfs_host = (known after apply)
+ primary_file_endpoint = (known after apply)
+ primary_file_host = (known after apply)
+ primary_location = (known after apply)
+ primary_queue_endpoint = (known after apply)
+ primary_queue_host = (known after apply)
+ primary_table_endpoint = (known after apply)
+ primary_table_host = (known after apply)
+ primary_web_endpoint = (known after apply)
+ primary_web_host = (known after apply)
+ queue_encryption_key_type = "Service"
+ resource_group_name = "tn0esbxeastus-storage01"
+ secondary_access_key = (sensitive value)
+ secondary_blob_connection_string = (sensitive value)
+ secondary_blob_endpoint = (known after apply)
+ secondary_blob_host = (known after apply)
+ secondary_connection_string = (sensitive value)
+ secondary_dfs_endpoint = (known after apply)
+ secondary_dfs_host = (known after apply)
+ secondary_file_endpoint = (known after apply)
+ secondary_file_host = (known after apply)
+ secondary_location = (known after apply)
+ secondary_queue_endpoint = (known after apply)
+ secondary_queue_host = (known after apply)
+ secondary_table_endpoint = (known after apply)
+ secondary_table_host = (known after apply)
+ secondary_web_endpoint = (known after apply)
+ secondary_web_host = (known after apply)
+ shared_access_key_enabled = true
+ table_encryption_key_type = "Service"
}
# module.storage_account.azurerm_storage_container.container will be created
+ resource "azurerm_storage_container" "container" {
+ container_access_type = "private"
+ has_immutability_policy = (known after apply)
+ has_legal_hold = (known after apply)
+ id = (known after apply)
+ metadata = (known after apply)
+ name = "examplecontainer"
+ resource_manager_id = (known after apply)
+ storage_account_name = "tn0esbxeastusstorage"
}
# module.virtual_network.azurerm_subnet.endpoint_subnet will be created
+ resource "azurerm_subnet" "endpoint_subnet" {
+ address_prefixes = [
+ "10.0.2.0/24",
]
+ enforce_private_link_endpoint_network_policies = false
+ enforce_private_link_service_network_policies = false
+ id = (known after apply)
+ name = "endpoint_subnet"
+ resource_group_name = "example-rg"
+ virtual_network_name = "prademovnet"
}
# module.virtual_network.azurerm_subnet.subnet[0] will be created
+ resource "azurerm_subnet" "subnet" {
+ address_prefixes = [
+ "10.0.1.0/24",
]
+ enforce_private_link_endpoint_network_policies = false
+ enforce_private_link_service_network_policies = false
+ id = (known after apply)
+ name = "appsubnet"
+ resource_group_name = "example-rg"
+ virtual_network_name = "prademovnet"
}
# module.virtual_network.azurerm_subnet.subnet[1] will be created
+ resource "azurerm_subnet" "subnet" {
+ address_prefixes = [
+ "10.0.2.0/24",
]
+ enforce_private_link_endpoint_network_policies = false
+ enforce_private_link_service_network_policies = false
+ id = (known after apply)
+ name = "endpoint_subnet"
+ resource_group_name = "example-rg"
+ virtual_network_name = "prademovnet"
}
# module.virtual_network.azurerm_virtual_network.acs will be created
+ resource "azurerm_virtual_network" "acs" {
+ address_space = [
+ "10.0.0.0/16",
]
+ dns_servers = (known after apply)
+ guid = (known after apply)
+ id = (known after apply)
+ location = "eastus"
+ name = "prademovnet"
+ resource_group_name = "example-rg"
+ subnet = (known after apply)
}
Plan: 12 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ private_endpoint_id = (known after apply)
+ storage_account_name = "tn0esbxeastusstorage"
+ subnet_id = (known after apply)
+ virtual_network_name = "prademovnet"
Terraform Plan : this is how my terraform plan output shows
module.private_endpoint.azurerm_resource_group.acs: Creating...
module.virtual_network.azurerm_resource_group.acs: Creating...
module.storage_account.azurerm_resource_group.acs: Creating...
module.dns.azurerm_resource_group.acs: Creating...
module.storage_account.azurerm_resource_group.acs: Creation complete after 4s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/tn0esbxeastus-storage01]
module.dns.azurerm_resource_group.acs: Creation complete after 4s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/tn0esbxeastus-storage01]
module.virtual_network.azurerm_resource_group.acs: Creation complete after 4s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/example-rg]
module.private_endpoint.azurerm_resource_group.acs: Creation complete after 4s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/tn0esbxeastus-storage01]
module.storage_account.azurerm_storage_account.acs: Creating...
module.dns.azurerm_private_dns_zone.acs: Creating...
module.virtual_network.azurerm_virtual_network.acs: Creating...
module.virtual_network.azurerm_subnet.subnet[0]: Creating...
module.virtual_network.azurerm_subnet.endpoint_subnet: Creating...
module.dns.azurerm_private_dns_zone.acs: Still creating... [10s elapsed]
module.storage_account.azurerm_storage_account.acs: Still creating... [10s elapsed]
module.virtual_network.azurerm_subnet.subnet[0]: Creation complete after 7s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/example-rg/providers/Microsoft.Network/virtualNetworks/prademovnet/subnets/appsubnet]
module.virtual_network.azurerm_subnet.endpoint_subnet: Still creating... [10s elapsed]
module.virtual_network.azurerm_subnet.subnet[1]: Still creating... [10s elapsed]
module.storage_account.azurerm_storage_account.acs: Still creating... [20s elapsed]
module.dns.azurerm_private_dns_zone.acs: Still creating... [20s elapsed]
module.virtual_network.azurerm_subnet.endpoint_subnet: Creation complete after 13s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/example-rg/providers/Microsoft.Network/virtualNetworks/prademovnet/subnets/endpoint_subnet]
module.virtual_network.azurerm_subnet.subnet[1]: Creation complete after 16s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/example-rg/providers/Microsoft.Network/virtualNetworks/prademovnet/subnets/endpoint_subnet]
module.storage_account.azurerm_storage_account.acs: Still creating... [30s elapsed]
module.dns.azurerm_private_dns_zone.acs: Still creating... [30s elapsed]
module.storage_account.azurerm_storage_account.acs: Creation complete after 32s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/tn0esbxeastus-storage01/providers/Microsoft.Storage/storageAccounts/tn0esbxeastusstorage]
module.storage_account.azurerm_storage_container.container: Creating...
module.private_endpoint.azurerm_private_endpoint.acs: Creating...
module.storage_account.azurerm_storage_container.container: Creation complete after 1s [id=https://tn0esbxeastusstorage.blob.core.windows.net/examplecontainer]
module.dns.azurerm_private_dns_zone.acs: Creation complete after 37s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/tn0esbxeastus-storage01/providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net]
module.dns.azurerm_private_dns_a_record.acs: Creating...
module.dns.azurerm_private_dns_a_record.acs: Creation complete after 2s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/tn0esbxeastus-storage01/providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net/A/local.output_variables.storage_account_name]
module.private_endpoint.azurerm_private_endpoint.acs: Still creating... [10s elapsed]
module.private_endpoint.azurerm_private_endpoint.acs: Still creating... [20s elapsed]
module.private_endpoint.azurerm_private_endpoint.acs: Still creating... [30s elapsed]
module.private_endpoint.azurerm_private_endpoint.acs: Creation complete after 37s [id=/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/tn0esbxeastus-storage01/providers/Microsoft.Network/privateEndpoints/tn0esbxeastusstorage-ep]
Apply complete! Resources: 13 added, 0 changed, 0 destroyed.
Outputs:
private_endpoint_id = "/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/tn0esbxeastus-storage01/providers/Microsoft.Network/privateEndpoints/tn0esbxeastusstorage-ep"
storage_account_name = "tn0esbxeastusstorage"
subnet_id = "/subscriptions/xxxxxxx-xxxxxxxx-xxxxxx/resourceGroups/example-rg/providers/Microsoft.Network/virtualNetworks/prademovnet/subnets/endpoint_subnet"
virtual_network_name = "prademovnet"
and then Vnet I have created it in different RG
you can find code at: https://github.com/learnprofile/Terraform-PrivateDNS-Storage
Conclusion: Each module in your Terraform project has a specific purpose and encapsulates the configuration for a particular set of infrastructure resources. By breaking your configuration into modules, you can achieve modularity, reusability, and maintainability in your Terraform code. This enables you to manage complex infrastructure deployments with ease and flexibility.