ARM series: creando en Azure VM un nuevo bosque de Active Directory con dos controladores de dominio

21 min de lectura
ARM series: creando en Azure VM un nuevo bosque de Active Directory con dos controladores de dominio

¡Feliz Año Nuevo!

Espero que todos hayáis disfrutado de las vacaciones y de seguramente un más que merecido descanso. El mes pasado os estuve hablando de la utilísima JsonADDomainExtension, que nos permite unir una máquina virtual de Azure a dominio de ADDS desde el propio ARM y en momento de implementación. Hoy os voy a hablar de algo igualmente útil: generar desde ARM un bosque de Active Directory con dos controladores de dominio y su correspondiente set de disponibilidad.

En esta ocasión no tenemos extensión de Azure -al menos oficial de Microsoft- que nos haga en trabajo, pero lo podemos automatizar igualmente gracias a PowerShell DSC.

Para los administradores de Active Directory esta tarea suena de lo más trivial. Servidor lo primero que pensó a la hora de enfrentarse a esto fue: ok Carlos, creamos en Azure dos máquinas virtuales y mediante script o DSC creamos el bosque de AD, promocionando al acabar la segunda máquina para que sea controlador de dominio. No es que fuera mal encaminado, pero la situación tenía más enjundia de la que inicialmente parecía debido a los siguientes temas:

  • DHCP. Como bien sabéis, en Azure la configuración IP de las máquinas virtuales se debe hacer siempre por DHCP que el propio Azure nos facilita. Sólo hay dos opciones que podamos configurar en este DHCP: asignaciones estáticas y DNS.
  • DNS. La primera máquina con la que vamos a crear el bosque de Active Directory no necesita ningunas DNS en especial, pero tras este proceso se convertirá el servidor DNS, apuntando a sí misma para la resolución. La segunda máquina deberá modificar la configuración de su interfaz para que apunte a la primera o no podrá promocionar adecuadamente.

Dada la situación, parece que podemos llevar todo a cabo, pero necesitamos pensarlo bien y planearlo acorde. Así pues hagamos la lista de la compra para cocinar nuestro proyecto ARM, necesitamos:

  • Plantillas JSON ARM. Como siempre, son la base de nuestro trabajo en Azure.
  • Linked templates. De la reflexión rápida de las tareas necesarias, vemos claramente que no sólo tendremos que crear una red virtual, sino que tendremos que actualizarla. Para este propósito nos vamos a servir de las plantillas enlazadas. Podría dedicar un artículo de este blog a explicar en qué consisten, pero ya hay una documentación estupenda aquí.
  • PowerShell DSC. Concretamente el módulo xActiveDirectory, que nos proporcionará la funcionalidad que necesitamos para promocionar las máquinas a dominio.
  • Un sitio web accesible públicamente o mediante token para que Azure pueda retirar las plantillas JSON enlazadas a nuestro deployment principal. Servidor lo hace desde una Storage Account y acceso SAS.

Antes de entrar en materia...

Debemos repasar las operaciones que necesitamos llevar a cabo, tal como las haríamos sin tener en cuenta Azure ni automatizaciones. Para efectos de nomenclatura llamaré a las máquinas DC1, que creará el bosque y alojará los roles FSMO; y DC2.

  1. Creamos una red virtual. Las direcciones se asignan por DHCP.
  2. Creamos la máquina virtual DC1. Le asignamos DHCP estático.
  3. Promocionamos DC1 a controlador de dominio y creamos el nuevo bosque.
  4. Actualizamos configuración del DHCP para que asigne como DNS la IP DHCP estática de DC1.
  5. Creamos la máquina virtual DC2. Le asignamos DHCP estático.
  6. Promocionamos DC2 a controlador de dominio.
  7. Actualizamos la configuración del DHCP para que se asigne como DNS la IP estática de DC1 y de DC2.

¿Sencillo? ¡Vamos a ver cómo lo hacemos!

Implementando el proceso en Azure Resource Manager

Tenemos la tarea de aplicar los 7 pasos comentados en ARM. No es un proceso complicado pero definitivamente necesitamos ser disciplinados en ello. Las tareas de alto nivel, pero ya en lenguaje Azure serían:

  1. Generamos al menos 3 archivos JSON: uno el principal, otro para la definición de la VNET y otro para actualizar los adaptadores de red (NIC) de las máquinas virtuales. Yo los he llamado azuredeploy.json, vnet.json y update-nic.json.
  2. vnet.json y update-nic.json deben ser cargados a una ubicación accesible mediante HTTP(S).
  3. azuredeploy.json crea todos los recursos de Azure necesarios:
  • Storage Account.
  • Virtual Network.
  • NICs de DC1 y DC2.
  • Máquina virtual de DC1 y DC2.
  1. Mediante xActiveDirectory DC1 promociona a controlador de dominio.
  2. Llamamos de vuelta a vnet.json para actualizar la configuración de red con el nuevo DNS. Como no podemos permitirnos el lujo de esperar a que la concesión anterior expire, llamamos también a update-nic.json para que fuerce el cambio en la interfaz de red de DC2. Esto último sería posible evitarlo no iniciando la creación de DC2 hasta que no hayamos actualizado la configuración de la red; pero el despliegue sería mucho más lento, recuerda que una de las grandes bazas de ARM es lo rápido que es implementando infraestructuras complejas... si lo programamos bien.
  3. Una vez más mediante xActiveDirectory promocionamos a DC2 como nuevo controlador de dominio del bosque que hemos creado en el paso 4.
  4. Llamamos por última vez a vnet.json para agregar el nuevo DNS a la red virtual.

