Using a PowerShell DSC extension to run a custom script on an Azure VM

Published by Marco Obinu on

Having the ability to deploy Azure VMs starting from images hosted in a gallery is lovely if you need to create a new virtual machine from scratch quickly. Unfortunately, you often need to customize them, and being able to do it during the deployment phase can save you a lot of time. PowerShell DSC and Custom Script Extensions are two ways you can use to execute a custom script on an Azure VM.

How to customize Azure VMs during deploy

You have several ways to deploy a customized virtual machine. Some of them are:

Since I’m working for several customers, the first option is not the ideal one for me: uploading a new image on each customer subscription can be time-consuming.

Then, I prefer to use DSC or CSE to run a custom script on the VM, depending on the scenario.

Custom script extension

With CSE, you can download custom scripts inside your Windows or Linux VM and execute them. Although you can assign a CSE to a VM via the Azure Portal or via PowerShell, I usually do it in the ARM template I use to deploy the VM.

The following is the JSON schema of a Custom Script Extension for a Windows VM:

{
    "apiVersion": "2018-06-01",
    "type": "Microsoft.Compute/virtualMachines/extensions",
    "name": "virtualMachineName/config-app",
    "location": "[resourceGroup().location]",
    "dependsOn": [
        "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'),copyindex())]",
        "[variables('musicstoresqlName')]"
    ],
    "tags": {
        "displayName": "config-app"
    },
    "properties": {
        "publisher": "Microsoft.Compute",
        "type": "CustomScriptExtension",
        "typeHandlerVersion": "1.10",
        "autoUpgradeMinorVersion": true,
        "settings": {
            "fileUris": [
                "script location"
            ],
            "timestamp":123456789
        },
        "protectedSettings": {
            "commandToExecute": "myExecutionCommand",
            "storageAccountName": "myStorageAccountName",
            "storageAccountKey": "myStorageAccountKey",
            "managedIdentity" : {}
        }
    }
}

The fileUris array contains all the URI of the files you want to download inside the VM. They can reside on a public URL, like a public GitHub repo. If you need to keep these files in a safe place, you can place them in a storage account and use the storageAccountKey or managedIdentity to access them. The commandToExecute property contains the command you want to run.

The above keys reside inside the protectedSettings section, to avoid passing to sensible data to the VM in cleartext.

Here you can see a simple real-world example I prepared for my StartingWithArmTemplates repo. This is the main part:

"properties": {
    "publisher": "Microsoft.Compute",
    "type": "CustomScriptExtension",
    "typeHandlerVersion": "1.9",
    "autoUpgradeMinorVersion": true,
    "settings": {
        "fileUris": [
            "[concat(variables('cseBaseUrl'), '/TestFile.txt')]",
            "[concat(variables('cseBaseUrl'), '/Read-File.ps1')]"
        ]
    },
    "protectedSettings": {
        "commandToExecute": "powershell -ExecutionPolicy Unrestricted -File Read-File.ps1"
    }
}

The CSE downloads TestFile.txt and Read-File.ps1 on the VM, and then commandToExecute invoke a PowerShell console that executes the PS1 script.

Easy, isn’t it?

There’s a caveat. CSE runs in the context of the LocalSystem account. It works fine for a vast majority of use cases, but sometimes you need to execute commands in the context of a specific user.

Doing this can be challenging.

In the past, I used CredSSP to execute the Invoke-Command cmdlet with a different set of credentials on the local machine. You can see an example of this in the previous version of my Azure SQL-optimized VM ARM template

It worked, but it wasn’t one of the best approaches I ever used. I had to leverage DSC configuration to create reg keys that enabled CredSSP, and then I was able to execute the CSE.

Then, I found a more straightforward approach, which relies on the Script DSC resource.

Using a DSC configuration as a fake CSE

PowerShell Desired State Configuration is an impressive multi-platform configuration management language. It permits you to describe the configuration you want to assign to the system in a declarative way. You don’t need to know the PowerShell imperative cmdLets that apply the configuration: DSC resources handle that part, by executing the cmdLets under the hood.

There are tons of DSC resources you can use to join domains, install software, create reg keys, and so on. You can list them in the PowerShell Gallery by using the Find-DscResource cmdLet. But what if you can’t find any suitable resource for your need?

You might want to author a DSC resource yourself, but it may require some effort. Or, you can use the Script DSC resource, to create a custom resource on the flight.

DSC configurations are idempotents: you can run them multiple times, and independently from the starting point, you should always obtain the same result. 

To act like this, inside a DSC resource, you can find three main blocks:

  • A Test function
  • A Get function
  • A Set function

When you invoke a DSC config on a system, the  Local Configuration Manager service executes the Test function, that leverages the Get function to discover if the desired configuration has already been applied. If not, it invokes the Set function to run the config.

The Script resource resumes all of the above concepts. You need to specify three different scripts in the GetScript, SetScripts, and TestScripts property to emulate the behavior of a standard DSC resource:

Script [string] #ResourceName
{
    GetScript = [string]
    SetScript = [string]
    TestScript = [string]
    [ Credential = [PSCredential] ]
    [ DependsOn = [string[]] ]
    [ PsDscRunAsCredential = [PSCredential] ]
}

If you want to simulate a fake CSE, you must write both GetScript and TestScript in a way they always return false, causing the execution of the SetScript. In our context, this isn’t an issue, since we’re interested in applying the config only during the deployment phase, so idempotence is an option here.

Then, if you need to execute your custom SetScript with specific credentials, you can leverage the native PsDscRunAsCredential property. It’s way easier than enabling CredSSP!

Show me the code!

Here it is a real example I’m using inside my new version of Azure SQL-optimized VM ARM template, that I use both on production and test environments:

script 'CustomScript'
{
    DependsOn            = "[PackageManagement]PSModuleDbaTools"
    PsDscRunAsCredential = $SqlAdministratorCredential
    GetScript            =  { return @{result = 'result'} }
    TestScript           = { return $false }
    SetScript            = {
        
        $logFile = "C:\SqlConfig.log"

        # Setting MaxDOP to recommended value
        Test-DbaMaxDop -SqlInstance $ENV:COMPUTERNAME |
            Set-DbaMaxDop |
            Out-File -FilePath $LogFile -Append
        
        # Setting MaxServerMemory to recommended value
        Test-DbaMaxMemory -SqlInstance $ENV:COMPUTERNAME -Verbose |
            Set-DbaMaxMemory -Verbose |
            Out-File -FilePath $LogFile -Append

        # Enabling IFI and lock pages in memory
        Set-DbaPrivilege -ComputerName $ENV:COMPUTERNAME `
            -Type IFI,LPIM `
            -Verbose |
            Out-File -FilePath $LogFile -Append
    }
}

The Test and Get functions always return false, so the Set part runs every time you apply the config.

Inside the SetScript, I used different DBATools cmdLets to configure my SQL Server instance. As you can see, I’ve never specified the credentials to connect to SQL Server, since the commands are executed in the context of the credentials specified in PsDscRunAsCredential.

The approach described above permits me to use the DSC config both for “normal” DSC resources, as well as for custom, impersonated scripts. I can now avoid using the CSE, resulting in a more straightforward template and a cleaner approach.

That’s all, folks!


Marco Obinu

Curious by nature, talkative geek who can speak in front of a public or a camera, in love with technology, especially SQL Server. I want to understand how things work and to solve problems by myself during work as in my many hobbies.

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: