TLDR: There are two sections of this article; feel free to scroll down to the titles for the applicable section.
Using VM Extensions with Terraform to Domain Join Virtual Machines
VM Extensions are a fantastic way to yield post deployment configurations via template as code in Azure. One of Azure's most common VM Extensions is the JoinADDomainExtension, which will join your Azure VM to an Active Directory machine after the machine has successfully been provisioned. For the purposes of this artcicle, we will assume you have a VM called testvm in the East US region.
Typically, VM extensions can be configured via the following block of ARM Template code (a fully working example building the virtual and running the extension can be found here).
{
"apiVersion": "2015-06-15",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "testvm/joindomain",
"location": "EastUS",
"properties": {
"publisher": "Microsoft.Compute",
"type": "JsonADDomainExtension",
"typeHandlerVersion": "1.3.2",
"autoUpgradeMinorVersion": true,
"settings": {
"Name": "JACKSTROMBERG.COM",
"OUPath": "OU=Users,OU=CustomOU,DC=jackstromberg,DC=com",
"User": "JACKSTROMBERG.COM\\jack",
"Restart": "true",
"Options": "3"
},
"protectedSettings": {
"Password": "SecretPassword!"
}
}
}
When looking at Terraform, the syntax is a bit different and there isn't much documentation on how to handle the settings and most importantly, the password/secret used when joining the machine to the domain. In this case, here is working translation of the ARM template to Terraform.
resource "azurerm_virtual_machine_extension" "MYADJOINEDVMADDE" {
name = "MYADJOINEDVMADDE"
virtual_machine_id = azurerm_virtual_machine.testvm.id
publisher = "Microsoft.Compute"
type = "JsonADDomainExtension"
type_handler_version = "1.3.2"
# What the settings mean: https://docs.microsoft.com/en-us/windows/desktop/api/lmjoin/nf-lmjoin-netjoindomain
settings = <<SETTINGS
{
"Name": "JACKSTROMBERG.COM",
"OUPath": "OU=Users,OU=CustomOU,DC=jackstromberg,DC=com",
"User": "JACKSTROMBERG.COM\\jack",
"Restart": "true",
"Options": "3"
}
SETTINGS
protected_settings = <<PROTECTED_SETTINGS
{
"Password": "SecretPassword!"
}
PROTECTED_SETTINGS
depends_on = ["azurerm_virtual_machine.MYADJOINEDVM"]
}
The key pieces here are the SETTINGS and PROTECTED_SETTINGS blocks that allow you to pass the traditional JSON attributes as you would in the ARM template. Luckily, terraform does a somewhat decent job documentation this on their public docs here, so if you have any additional questions on any of the attributes you can find them all here: https://www.terraform.io/docs/providers/azurerm/r/virtual_machine_extension.html
The last block of code I have specified at the very end is a depends_on statement. This simpy ensures that this resource is not created until the Virtual Machine itself has successfully been provisioned and can be very beneficial if you have other scripts that may need to run prior to domain join.
Using VM Extensions with Terraform to customize a machine post deployment
Continuing along the lines of customizing a virtual machine post deployment, Azure has a handy dandy extension called CustomScriptExtension. What this extension does is allow you to arbitrarily download and execute files (typically PowerShell) after a virtual machine has been deployed. Unlike the domain join example above, Azure has extensive documentation on this extension and provides support for both Windows and Linux (click the links for Windows or Linux to see the Azure docs on this).
Following similar suite as the above Domain Join example, within the ARM world, we can leverage the following template to execute code post deployment:
{
"apiVersion": "2018-06-01",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "testvm",
"location": "EastUS",
"properties": {
"publisher": "Microsoft.Azure.Extensions",
"type": "CustomScript",
"typeHandlerVersion": "2.1.3",
"autoUpgradeMinorVersion": true,
"settings": {
"fileUris": [
"script location"
]
},
"protectedSettings": {
"commandToExecute": "myExecutionCommand",
"storageAccountName": "mystorageaccountname",
"storageAccountKey": "myStorageAccountKey"
}
}
}
When we look at the translation over to Terraform, for the most part the structure is the exact same. Similar to our Active Directory Domain Join script above, the tricky piece is knowing to use the PROTECTED_SETTINGS to encapsulate our block of code that in this case authenticates to the Azure Storage Account to pull down our post-deployment script. Now per the Azure documentation, those variables are optional; if the scripts you have don't contain sensitive information, you are more than welcome to simply specify the fileUri and specify the commandToExecute via the regular SETTINGS block.
resource "azurerm_virtual_machine_extension" "MYADJOINEDVMCSE" {
name = "MYADJOINEDVMCSE"
virtual_machine_id = azurerm_virtual_machine.testvm.id
publisher = "Microsoft.Azure.Extensions"
type = "CustomScript"
type_handler_version = "2.1.3"
# CustomVMExtension Documetnation: https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-windows
settings = <<SETTINGS
{
"fileUris": ["https://mystorageaccountname.blob.core.windows.net/postdeploystuff/post-deploy.ps1"]
}
SETTINGS
protected_settings = <<PROTECTED_SETTINGS
{
"commandToExecute": "powershell -ExecutionPolicy Unrestricted -File post-deploy.ps1",
"storageAccountName": "mystorageaccountname",
"storageAccountKey": "myStorageAccountKey"
}
PROTECTED_SETTINGS
depends_on = ["azurerm_virtual_machine_extension.MYADJOINEDVMADDE"]
}
At this point you should be able to leverage both extensions to join a machine to the domain and then customize virtually any aspect of the machine thereafter.
The only thing I'll leave you with is typically it is recommended to not leave clear-text passwords scattered through your templates. In either case, I highly recommend looking at leveraging Azure Key Vault or an alternative solution that can ensure proper security in handling those secrets.
Notes
Aside from Terraform, one question I've received is what happens if the extension runs against a machine that is already domain joined?
A: The VM extension will still install against the Azure Virtual Machine, but will immediately return back the following response: "Join completed for Domain 'yourdomain.com'"
Specifically, the following is returned back to Azure: [{"version":"1","timestampUTC":"2019-03-27T16:30:57.9274393Z","status":{"name":"ADDomainExtension","operation":"Join Domain/Workgroup","status":"success","code":0,"formattedMessage":{"lang":"en-US","message":"Join completed for Domain 'yourdomain.com'"},"substatus":null}}]
What does Options mean for domain join?
A: Copied from here: The options are a set of bit flags that define the join options. Default value of 3 is a combination of NETSETUP_JOIN_DOMAIN (0x00000001) & NETSETUP_ACCT_CREATE (0x00000002) i.e. will join the domain and create the account on the domain. For more information see https://msdn.microsoft.com/en-us/library/aa392154(v=vs.85).aspx