¡Al final me han quedado 7 pasos igual que en el apartado anterior! ¿Está claro? Para visualizarlo de forma más gráfica os presento el siguiente esquema:

Plantillas JSON ARM

Empecemos por el principio, implementemos las plantillas JSON que definen lo que vamos a querer establecer en Azure. Lo que vamos a hacer a continuación está basado en la plantilla de Simon Davies que se puede encontrar en el repositorio de Github de las Azure Quickstart Templates.

Veamos el aspecto de vnet.json:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "apiVersion": {
      "type": "string"
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]"
    },
    "nsgRdpName": {
      "type": "string",
      "defaultValue": "[concat(resourceGroup().Name,'-nsg-rdp')]"
    },
    "vNetName": {
      "type": "string"
    },
    "vnetAddressPrefix": {
      "type": "string"
    },
    "subnets": {
      "type": "array"
    },
    "dnsServers": {
      "type": "array"
    }
  },
  "variables": {
  },
  "resources": [
    {
      "apiVersion": "[parameters('apiVersion')]",
      "type": "Microsoft.Network/virtualNetworks",
      "name": "[parameters('vNetName')]",
      "location": "[parameters('location')]",
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "[parameters('vnetAddressPrefix')]"
          ]
        },
        "dhcpOptions": {
          "dnsServers": "[parameters('dnsServers')]"
        },
        "subnets": "[parameters('subnets')]"
      }
    },
    {
      "apiVersion": "[parameters('apiVersion')]",
      "type": "Microsoft.Network/networkSecurityGroups",
      "name": "[parameters('nsgRdpName')]",
      "location": "[parameters('location')]",
      "properties": {
        "securityRules": [
          {
            "name": "RDP",
            "properties": {
              "description": "Allow RDP",
              "protocol": "Tcp",
              "sourcePortRange": "*",
              "destinationPortRange": "3389",
              "sourceAddressPrefix": "VirtualNetwork",
              "destinationAddressPrefix": "*",
              "access": "Allow",
              "priority": 100,
              "direction": "Inbound"
            }
          }
        ]
      }
    }
  ]
}

No hay mucho especial que comentar con esta plantilla. Los parámetros declarados serán recibidos a su vez de la plantilla principal que llamará a esta. La clave está en el dnsOptions (línea 43), que es la opción a actualizar tras cada una de las implementaciones.

Veamos ahora update-nic.json:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "nicName": {
      "type": "string",
      "metadata": {
        "Description": "The name of the NIC to Create or Update"
      }
    },
    "dnsServers": {
      "type": "array",
      "metadata": {
        "Description": "The DNS Servers of the NIC"
      }
    },
    "apiVersion": {
      "type": "string",
      "defaultValue": "2015-06-15",
      "metadata": {
        "Description": "The API version used for deployment"
      }
    },
    "ipConfigurations": {
      "type": "array",
      "metadata": {
        "Description": "The adapter IP configuration"
      }
    },
    "nsgName": {
      "type": "string",
      "metadata": {
        "Description": "The network security group name"
      }
    }
  },
  "resources": [
    {
      "apiVersion": "[parameters('apiVersion')]",
      "type": "Microsoft.Network/networkInterfaces",
      "name": "[parameters('nicName')]",
      "location": "[resourceGroup().location]",
      "properties": {
        "ipConfigurations": "[parameters('ipConfigurations')]",
        "networkSecurityGroup": {
          "id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('nsgName'))]"
        },
        "dnsSettings": {
          "dnsServers": "[parameters('dnsServers')]"
        }
      }
    }
  ]
}

Fijáos que aquí la declaración del networkInterface es mínima, ya que sólo contiene los elementos que vamos a actualizar, que vienen a ser igualmente, los servidores DNS asignados a dicho adaptador.

