综合技术

Deploying Agents to Azure IaaS VMs using the Custom Script Extension

微信扫一扫,分享到朋友圈

Deploying Agents to Azure IaaS VMs using the Custom Script Extension
0

In an ideal world organizations should try to avoid creating custom images with their own special agents and configurations. This means a lot of image management as each time an agent is updated the image has to be updated in addition to the normal patching of OS instances. The Azure marketplace has a large number of OS images that are kept up-to-date which should be used if possible and any customization performed on top of that. I recently had a Proof of Concept where a number of agents needed to be deployed post VM deployment along with other configurations. Items such as domain join can be done with the domain join extension but for the other agent installs we decided to use the Custom Script Extension to call a bootstrap script which would do nothing other than pull down all content from a certain container using azcopy.exe and then launch a master script. The master script would be part of the downloaded content and would then perform all the silent installations and customization’s required.

A storage account is utilized with two containers:

  • Artifacts – This contains the master script and all the agent installers etc. This could use a zip file to enable a structure to be maintained of the various agents and the master script could unzip at the start
  • Bootstrap – This contains azcopy.exe (in my case version 10) and the bootstrap.ps1 file that does nothing other than call azcopy to copy everything from the artifacts container to the c: root, then launch the master script from the local copy

Below is my example bootstrap.ps1 file. Notice it has one parameter, the URI of the container which will be the shared access signature enabling access.

param($SourceURI)
 
$cmd = "&`".azcopy.exe`" cp `"$SourceURI`" `"C:`" --recursive=true"
try {
    Invoke-Expression -Command $cmd -ErrorAction Stop
    if ($LASTEXITCODE -ne 0) {
        # Error.
    }
} catch {
    # Error.
}
 
c:artifactsMainScript.ps1

Azcopy.exe was downloaded from https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-v10
and copied to the bootstrap container along with the bootstrap.ps1 file. In my case there is nothing sensitive in the file and so I made the container public
. This would avoid having to have an access key as part of my ARM template that would ultimately call this script.

All the installers and the master script were uploaded to the artifacts container. For this container I wanted a shared access signature (SAS) that would give read and list rights. The idea would be some automation would generate a new SAS each week and write to a secret in key vault that only people that should deploy had access to. The SAS would have a lifetime of 2 weeks to have an overlap with the newly generated. In addition to generating and storing the complete SAS I needed a second version that was escaped for cmd.exe. This is because the SAS has & in it which was being interpreted during my testing breaking its use. I tried to use no parse (–%) but this did not work since it was being called by cmd.exe therefore the escape is to use ^&. The script below generates the SAS and the escaped SAS and writes both versions as secrets to key vault.

$resourceGroupName = "RG-Infra-SCUS"
$storageAccountName = "sasavpocscus"
$containerName = "artifacts"
 
# Get the access key for the Azure Storage account
$storageAccountKey = (Get-AzStorageAccountKey `
    -ResourceGroupName $resourceGroupName `
    -Name $storageAccountName)[0].Value
 
# Create an Azure Storage context
$storageContext = New-AzStorageContext `
    -StorageAccountName $storageAccountName `
    -StorageAccountKey $storageAccountKey
 
$expiryTime = (Get-Date).AddDays(14)
 
# Generates an SAS token for the Azure storage container
$SASToken = New-AzStorageContainerSASToken `
    -Name $containerName `
    -Permission "rl" `
    -ExpiryTime $expiryTime `
    -Context $storageContext
 
$fullSASURI = "$($storageContext.BlobEndPoint)$($containerName)$($SASToken)"
$fullSASURI
 
$secretvalue = ConvertTo-SecureString $fullSASURI -AsPlainText -Force
$secret = Set-AzKeyVaultSecret -VaultName 'SavKeyVault' -Name 'POCBLOBSASURI' -SecretValue $secretvalue
 
#Need an escaped for CMD since this is how passed when using the Custom Script Extension in Azure
$secretescapevalue = ConvertTo-SecureString $BLOBSASURI.Replace("&","^&") -AsPlainText -Force
$secret = Set-AzKeyVaultSecret -VaultName 'SavKeyVault' -Name 'POCBLOBSASURIEsc' -SecretValue $secretescapevalue

Once this was done I now had a SAS available in the key vault that would give read and list to the artifacts container. A test of this process to my local machine worked, i.e.

$BLOBSASURI = (Get-AzKeyVaultSecret -vaultName 'SavKeyVault' -Name 'POCBLOBSASURI').SecretValueText
.bootstrap.ps1 $BLOBSASURI

