An introduction to Deployment Scripts resource in ARM Template
Tags: azure, devops, powershell, iac
Published: 2020-07-18 09:01:44

You can do many things by using ARM templates for your resource groups, deploying a VM, a complete network, a Kubernetes cluster, you can event start VMs configuration through script or DSC. At higher scope, you can deploy subscriptions and Management Groups …. But there are some situations where ARM Template alone is not enough. 

There are some situations where you need more than ARM deployments; I often need to populate a blob container in storage account with scripts and contents based on the location or the environment type, sometimes an internal policy prevent me to create a password in a KeyVault by using a parameter in the template, and often I need to populate databases with external sources.

Theses tasks need some extras steps in PowerShell, CLI, or via the portal.  
What-if you could run theses extra steps directly during the ARM Deployment phase. This is the purpose of the Deployment Scripts resources, completing the last kilometer tasks. 

How does it work? During the deployment, the deploymentScripts resource creates an Azure Container instance and a storage account in the target resource group. This environment will run the script you provide. 
To perform any operation in Azure, the ACI needs an identity, currently, only User-Managed Identity is supported. The User-Managed Identity needs to be created before. It needs to have a contributor role in the target resource group and on any other additional resources if needed.

To create the User-Managed Identity 

```powershell
if ($null -ne (get-module -name az.ManagedServiceIdentity -ListAvailable -ErrorAction SilentlyContinue)) {
    Import-module -name az.ManagedServiceIdentity 
}
else {
    install-module -name az.ManagedServiceIdentity -force
}

$ScriptIdentity = New-AzUserAssignedIdentity -ResourceGroupName omc-lab-deploymentscripts -Name DemoDepScriptsUID

New-AzRoleAssignment -ObjectId $ScriptIdentity.PrincipalId -ResourceGroupName omc-lab-deploymentscripts -RoleDefinitionName Contributor

# Get the ID

$ScriptIdentity.ID
```
I found the ManagedServiceIdentity was not installed by default on my computer so I need to install the module first. 
I created the managed Identity object inside the target resource group. It’s not mandatory, you can create the object anywhere in your Azure Tenant event if in another subscription. 

The Managed Identity ID needed by the container instance should look like this 
/subscriptions/XXXX-XXXX-XXXX/resourcegroups/omc-lab-deploymentscripts/providers/Microsoft.ManagedIdentity/userAssignedIdentities/DemoDepScriptsUID

To use it in the deploymentScripts

```json
    "identity": {
      "type": "userAssigned",
      "userAssignedIdentities": {
        "/subscriptions/XXXX-XXXXX-XXXXX-XXXXX/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/DemoDepScriptsUID": {}
      }
    }
```