Vamos a por el archivo principal, azuredeploy.json:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "adminUsername": {
      "type": "string",
      "metadata": {
        "description": "Local administrator username for created virtual machines."
      }
    },
    "adminPassword": {
      "type": "securestring",
      "metadata": {
        "description": "Local administrator password for created virtual machines."
      }
    },
    "mainStorageAccountName": {
      "type": "string",
      "metadata": {
        "description": "Main storage account for common environment."
      }
    },
    "diagStorageAccountName": {
      "type": "string",
      "metadata": {
        "description": "Storage account used for diagnostics."
      }
    },
    "addsRootDomainName": {
      "type": "string",
      "defaultValue": "contoso.com",
      "metadata": {
        "description": "Root domain name for the new ADDS forest. Ex: contoso.com"
      }
    },
    "vmSize-DC": {
      "type": "string",
      "defaultValue": "Standard_F1",
      "allowedValues": [
        "Standard_A0",
        "Standard_A1",
        "Standard_A2",
        "Standard_A3",
        "Standard_A4",
        "Standard_A5",
        "Standard_A6",
        "Standard_A7",
        "Standard_A8",
        "Standard_A9",
        "Standard_A10",
        "Standard_A11",
        "Standard_D1_v2",
        "Standard_D2_v2",
        "Standard_D3_v2",
        "Standard_D4_v2",
        "Standard_D5_v2",
        "Standard_D11_v2",
        "Standard_D12_v2",
        "Standard_D13_v2",
        "Standard_D14_v2",
        "Standard_D15_v2",
        "Standard_F1",
        "Standard_F2",
        "Standard_F4",
        "Standard_F8",
        "Standard_F16"
      ],
      "metadata": {
        "description": "Domain controllers Virtual Machine Size"
      }
    }
  },
  "variables": {
    "apiVersion": "2015-06-15",
    "apiVersionTemplate": "2015-01-01",
    "_artifactsLocation": "https://<pon tu cuenta aquí>.file.core.windows.net/assets",
    "_artifactsLocationSasToken": "<Pon aquí tu token SAS>",
    "_addsPdcDscFileName": "CreateADPDC.ps1.zip",
    "_addsBdcDscFileName": "CreateADBDC.ps1.zip",
    "adPdcConfigurationFunction": "CreateADPDC.ps1\\CreateADPDC",
    "adBdcConfigurationFunction": "CreateADBDC.ps1\\CreateADBDC",
    "nicTemplateUri": "[concat(variables('_artifactsLocation'),'/templates/common/update-nic.json',variables('_artifactsLocationSasToken'))]",
    "vnetTemplateUri": "[concat(variables('_artifactsLocation'),'/templates/common/vnet.json',variables('_artifactsLocationSasToken'))]",
    "vNetName": "[concat(resourceGroup().Name,'-vnet')]",
    "vnetaddressPrefix": "192.168.0.0/22",
    "subnetaddressPrefixCommon": "192.168.0.0/24",
    "subnetNameCommon": "[concat(variables('vNetName'),'-common')]",
    "vmName-DC1": "[take(replace(toUpper(concat(resourceGroup().Name,'DC1')),'-',''),15)]",
    "privIPAddress-DC1": "192.168.0.4",
    "vmName-DC2": "[take(replace(toUpper(concat(resourceGroup().Name,'DC2')),'-',''),15)]",
    "privIPAddress-DC2": "192.168.0.5",
    "asName-DC": "[concat(resourceGroup().Name,'-dc','-as')]",
    "standardStorageAccountType": "Standard_LRS",
    "location": "[resourceGroup().location]",
    "windowsImagePublisher": "MicrosoftWindowsServer",
    "windowsImageOffer": "WindowsServer",
    "windowsImageSku": "2012-R2-Datacenter",
    "nsgRdpName": "nsg-rdp",
    "vmStorageAccountContainerName": "vhds",
    "sizeOfDataDiskInGB-DC": "20",
    "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('vNetName'))]",
    "subnetRefGw": "[concat(variables('vnetID'),'/subnets/',variables('subnetNameGw'))]",
    "subnetRefCommon": "[concat(variables('vnetID'),'/subnets/',variables('subnetNameCommon'))]",
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "name": "[parameters('mainStorageAccountName')]",
      "apiVersion": "[variables('apiVersion')]",
      "location": "[variables('location')]",
      "properties": {
        "accountType": "[variables('standardStorageAccountType')]"
      }
    },
    {
      "type": "Microsoft.Storage/storageAccounts",
      "name": "[parameters('diagStorageAccountName')]",
      "apiVersion": "[variables('apiVersion')]",
      "location": "[variables('location')]",
      "properties": {
        "accountType": "[variables('standardStorageAccountType')]"
      }
    },
    {
      "type": "Microsoft.Compute/availabilitySets",
      "name": "[variables('asName-DC')]",
      "apiVersion": "[variables('apiVersion')]",
      "location": "[variables('location')]",
      "properties": {
        "platformFaultDomainCount": "3",
        "platformUpdateDomainCount": "5"
      }
    },
    {
      "name": "VirtualNetwork",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "[variables('apiVersionTemplate')]",
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "uri": "[variables('vnetTemplateUri')]",
          "contentVersion": "1.0.0.0"
        },
        "parameters": {
          "vNetName": {
            "value": "[variables('vNetName')]"
          },
          "vnetAddressPrefix": {
            "value": "[variables('vnetaddressPrefix')]"
          },
          "subnets": {
            "value": [
              {
                "name": "[variables('subnetNameCommon')]",
                "properties": {
                  "addressPrefix": "[variables('subnetAddressPrefixCommon')]"
                }
              },
            ]
          },
          "dnsServers": {
            "value": []
          },
          "apiVersion": {
            "value": "[variables('apiVersion')]"
          }
        }
      }
    },
    {
      "apiVersion": "[variables('apiVersion')]",
      "type": "Microsoft.Network/networkInterfaces",
      "name": "[concat(variables('vmName-DC1'),'-nic')]",
      "location": "[variables('location')]",
      "dependsOn": [
        "Microsoft.Resources/deployments/VirtualNetwork",
        "[concat('Microsoft.Network/networkSecurityGroups/', variables('nsgRdpName'))]"
      ],
      "properties": {
        "ipConfigurations": [
          {
            "name": "[concat(variables('vmName-DC1'),'-ipconfig')]",
            "properties": {
              "privateIPAllocationMethod": "Static",
              "privateIPAddress": "[variables('privIPAddress-DC1')]",
              "subnet": {
                "id": "[variables('subnetRefCommon')]"
              }
            }
          }
        ],
        "networkSecurityGroup": {
          "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgRdpName'))]"
        }
      }
    },
    {
      "apiVersion": "[variables('apiVersion')]",
      "type": "Microsoft.Network/networkInterfaces",
      "name": "[concat(variables('vmName-DC2'),'-nic')]",
      "location": "[variables('location')]",
      "dependsOn": [
        "Microsoft.Resources/deployments/VirtualNetwork",
        "[concat('Microsoft.Network/networkSecurityGroups/', variables('nsgRdpName'))]"
      ],
      "properties": {
        "ipConfigurations": [
          {
            "name": "[concat(variables('vmName-DC2'),'-ipconfig')]",
            "properties": {
              "privateIPAllocationMethod": "Static",
              "privateIPAddress": "[variables('privIPAddress-DC2')]",
              "subnet": {
                "id": "[variables('subnetRefCommon')]"
              }
            }
          }
        ],
        "networkSecurityGroup": {
          "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgRdpName'))]"
        }
      }
    },
    {
      "apiVersion": "[variables('apiVersion')]",
      "type": "Microsoft.Compute/virtualMachines",
      "name": "[variables('vmName-DC1')]",
      "location": "[variables('location')]",
      "dependsOn": [
        "[concat('Microsoft.Storage/storageAccounts/', parameters('mainStorageAccountName'))]",
        "[concat('Microsoft.Storage/storageAccounts/', parameters('diagStorageAccountName'))]",
        "[concat('Microsoft.Network/networkInterfaces/', concat(variables('vmName-DC1'),'-nic'))]",
        "[concat('Microsoft.Compute/availabilitySets/',variables('asName-DC'))]"
      ],
      "properties": {
        "hardwareProfile": {
          "vmSize": "[parameters('vmSize-DC')]"
        },
        "availabilitySet": {
          "id": "[resourceId('Microsoft.Compute/availabilitySets',variables('asName-DC'))]"
        },
        "osProfile": {
          "computerName": "[variables('vmName-DC1')]",
          "adminUsername": "[parameters('adminUsername')]",
          "adminPassword": "[parameters('adminPassword')]"
        },
        "storageProfile": {
          "imageReference": {
            "publisher": "[variables('windowsImagePublisher')]",
            "offer": "[variables('windowsImageOffer')]",
            "sku": "[variables('windowsImageSku')]",
            "version": "latest"
          },
          "osDisk": {
            "name": "osdisk",
            "vhd": {
              "uri": "[concat('http://',parameters('mainStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('vmName-DC1'),'-osdisk','.vhd')]"
            },
            "caching": "ReadWrite",
            "createOption": "FromImage"
          },
          "dataDisks": [
            {
              "name": "[concat(variables('vmName-DC1'),'-DATA0')]",
              "diskSizeGB": "[variables('sizeOfDataDiskInGB-DC')]",
              "lun": 0,
              "vhd": {
                "uri": "[concat('http://',parameters('mainStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('vmName-DC1'),'-DATA0','.vhd')]"
              },
              "caching": "None",
              "createOption": "Empty"
            }
          ]
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(variables('vmName-DC1'),'-nic'))]"
            }
          ]
        },
        "diagnosticsProfile": {
          "bootDiagnostics": {
            "enabled": "true",
            "storageUri": "[concat('http://',parameters('diagStorageAccountName'),'.blob.core.windows.net')]"
          }
        }
      },
      "resources": [
        {
          "type": "extensions",
          "name": "CreateADForest",
          "apiVersion": "[variables('apiVersion')]",
          "location": "[variables('location')]",
          "dependsOn": [
            "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName-DC1'))]"
          ],
          "properties": {
            "publisher": "Microsoft.Powershell",
            "type": "DSC",
            "typeHandlerVersion": "2.19",
            "autoUpgradeMinorVersion": true,
            "settings": {
              "ModulesUrl": "[concat(variables('_artifactsLocation'), '/', 'dsc', '/', variables('_addsPdcDscFileName'), variables('_artifactsLocationSasToken'))]",
              "ConfigurationFunction": "[variables('adPdcConfigurationFunction')]",
              "Properties": {
                "DomainName": "[parameters('addsRootDomainName')]",
                "AdminCreds": {
                  "UserName": "[parameters('adminUsername')]",
                  "Password": "PrivateSettingsRef:AdminPassword"
                }
              }
            },
            "protectedSettings": {
              "Items": {
                "AdminPassword": "[parameters('adminPassword')]"
              }
            }
          }
        },
        {
          "type": "Microsoft.Compute/virtualMachines/extensions",
          "name": "[concat(variables('vmName-DC1'),'/BGInfo')]",
          "apiVersion": "[variables('apiVersion')]",
          "location": "[resourceGroup().location]",
          "properties",
          {
            "publisher": "Microsoft.Compute",
            "type": "BGInfo",
            "typeHandlerVersion": "2.1",
            "autoUpgradeMinorVersion": "true",
            "settings": {
              "Properties": [
              ]
            }
          }
        }
      ]
    },
    {
      "apiVersion": "[variables('apiVersion')]",
      "type": "Microsoft.Compute/virtualMachines",
      "name": "[variables('vmName-DC2')]",
      "location": "[variables('location')]",
      "dependsOn": [
        "[concat('Microsoft.Storage/storageAccounts/', parameters('mainStorageAccountName'))]",
        "[concat('Microsoft.Storage/storageAccounts/', parameters('diagStorageAccountName'))]",
        "[concat('Microsoft.Network/networkInterfaces/', concat(variables('vmName-DC2'),'-nic'))]",
        "[concat('Microsoft.Compute/availabilitySets/',variables('asName-DC'))]"
      ],
      "properties": {
        "hardwareProfile": {
          "vmSize": "[parameters('vmSize-DC')]"
        },
        "availabilitySet": {
          "id": "[resourceId('Microsoft.Compute/availabilitySets',variables('asName-DC'))]"
        },
        "osProfile": {
          "computerName": "[variables('vmName-DC2')]",
          "adminUsername": "[parameters('adminUsername')]",
          "adminPassword": "[parameters('adminPassword')]"
        },
        "storageProfile": {
          "imageReference": {
            "publisher": "[variables('windowsImagePublisher')]",
            "offer": "[variables('windowsImageOffer')]",
            "sku": "[variables('windowsImageSku')]",
            "version": "latest"
          },
          "osDisk": {
            "name": "osdisk",
            "vhd": {
              "uri": "[concat('http://',parameters('mainStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('vmName-DC2'),'-osdisk','.vhd')]"
            },
            "caching": "ReadWrite",
            "createOption": "FromImage"
          },
          "dataDisks": [
            {
              "name": "[concat(variables('vmName-DC2'),'-DATA0')]",
              "diskSizeGB": "[variables('sizeOfDataDiskInGB-DC')]",
              "lun": 0,
              "vhd": {
                "uri": "[concat('http://',parameters('mainStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('vmName-DC2'),'-DATA0','.vhd')]"
              },
              "caching": "None",
              "createOption": "Empty"
            }
          ]
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(variables('vmName-DC2'),'-nic'))]"
            }
          ]
        },
        "diagnosticsProfile": {
          "bootDiagnostics": {
            "enabled": "true",
            "storageUri": "[concat('http://',parameters('diagStorageAccountName'),'.blob.core.windows.net')]"
          }
        }
      },
      "resources": [
        {
          "type": "Microsoft.Compute/virtualMachines/extensions",
          "name": "[concat(variables('vmName-DC2'),'/BGInfo')]",
          "apiVersion": "[variables('apiVersion')]",
          "location": "[resourceGroup().location]",
          "properties": {
            "publisher": "Microsoft.Compute",
            "type": "BGInfo",
            "typeHandlerVersion": "2.1",
            "autoUpgradeMinorVersion": "true",
            "settings": {
              "Properties": [
              ]
            }
          }
        }
      ]
    },
    {
      "type": "Microsoft.Compute/virtualMachines/extensions",
      "name": "[concat(variables('vmName-DC2'),'/CreateBDC')]",
      "apiVersion": "[variables('apiVersion')]",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Compute/virtualMachines/', variables('vmName-DC1'))]",
        "Microsoft.Resources/deployments/Update-NIC-DC2"
      ],
      "properties": {
        "publisher": "Microsoft.Powershell",
        "type": "DSC",
        "typeHandlerVersion": "2.19",
        "autoUpgradeMinorVersion": true,
        "settings": {
          "ModulesUrl": "[concat(variables('_artifactsLocation'), '/', 'dsc', '/', variables('_addsBdcDscFileName'), variables('_artifactsLocationSasToken'))]",
          "ConfigurationFunction": "[variables('adBDCConfigurationFunction')]",
          "Properties": {
            "DomainName": "[parameters('addsRootDomainName')]",
            "AdminCreds": {
              "UserName": "[parameters('adminUserName')]",
              "Password": "PrivateSettingsRef:AdminPassword"
            }
          }
        },
        "protectedSettings": {
          "Items": {
            "AdminPassword": "[parameters('adminPassword')]"
          }
        }
      }
    },
    {
      "name": "Update-VNet-DNS1",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "[variables('apiVersionTemplate')]",
      "dependsOn": [
        "[concat('Microsoft.Compute/virtualMachines/', variables('vmName-DC1'),'/extensions/CreateADForest')]"
      ],
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "uri": "[variables('vnetTemplateUri')]",
          "contentVersion": "1.0.0.0"
        },
        "parameters": {
          "vNetName": {
            "value": "[variables('vNetName')]"
          },
          "vnetAddressPrefix": {
            "value": "[variables('vnetaddressPrefix')]"
          },
          "subnets": {
            "value": [
               {
                "name": "[variables('subnetNameCommon')]",
                "properties": {
                  "addressPrefix": "[variables('subnetAddressPrefixCommon')]"
                }
              },
            ]
          },
          "dnsServers": {
            "value": [
              "[variables('privIPAddress-DC1')]"
            ]
          },
          "apiVersion": {
            "value": "[variables('apiVersion')]"
          }
        }
      }
    },
    {
      "name": "Update-NIC-DC2",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "[variables('apiVersionTemplate')]",
      "dependsOn": [
        "Microsoft.Resources/deployments/Update-VNet-DNS1"
      ],
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "uri": "[variables('nicTemplateUri')]",
          "contentVersion": "1.0.0.0"
        },
        "parameters": {
          "nicName": {
            "value": "[concat(variables('vmName-DC2'),'-nic')]"
          },
          "nsgName": {
            "value": "[variables('nsgRdpName')]"
          },
          "ipConfigurations": {
            "value": [
              {
                "name": "[concat(variables('vmName-DC2'),'-ipconfig')]",
                "properties": {
                  "privateIPAllocationMethod": "Static",
                  "privateIPAddress": "[variables('privIPAddress-DC2')]",
                  "subnet": {
                    "id": "[variables('subnetRefCommon')]"
                  }
                }
              }
            ]
          },
          "dnsServers": {
            "value": [
              "[variables('privIPAddress-DC1')]"
            ]
          },
          "apiVersion": {
            "value": "[variables('apiVersion')]"
          }
        }
      }
    },
    {
      "name": "Update-VNet-DNS2",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "[variables('apiVersionTemplate')]",
      "dependsOn": [
        "[concat('Microsoft.Compute/virtualMachines/',variables('vmName-DC2'),'/extensions/CreateBDC')]"
      ],
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "uri": "[variables('vnetTemplateUri')]",
          "contentVersion": "1.0.0.0"
        },
        "parameters": {
          "vNetName": {
            "value": "[variables('vNetName')]"
          },
          "vnetAddressPrefix": {
            "value": "[variables('vnetaddressPrefix')]"
          },
          "subnets": {
            "value": [
              {
                "name": "[variables('subnetNameCommon')]",
                "properties": {
                  "addressPrefix": "[variables('subnetAddressPrefixCommon')]"
                }
              },
            ]
          },
          "dnsServers": {
            "value": [
              "[variables('privIPAddress-DC1')]",
              "[variables('privIPAddress-DC2')]"
            ]
          },
          "apiVersion": {
            "value": "[variables('apiVersion')]"
          }
        }
      }
    }
  ]
}