Next I tried calling as I would via the Custom Script Extension which with the escaped version worked great (note its the escaped URL as this will get expanded in the template).

powershell.exe -ExecutionPolicy Unrestricted -Command .BootStrap.ps1 -SourceURI 'https://sasavpocscus.blob.core.windows.net/artifacts?sv=2018-11-09^&sr=c^&sig=blahblahD^&se=2019-05-30T14%3A21%3A50Z^&sp=rl'

Initially my test was to an existing Azure VM so I used the following (note I’m getting the escaped version of the secret from Key Vault):

$BLOBSASURI = (Get-AzKeyVaultSecret -vaultName 'SavKeyVault' -Name 'POCBLOBSASURIEsc').SecretValueText
 
#Container has public access since no sensitive information in files therefore no account or key required in private configuration
$fileUri = @("https://sasavpocscus.blob.core.windows.net/bootstrap/BootStrap.ps1","https://sasavpocscus.blob.core.windows.net/bootstrap/azcopy.exe")
 
$ExtensionName = 'BootStrap'
$ExtensionType = 'CustomScriptExtension'
$Publisher = 'Microsoft.Compute'
$Version = '1.9'
$timestamp = (Get-Date).Ticks
 
$PrivateConfiguration = @{"commandToExecute" = "powershell.exe -ExecutionPolicy Unrestricted -Command .BootStrap.ps1 -SourceURI '$BLOBSASURI'"}
$PublicConfiguration = @{"fileUris" = $fileUri;"timestamp" = "$timestamp"}
 
Set-AzVMExtension -ResourceGroupName RG-Infra-SCUS -Location southcentralus -VMName Savazuusscwin10 `
    -Name $ExtensionName -Publisher $Publisher -ExtensionType $ExtensionType -TypeHandlerVersion $Version `
    -Settings $PublicConfiguration -ProtectedSettings $PrivateConfiguration

Once this worked I finally created an ARM template that included a reference to the secret and all worked as planned.

The parameter file (note I also get a secret to join the domain even though I’m not using the domain join extension in this example):

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "adminUsername": {
            "value": "demoadmin"
        },
        "adminPassword": {
            "value": "Pa55wordPa55word"
        },
        "virtualMachineSize": {
            "value": "Standard_DS2_v2"
          },
          "virtualNetworkName": {
            "value": "VNet-Infra-EUS"
          },
          "subnetName": {
            "value": "TestSub"
          },
          "virtualNetworkResourceGroup": {
            "value": "RG-Infra-EUS"
          },
          "domainAdminUserName": {
            "value": "savilltech\savadmin"
          },
          "domainAdminPassword": {
            "reference": {
              "keyVault": {
                "id": "/subscriptions/414a658a207/resourceGroups/RG-Infra-SCUS/providers/Microsoft.KeyVault/vaults/SavKeyVault"
              },
              "secretName": "savadminpass"
            }
          },
          "domainFQDN": {
            "value": "savilltech.net"
          },
          "artifactURI": {
            "reference": {
                "keyVault": {
                    "id": "/subscriptions/414a658a207/resourceGroups/RG-Infra-SCUS/providers/Microsoft.KeyVault/vaults/SavKeyVault"
                },
                "secretName": "POCBLOBSASURIEsc"
            }
        }
   }
}

