bicep icon indicating copy to clipboard operation
bicep copied to clipboard

Documentation/sample - loadTextContent() or loadFileAsBase64() for VMSS script extension

Open johndowns opened this issue 4 years ago • 4 comments

The new loadTextContent() and loadFileAsBase64() functions look like they're ideal for scenarios like sending a custom script into the VMSS custom script extension. However, in my testing, I wasn't able to find an elegant way to do actually use this. The way we ended up getting this to work was by using this:

{
  name: 'customScriptExtension'
  properties: {
    publisher: 'Microsoft.Compute'
    type: 'CustomScriptExtension'
    typeHandlerVersion: '1.10'
    protectedSettings: {
      commandToExecute:'powershell.exe -ExecutionPolicy Unrestricted -Command "& Invoke-Expression -Command ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(\'${loadFileAsBase64('../scripts/vmss-configure-iis.ps1')}\')))"'
    }
  }
}

(source)

It'd be good to know if there's a better way to do this, and to have clear docs and a sample to help with this since it's likely to be a common scenario.

johndowns avatar Jul 21 '21 04:07 johndowns

For context powershell does have a -encodedcommand param that takes base64 content but it uses utf16LE as the charset and it looks like bicep is utf8

timleyden avatar Jul 21 '21 04:07 timleyden

@mumian FYI - something we will need to take care of after the we finish the migration to MS docs

alex-frankel avatar Jul 21 '21 15:07 alex-frankel

I think the problem here is with the script extension itself not supporting new lines rather than bicep. I had great problems using multi line strings with it, and ended up having to strip all newlines out.

I ended up refactoring my ps1 script to terminate statements with ;, then strip the new lines out in bicep:

  resource customScript 'extensions@2021-03-01' = if (!empty(vmCustomScript)) {
    name: 'CustomScript'
    properties: {
      publisher: 'Microsoft.Compute'
      type: 'CustomScriptExtension'
      typeHandlerVersion: '1.9'
      autoUpgradeMinorVersion: true
      protectedSettings: {
        commandToExecute: replace(replace(vmCustomScript, '\r', ' '), '\n', ' ')
      }
    }
  }

afscrome avatar Jul 21 '21 15:07 afscrome

Only my 2 cents. I'm using the customData attribute to pass the script to the VM and run it via CustomScript extension (on Windows) and cloud-init on Linux. The new functions make it very easy and very elegant. Awesome work from the Team!

I'm also using sth. like base64(format(loadTextContent(...), ...)) which makes it even more awesome. You only have to pay attention to {} in you nginx e.g. config files. As it uses .NET String.format() i'm using server {{ ... hostname: {0} }} in my cloud-init.yml. I think we should add sth. like that to the docs.

splitt3r avatar Jul 22 '21 11:07 splitt3r

The new loadTextContent() and loadFileAsBase64() functions look like they're ideal for scenarios like sending a custom script into the VMSS custom script extension. However, in my testing, I wasn't able to find an elegant way to do actually use this. The way we ended up getting this to work was by using this:

{
  name: 'customScriptExtension'
  properties: {
    publisher: 'Microsoft.Compute'
    type: 'CustomScriptExtension'
    typeHandlerVersion: '1.10'
    protectedSettings: {
      commandToExecute:'powershell.exe -ExecutionPolicy Unrestricted -Command "& Invoke-Expression -Command ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(\'${loadFileAsBase64('../scripts/vmss-configure-iis.ps1')}\')))"'
    }
  }
}

(source)

It'd be good to know if there's a better way to do this, and to have clear docs and a sample to help with this since it's likely to be a common scenario.

Any idea how I should format commandToExecute to push any parameters to my powershell script?

ghost avatar Oct 20 '23 15:10 ghost

This works for me locally:

iex "& { $([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('cGFyYW0oCiAgIFtzdHJpbmddICROYW1lCikKCldyaXRlLUhvc3QgIkhlbGxvICROYW1lISI='))) } -Name Anthony"

So something like the following should work (untested, may need some refinement!):

var command = '''
param(
   [string] $Name
)

Write-Host "Hello $Name!"
'''
var arguments = {
  Name: 'Anthony'
}
// builds a string of form '-ArgA ValA -ArgB ValB'
var argumentString = join(map(items(arguments), i => '-${i.key} ${i.value}'), ' ')

var commandToExecute = 'powershell.exe -ExecutionPolicy Unrestricted -Command "iex \\"& { $([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(\'${base64(command)}\'))) } ${argumentString}\\""'

anthony-c-martin avatar Oct 20 '23 16:10 anthony-c-martin

@anthony-c-martin thanks for your help.

I was not able to make it work with your examples. Eventually got it working by using a runcommand.

For everyone struggling to automate the process of installing the integration runtime on an azure VM and register it with the data factory: this repository is a good starting point.

My runcommand script:

param location string
param name string
param shirId string
var scriptContent = loadTextContent('./test1.ps1', 'utf-8')

// https://learn.microsoft.com/en-us/azure/templates/microsoft.compute/virtualmachines/runcommands?pivots=deployment-language-bicep
resource res_run_cmd 'Microsoft.Compute/virtualMachines/runCommands@2023-03-01' = {
  name: '${name}-vm-0-0/testtestinstall'
  location: location
  properties: {
    asyncExecution: false
    protectedParameters: [{
      name: 'gatewayKey'
      value: listAuthKeys(shirId, '2018-06-01').authKey1
    }]
    source: {
      script: scriptContent
    }
    timeoutInSeconds: 600
    treatFailureAsDeploymentFailure: true
  }
}

The shirId is the id of the integration runtime (child of data factory). Content of test1.ps1 = gatewayInstall.ps1

ghost avatar Oct 21 '23 15:10 ghost