Aquí definitivamente tenemos algunas cosas más que comentar. Lo primero es tener claro un concepto. Por defecto, el modo de operación de ARM es incremental, lo cual quiere decir que si desplegamos un elemento ya existente, Azure intentará actualizar el recurso con la nueva configuración que le especifiquemos.

Examinemos el código con detenimiento:

  • Líneas 1-72. Declaración de parámetros del entorno. Especial atención al nombre del bosque de Active Directory que vamos a crear.
  • Líneas 76-77. Storage Account y su correspondiente contenedor donde almacenamos las otras dos plantillas JSON a las que vamos a llamar. En la línea 77 ponemos el token SAS para evitar que estas plantillas estén públicamente expuestas. No es obligatorio hospedarlas en una Storage Account, no serviría cualquier otra ubicación HTTP públicamente accesible, como por ejemplo, un repositorio de Github.
  • Líneas 78-81. Archivos con la configuración DSC y las funciones a las que vamos a llamar una vez se ejecuten. Tenemos uno para el controlador de dominio primario y otro para el secundario, ya que no es lo mismo crear un nuevo bosque que agregar un controlador de dominio a otro existente.
  • Líneas 82-83. La localización completa de los archivos JSON. Como podéis ver, es una concatenación del valor de las líneas 76 y 77.
  • Líneas 84-104. Resto de declaración de variables para otras partes de la implementación que no comentaré en esta publicación.
  • Líneas 124-132. Declaración del set de disponibilidad que va a contener nuestras máquinas virtuales.
  • Líneas 134-169. Aquí encontramos una llamada a otra plantilla, en este caso a vnet.json. Básicamente se guía por tres atributos principales:
    • modo, que nos interesa que sea Incremental.
    • templateLink, donde deberemos decirle dónde obtener la plantilla a ejecutar.
    • parameters, que les la lista de parámetros que plantilla espera. Deben coincidir en nombre y tipo. En este sentido, estamos ante la misma situación que el paso de parámetros que haríamos a una función o procedimiento en el mundo del desarrollo. Prestad atención que entre los parámetros tenemos dnsServers que en esta llamada está vacío, de forma totalmente intencionada.
  • Líneas 289-320 y 424-455. Aquí llamamos a la extensión DSC para realizar la configuración de los controladores de dominio. Entraremos en detalle en el siguiente apartado.
  • Líneas 457-496. Aquí podemos ver que bajo Update-VNet-DNS1 llamamos otra vez a vnet.json. Se corresponde con la llamada que vamos a hacer tras terminar la creación del bosque en DC1 (fijáos en el dependsOn). Tras terminar la operación vamos a actualizar la configuración de la red virtual para incluir el DNS de DC1.
  • Líneas 497-541. Llamamos a update-nic.json para actualizar los DNS en la interfaz de red de DC2 o de lo contrario vamos a tener que esperar a que caduce su concesión de DHCP para poder hacer su promoción a controlador de dominio. Especial atención al dependsOn donde vemos que no se hace este cambio hasta que hayamos cambiado la configuración de la red virtual, que a su vez no se producirá hasta que DC1 esté promocionado.
  • Líneas 542-583. El último paso de todos, actualizar la configuración de la red virtual poniendo como DNS las dos IPs de ambos controladores de dominio. Una vez más, el dependsOn especifica que no se debe proceder a esta operación hasta que DC2 no haya promocionado a controlador de dominio.