The actual template (note in the CSE extension at the end I need the single quotes around the URI or it once again tries to interpret it so you have to use two, i.e. ”, to get one ‘ when it actually executes):

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "adminUsername": {
        "type": "string",
        "metadata": {
          "description": "Username for the Virtual Machine."
        }
      },
      "adminPassword": {
        "type": "securestring",
        "metadata": {
          "description": "Password for the Virtual Machine."
        }
      },
      "vmName": {
        "type": "string",
        "defaultValue": "myVM",
        "metadata": {
          "description": "Name of the VM"
        }
      },
      "virtualMachineSize": {
        "type": "string",
        "defaultValue": "Standard_DS2_v2",
        "allowedValues": [
          "Standard_DS2_v2",
          "Standard_M8-2ms",
          "Standard_M8-4ms"
        ],
        "metadata": {
          "description": "Virtual Machine Size"
        }
      },
      "domainAdminUserName": {
        "defaultValue": "savilltech\savadmin",
        "type": "string",
        "metadata": {
          "description": "Administrator Username for the domain join account"
        }
      },
      "domainAdminPassword": {
        "type": "securestring",
        "metadata": {
          "description": "Administrator password for the domain join account"
        }
      },
      "domainFQDN": {
        "type": "string",
        "metadata": {
          "description": "Domain FQDN where the virtual machine will be joined"
        }
      },
      "ouPath": {
        "type": "string",
        "defaultValue": "",
        "metadata": {
          "description": "Specifies an organizational unit (OU) for the domain account. Enter the full distinguished name of the OU in quotation marks. Example: "OU=testOU; DC=domain; DC=Domain; DC=com""
        }
      },
      "virtualNetworkName": {
        "type": "string",
        "metadata": {
          "description": "VNET Name"
        }
      },
      "virtualNetworkResourceGroup": {
        "type": "string",
        "metadata": {
          "description": "Resource Group VNET is deployed in"
        }
      },
      "subnetName": {
        "type": "string",
        "metadata": {
          "description": "Name of the subnet inside the VNET"
        }
      },
      "artifactURI": {
        "type": "securestring",
        "metadata": {
          "description": "URI for the artifact BLOB including SAS"
        }
      },
      "location": {
        "type": "string",
        "defaultValue": "[resourceGroup().location]",
        "metadata": {
          "description": "Location for all resources."
        }
      }
    },
    "variables": {
      "vnetID": "[resourceId(parameters('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]",
      "subnetRef": "[concat(variables('vnetID'),'/subnets/', parameters('subnetName'))]",
      "domainJoinOptions": 3,
      "nicName": "[concat(parameters('vmName'), 'Nic')]"
    },
    "resources": [
      {
        "apiVersion": "2018-04-01",
        "type": "Microsoft.Network/networkInterfaces",
        "name": "[variables('nicName')]",
        "location": "[parameters('location')]",
        "dependsOn": [],
        "properties": {
          "ipConfigurations": [
            {
              "name": "ipconfig1",
              "properties": {
                "privateIPAllocationMethod": "Dynamic",
                "subnet": {
                  "id": "[variables('subnetRef')]"
                }
              }
            }
          ]
        }
      },
      {
        "apiVersion": "2018-04-01",
        "type": "Microsoft.Compute/virtualMachines",
        "name": "[parameters('vmName')]",
        "location": "[parameters('location')]",
        "dependsOn": [
          "[variables('nicName')]"
        ],
        "properties": {
          "hardwareProfile": {
            "vmSize": "[parameters('virtualMachineSize')]"
          },
          "osProfile": {
            "computerName": "[parameters('vmName')]",
            "adminUsername": "[parameters('adminUsername')]",
            "adminPassword": "[parameters('adminPassword')]"
          },
          "storageProfile": {
            "imageReference": {
              "publisher": "MicrosoftWindowsServer",
              "offer": "WindowsServer",
              "sku": "2016-Datacenter",
              "version": "latest"
            },
            "osDisk": {
              "createOption": "FromImage"
            }
          },
          "networkProfile": {
            "networkInterfaces": [
              {
                "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
              }
            ]
          }
        },
        "resources": [
          {
            "type": "extensions",
            "name": "CustomScriptExtension",
            "apiVersion": "2017-03-30",
            "location": "[parameters('location')]",
            "dependsOn": [
              "[parameters('vmName')]"
            ],
            "properties": {
              "publisher": "Microsoft.Compute",
              "type": "CustomScriptExtension",
              "typeHandlerVersion": "1.9",
              "autoUpgradeMinorVersion": true,
              "settings": {
                "fileUris": [
                  "https://sasavpocscus.blob.core.windows.net/bootstrap/BootStrap.ps1","https://sasavpocscus.blob.core.windows.net/bootstrap/azcopy.exe"
                ],
                "commandToExecute": "[concat('powershell.exe -ExecutionPolicy Unrestricted -Command .\BootStrap.ps1 -SourceURI ''', parameters('artifactURI'),'''')]"
              }
            }
          }
        ]
      }
    ]
  }

And the execution (note the network and RG already existed in my environment).

New-AzResourceGroupDeployment -ResourceGroupName RG-Test-EUS `
    -TemplateFile 'vmdeploy.json' `
    -TemplateParameterFile 'vmdeploy.parameters.json'

Hope this helps!

阅读原文...


微信扫一扫,分享到朋友圈

Deploying Agents to Azure IaaS VMs using the Custom Script Extension
0

Avatar

What is WebAssembly?

上一篇

Information Measurement with SQL Server Part 4.5: The Squared L2 Family of Distances

下一篇

评论已经被关闭。

插入图片

热门分类

往期推荐

Deploying Agents to Azure IaaS VMs using the Custom Script Extension

长按储存图像,分享给朋友