I have a CustomScript running via a VM Extension on a windows VM. I am passing powershell commands into this extension to run at deploy time.
I started running into issues when I started to implement Windows UNC paths when I figured out that ‘\’ in terraform is equal to '' in Windows. I believe this issue is either due to not escaping from a string correctly or a bug in how backslashes are handled.
Below are snippets of code and the associated error. Would appreciate any advice or feedback, thanks!
The errors are essentially the same and I’ve correlated it to the copy_fileshare_command locals variable.
locals {
fileshare_file_name = "\"win_fileshare_mount.ps1\""
fileshare_path = "\\\\${local.privatelink_fqdn}\\${var.fileshare_name}"
fileshare_script_path_upload = "${local.fileshare_path}\\${local.fileshare_file_name}"
copy_source = "\\\\${local.privatelink_fqdn}\\${var.fileshare_name}\\${local.fileshare_file_name}"
copy_destination = "c:\\terraform"
fileshare_mount_command = "net use S: ${local.fileshare_path} /persistent:yes /user:${var.sa_name} ${var.sa_primary_access_key}"
copy_fileshare_command = "copy ${local.copy_source} ${local.copy_destination}"
srv_powershell_command = "${local.firewall_command}; ${local.fileshare_mount_command}; ${local.copy_fileshare_command}; ${local.exit_code_hack};"
wks_powershell_command = "${local.firewall_command}; ${local.fileshare_mount_command}; ${local.copy_fileshare_command}; ${local.exit_code_hack};"
}
# win10 VM Extension
resource "azurerm_virtual_machine_extension" "wks_win10_vm_extension_bootstrap" {
count = var.number_of_win10_wks
depends_on = [azurerm_windows_virtual_machine.wks_win10]
name = "win10postdeploy${count.index}"
virtual_machine_id = "${element(azurerm_windows_virtual_machine.wks_win10.*.id, count.index )}"
publisher = "Microsoft.Compute" # "Microsoft.Azure.Extensions"
type = "CustomScriptExtension" # "CustomScript"
type_handler_version = "1.9"
# auto_upgrade_minor_version = true
settings = <<SETTINGS
{
"commandToExecute": "powershell -ExecutionPolicy unrestricted -NoProfile -NonInteractive -command \"${local.wks_powershell_command}\""
}
SETTINGS
}
# Windows Server 2016 VM Extension
resource "azurerm_virtual_machine_extension" "srv_2016_vm_extension_bootstrap" {
count = var.number_of_win_srv_2016
depends_on = [azurerm_windows_virtual_machine.srv_win_2016]
name = "srv2016postdeploy${count.index}"
virtual_machine_id = "${element(azurerm_windows_virtual_machine.srv_win_2016.*.id, count.index )}"
publisher = "Microsoft.Compute" # "Microsoft.Azure.Extensions"
type = "CustomScriptExtension" # "CustomScript"
type_handler_version = "1.9"
# auto_upgrade_minor_version = true
settings = <<SETTINGS
{
"commandToExecute": "powershell -ExecutionPolicy unrestricted -NoProfile -NonInteractive -command \"${local.srv_powershell_command}\""
}
SETTINGS
}
Errors:
│ Error: "settings" contains an invalid JSON: invalid character 'c' after object key:value pair
│
│ with module.compute.azurerm_virtual_machine_extension.wks_win10_vm_extension_bootstrap[0],
│ on modules/compute/win_10.tf line 94, in resource "azurerm_virtual_machine_extension" "wks_win10_vm_extension_bootstrap":
│ 94: settings = <<SETTINGS
│ 95: {
│ 96: "commandToExecute": "powershell -ExecutionPolicy unrestricted -NoProfile -NonInteractive -command \"${local.wks_powershell_command}\""
│ 97: }
│ 98: SETTINGS
│ Error: "settings" contains an invalid JSON: invalid character 'c' after object key:value pair
│
│ with module.compute.azurerm_virtual_machine_extension.srv_2016_vm_extension_bootstrap[0],
│ on modules/compute/win_srv_2016.tf line 92, in resource "azurerm_virtual_machine_extension" "srv_2016_vm_extension_bootstrap":
│ 92: settings = <<SETTINGS
│ 93: {
│ 94: "commandToExecute": "powershell -ExecutionPolicy unrestricted -NoProfile -NonInteractive -command \"${local.srv_powershell_command}\""
│ 95: }
│ 96: SETTINGS
I attempted to apply the code after escaping from the string
"commandToExecute": "powershell -ExecutionPolicy unrestricted -NoProfile -NonInteractive -command "${local.srv_powershell_command}""
However its getting caught by this local variable: wks_powershell_command
Which contains these three variables, as strings, concurrently:
"${local.firewall_command}; ${local.fileshare_mount_command}; ${local.copy_fileshare_command}; ${local.exit_code_hack};"
The variable causing this issue is fileshare_mount_command. I believe I'm not fully escaping the string and am hoping someone here will be able to assist.
Appreciate anyone's guidance with this issue. Thank you!
Related
I'm attempting to add a domain controller to an existing domain using terraform (in Azure). I declared some local values in the main terraform file for my servers, like so:
locals {
username_command = "$username = ${var.domainAdminUsername}"
password_command = "$password = ConvertTo-SecureString ${var.domainAdminPassword} -AsPlainText -Force"
credentials_command = "$credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($username,$password)"
install_ad_command = "Add-WindowsFeature -name ad-domain-services -IncludeManagementTools"
configure_ad_command = "Install-ADDSDomainController -DomainName ${var.domainName} -InstallDns -Credential $credentials -SafeModeAdministratorPassword $password -Force:$true"
shutdown_command = "shutdown -r -t 10"
exit_code_hack = "exit 0"
powershell_command = "${local.username_command}; ${local.password_command}; ${local.credentials_command}; ${local.install_ad_command}; ${local.configure_ad_command}; ${local.shutdown_command}; ${local.exit_code_hack}"
}
I'm then applying a custom script extension resource on the VM, that run the powershell cmdlets built by the local values:
resource "azurerm_virtual_machine_extension" "promote-to-domain-controller" {
count = 2
depends_on = [
azurerm_windows_virtual_machine.vm
]
name = "promote-to-domain-controller"
virtual_machine_id = azurerm_windows_virtual_machine.vm[count.index].id
publisher = "Microsoft.Compute"
type = "CustomScriptExtension"
type_handler_version = "1.9"
settings = <<SETTINGS
{
"commandToExecute": "powershell.exe -Command \"${local.powershell_command}\""
}
SETTINGS
}
Everything seems to work, but the VM extension. [ { "code": "ComponentStatus/StdOut/succeeded", "level": "Info", "displayStatus": "Provisioning succeeded", "message": "" }, { "code": "ComponentStatus/StdErr/succeeded", "level": "Info", "displayStatus": "Provisioning succeeded", "message": "azadmin : The term 'azadmin' is not recognized as the name of a cmdlet, function, script file, or operable program. \r\nCheck the spelling of the name, or if a path was included, verify that the path is correct and try again.\r\nAt line:1 char:13\r\n+ $username = azadmin; $password = ConvertTo-SecureString <redacted>...\r\n+ ~~~~~~~\r\n + CategoryInfo : ObjectNotFound: (azadmin:String) [], CommandNotFoundException\r\n + FullyQualifiedErrorId : CommandNotFoundException\r\n \r\nNew-Object : Exception calling \".ctor\" with \"2\" argument(s): \"Cannot process argument because the value of argument \r\n\"userName\" is not valid. Change the value of the \"userName\" argument and run the operation again.\"\r\nAt line:1 char:118\r\n+ ... edentials = New-Object -TypeName System.Management.Automation.PSCrede ...\r\n+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\r\n + CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException\r\n + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand\r\n \r\n" } ]
var.domainAdminUsername is set to 'azadmin'. In the first screenshot, on line 11, you can see that I'm passing this value ($username) to the PSCredential constructor. This constructor takes two overloads: a string and a securestring. It's like the value that's getting passed to the constructor isn't a string, though it should be.
You can use the below to promote your VM as a domain controller for a exisitng forest .
Main.tf file:
provider "azurerm" {
features {}
}
locals {
password_command = "$password = ConvertTo-SecureString ${var.admin_password} -AsPlainText -Force"
credentials_command = "$credentials = Get-Credential ${var.domainAdminUsername}"
install_ad_command = "Add-WindowsFeature -name ad-domain-services -IncludeManagementTools"
configure_ad_command = "Install-ADDSDomainController -DomainName ${var.active_directory_domain} -InstallDns -Credential $credentials -SafeModeAdministratorPassword $password -Force:$true"
shutdown_command = "shutdown -r -t 10"
exit_code_hack = "exit 0"
powershell_command = " ${local.password_command};${local.credentials_command}; ${local.install_ad_command}; ${local.configure_ad_command}; ${local.shutdown_command}; ${local.exit_code_hack}"
}
data "azurerm_virtual_machine" "example" {
name = "${var.vmname}"
resource_group_name = "${var.resource_group_name}"
}
resource "azurerm_virtual_machine_extension" "promote-to-domain-controller" {
name = "promote-to-domain-controller"
virtual_machine_id = data.azurerm_virtual_machine.example.id
publisher = "Microsoft.Compute"
type = "CustomScriptExtension"
type_handler_version = "1.9"
settings = <<SETTINGS
{
"commandToExecute": "powershell.exe -Command \"${local.powershell_command}\""
}
SETTINGS
}
variable.tf file:
variable resource_group_name {
description = "The name of the Resource Group where the VM is"
default = "resourcegroup"
}
variable location {
description = "The Azure Region in which the Resource Group exists"
default = "resourcegrouplocation"
}
# Active Directory & Domain Controller
variable vmname {
description = "The Virtual Machine name that you wish to join to the domain"
default = "vmname"
}
variable "active_directory_domain" {
description = "The name of the Active Directory domain, for example `consoto.local`"
default = "domainname"
}
variable "domainAdminUsername" {
description = "The local administrator account on the Domain"
default = "Domain\admin or admin#domain.com"
}
variable "admin_password" {
description = "The password associated with the local administrator account on the virtual machine"
default = "password"
}
Output: (Terraform Plan)
Adding VM to existing domain:
Main.tf
resource "azurerm_virtual_machine_extension" "join-domain" {
name = "join-domain"
location = "${var.location}"
resource_group_name = "${var.resource_group_name}"
virtual_machine_name = "${var.vmname}"
publisher = "Microsoft.Compute"
type = "JsonADDomainExtension"
type_handler_version = "1.3"
settings = <<SETTINGS
{
"Name": "${var.active_directory_domain}",
"OUPath": "",
"User": "${var.active_directory_domain}\\${var.active_directory_username}",
"Restart": "true",
"Options": "3"
}
SETTINGS
protected_settings = <<SETTINGS
{
"Password": "${var.active_directory_password}"
}
SETTINGS
}
Variable.tf
variable resource_group_name {
description = "The name of the Resource Group where the VM is"
}
variable location {
description = "The Azure Region in which the Resource Group exists"
}
# Active Directory & Domain Controller
variable vmname {
description = "The Virtual Machine name that you wish to join to the domain"
}
variable "active_directory_domain" {
description = "The name of the Active Directory domain, for example `consoto.local`"
}
variable "active_directory_username" {
description = "The username of an account with permissions to bind machines to the Active Directory Domain"
}
variable "active_directory_password" {
description = "The password of the account with permissions to bind machines to the Active Directory Domain"
}
Note: Please ensure to use username as domain\adminusername for the code to run as its expecting username to be domain's username.
I am trying to have my terraform script run a powershell script on a VM when it's provisioned. I know it's trying to run but it's erroring out. I believe it's because the backslash in the file paths. I've tried escaping it, by making each single back slash into a double, but then it seems to be passed literally instead of as a simple single backslash, and that is failing too.
So how do I do it? anyone? thank you much
resource "azurerm_virtual_machine_extension" "dc" {
name = var.DomainControllerVMName
virtual_machine_id = azurerm_windows_virtual_machine.dc.id
publisher = "Microsoft.Azure.Extensions"
type = "CustomScript"
type_handler_version = "2.0"
settings = jsonencode({
commandToExecute = "$password = convertto-securestring RkP83Ls4S8wV -asplaintext -force;Install-windowsfeature -name AD-Domain-Services –IncludeManagementTool;Install-ADDSForest -CreateDnsDelegation:$false -DatabasePath C:\windows\NTDS -DomainMode WinThreshold -DomainName mdk.mydomain.com -DomainNetbiosName MDK -ForestMode WinThreshold -InstallDns:$true -SafeModeAdministratorPassword $password -LogPath C:\windows\NTDS -NoRebootOnCompletion:$false -SysvolPath C:\windows\SYSVOL -Force:$true -Confirm:$false"
})
tags = {
environment = "Production"
}
depends_on = [azurerm_windows_virtual_machine.dc]
}
After my validation, the following terraform template is working. For more information, you could refer to this terraform-azurerm-promote-dc sample.
resource "azurerm_virtual_machine_extension" "create-active-directory-forest" {
name = var.DomainControllerVMName
virtual_machine_id = azurerm_windows_virtual_machine.dc.id
publisher = "Microsoft.Compute"
type = "CustomScriptExtension"
type_handler_version = "1.10"
settings = <<SETTINGS
{
"commandToExecute": "powershell.exe -Command \"$password = convertto-securestring Password12345 -asplaintext -force;Install-windowsfeature -name AD-Domain-Services –IncludeManagementTool;Install-ADDSForest -CreateDnsDelegation:$false -DatabasePath C:\\windows\\NTDS -DomainMode WinThreshold -DomainName mdk.mydomain.com -DomainNetbiosName MDK -ForestMode WinThreshold -InstallDns:$true -SafeModeAdministratorPassword $password -LogPath C:\\windows\\NTDS -NoRebootOnCompletion:$false -SysvolPath C:\\windows\\NTDS -Force:$true -Confirm:$false;shutdown -r -t 10;exit 0\""
}
SETTINGS
}
I am provisioning a Windows VM using Terraform v0.12.9 on Azure cloud. On that VM I want to perform below tasks using Terraform. Basically to avoid RDP to the VM and perform manual srcipts execution.
1. Enable PSRemoting
2. Create a new FirewallRule
3. Create a SelfSignedCertificate
I have a vm_provisioning.tf as follows:
resource "azurerm_virtual_machine" "vm" {
#count = "${var.env == "dev" ? 0 : 1}"
count = "${var.env == "dev" ? 0 : 1}"
name = var.vm_name
location = "${azurerm_resource_group.rg.location}"
resource_group_name = "${azurerm_resource_group.rg.name}"
network_interface_ids = ["${azurerm_network_interface.network-interface[count.index].id}"]
vm_size = "Standard_D13_v2"
storage_image_reference {
publisher = "MicrosoftWindowsDesktop"
offer = "Windows-10"
sku = "rs4-pro"
version = "latest"
}
storage_os_disk {
name = "Primary-disk"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
disk_size_gb = "127"
}
os_profile {
computer_name = var.vm_name
admin_username = "${var.vm-username}"
admin_password = "${random_password.vm_password.result}"
}
os_profile_windows_config {
}
provisioner "remote-exec" {
connection {
host = "${element(azurerm_public_ip.PublicIP.*.ip_address, count.index)}"
type = "winrm"
user = var.vm-username
password = "${random_password.vm_password.result}"
agent = "false"
insecure = "true"
}
**inline = [
"powershell.exe Set-ExecutionPolicy Bypass -force",
"powershell.exe $DNSName = $env:COMPUTERNAME",
"powershell.exe Enable-PSRemoting -Force",
"powershell.exe New-NetFirewallRule -Name 'WinRM HTTPS' -DisplayName 'WinRM HTTPS' -Enabled True -Profile 'Any' -Action 'Allow' -Direction 'Inbound' -LocalPort 5986 -Protocol 'TCP'",
"powershell.exe $thumbprint = (New-SelfSignedCertificate -DnsName $DNSName -CertStoreLocation Cert:/LocalMachine/My).Thumbprint",
"powershell.exe $cmd = 'winrm create winrm/config/Listener?Address=*+Transport=HTTPS #{Hostname=''$DNSName''; CertificateThumbprint=''$thumbprint''}'",
"powershell.exe cmd.exe /C $cmd"
]**
}
}
I tried with azurerm_virtual_machine_extension as well.
resource "azurerm_virtual_machine_extension" "winrm" {
name = var.name
location = "${azurerm_resource_group.rg.location}"
resource_group_name = "${azurerm_resource_group.rg.name}"
virtual_machine_name = var.vm_name
publisher = "Microsoft.Azure.Extensions"
type = "CustomScriptExtension"
type_handler_version = "2.0"
settings = <<SETTINGS
{
"commandToExecute": "hostname && uptime"
}
SETTINGS
}
With azurerm_virtual_machine_extension I am getting below error.
##[error]Terraform command 'apply' failed with exit code '1'.: compute.VirtualMachineExtensionsClient#CreateOrUpdate: Failure sending request: StatusCode=0 -- Original Error: autorest/azure: Service returned an error. Status=<nil> Code="OperationNotAllowed" Message="This operation cannot be performed when extension operations are disallowed. To allow, please ensure VM Agent is installed on the VM and the osProfile.allowExtensionOperations property is true."
According to the error message, you need to include an os_profile_windows_config block. It supports the following:
provision_vm_agent - (Optional) Should the Azure Virtual Machine Guest
Agent be installed on this Virtual Machine? Defaults to false.
os_profile_windows_config {
provision_vm_agent = true
}
Edit
This example provisions a Virtual Machine running Windows Server 2016 with a Public IP Address and runs a remote-exec provisioner via WinRM.
main.tf
locals {
custom_data_params = "Param($ComputerName = \"${local.virtual_machine_name}\")"
custom_data_content = "${local.custom_data_params} ${file("./files/winrm.ps1")}"
}
winrm.ps1
Trying to figure out the proper way to pass named arguments to my powershell script using terraform local-exec.
Do I need to quote delimit params in this case like so?
provisioner "local-exec" {
command = "powershell -file ../BindCert.ps1 -certString '${var.cert_string_b64}' -certPassword '${var.cert_password}' -certThumbprint '${var.cert_thumbprint}' -certName '${var.cert_name}'"
}
Windows 10
Powershell 5.1
For your issue, you can change the code like below:
provisioner "local-exec" {
command = "powershell -file ../BindCert.ps1 -certString ${var.cert_string_b64} -certPassword ${var.cert_password} -certThumbprint ${var.cert_thumbprint} -certName ${var.cert_name}"
}
I will show you the test which I did on my side.
PowerShell script:
param([String]$rgName = "rgName")
Get-AzResourceGroup -Name $rgName
Terraform code:
variable "test" {
type = "string"
default = "charles"
}
resource "null_resource" "test" {
provisioner "local-exec" {
command = "PowerShell -file ./test.ps1 -rgName ${var.test}"
}
}
The screenshot of the result:
For more details, see Terraform local-exec Provisioner.
I'm trying to automate some processes in our project which includes some steps like create the VM, connect to the newly created VM and run some commands remotely.
Now previously what I used to do is run this commands in sequence manually.
1.Create a VM
New-AzureRmResourceGroupDeployment -Name VmDeployment -ResourceGroupName XYZ`
-TemplateFile "C:\Templates\template.json" `
-TemplateParameterFile "C:\Templates\parameters.json"
2.Connect to the VM.
Set-Item WSMan:\localhost\Client\TrustedHosts -Value 100.9.4.12
$UserName = "100.9.4.12\admin"
$Password = ConvertTo-SecureString "admin#123" -AsPlainText -Force
$psCred = New-Object System.Management.Automation.PSCredential($UserName, $Password)
$s = New-PSSession -ComputerName 100.9.4.12 -Credential $psCred
Invoke-Command -Session $s -ScriptBlock {Get-Service 'ServiceName'}
In this the IP address is used to add that in the trusted hosts on the client.I used to check the generated IP address on Azure Portal, replace that IP in the command and run them manually. But now , since I'm automating, there will be no manual intervention in the process.
So how should I retrieve the IP address of the newly created VM?
Not directly related to your question, but have you thought about using Terraform to automate the creation of your resources? http://terraform.io
Terraform is very similar to ARM (only much nicer) here's an example of a VM creation and the public IP export:
resource "azurerm_resource_group" "main" {
name = "test-resources"
location = "West US 2"
}
resource "azurerm_virtual_network" "main" {
name = "test-network"
address_space = ["10.0.0.0/16"]
location = "${azurerm_resource_group.main.location}"
resource_group_name = "${azurerm_resource_group.main.name}"
}
resource "azurerm_subnet" "internal" {
name = "internal"
resource_group_name = "${azurerm_resource_group.main.name}"
virtual_network_name = "${azurerm_virtual_network.main.name}"
address_prefix = "10.0.2.0/24"
}
resource "azurerm_public_ip" "test" {
name = "test-public-ip"
location = "${azurerm_resource_group.main.location}"
resource_group_name = "${azurerm_resource_group.main.name}"
public_ip_address_allocation = "dynamic"
tags {
environment = "production"
}
}
resource "azurerm_network_interface" "main" {
name = "test-nic"
location = "${azurerm_resource_group.main.location}"
resource_group_name = "${azurerm_resource_group.main.name}"
ip_configuration {
name = "testconfiguration1"
subnet_id = "${azurerm_subnet.internal.id}"
private_ip_address_allocation = "dynamic"
public_ip_address_id = "${azurerm_public_ip.test.id}"
}
}
resource "azurerm_virtual_machine" "main" {
name = "test-vm"
location = "${azurerm_resource_group.main.location}"
resource_group_name = "${azurerm_resource_group.main.name}"
network_interface_ids = ["${azurerm_network_interface.main.id}"]
vm_size = "Standard_DS1_v2"
# Uncomment this line to delete the OS disk automatically when deleting the VM
# delete_os_disk_on_termination = true
# Uncomment this line to delete the data disks automatically when deleting the VM
# delete_data_disks_on_termination = true
storage_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "16.04-LTS"
version = "latest"
}
storage_os_disk {
name = "myosdisk1"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}
os_profile {
computer_name = "hostname"
admin_username = "testadmin"
admin_password = "Password1234!"
}
os_profile_linux_config {
disable_password_authentication = false
}
tags {
environment = "production"
}
}
output "vm-ip" {
value = "${azurerm_public_ip.test.ip_address}"
}
why do you care about the ip address? just use dns name? you always know that one (since you define it when you create vm, at least you can do that). Another option is to output the ip address as part of the arm template.
or query the ip address in powershell:
Get-AzureRmVM -ResourceGroupName ‘HSG-ResourceGroup’ -Name ‘HSG-LinuxVM’ | Get-AzureRmPublicIpAddress
You can use the Powershell command to get all the private IP of the VMs. And the command will like this:
Get-AzureRmNetworkInterface -ResourceGroupName resourceGroupName | Select-Object {$_.IpConfigurations.PrivateIpAddress}
Update
Also the command to set a variable like this:
$vm = Get-AzureRmNetworkInterface -ResourceGroupName charles | Select-Object -Property #{Name="Ip"; Expression = {$_.IpConfigurations.PrivateIpAddress}}
$vm[0].Ip
Then you will only get the IP address.