PowerShell DSC: xActiveDirectory, xDisk, xNetworking, xPendingReboot, cDisk

Ninguna de nuestras pretensiones sería posible si no tuviéramos forma de automatizar la promoción al controlador de dominio. Hay dos formas de hacerlo: mediante un script de PowerShell que realice todas las operaciones al aprovisionar la máquina virtual o bien mediante PowerShell DSC con el correspondiente módulo de gestión de Active Directory. Para este caso recomiendo encarecidamente la última. ¿Por qué?

  • El módulo es oficial y desarrollado por Microsoft.
  • Se encuentra en un estado muy maduro, aunque hace tres meses lo probé con Windows Server 2016 sin éxito, probablemente es un problema que ya esté solucionado.
  • Dispone de funcionalidades muy interesantes a las que les he sacado partido en algunos proyectos y que comentaré en otras publicaciones.

Como he comentado en alguna ocasión, PowerShell DSC se basa en un modelo de configuración declarativa en la cual, de forma parecida a como ocurre en ARM, nosotros sólo nos tenemos que preocupar de especificar lo que queremos y las dependencias entre configuraciones y el proveedor DSC hace el resto. Para el caso que nos atañe me voy a centrar en DC1, siendo el de DC2 muy análogo.

¿Pero vamos sólo a promocionar la máquina a controlador de dominio y crear el bosque? No, en realidad tenemos algunas cosas más que hacer:

  • Particionar y formatear el disco de datos.
  • Configurar la interfaz de red.
  • Reiniciar la máquina cuando sea necesario.

