Generate production-ready Azure spoke workload infrastructure with VNet, subnet, VM, and optional data disk using Bicep and Terraform. Use when creating Azure workloads, spoke networks, virtual machines, or when user mentions workload deployment, IaC generation, or spoke infrastructure. Follows naming-convention.md and ip-addressing-scheme.md standards.
Generate complete Azure spoke workload infrastructure in both Bicep and Terraform, following enterprise naming conventions and IP addressing standards.
This skill creates production-ready infrastructure code for Azure spoke workloads including:
Output: Both Bicep and Terraform code with proper directory structure under azure-workloads/Customer/<customerCode>/
CRITICAL: You MUST gather ALL required inputs from the user BEFORE generating ANY code. NEVER assume, guess, or use placeholder values for required inputs. ALWAYS use the
AskUserQuestiontool to collect missing information.
The naming convention (docs/standards/naming-convention.md) requires specific inputs to generate valid resource names:
| Input | Used In Naming Pattern | Example |
|---|---|---|
| Customer Code | {customerCode}- prefix on ALL resources | cc-vnet-... |
| Workload Name | {workload} component | cc-vnet-webapp-... |
| Environment | {environment} code (d/t/a/p) | cc-vnet-webapp-p-... |
| Region | {region} code (default: weu) | cc-vnet-webapp-p-weu-01 |
Without these inputs, you CANNOT generate compliant resource names.
Before proceeding to code generation, you MUST have explicit user confirmation for:
These have sensible defaults but should be confirmed:
If the user's request is missing ANY of the 5 required inputs above:
STOP → Use AskUserQuestion → Gather missing inputs → THEN proceed
Example: If user says "create an exact-financials workload", you are MISSING:
You MUST ask for these before generating any code.
Use the AskUserQuestion tool to collect the following information from the user:
Customer Code (e.g., "contoso", "fabrikam")
Workload Name (e.g., "webapp", "database", "api")
Environment Code
d = Developmentt = Testa = Acceptancep = ProductionVNet Address Space (e.g., "10.10.0.0/16", "172.16.0.0/16")
Subnet Name (e.g., "app", "data", "web")
Subnet Size (e.g., "/24", "/26")
VM Operating System
Windows or LinuxVM SKU (e.g., "Standard_D2s_v3", "Standard_B2ms")
Data Disk Required
yes or noData Disk Size in GB (if data disk required)
Instance Number (default: "01")
Tag Values (for mandatory tags)
Before generating code, read the following standards:
Read docs/standards/naming-convention.md to understand:
weu for West Europe)Reference docs/standards/ip-addressing-scheme.md for context:
Based on the naming convention standard, generate resource names following these patterns:
Resource Naming Format: <resourceType>-<workload>-<environment>-<region>-<instance>
VNet: vnet-<workload>-<environment>-weu-<instance>
vnet-webapp-p-weu-01Subnet: snet-<subnetName>-<environment>-weu-<instance>
snet-app-p-weu-01VM: vm<workload><environment>weu<instance> (no hyphens, max 15 chars for Windows)
vmwebapppweu01vm-<workload>-<environment>-weu-<instance> (up to 64 chars)NIC: nic-<workload>-<environment>-weu-<instance>
nic-webapp-p-weu-01Data Disk (if required): disk-<workload>-data-<environment>-weu-<instance>
disk-webapp-data-p-weu-01Public IP (if required): pip-<workload>-<environment>-weu-<instance>
pip-webapp-p-weu-01NSG: nsg-<workload>-<environment>-weu-<instance>
nsg-webapp-p-weu-01Validate Name Lengths:
Create a tags object with all mandatory tags:
{
"Environment": "<d|t|a|p - full name>",
"CostCenter": "<from user input or placeholder>",
"Owner": "<from user input or placeholder>",
"ManagedBy": "Terraform" or "Bicep",
"Workload": "<workload name>",
"Criticality": "<from user input or placeholder>"
}
Tag Values:
Create the following directory structure under azure-workloads/:
azure-workloads/
├── Customer/
│ ├── bicep-modules/ (for reusable modules)
│ ├── terraform-modules/ (for reusable modules)
│ └── <customerCode>/ (e.g., contoso/)
│ ├── bicep/
│ │ ├── <workloadname>.bicep
│ │ └── <workloadname>.bicepparam
│ └── terraform/
│ ├── <workloadname>.tf
│ ├── variables.tf
│ └── terraform.tfvars
Use the Write tool to create directories and files.
Create two files in azure-workloads/Customer/<customerCode>/bicep/:
<workloadname>.bicepStructure:
// Azure Spoke Workload: <workload>
// Generated by azure-workload-generator
// Customer: <customerCode>
// Environment: <environment>
targetScope = 'resourceGroup'
// ============================================================================
// PARAMETERS
// ============================================================================
@description('Location for all resources')
param location string = 'westeurope'
@description('VNet address space')
param vnetAddressSpace string
@description('Subnet address prefix')
param subnetAddressPrefix string
@description('Virtual Machine SKU')
param vmSize string
@description('Admin username for the VM')
@secure()
param adminUsername string
@description('Admin password for the VM')
@secure()
param adminPassword string
@description('Data disk size in GB (0 for no data disk)')
param dataDiskSizeGB int
@description('Resource tags')
param tags object
// ============================================================================
// VARIABLES
// ============================================================================
var vnetName = '<generated-vnet-name>'
var subnetName = '<generated-subnet-name>'
var vmName = '<generated-vm-name>'
var nicName = '<generated-nic-name>'
var nsgName = '<generated-nsg-name>'
var osDiskName = '${vmName}-osdisk'
var dataDiskName = '<generated-datadisk-name>'
// ============================================================================
// RESOURCES
// ============================================================================
// Network Security Group
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-05-01' = {
name: nsgName
location: location
tags: tags
properties: {
securityRules: [
// Add security rules based on OS type
]
}
}
// Virtual Network
resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: vnetName
location: location
tags: tags
properties: {
addressSpace: {
addressPrefixes: [
vnetAddressSpace
]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: subnetAddressPrefix
networkSecurityGroup: {
id: nsg.id
}
}
}
]
}
}
// Network Interface
resource nic 'Microsoft.Network/networkInterfaces@2023-05-01' = {
name: nicName
location: location
tags: tags
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
subnet: {
id: vnet.properties.subnets[0].id
}
privateIPAllocationMethod: 'Dynamic'
}
}
]
}
}
// Virtual Machine
resource vm 'Microsoft.Compute/virtualMachines@2023-03-01' = {
name: vmName
location: location
tags: tags
properties: {
hardwareProfile: {
vmSize: vmSize
}
osProfile: {
computerName: vmName
adminUsername: adminUsername
adminPassword: adminPassword
}
storageProfile: {
imageReference: {
// Set based on OS type (Windows or Linux)
publisher: '<publisher>'
offer: '<offer>'
sku: '<sku>'
version: 'latest'
}
osDisk: {
name: osDiskName
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'Premium_LRS'
}
}
dataDisks: dataDiskSizeGB > 0 ? [
{
name: dataDiskName
diskSizeGB: dataDiskSizeGB
lun: 0
createOption: 'Empty'
managedDisk: {
storageAccountType: 'Premium_LRS'
}
}
] : []
}
networkProfile: {
networkInterfaces: [
{
id: nic.id
}
]
}
}
}
// ============================================================================
// OUTPUTS
// ============================================================================
output vnetId string = vnet.id
output vnetName string = vnet.name
output subnetId string = vnet.properties.subnets[0].id
output vmId string = vm.id
output vmName string = vm.name
output privateIPAddress string = nic.properties.ipConfigurations[0].properties.privateIPAddress
Key Considerations:
bicep-modules/ directory if they exist<workloadname>.bicepparamStructure:
using './<workloadname>.bicep'
param location = 'westeurope'
param vnetAddressSpace = '<user-provided-vnet-address>'
param subnetAddressPrefix = '<calculated-subnet-cidr>'
param vmSize = '<user-provided-vm-sku>'
param adminUsername = '<placeholder-or-keyvault-reference>'
param adminPassword = '<placeholder-or-keyvault-reference>'
param dataDiskSizeGB = <user-provided-size-or-0>
param tags = {
Environment: '<Development|Test|Acceptance|Production>'
CostCenter: 'TO_BE_DEFINED'
Owner: 'TO_BE_DEFINED'
ManagedBy: 'Bicep'
Workload: '<workload-name>'
Criticality: 'TO_BE_DEFINED'
}
Subnet Calculation:
Create three files in azure-workloads/Customer/<customerCode>/terraform/:
<workloadname>.tfStructure:
# Azure Spoke Workload: <workload>
# Generated by azure-workload-generator
# Customer: <customerCode>
# Environment: <environment>
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "azurerm" {
features {}
}
# ============================================================================
# LOCAL VARIABLES
# ============================================================================
locals {
vnet_name = "<generated-vnet-name>"
subnet_name = "<generated-subnet-name>"
vm_name = "<generated-vm-name>"
nic_name = "<generated-nic-name>"
nsg_name = "<generated-nsg-name>"
os_disk_name = "${local.vm_name}-osdisk"
data_disk_name = "<generated-datadisk-name>"
common_tags = {
Environment = var.environment
CostCenter = var.cost_center
Owner = var.owner
ManagedBy = "Terraform"
Workload = var.workload_name
Criticality = var.criticality
}
}
# ============================================================================
# RESOURCE GROUP
# ============================================================================
# Assumes resource group already exists
data "azurerm_resource_group" "main" {
name = var.resource_group_name
}
# ============================================================================
# NETWORK SECURITY GROUP
# ============================================================================
resource "azurerm_network_security_group" "main" {
name = local.nsg_name
location = data.azurerm_resource_group.main.location
resource_group_name = data.azurerm_resource_group.main.name
tags = local.common_tags
# Add security rules based on OS type
}
# ============================================================================
# VIRTUAL NETWORK
# ============================================================================
resource "azurerm_virtual_network" "main" {
name = local.vnet_name
location = data.azurerm_resource_group.main.location
resource_group_name = data.azurerm_resource_group.main.name
address_space = [var.vnet_address_space]
tags = local.common_tags
}
resource "azurerm_subnet" "main" {
name = local.subnet_name
resource_group_name = data.azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = [var.subnet_address_prefix]
}
resource "azurerm_subnet_network_security_group_association" "main" {
subnet_id = azurerm_subnet.main.id
network_security_group_id = azurerm_network_security_group.main.id
}
# ============================================================================
# NETWORK INTERFACE
# ============================================================================
resource "azurerm_network_interface" "main" {
name = local.nic_name
location = data.azurerm_resource_group.main.location
resource_group_name = data.azurerm_resource_group.main.name
tags = local.common_tags
ip_configuration {
name = "ipconfig1"
subnet_id = azurerm_subnet.main.id
private_ip_address_allocation = "Dynamic"
}
}
# ============================================================================
# VIRTUAL MACHINE
# ============================================================================
resource "azurerm_<windows|linux>_virtual_machine" "main" {
name = local.vm_name
location = data.azurerm_resource_group.main.location
resource_group_name = data.azurerm_resource_group.main.name
size = var.vm_size
admin_username = var.admin_username
admin_password = var.admin_password
tags = local.common_tags
network_interface_ids = [
azurerm_network_interface.main.id
]
os_disk {
name = local.os_disk_name
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
# Set source_image_reference based on OS type
source_image_reference {
publisher = "<publisher>"
offer = "<offer>"
sku = "<sku>"
version = "latest"
}
}
# ============================================================================
# DATA DISK (CONDITIONAL)
# ============================================================================
resource "azurerm_managed_disk" "data" {
count = var.data_disk_size_gb > 0 ? 1 : 0
name = local.data_disk_name
location = data.azurerm_resource_group.main.location
resource_group_name = data.azurerm_resource_group.main.name
storage_account_type = "Premium_LRS"
create_option = "Empty"
disk_size_gb = var.data_disk_size_gb
tags = local.common_tags
}
resource "azurerm_virtual_machine_data_disk_attachment" "data" {
count = var.data_disk_size_gb > 0 ? 1 : 0
managed_disk_id = azurerm_managed_disk.data[0].id
virtual_machine_id = azurerm_<windows|linux>_virtual_machine.main.id
lun = 0
caching = "ReadWrite"
}
# ============================================================================
# OUTPUTS
# ============================================================================
output "vnet_id" {
description = "The ID of the Virtual Network"
value = azurerm_virtual_network.main.id
}
output "vnet_name" {
description = "The name of the Virtual Network"
value = azurerm_virtual_network.main.name
}
output "subnet_id" {
description = "The ID of the Subnet"
value = azurerm_subnet.main.id
}
output "vm_id" {
description = "The ID of the Virtual Machine"
value = azurerm_<windows|linux>_virtual_machine.main.id
}
output "vm_name" {
description = "The name of the Virtual Machine"
value = azurerm_<windows|linux>_virtual_machine.main.name
}
output "private_ip_address" {
description = "The private IP address of the VM"
value = azurerm_network_interface.main.private_ip_address
}
Key Considerations:
azurerm_windows_virtual_machine for Windows, azurerm_linux_virtual_machine for Linuxterraform-modules/ directory if they existcount for conditional data disk creationdisable_password_authentication = false is not applicablevariables.tfStructure:
# Variable definitions for <workload> workload
variable "resource_group_name" {
description = "Name of the resource group"
type = string
}
variable "vnet_address_space" {
description = "Address space for the virtual network"
type = string
}
variable "subnet_address_prefix" {
description = "Address prefix for the subnet"
type = string
}
variable "vm_size" {
description = "Size of the virtual machine"
type = string
}
variable "admin_username" {
description = "Admin username for the VM"
type = string
sensitive = true
}
variable "admin_password" {
description = "Admin password for the VM"
type = string
sensitive = true
}
variable "data_disk_size_gb" {
description = "Size of the data disk in GB (0 for no data disk)"
type = number
default = 0
}
variable "environment" {
description = "Environment name (Development, Test, Acceptance, Production)"
type = string
}
variable "cost_center" {
description = "Cost center tag value"
type = string
default = "TO_BE_DEFINED"
}
variable "owner" {
description = "Owner tag value"
type = string
default = "TO_BE_DEFINED"
}
variable "workload_name" {
description = "Workload name for tagging"
type = string
}
variable "criticality" {
description = "Criticality level (Low, Medium, High, Critical)"
type = string
default = "TO_BE_DEFINED"
}
terraform.tfvarsStructure:
# Terraform variables for <workload> workload
# Customer: <customerCode>
# Environment: <environment>
resource_group_name = "rg-<workload>-<environment>-weu-01"
vnet_address_space = "<user-provided-vnet-address>"
subnet_address_prefix = "<calculated-subnet-cidr>"
vm_size = "<user-provided-vm-sku>"
admin_username = "azureadmin" # Change as needed
admin_password = "CHANGE_ME_SECURE_PASSWORD" # Use Key Vault or environment variable
data_disk_size_gb = <user-provided-size-or-0>
environment = "<Development|Test|Acceptance|Production>"
workload_name = "<workload-name>"
cost_center = "TO_BE_DEFINED"
owner = "TO_BE_DEFINED"
criticality = "TO_BE_DEFINED"
After generating all files:
Provide a summary to the user:
✓ Generated Azure Workload: <workload>
✓ Customer: <customerCode>
✓ Environment: <environment>
Files Created:
- azure-workloads/Customer/<customerCode>/bicep/<workloadname>.bicep
- azure-workloads/Customer/<customerCode>/bicep/<workloadname>.bicepparam
- azure-workloads/Customer/<customerCode>/terraform/<workloadname>.tf
- azure-workloads/Customer/<customerCode>/terraform/variables.tf
- azure-workloads/Customer/<customerCode>/terraform/terraform.tfvars
Resources:
- VNet: <vnet-name> (<vnet-address-space>)
- Subnet: <subnet-name> (<subnet-cidr>)
- VM: <vm-name> (<vm-sku>, <os-type>)
- Data Disk: <Yes/No> (<size-if-yes>)
Next Steps:
1. Review and update placeholder tag values (CostCenter, Owner, Criticality)
2. Update admin credentials in parameter files
3. Create resource group: rg-<workload>-<environment>-weu-01
4. Deploy using:
- Bicep: az deployment group create --resource-group <rg-name> --parameters <workloadname>.bicepparam
- Terraform: terraform init && terraform plan && terraform apply
User Input:
contosowebappp (Production)10.10.0.0/16app/24LinuxStandard_D2s_v3yes128 GB01CC-12345, Owner=WebTeam, Criticality=HighGenerated Resources:
vnet-webapp-p-weu-01snet-app-p-weu-01vm-webapp-p-weu-01 (Linux, up to 64 chars allowed)nic-webapp-p-weu-01nsg-webapp-p-weu-01disk-webapp-data-p-weu-01Files Created:
azure-workloads/Customer/contoso/
├── bicep/
│ ├── webapp.bicep
│ └── webapp.bicepparam
└── terraform/
├── webapp.tf
├── variables.tf
└── terraform.tfvars
User Input:
fabrikamsqldbt (Test)172.16.0.0/16data/26WindowsStandard_E4s_v3yes512 GB01CC-67890, Owner=DataTeam, Criticality=CriticalGenerated Resources:
vnet-sqldb-t-weu-01snet-data-t-weu-01vmsqldbtweu01 (Windows, max 15 chars, no hyphens)nic-sqldb-t-weu-01nsg-sqldb-t-weu-01disk-sqldb-data-t-weu-01Files Created:
azure-workloads/Customer/fabrikam/
├── bicep/
│ ├── sqldb.bicep
│ └── sqldb.bicepparam
└── terraform/
├── sqldb.tf
├── variables.tf
└── terraform.tfvars
docs/standards/naming-convention.md before generating namesdocs/standards/ip-addressing-scheme.md for network contextUsers deploying the generated code need:
01 as default, increment for additional instancesIf reusable Bicep or Terraform modules exist in the bicep-modules/ or terraform-modules/ directories, the skill should reference them instead of inline resources:
Bicep Module Reference Example:
module vnet '../../../bicep-modules/vnet/main.bicep' = {
name: 'vnet-deployment'
params: {
vnetName: vnetName
addressSpace: vnetAddressSpace
location: location
tags: tags
}
}
Terraform Module Reference Example:
module "vnet" {
source = "../../../terraform-modules/vnet"
vnet_name = local.vnet_name
address_space = var.vnet_address_space
location = data.azurerm_resource_group.main.location
tags = local.common_tags
}
For deploying multiple instances of the same workload:
01, 02, 03, etc.The generated code can be integrated into CI/CD pipelines:
az deployment group createterraform applyIssue: VM name exceeds 15 characters for Windows
Solution: Remove hyphens and shorten workload name. Example: vmsqldbtweu01 instead of vm-sqldb-t-weu-01
Issue: Subnet CIDR calculation incorrect Solution: Ensure subnet size suffix is applied to VNet base address correctly
Issue: IP address space conflicts Solution: Verify VNet address doesn't overlap with existing networks or hub VNets
Issue: Mandatory tags missing Solution: Ensure all six mandatory tags are included in tags object
Issue: Module references not found
Solution: Check if shared modules exist in bicep-modules/ or terraform-modules/ directories
The azure-workload-generator skill automates the creation of production-ready Azure spoke workload infrastructure following enterprise standards. It generates both Bicep and Terraform code with proper:
Use this skill whenever you need to create new Azure workload infrastructure for customers.