As the deploymentScripts resource uses a Container Instance makes it available only where Azure Containers Instance can be deployed. 
You can check the availability with this [link](https://azure.microsoft.com/en-us/global-infrastructure/services/?regions=europe-west,us-central,europe-north,us-east,us-east-2,us-north-central,us-south-central,us-west-central,us-west,us-west-2,united-kingdom-south,united-kingdom-west&products=container-instances). 

When you create a deploymentScripts resource, it will create a deploymentScripts object that will govern the creation of a storage account and a file share where the script and other components are copied and a Linux docker image in a Container Instance. The instance mounts the file share and starts executing the script.  

You can use two scripting languages in DeploymentScripts resource, PowerShell, and Bash with Azure CLI. As you understand, beyond the script language there is a docker image, one for PowerShell (mrc.microsoft.com/azuredeploymentscripts-powershell) and one for Azure CLI (mrc.microsoft.com/Azure-Cli). The version you can use in your templates correspond to a tag of one of these two images. 

To get all the version you need to list the tags.


* For PowerShell
```powershell
((Invoke-WebRequest -Uri "https://mcr.microsoft.com/v2/azuredeploymentscripts-powershell/tags/list" -Method get).Content | ConvertFrom-Json).tags
```

* For Azure CLI 
```powershell
((Invoke-WebRequest -Uri "https://mcr.microsoft.com/v2/azure-cli/tags/list" -Method get).Content | ConvertFrom-Json).tags
```

To define which image to use. You will need to use the Kind value. 

```json
{
    "type": "Microsoft.Resources/deploymentScripts",
    "apiVersion": "2019-10-01-preview",
    "name": "PowerShellScript",
    "location": "[resourceGroup().location]",
    "kind": "AzurePowerShell Or AzureCLI", 
```

To define the version of AzurePowerShell or AzureCLI you will need to use the AzPowerShellVersion or AzCliVersion in the ScriptObject properties. 

```json
 "properties": {
        "azPowerShellVersion": "3.0", 
```
For Azure PowerShell

or 

```json
    "properties": {
        "azCliVersion": "2.3.1", 
```
For Azure CLI


You can control the container instance instance settings. The first things you can control are the resources used to run the script in Azure, Storage Account, and Container instance. 

The storageAccountSettings let you specify an existing storage account by specifying the storage account name and the Key. The storage account doesn’t need to be in the same resource group used in the deployment. But it is a best practice to use one in the same Azure region as a file share will be mounted in the container instance. 

```json
"storageAccountSettings": {
        "storageAccountName": "testomc00004",
        "storageAccountKey": "XXXXXXXXXXXXXXXXXX"
      }
```
In the containerSettings property, you can specify the name of the ACI to be created.
```json
      "containerSettings": {
        "containerGroupName": "omcacids01"
      }
```

These two optional properties can help you to comply with your naming convention and other policies. If not specify the resource will create the container group and the storage account with auto-generated names.

The deploymentScripts resource is like any other resource in ARM, if there is no drift between your template and the resource deployed in Azure nothing will happen. It’s the idempotent feature of ARM. But with the DeploymentScripts resource, you may want to replay the script each time you deploy the template even if there is no modification to the deploymentScripts resource. Take this example, you have made a template to deploy resources for the production and the development environment. When you run your template for the dev the deploymentScripts resource is created and the script runs, but by default, it will not be the case for the prod.  
To force the execution of the script event if the deploymentScripts resource is already deployed you can use the forceUpdateTag property. 

```json
   "parameters": {
       "updateGuid": {
            "type": "string",
            "defaultValue": "[newGuid()]",
            "metadata": {
                "description": "Guid used by the forceUpdateTag property to force execution of the script"
            }
        }
    }
...
"forceUpdateTag": "[parameters('updateGuid')]",
```
(the newGuid() function can only be used in the parameters section). 

You may need to set up a timeout to kill the script execution if something doesn’t work as expected. It’s the purpose of the timeout property. You need to use the international representation of date and period. It must start with a PT (T specify you will use month because you don’t need a 3 months' timeout).

```json
"timeout": "PT6M",
```

If you need a timeout you may also want to retain resources deployed by the deploymentScripts. By default, the ACI and the storage account are deleted after the execution.
The cleanupPreference property lets you control which event triggers the resource deletion. The default value is "always" but you can also choose, "onSuccess", deletion occurs if there is no error with the script or "onExpiration".
The last parameter work with the "retentionInterval" property. It keeps the resources for a given amount of time. Like the timeout property, you will need to provide an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) international time and period format. The minimum value is one day and the maximum is 30 days.

```json
      "cleanupPreference": "OnExpiration",
      "retentionInterval": "P4D"
```
To provide configuration information to the script environment can be done by setting up environment variables. In the DeploymentsScripts resource, environment variables work like in the Azure Container Instance. You need to use the "environmentVariables" array where you can add your environment variables. There are two types of variable normal ones with the couple "name"/"value" and secure with the couple "name"/"secureValue". "secureValue" can hold sensitive information like password or key and the value will not be displayed in the container’s properties pane unlike normal ones.

```json
      "environmentVariables": [
        {
          "name": "thePassword",
          "secureValue": "XXXXX"
        },
        {
          "name": "AnOtherValue",
          "value": "yyyyyy"
        }
      ],
``` 

Another way to send data to the script is to use arguments. To have the equivalent of "myscript.ps1 -arg value" in the deploymentScripts resource. You will need to use the argument property. It’s a single string value and if you need more than one argument, they must be separated by spaces.  
For example, if you need something like "-env dev -name vdev" you will have

```json
"arguments": "-env dev -name vdev"
``` 

If you need to use double quotes, they must be escaped. For -env "dev" -name "vdev001"

```json
"arguments": "-env \\\"dev\\\" -name \\\"vdev\\\"",
``` 

Lastly, you can add your script to the deploymentScripts resource. You have two options, inline where you wrote your script in the ARM template or linked via an URI. 
The inline option is simple, you put your script in the scriptContent property. But you have two limitations, the script is limited to 32000 characters, and you will have to escape every double quotes. 

```json
   "scriptContent": "
        param(
  [string] 
  $env,
  [string] 
  $name
 )

        $result = 'you create the {0} in the {1} environment' -f $name, $env 
# doing something usefull
# ...
        Write-Output $result 
        $DeploymentScriptOutputs = @{}
        $DeploymentScriptOutputs['text'] = $result 
      ",
```

But using inline script has other limitations. It may not be suitable if you need to test your scripts. You may prefer to put the script in a remote location to separate the code from the template. You need to use primaryScriptUri property and provide a globally available URI. It can be a link to Github file or in a blob container from an Azure storage account using Shared Access Signature. 

```json
"primaryScriptUri": "https://testomcdepscript001.blob.core.windows.net/scripts/script.ps1?sv=2019-10-10&ss=b&srt=sco&sp=r&se=2020-09-13T21:51:25Z&st=2020-07-13T13:51:25Z&spr=https&sig=XXXXXXX",
```
You can also add any other files to the running environment by using the supportingScriptUris array property array. It could be a configuration file, data, module files. 

```json
"supportingScriptUris":[
      "https://testomcdepscript001.blob.core.windows.net/scripts/data.json?sv=2019-10-10&ss=b&srt=sco&sp=r&se=2020-09-13T21:51:25Z&st=2020-07-13T13:51:25Z&spr=https&sig=XXXXXXX"
      ],
``` 

The script from primaryScriptUri or scriptContent and files from the supportingScriptUris are downloaded into a file share, with an autogenerated name, in the storage account used by the deploymentScripts resources. Files are stored in a folder named azscriptinput.  
But keep in mind that there is a 2 Gib quota for the file share. 

The deploymentScripts resource creates another folder in the file share, azscriptoutput, where the execution result and any output are stored. 
The file share is mounted in the container instance on /mnt/azscripts/. You can also access this share to store any output. But remember that the storage account and the Azure Container instance will be deleted by default after the script execution. 
To store the script result you can use the DeploymentScriptOutputs variable. This variable is used by the resource to create the scriptoutputs.json file in the azscriptoutput. 

For example, in PowerShell 
```powershell
param(
    [string] 
    $env,

    [string]
    $name
    )
    
$output = "Setting up deployment name $($name) in env $($envs)"
Write-Output $output
$DeploymentScriptOutputs = @{}
$DeploymentScriptOutputs['text'] = $output
$DeploymentScriptOutputs['date'] = (get-date -Format FileDate).toString()
```
will give you in the output file. 

```json
{"text":"Hello from Deployment Scripts ","date":"20200714"}
``` 

The deploymentScripts resource allows you to do tasks impossible to achieve with ARM templates alone. You can run complex scripts and workflows. You can use the resource to copy data, create certificates, migrate databases, …. there is no real limitation. But remember the goals of deploymentScripts is to perform actions impossible to do with ARM and not to replace ARM code. You should not use it to deploy a VM or anything you can do with ARM.