Es por eso que vamos a necesitar todos esos módulos: xActiveDirectory, xDisk, xNetworking, xPendingReboot y cDisk. El aspecto de nuestro archivo CreateADPDC.zip es el siguiente:

Como veis tenemos una carpeta con cada uno de los módulos, y un archivo PowerShell que es el que llamaremos desde el JSON. Este archivo contiene la declaración de la configuración. ¡Echémosle un vistazo!

configuration CreateADPDC 
{ 
   param 
   ( 
        [Parameter(Mandatory)]
        [String]$DomainName,

        [Parameter(Mandatory)]
        [System.Management.Automation.PSCredential]$Admincreds,

        [Int]$RetryCount=20,
        [Int]$RetryIntervalSec=30
    ) 
    
    Import-DscResource -ModuleName xActiveDirectory, xDisk, xNetworking, xPendingReboot, cDisk
    [System.Management.Automation.PSCredential ]$DomainCreds = New-Object System.Management.Automation.PSCredential ("${DomainName}\$($Admincreds.UserName)", $Admincreds.Password)
    $firstActiveAdapter = Get-NetAdapter -InterfaceDescription "Microsoft Hyper-V Network Adapter*" | Sort-Object -Property ifIndex | Select-Object -First 1

    Node localhost
    {
        LocalConfigurationManager            
        {            
            ActionAfterReboot = 'ContinueConfiguration'            
            ConfigurationMode = 'ApplyOnly'            
            RebootNodeIfNeeded = $true            
        } 

        WindowsFeature DNS 
        { 
            Ensure = "Present" 
            Name = "DNS"
        }
        
        WindowsFeature ADDSTools            
        {             
            Ensure = "Present"             
            Name = "RSAT-ADDS"             
        }
        
        WindowsFeature TelnetClient {
            Ensure = 'Present'
            Name = 'Telnet-Client'
        }
        
        xDnsServerAddress DnsServerAddress 
        { 
            Address        = '127.0.0.1' 
            InterfaceAlias = $firstActiveAdapter.InterfaceAlias
            AddressFamily  = 'IPv4'
            DependsOn = "[WindowsFeature]DNS"
        }

        xWaitforDisk Disk2
        {
             DiskNumber = 2
             RetryIntervalSec =$RetryIntervalSec
             RetryCount = $RetryCount
        }

        cDiskNoRestart ADDataDisk
        {
            DiskNumber = 2
            DriveLetter = "F"
        }

        WindowsFeature ADDSInstall 
        { 
            Ensure = "Present" 
            Name = "AD-Domain-Services"
        }  

        xADDomain FirstDS 
        {
            DomainName = $DomainName
            DomainAdministratorCredential = $DomainCreds
            SafemodeAdministratorPassword = $DomainCreds
            DatabasePath = "F:\NTDS"
            LogPath = "F:\NTDS"
            SysvolPath = "F:\SYSVOL"
            DependsOn = "[WindowsFeature]ADDSInstall","[xDnsServerAddress]DnsServerAddress","[cDiskNoRestart]ADDataDisk"
        }

        xWaitForADDomain DscForestWait
        {
            DomainName = $DomainName
            DomainUserCredential = $DomainCreds
            RetryCount = $RetryCount
            RetryIntervalSec = $RetryIntervalSec
            DependsOn = "[xADDomain]FirstDS"
        }
        
        xADRecycleBin RecycleBin
        {
           EnterpriseAdministratorCredential = $DomainCreds
           ForestFQDN = $DomainName
           DependsOn = "[xWaitForADDomain]DscForestWait"
        }

        xPendingReboot Reboot1
        { 
            Name = "RebootServer"
            DependsOn = "[xWaitForADDomain]DscForestWait"
        }
   }
} 

