diff --git a/modules/kustomize/stretch/setup_ipvlan.sh b/modules/kustomize/stretch/setup_ipvlan.sh new file mode 100755 index 0000000000..588b28d5bf --- /dev/null +++ b/modules/kustomize/stretch/setup_ipvlan.sh @@ -0,0 +1,189 @@ +#!/bin/bash +set -euo pipefail + +# Script to setup ipvlan CNI configuration on an Azure VM node using IMDS +# This script is designed to run inside the node (not externally via az vm run-command) + +# Default values +ADDRESS_VERSION="IPv4" +INTERFACE_NAME="eth0" +CNI_NAME="ipvlan-eth0" +API_VERSION="2025-04-07" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --address-version) + ADDRESS_VERSION="$2" + shift 2 + ;; + --interface) + INTERFACE_NAME="$2" + shift 2 + ;; + --cni-name) + CNI_NAME="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [options]" + echo "Options:" + echo " --address-version [IPv4|IPv6] IP address version (default: IPv4)" + echo " --interface NAME Network interface name (default: eth0)" + echo " --cni-name NAME CNI configuration name (default: ipvlan-eth0)" + exit 0 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Query Azure IMDS +query_imds() { + local endpoint_path="$1" + local url="http://169.254.169.254${endpoint_path}?api-version=${API_VERSION}&format=json" + + if ! curl -s -H "Metadata: true" "$url"; then + echo "Error querying IMDS: $url" >&2 + exit 1 + fi +} + +# Derive IP range from CIDR +derive_range() { + local ip_addr="$1" + local version="$2" + + if [[ "$version" == "IPv6" ]]; then + # For IPv6, use ipcalc or manual calculation + python3 -c "import ipaddress; net = ipaddress.IPv6Network('$ip_addr', strict=False); print(f'{net.network_address + 1} {net.broadcast_address - 1}')" + else + # For IPv4 + python3 -c "import ipaddress; net = ipaddress.IPv4Network('$ip_addr', strict=False); print(f'{net.network_address + 1} {net.broadcast_address - 1}')" + fi +} + +# Check for required CNI plugins +if ls /opt/cni/bin/ipvlan 1> /dev/null 2>&1; then + echo "Found ipvlan CNI plugin." +else + echo "Install all CNI plugins in /opt/cni/bin before running this script." + CNI_PLUGIN_URL="https://github.com/containernetworking/plugins/releases/download/v1.9.0/cni-plugins-linux-amd64-v1.9.0.tgz" + echo "Downloading CNI plugins from $CNI_PLUGIN_URL..." + curl -L "$CNI_PLUGIN_URL" | tar -xz -C /opt/cni/bin + ls -la /opt/cni/bin +fi + +# Set default route and iptables command based on IP version +if [[ "$ADDRESS_VERSION" == "IPv6" ]]; then + DEFAULT_ROUTE="::/0" + IPTABLES_CMD="ip6tables" +else + DEFAULT_ROUTE="0.0.0.0/0" + IPTABLES_CMD="iptables" +fi + +echo "Querying Azure IMDS for network interface information..." +IMDS_DATA=$(query_imds "/metadata/instance/network") +echo "Successfully retrieved IMDS data: $IMDS_DATA" + +# Parse IMDS response and extract interface data +if [[ "$ADDRESS_VERSION" == "IPv6" ]]; then + IP_VERSION_KEY="ipv6" +else + IP_VERSION_KEY="ipv4" +fi + +# Extract subnet and address blocks +SUBNET=$(echo "$IMDS_DATA" | jq -r ".interface[0].${IP_VERSION_KEY}.subnet[0] | .address + \"/\" + (.prefix | tostring)") +ADDRESS_BLOCKS=$(echo "$IMDS_DATA" | jq -c ".interface[0].${IP_VERSION_KEY}.ipAddressBlock[]") + +if [[ -z "$SUBNET" || "$SUBNET" == "null/null" ]]; then + echo "Error: Missing subnet information" >&2 + exit 1 +fi + +if [[ -z "$ADDRESS_BLOCKS" ]]; then + echo "Error: Missing address blocks" >&2 + exit 1 +fi + +echo "Found subnet: $SUBNET" + +# Build CNI configuration +CNI_RANGES="[" + +FIRST_BLOCK=true +while IFS= read -r block; do + BLOCK_ADDR=$(echo "$block" | jq -r '.privateIpAddress') + + if [[ -z "$BLOCK_ADDR" || "$BLOCK_ADDR" == "null" ]]; then + echo "Missing address in block, skipping" + continue + fi + + echo "Processing address block: $BLOCK_ADDR" + + # Derive range + read -r START_IP END_IP <<< "$(derive_range "$BLOCK_ADDR" "$ADDRESS_VERSION")" + + # Add address to interface + echo "Adding address block $BLOCK_ADDR to $INTERFACE_NAME" + ip addr replace "$BLOCK_ADDR" dev "$INTERFACE_NAME" + + # Add iptables MASQUERADE rule + echo "Adding iptables MASQUERADE rule for $BLOCK_ADDR" + if ! $IPTABLES_CMD -t nat -C POSTROUTING -s "$BLOCK_ADDR" ! -d "$SUBNET" -j MASQUERADE 2>/dev/null; then + echo "MASQUERADE rule not found, adding it" + $IPTABLES_CMD -t nat -A POSTROUTING -s "$BLOCK_ADDR" ! -d "$SUBNET" -j MASQUERADE + fi + + # Build subnet entry for CNI config + if [[ "$FIRST_BLOCK" == true ]]; then + FIRST_BLOCK=false + else + CNI_RANGES+="," + fi + + CNI_RANGES+="{\"subnet\":\"$BLOCK_ADDR\",\"rangeStart\":\"$START_IP\",\"rangeEnd\":\"$END_IP\"}" + +done <<< "$ADDRESS_BLOCKS" + +CNI_RANGES+="]" + +# Create CNI configuration +CNI_CONFIG=$(cat < "$CNI_CONFIG_PATH" + +if [[ $? -eq 0 ]]; then + echo "Successfully wrote CNI config to $CNI_CONFIG_PATH" +else + echo "Error writing CNI config to $CNI_CONFIG_PATH" >&2 + exit 1 +fi + +echo "" +echo "Setup completed successfully!" diff --git a/modules/kustomize/stretch/stretch.sh b/modules/kustomize/stretch/stretch.sh new file mode 100644 index 0000000000..caa48f64f0 --- /dev/null +++ b/modules/kustomize/stretch/stretch.sh @@ -0,0 +1,351 @@ +LOCATION="eastus2" +RG=stretch-rg +CLUSTER=cni-stretch +VNET_NAME="aks-vnet" +SUBNET="aks-subnet" +IDENTITY_NAME="${CLUSTER}-identity" +VMSS_VNET="vmss-vnet" +VMSS_SUBNET="vmss-subnet" +VMSS_LOCATION="westus2" +VMSS_RG="${RG}-vmss" + +BOOTSTRAP_SCRIPT_PATH=$1 + +create_cluster() { + az group create -n ${RG} -l $LOCATION --tags "SkipAKSCluster=1" "SkipASB_Audit=true" "SkipLinuxAzSecPack=true" + + az network vnet create --resource-group $RG --location $LOCATION --name $VNET_NAME --address-prefixes 10.0.0.0/8 -o none + az network vnet subnet create --resource-group $RG --vnet-name $VNET_NAME --name $SUBNET --address-prefixes 10.128.0.0/10 -o none + + vnet_id=$(az network vnet show --resource-group $RG --name $VNET_NAME --query id -o tsv) + subnet_id=$(az network vnet subnet show --resource-group $RG --vnet-name $VNET_NAME --name $SUBNET --query id -o tsv) + + # Create user-assigned managed identity + az identity create --name $IDENTITY_NAME --resource-group $RG --location $LOCATION + + # Get the identity resource ID and principal ID + identity_id=$(az identity show --name $IDENTITY_NAME --resource-group $RG --query id -o tsv) + identity_principal_id=$(az identity show --name $IDENTITY_NAME --resource-group $RG --query principalId -o tsv) + + # Assign Network Contributor role to the identity to vnet + az role assignment create --assignee $identity_principal_id --role "Network Contributor" --scope $vnet_id + + az aks create \ + --resource-group "${RG}" \ + --name "${CLUSTER}" \ + --location $LOCATION \ + --kubernetes-version 1.34.0 \ + --tier Standard \ + --node-count 3 \ + --nodepool-name system \ + --vm-set-type VirtualMachineScaleSets \ + --node-vm-size Standard_D8ds_v6 \ + --os-sku Ubuntu2404 \ + --network-plugin none \ + --vnet-subnet-id $subnet_id \ + --enable-managed-identity \ + --assign-identity $identity_id + + # Assign AKS RBAC Cluster Admin role to the identity at the cluster scope + cluster_id=$(az aks show --resource-group $RG --name $CLUSTER --query id -o tsv) + az role assignment create --assignee $identity_principal_id --role "Azure Kubernetes Service RBAC Cluster Admin" \ + --scope $cluster_id +} + +update_aks_vmss() { + node_rg=$(az aks show --resource-group $RG --name $CLUSTER --query nodeResourceGroup -o tsv) + vmss_name=$(az vmss list --resource-group $node_rg --query "[0].name" -o tsv) + + echo "Updating VMSS: $vmss_name in resource group: $node_rg" + subnet_id=$(az network vnet subnet show --resource-group $RG --vnet-name $VNET_NAME --name $SUBNET --query id -o tsv) + vmss_json=$(az vmss show --resource-group $node_rg --name $vmss_name) + vmss_location=$(echo "$vmss_json" | jq -r '.location') + + # Get network interface configuration (az vmss show uses camelCase in properties) + nic_config=$(echo "$vmss_json" | jq '.virtualMachineProfile.networkProfile.networkInterfaceConfigurations[0]') + nic_name=$(echo "$nic_config" | jq -r '.name') + nic_primary=$(echo "$nic_config" | jq -r '.primary') + nic_accel=$(echo "$nic_config" | jq -r '.enableAcceleratedNetworking') + nic_ipfwd=$(echo "$nic_config" | jq -r '.enableIpForwarding') + + # Get existing IP configurations (these are at the root level, not nested under .properties in az vmss show) + existing_ipconfigs=$(echo "$nic_config" | jq -c '.ipConfigurations') + + echo "Found NIC: $nic_name with $(echo "$existing_ipconfigs" | jq 'length') IP configuration(s)" + + # Transform existing IP configs to ARM format (az vmss show has flat structure, ARM needs .properties) + transformed_ipconfigs=$(echo "$existing_ipconfigs" | jq '[.[] | { + name: .name, + properties: { + loadBalancerBackendAddressPools: .loadBalancerBackendAddressPools, + primary: .primary, + subnet: { + id: .subnet.id + }, + privateIPAddressVersion: .privateIpAddressVersion + } + }]') + + ipvlan_config=$(echo "$existing_ipconfigs" | jq -c '.[] | select(.name == "ipvlan")') + if [[ -n "$ipvlan_config" ]]; then + echo "IPvlan IP configuration already exists in VMSS" + # Append privateIPAddressPrefixLength to the existing ipvlan config + updated_ipvlan_config=$(echo "$ipvlan_config" | jq '{ + name: .name, + properties: { + primary: .primary, + subnet: { + id: .subnet.id + }, + privateIPAddressVersion: .privateIpAddressVersion, + privateIPAddressPrefixLength: "28" + } + }') + + # Replace the existing ipvlan config with the updated one + updated_ipconfigs=$(echo "$transformed_ipconfigs" | jq --argjson updated "$updated_ipvlan_config" ' + map(if .name == "ipvlan" then $updated else . end) + ') + # echo "Updated config: $updated_ipconfigs" + else + echo "IPvlan IP configuration not found. Proceeding to add it." + # Create new secondary IP config + new_ipconfig=$(jq -n \ + --arg subnet_id "$subnet_id" \ + '{ + name: "ipvlan", + properties: { + primary: false, + subnet: { + id: $subnet_id + }, + privateIPAddressVersion: "IPv4", + privateIPAddressPrefixLength: "28" + } + }') + + # Merge new IP config with existing ones + updated_ipconfigs=$(echo "$transformed_ipconfigs" | jq --argjson new "$new_ipconfig" '. + [$new]') + fi + + # Extension profile + extension_profile=$(echo "$vmss_json" | jq '.virtualMachineProfile.extensionProfile') + # Remove existing CustomScript extension and transform others + transformed_extension_profile=$(echo "$extension_profile" | jq '{ + extensions: [.extensions[] | select(.typePropertiesType != "CustomScript") | { + name: .name, + properties: ({ + autoUpgradeMinorVersion: .autoUpgradeMinorVersion, + publisher: .publisher, + type: .typePropertiesType, + typeHandlerVersion: .typeHandlerVersion, + settings: .settings + } + (if .enableAutomaticUpgrade != null then {enableAutomaticUpgrade: .enableAutomaticUpgrade} else {} end) + + (if .suppressFailures != null then {suppressFailures: .suppressFailures} else {} end) + + (if .protectedSettings != null then {protectedSettings: .protectedSettings} else {} end)) + }] + }') + # Add the new CustomScript extension (replaces the old one) + updated_extension_profile=$(echo "$transformed_extension_profile" | jq '.extensions += [{ + name: "ipvlan-setup", + properties: { + autoUpgradeMinorVersion: false, + publisher: "Microsoft.Azure.Extensions", + type: "CustomScript", + typeHandlerVersion: "2.1", + settings: { + fileUris: ["https://raw.githubusercontent.com/Azure/telescope/refs/heads/setup-cni/modules/kustomize/stretch/setup_ipvlan.sh"], + commandToExecute: "bash setup_ipvlan.sh" + } + } + }]') + + echo "Creating ARM template with $(echo "$updated_ipconfigs" | jq 'length') IP configuration(s)..." + echo "Updated Extension Profile: $(echo "$updated_extension_profile" | jq '.extensions | length') extensions" + + # Create clean ARM template with only the network profile update + jq -n \ + --arg vmss_name "$vmss_name" \ + --arg location "$vmss_location" \ + --arg nic_name "$nic_name" \ + --argjson nic_primary "$nic_primary" \ + --argjson nic_accel "$nic_accel" \ + --argjson nic_ipfwd "$nic_ipfwd" \ + --argjson ipconfigs "$updated_ipconfigs" \ + --argjson updated_extension_profile "$updated_extension_profile" \ + '{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/virtualMachineScaleSets", + "apiVersion": "2025-04-01", + "name": $vmss_name, + "location": $location, + "properties": { + "virtualMachineProfile": { + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": $nic_name, + "properties": { + "primary": $nic_primary, + "enableAcceleratedNetworking": $nic_accel, + "enableIPForwarding": $nic_ipfwd, + "ipConfigurations": $ipconfigs + } + } + ] + }, + "extensionProfile": $updated_extension_profile + } + } + } + ] + }' > vmss-secondary-ip-template-generated.json + + echo "Deploying ARM template with updated IP configuration..." + + # Deploy the generated ARM template + az deployment group create \ + --resource-group $node_rg \ + --name "update-vmss-secondary-ip-$(date +%s)" \ + --template-file vmss-secondary-ip-template-generated.json \ + --mode Incremental + + deployment_status=$? + + if [ $deployment_status -eq 0 ]; then + echo "Successfully updated VMSS with secondary IP configuration" + + # Update existing instances to apply the new configuration + echo "Updating VMSS instances..." + az vmss update-instances \ + --resource-group $node_rg \ + --name $vmss_name \ + --instance-ids "*" + + echo "VMSS update complete. Secondary IP configuration 'ipvlan' added with prefix length 28." + else + echo "Failed to update VMSS" + cat vmss-secondary-ip-template-generated.json + return 1 + fi + + # Clean up temporary files + rm -f vmss-secondary-ip-template-generated.json +} + +create_vm() { + NODE_NAME="vm-node" + NODE_USER="ubuntu" + NODE_VM_SIZE="Standard_D8ds_v6" + identity_id=$(az identity show --name $IDENTITY_NAME --resource-group $RG --query id -o tsv) + az vm create \ + --resource-group $VMSS_RG \ + --name $NODE_NAME \ + --image Ubuntu2404 \ + --admin-username $NODE_USER \ + --generate-ssh-keys \ + --assign-identity $identity_id \ + --public-ip-sku Standard \ + --vnet-name $VMSS_VNET \ + --subnet $VMSS_SUBNET \ + --size $NODE_VM_SIZE \ + --nsg-rule SSH +} + +create_vmss() { + bootstrap_script_path=$1 + LB_NAME="vmss-lb" + BACKEND_POOL="lb-backend-pool" + FRONTEND_IP="lb-frontend" + az group create -n $VMSS_RG -l $VMSS_LOCATION --tags "SkipAKSCluster=1" "SkipASB_Audit=true" "SkipLinuxAzSecPack=true" + az network vnet create --resource-group $VMSS_RG --location $VMSS_LOCATION --name $VMSS_VNET --address-prefixes 172.16.0.0/12 -o none + az network vnet subnet create --resource-group $VMSS_RG --vnet-name $VMSS_VNET --name $VMSS_SUBNET --address-prefixes 172.16.0.0/16 -o none + + az network lb create \ + --resource-group $VMSS_RG \ + --name $LB_NAME \ + --sku Standard \ + --frontend-ip-name $FRONTEND_IP \ + --backend-pool-name $BACKEND_POOL + lb_backend_pool_id=$(az network lb address-pool show \ + --resource-group $VMSS_RG \ + --lb-name $LB_NAME \ + --name $BACKEND_POOL \ + --query id -o tsv) + az network lb outbound-rule create \ + --resource-group $VMSS_RG \ + --lb-name $LB_NAME \ + --name lb-outbound-rule \ + --address-pool $BACKEND_POOL \ + --protocol All \ + --idle-timeout 30 \ + --enable-tcp-reset true \ + --frontend-ip-configs $FRONTEND_IP \ + --outbound-ports 0 + + identity_id=$(az identity show --name $IDENTITY_NAME --resource-group $RG --query id -o tsv) + + # First create a VMSS with bootstrap script using CustomScript extension + az deployment group create \ + --resource-group $VMSS_RG \ + --template-file vmss-dual-ipconfig.json \ + --parameters vmssName="test-vmss" \ + --parameters sshPublicKey="$(cat ~/.ssh/id_rsa.pub)" \ + --parameters vnetName=$VMSS_VNET \ + --parameters subnetName=$VMSS_SUBNET \ + --parameters loadBalancerBackendPoolId="$lb_backend_pool_id" \ + --parameters managedIdentityId="$identity_id" \ + --parameters bootstrapScript="$(base64 -w 0 $bootstrap_script_path)" + + echo "VMSS created in resource group: $VMSS_RG" + # Next, update VMSS to uset ipvlan setup script + az deployment group create \ + --resource-group $VMSS_RG \ + --template-file vmss-dual-ipconfig.json \ + --parameters vmssName="test-vmss" \ + --parameters sshPublicKey="$(cat ~/.ssh/id_rsa.pub)" \ + --parameters vnetName=$VMSS_VNET \ + --parameters subnetName=$VMSS_SUBNET \ + --parameters loadBalancerBackendPoolId="$lb_backend_pool_id" \ + --parameters managedIdentityId="$identity_id" \ + --parameters bootstrapScript="$(base64 -w 0 setup_ipvlan.sh)" + + echo "Run ipvlan setup script" + az vmss update-instances \ + --resource-group $node_rg \ + --name $vmss_name \ + --instance-ids "*" +} + +setup_vnet_peering() { + # Get VNet IDs + aks_vnet_id=$(az network vnet show --resource-group $RG --name $VNET_NAME --query id -o tsv) + vmss_vnet_id=$(az network vnet show --resource-group $VMSS_RG --name $VMSS_VNET --query id -o tsv) + + # Create peering from AKS VNet to VMSS VNet + az network vnet peering create \ + --name aks-to-vmss-peering \ + --resource-group $RG \ + --vnet-name $VNET_NAME \ + --remote-vnet $vmss_vnet_id \ + --allow-vnet-access \ + --allow-forwarded-traffic + + # Create peering from VMSS VNet to AKS VNet + az network vnet peering create \ + --name vmss-to-aks-peering \ + --resource-group $VMSS_RG \ + --vnet-name $VMSS_VNET \ + --remote-vnet $aks_vnet_id \ + --allow-vnet-access \ + --allow-forwarded-traffic +} + +create_cluster +# update_aks_vmss +# create_vm +# create_vmss $BOOTSTRAP_SCRIPT_PATH +# setup_vnet_peering \ No newline at end of file diff --git a/modules/kustomize/stretch/vmss-dual-ipconfig.json b/modules/kustomize/stretch/vmss-dual-ipconfig.json new file mode 100644 index 0000000000..8cee1506f2 --- /dev/null +++ b/modules/kustomize/stretch/vmss-dual-ipconfig.json @@ -0,0 +1,208 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "vmssName": { + "type": "string", + "metadata": { + "description": "Name of the VMSS" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources" + } + }, + "adminUsername": { + "type": "string", + "defaultValue": "azureuser", + "metadata": { + "description": "Admin username" + } + }, + "sshPublicKey": { + "type": "string", + "metadata": { + "description": "SSH public key for authentication" + } + }, + "instanceCount": { + "type": "int", + "defaultValue": 3, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Number of VM instances" + } + }, + "vmSku": { + "type": "string", + "defaultValue": "Standard_D8ds_v6", + "metadata": { + "description": "Size of VMs in the VMSS" + } + }, + "vnetName": { + "type": "string", + "metadata": { + "description": "Name of the virtual network" + } + }, + "subnetName": { + "type": "string", + "metadata": { + "description": "Name of the subnet" + } + }, + "secondaryAddressPrefixLength": { + "type": "int", + "defaultValue": 28, + "metadata": { + "description": "Address prefix length for secondary IP configuration" + } + }, + "loadBalancerBackendPoolId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of the load balancer backend pool (optional)" + } + }, + "managedIdentityId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of the user-assigned managed identity to associate with the VMSS (optional)" + } + }, + "bootstrapScript": { + "type": "string", + "metadata": { + "description": "A Base64-encoded bootstrap script to run on VMSS instances" + } + } + }, + "variables": { + "nicName": "[concat(parameters('vmssName'), 'Nic')]", + "primaryIpConfigName": "ipconfig1", + "secondaryIpConfigName": "ipvlan", + "subnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('subnetName'))]", + "imageReference": { + "publisher": "Canonical", + "offer": "ubuntu-24_04-lts", + "sku": "server", + "version": "latest" + } + }, + "resources": [ + { + "type": "Microsoft.Compute/virtualMachineScaleSets", + "apiVersion": "2025-04-01", + "name": "[parameters('vmssName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('vmSku')]", + "tier": "Standard", + "capacity": "[parameters('instanceCount')]" + }, + "identity": "[if(empty(parameters('managedIdentityId')), json('null'), json(concat('{\"type\": \"UserAssigned\", \"userAssignedIdentities\": {\"', parameters('managedIdentityId'), '\": {}}}')))]", + "properties": { + "overprovision": false, + "upgradePolicy": { + "mode": "Manual" + }, + "virtualMachineProfile": { + "storageProfile": { + "imageReference": "[variables('imageReference')]", + "osDisk": { + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Premium_LRS" + } + } + }, + "osProfile": { + "computerNamePrefix": "[parameters('vmssName')]", + "adminUsername": "[parameters('adminUsername')]", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "[concat('/home/', parameters('adminUsername'), '/.ssh/authorized_keys')]", + "keyData": "[parameters('sshPublicKey')]" + } + ] + } + } + }, + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "[variables('nicName')]", + "properties": { + "primary": true, + "enableAcceleratedNetworking": true, + "enableIPForwarding": true, + "ipConfigurations": [ + { + "name": "[variables('primaryIpConfigName')]", + "properties": { + "primary": true, + "subnet": { + "id": "[variables('subnetId')]" + }, + "privateIPAddressVersion": "IPv4", + "loadBalancerBackendAddressPools": "[if(empty(parameters('loadBalancerBackendPoolId')), json('null'), json(concat('[{\"id\": \"', parameters('loadBalancerBackendPoolId'), '\"}]')))]" + } + }, + { + "name": "[variables('secondaryIpConfigName')]", + "properties": { + "primary": false, + "subnet": { + "id": "[variables('subnetId')]" + }, + "privateIPAddressVersion": "IPv4", + "privateIPAddressPrefixLength": "[parameters('secondaryAddressPrefixLength')]" + } + } + ] + } + } + ] + }, + "extensionProfile": { + "extensions": [ + { + "name": "bootstrapExtension", + "properties": { + "publisher": "Microsoft.Azure.Extensions", + "type": "CustomScript", + "typeHandlerVersion": "2.1", + "autoUpgradeMinorVersion": true, + "settings": { + "script": "[parameters('bootstrapScript')]" + } + } + } + ] + } + } + } + } + ], + "outputs": { + "vmssId": { + "type": "string", + "value": "[resourceId('Microsoft.Compute/virtualMachineScaleSets', parameters('vmssName'))]" + }, + "vmssName": { + "type": "string", + "value": "[parameters('vmssName')]" + } + } +}