El archivo en sí es muy autoexplicativo. Como véis, vamos configurando elementos de la máquina y estableciendo dependencias entre ellos. Por ejemplo, no ejecutamos FirstDS sin que antes hayamos formateado disco de datos, agregada la características de Active Directory Domain Services en Windows Server y configurado la DNS del adaptador de red.

Hay un par de detalles muy importantes en este DSC que a priori pasan desapercibido, pero serán motivo de otra publicación.

Este DSC recibe un par de parámetros, que son el nombre del dominio y las credenciales que vamos a utilizar. ¿De dónde recibe estos parámetros? La extensión DSC de Azure Resource Manager se los hace llegar desde la plantilla JSON. Echemos un vistazo a ese código concreto.

   {
      "type": "Microsoft.Compute/virtualMachines/extensions",
      "name": "[concat(variables('vmName-DC2'),'/CreateBDC')]",
      "apiVersion": "[variables('apiVersion')]",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Compute/virtualMachines/', variables('vmName-DC1'))]",
        "Microsoft.Resources/deployments/Update-NIC-DC2"
      ],
      "properties": {
        "publisher": "Microsoft.Powershell",
        "type": "DSC",
        "typeHandlerVersion": "2.19",
        "autoUpgradeMinorVersion": true,
        "settings": {
          "ModulesUrl": "[concat(variables('_artifactsLocation'), '/', 'dsc', '/', variables('_addsBdcDscFileName'), variables('_artifactsLocationSasToken'))]",
          "ConfigurationFunction": "[variables('adBDCConfigurationFunction')]",
          "Properties": {
            "DomainName": "[parameters('addsRootDomainName')]",
            "AdminCreds": {
              "UserName": "[parameters('adminUserName')]",
              "Password": "PrivateSettingsRef:AdminPassword"
            }
          }
        },
        "protectedSettings": {
          "Items": {
            "AdminPassword": "[parameters('adminPassword')]"
          }
        }
      }
    },
  • Líneas 16-22. Al igual que ocurre con las plantillas JSON, el archivo con el DSC también debe descargarse desde una ubicación pública o bien protegida por token de acceso. El resto de atributos son los parámetros que le hacemos llegar: DomainName y AdminCreds.
  • Líneas 26-28. Dado que la contraseña es un parámetro especialmente sensible, la facilitamos como un protectedSettings. Esto indicará a ARM que la trate de forma especial y no figure en ninguno de los logs de auditoría generados por la operación.

Y finalmente, con esto ya tendríamos establecida nuestra configuración de Active Directory.

Conclusiones

Implementar un nuevo bosque de Active Directory mediante máquinas virtuales en Azure es una operación trivial, pero que si queremos automatizar utilizando la potencia de ARM debemos considerar con detenimiento todas las operaciones que llevamos a cabo para que sean reproducidas de forma fiel por Azure.

Aunque esto a priori nos suponga una inversión en tiempo y esfuerzo, se ve ampliamente compensada por el ahorro de tiempo que nos supone en futuros proyectos, dado que crear un nuevo bosque de Active Directory es una operación muy común. Ejecutando este proyecto de ARM, será cuestión de esperar plácidamente unos minutos hasta que todas las operaciones se completen.

Uniendo lo expuesto en esta publicación con lo comentado sobre JsonADDomainExtension tendremos a nuestro alcance una automatización muy potente de operaciones básicas de Active Directory.

Happy ADDS!