SQL Database Backup on IaaS using Azure Automation

I had a need to take a full SQL Database backup from a virtual machine with SQL Server hosted on Azure. This is done via an Azure Automation account, executing a runbook on a hybrid worker. This is a great way to take a offline copy of your production SQL and store it someplace safe.

To accomplish this we will use the PowerShell module ‘sqlps‘ that should be installed with SQL Server and run the command Backup-SqlDatabase.

Backup-SqlDatabase (SqlServer) | Microsoft Docs

Store SQL Storage Account Credentials

Before we can run the Backup-SqlDatabase command we must have a saved credential stored in SQL for the Storage Account using New-SqlCredential.

New-SqlCredential (SqlServer) | Microsoft Docs

Import-Module sqlps
# set parameters
$sqlPath = "sqlserver:\sql\$($env:COMPUTERNAME)"
$storageAccount = "<storageAccountName>"  
$storageKey = "<storageAccountKey>"  
$secureString = ConvertTo-SecureString $storageKey -AsPlainText -Force  
$credentialName = "azureCredential-"+$storageAccount

Write-Host "Generate credential: " $credentialName
  
#cd to sql server and get instances  
cd $sqlPath
$instances = Get-ChildItem

#loop through instances and create a SQL credential, output any errors
foreach ($instance in $instances)  {
    try {
        $path = "$($sqlPath)\$($instance.DisplayName)\credentials"
        New-SqlCredential -Name $credentialName -Identity $storageAccount -Secret $secureString -Path $path -ea Stop | Out-Null
        Write-Host "...generated credential $($path)\$($credentialName)."  }
    catch { Write-Host $_.Exception.Message } }

Backup SQL Databases with an Azure Runbook

The runbook below works on the DEFAULT instance and excludes both tempdb and model from backup.

Import-Module sqlps
$sqlPath = "sqlserver:\sql\$($env:COMPUTERNAME)"
$storageAccount = "<storageAccount>"  
$blobContainer = "<containerName>"  
$backupUrlContainer = "https://$storageAccount.blob.core.windows.net/$blobContainer/"  
$credentialName = "azureCredential-"+$storageAccount
$prefix = Get-Date -Format yyyyMMdd

Write-Host "Generate credential: " $credentialName

Write-Host "Backup database: " $backupUrlContainer
  
cd $sqlPath
$instances = Get-ChildItem

#loop through instances and backup all databases (excluding tempdb and model)
foreach ($instance in $instances)  {
    $path = "$($sqlPath)\$($instance.DisplayName)\databases"
    $databases = Get-ChildItem -Force -Path $path | Where-object {$_.name -ne "tempdb" -and $_.name -ne "model"}

    foreach ($database in $databases) {
        try {
            $databasePath = "$($path)\$($database.Name)"
            Write-Host "...starting backup: " $databasePath
            $fileName = $prefix+"_"+$($database.Name)+".bak"
            $destinationBakFileName = $fileName
            $backupFileURL = $backupUrlContainer+$destinationBakFileName
            Write-Host "...backup URL: " $backupFileURL
            Backup-SqlDatabase -Database $database.Name -Path $path -BackupFile $backupFileURL -SqlCredential $credentialName -Compression On 
            Write-Host "...backup complete."  }
        catch { Write-Host $_.Exception.Message } } }


NOTE: You will notice a performance hit on the SQL Server so schedule this runbook in a maintanence window.


Deploy Craft CMS with Azure App Service for Linux Containers

Here is some key points to deploy a Craft CMS installation on Azure Web App using container images. In this blog we will step you through some of the modifications needed to make the container image run in Azure and the deployment steps to run in an Azure DevOps Pipeline.

CraftCMS have reference material for their docker deployments found here:
GitHub – craftcms/docker: Craft CMS Docker images

Components

The components required are:

  • Azure Web App for Linux Containers
  • Azure Database for MySQL
  • Azure Storage Account
  • Azure Front Door with WAF
  • Azure Container Registry

Custom Docker Image

To make this work in an Azure Web App we have to do the following additional steps:

  • Install OpenSSH & Enable SSH daemon on 2222 at startup
  • Set the password for root to “Docker!”
  • Install the Azure Database for MySQL root certificates for SSL connections from the Container

We do this in the Dockerfile. We are customizing the NGINX implementation of CraftCMS to allow for the front end to service the HTTP/HTTPS requests from the App Service.

# composer dependencies
FROM composer:1 as vendor
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer install --ignore-platform-reqs --no-interaction --prefer-dist

FROM craftcms/nginx:7.4
# Install OpenSSH and set the password for root to "Docker!". In this example, "apk add" is the install instruction for an Alpine Linux-based image.
USER root
RUN apk add openssh sudo \
     && echo "root:Docker!" | chpasswd 
# Copy the sshd_config file to the /etc/ directory
COPY sshd_config /etc/ssh/
COPY start.sh /etc/start.sh
COPY BaltimoreCyberTrustRoot.crt.pem /etc/BaltimoreCyberTrustRoot.crt.pem 
RUN ssh-keygen -A
RUN addgroup sudo
RUN adduser www-data sudo
RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

# the user is `www-data`, so we copy the files using the user and group
USER www-data
COPY --chown=www-data:www-data --from=vendor /app/vendor/ /app/vendor/
COPY --chown=www-data:www-data . .

EXPOSE 8080 2222
ENTRYPOINT ["sh", "/etc/start.sh"]

The corresponding ‘start.sh’

#!/bin/bash
sudo /usr/sbin/sshd &
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisor.conf

Build the Web App

The Azure Web App resource is deployed using a ARM template. Here is a snippet of the template, the key is to have your environment variables defined:

{
            "comments": "This is the docker web app running craftcms/custom Docker image",
            "type": "Microsoft.Web/sites",
            "name": "[parameters('siteName')]",
            "apiVersion": "2020-06-01",
            "location": "[parameters('location')]",
            "tags": "[parameters('tags')]",
            "dependsOn": [
                "[variables('hostingPlanName')]",
                "[variables('databaseName')]"
            ],
            "properties": {
                "siteConfig": {
                    "appSettings": [
                        {
                            "name": "DOCKER_REGISTRY_SERVER_URL",
                            "value": "[reference(variables('registryResourceId'), '2019-05-01').loginServer]"
                        },
                        {
                            "name": "DOCKER_REGISTRY_SERVER_USERNAME",
                            "value": "[listCredentials(variables('registryResourceId'), '2019-05-01').username]"
                        },
                        {
                            "name": "DOCKER_REGISTRY_SERVER_PASSWORD",
                            "value": "[listCredentials(variables('registryResourceId'), '2019-05-01').passwords[0].value]"
                        },
                        {
                            "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
                            "value": "false"
                        },
                        {
                            "name": "DB_DRIVER",
                            "value": "mysql"
                        },
                        {
                            "name": "DB_SERVER",
                            "value": "[reference(resourceId('Microsoft.DBforMySQL/servers',variables('serverName'))).fullyQualifiedDomainName]"
                        },
                        {
                            "name": "DB_PORT",
                            "value": "3306"
                        },
                        {
                            "name": "DB_DATABASE",
                            "value": "[variables('databaseName')]"
                        },
                        {
                            "name": "DB_USER",
                            "value": "[variables('databaseUserName')]"
                        },
                        {
                            "name": "DB_PASSWORD",
                            "value": "[parameters('administratorLoginPassword')]"
                        },
                        {
                            "name": "DB_SCHEMA",
                            "value": "public"
                        },
                        {
                            "name": "DB_TABLE_PREFIX",
                            "value": ""
                        },
                        {
                            "name": "SECURITY_KEY",
                            "value": "[parameters('cmsSecurityKey')]"
                        },
                        {
                            "name": "WEB_IMAGE",
                            "value": "[parameters('containerImage')]"
                        },
                        {
                            "name": "WEB_IMAGE_PORTS",
                            "value": "80:8080"
                        }

                    ],
                    "linuxFxVersion": "[variables('linuxFxVersion')]",
                    "scmIpSecurityRestrictions": [
                        
                    ],
                    "scmIpSecurityRestrictionsUseMain": false,
                    "minTlsVersion": "1.2",
                    "scmMinTlsVersion": "1.0"
                },
                "name": "[parameters('siteName')]",
                "serverFarmId": "[variables('hostingPlanName')]",
                "httpsOnly": true      
            },
            "resources": [
                {
                    "apiVersion": "2020-06-01",
                    "name": "connectionstrings",
                    "type": "config",
                    "dependsOn": [
                        "[resourceId('Microsoft.Web/sites/', parameters('siteName'))]"
                    ],
                    "tags": "[parameters('tags')]",
                    "properties": {
                        "dbstring": {
                            "value": "[concat('Database=', variables('databaseName'), ';Data Source=', reference(resourceId('Microsoft.DBforMySQL/servers',variables('serverName'))).fullyQualifiedDomainName, ';User Id=', parameters('administratorLogin'),'@', variables('serverName'),';Password=', parameters('administratorLoginPassword'))]",
                            "type": "MySQL"
                        }
                    }
                }
            ]
        },

All other resources should be ARM defaults. No customisation required. Either put them all in a single ARM template or seperate them out on their own. Your choice to be creative.

Build Pipeline

The infrastructure build pipeline looks something like the below:

# Infrastructure pipeline
trigger: none

pool:
  vmImage: 'windows-2019'
variables:
  TEMPLATEURI: 'https://storageAccountName.blob.core.windows.net/templates/portal/'
  CMSSINGLE: 'singleCraftCMSTemplate.json'
  CMSSINGLEPARAM: 'singleCraftCMSTemplate.parameters.json'
  CMSFILEREG: 'ContainerRegistry.json'
  CMSFRONTDOOR: 'frontDoor.json'
  CMSFILEREGPARAM: 'ContainerRegistry.parameters.json'
  CMSFRONTDOORPARAM: 'frontDoor.parameters.json'
  LOCATION: 'Australia East'
  SUBSCRIPTIONID: ''
  AZURECLISPID: ''
  TENANTID: ''
  RGNAME: ''
  TOKEN: ''
  ACS : 'registryName.azurecr.io'
resources:
  repositories:
    - repository: coderepo
      type: git
      name: Project/craftcms
stages:
- stage: BuildContainerRegistry
  displayName: BuildRegistry
  jobs:
  - job: BuildContainerRegistry
    displayName: Azure Git Repository
    pool:
      vmImage: 'windows-latest'
    steps:
    - task: CopyFiles@2
      name: copyToBuildHost
      displayName: 'Copy files to the build host for execution'
      inputs:
        Contents: '**'
        TargetFolder: '$(Build.ArtifactStagingDirectory)'
    - task: AzureFileCopy@4
      inputs:
        SourcePath: '$(Build.Repository.LocalPath)\CMS\template\*'
        azureSubscription: ''
        Destination: 'AzureBlob'
        storage: ''
        ContainerName: 'templates'
        BlobPrefix: portal
        AdditionalArgumentsForBlobCopy: --recursive=true
    - task: AzureResourceManagerTemplateDeployment@3
      displayName: "Deploy Azure ARM template for Azure Container Registry"
      inputs:
        deploymentScope: 'Resource Group'
        azureResourceManagerConnection: 'azureDeployCLI-SP'
        subscriptionId: '$(SUBSCRIPTIONID)'
        action: 'Create Or Update Resource Group'
        resourceGroupName: '$(RGNAME)'
        location: '$(LOCATION)'
        templateLocation: 'URL of the file'
        csmFileLink: '$(TEMPLATEURI)$(CMSFILEREG)$(TOKEN)' 
        csmParametersFileLink: '$(TEMPLATEURI)$(CMSFILEREGPARAM)$(TOKEN)' 
        deploymentMode: 'Incremental'
    - task: AzurePowerShell@5
      displayName: 'Import the public docker images to the Azure Container Repository'
      inputs:
        azureSubscription: 'azureDeployCLI-SP'
        ScriptType: 'FilePath'
        ScriptPath: '$(Build.ArtifactStagingDirectory)\CMS\template\dockerImages.ps1'
        errorActionPreference: 'silentlyContinue'
        azurePowerShellVersion: 'LatestVersion'

- stage: BuildGeneralImg
  dependsOn: BuildContainerRegistry
  displayName: BuildImages
  jobs:
  - job: BuildCraftCMSImage
    displayName: General Docker Image
    pool:
      vmImage: 'ubuntu-18.04'
    steps:
    - checkout: self
    - checkout: coderepo
    - task: CopyFiles@2
      name: copyToBuildHost
      displayName: 'Copy files to the build host for execution'
      inputs:
        Contents: '**'
        TargetFolder: '$(Build.ArtifactStagingDirectory)'
    - task: Docker@2
      displayName: Build and push
      inputs:
        containerRegistry: ''
        repository: craftcms
        command: buildAndPush
        dockerfile: 'craftcms/Dockerfile'
        tags: |
          craftcms
          latest

- stage: Deploy 
  dependsOn: BuildGeneralImg
  displayName: DeployWebService
  jobs:
  - job:
    displayName: ARM Templates
    pool:
      vmImage: 'windows-latest'
    steps:
    - checkout: self
    - checkout: coderepo
    - task: CopyFiles@2
      name: copyToBuildHost
      displayName: 'Copy files to the build host for execution'
      inputs:
        Contents: '**'
        TargetFolder: '$(Build.ArtifactStagingDirectory)'
    
    - task: AzureResourceManagerTemplateDeployment@3
      displayName: "Deploy Azure ARM single template for remaining assets"
      inputs:
        deploymentScope: 'Resource Group'
        azureResourceManagerConnection: ''
        subscriptionId: '$(SUBSCRIPTIONID)'
        action: 'Create Or Update Resource Group'
        resourceGroupName: '$(RGNAME)'
        location: '$(LOCATION)'
        templateLocation: 'URL of the file'
        csmFileLink: '$(TEMPLATEURI)$(CMSSINGLE)$(TOKEN)' 
        csmParametersFileLink: '$(TEMPLATEURI)$(CMSSINGLEPARAM)$(TOKEN)' 
        deploymentMode: 'Incremental'

- stage: Secure 
  dependsOn: Deploy
  displayName: DeployFrontDoor
  jobs:
  - job:
    displayName: ARM Templates
    pool:
      vmImage: 'windows-latest'
    steps:
    - task: CopyFiles@2
      name: copyToBuildHost
      displayName: 'Copy files to the build host for execution'
      inputs:
        Contents: '**'
        TargetFolder: '$(Build.ArtifactStagingDirectory)'
    
    - task: AzureResourceManagerTemplateDeployment@3
      displayName: "Deploy Azure ARM single template for Front Door"
      inputs:
        deploymentScope: 'Resource Group'
        azureResourceManagerConnection: ''
        subscriptionId: '$(SUBSCRIPTIONID)'
        action: 'Create Or Update Resource Group'
        resourceGroupName: '$(RGNAME)'
        location: '$(LOCATION)'
        templateLocation: 'URL of the file'
        csmFileLink: '$(TEMPLATEURI)$(CMSFRONTDOOR)$(TOKEN)' 
        csmParametersFileLink: '$(TEMPLATEURI)$(CMSFRONTDOORPARAM)$(TOKEN)' 
        deploymentMode: 'Incremental'
    - task: AzurePowerShell@5
      displayName: 'Apply Front Door service tags to Web App ACLs'
      inputs:
        azureSubscription: 'azureDeployCLI-SP'
        ScriptType: 'FilePath'
        ScriptPath: '$(Build.ArtifactStagingDirectory)\CMS\template\enableFrontDoorOnWebApp.ps1'
        errorActionPreference: 'silentlyContinue'
        azurePowerShellVersion: 'LatestVersion'    

Enable Front Door with WAF

The pipeline stage DeployFrontDoor has an enableFrontDoorOnWebApp.ps1

$azFrontDoorName = ""
$webAppName = ""
$resourceGroup = ""

Write-Host "INFO: Restrict access to a specific Azure Front Door instance"
try{
    $afd = Get-AzFrontDoor -Name $azFrontDoorName -ResourceGroupName $resourceGroup
}
catch{
    Write-Host "ERROR: $($_.Exception.Message)"
}

Write-Host "INFO: Setting the IP ranges defined in the AzureFrontDoor.Backend service tag to the Web App"
try{
    Add-AzWebAppAccessRestrictionRule -ResourceGroupName $resourceGroup -WebAppName $webAppName -Name "Front Door Restrictions" -Priority 100 -Action Allow -ServiceTag AzureFrontDoor.Backend -HttpHeader @{'x-azure-fdid' = $afd.FrontDoorId}}
catch{
    Write-Host "ERROR: $($_.Exception.Message)"
}


You should now have a CraftCMS web app that is only available through the FrontDoor URL.

Continuous Deployment

There are many ways to deploy updates to your website, an Azure Web App has a beautiful thing called slots that can be used.

# Trigger on commit
# Build and push an image to Azure Container Registry
# Update Web App Slot

trigger:
  branches:
    include:
      - main
  paths:
    exclude:
      - pipelines
      - README.md
  batch: true

resources:
- repo: self

pool:
  vmImage: 'windows-2019'
variables:
  TEMPLATEURI: 'https://storageAccountName.blob.core.windows.net/templates/portal/'
  LOCATION: 'Australia East'
  SUBSCRIPTIONID: ''
  RGNAME: ''
  TOKEN: ''
  SASTOKEN: ''
  TAG: '$(Build.BuildId)'
  CONTAINERREGISTRY: 'registryName.azurecr.io'
  IMAGEREPOSITORY: 'craftcms'
  APPNAME: ''

stages:
- stage: BuildImg
  displayName: BuildLatestImage
  jobs:
  - job: BuildCraftCMSImage
    displayName: General Docker Image
    pool:
      vmImage: 'ubuntu-18.04'
    steps:
    - checkout: self
    - task: CopyFiles@2
      name: copyToBuildHost
      displayName: 'Copy files to the build host for execution'
      inputs:
        Contents: '**'
        TargetFolder: '$(Build.ArtifactStagingDirectory)'
    - task: Docker@2
      displayName: Build and push
      inputs:
        containerRegistry: ''
        repository: $(IMAGEREPOSITORY)
        command: buildAndPush
        dockerfile: 'Dockerfile'
        tags: |
          $(IMAGEREPOSITORY)
          $(TAG)


- stage: UpdateApp 
  dependsOn: BuildImg
  displayName: UpdateTestSlot
  jobs:
  - job:
    displayName: 'Update Web App Slot'
    pool:
      vmImage: 'windows-latest'
    steps:
    - task: AzureWebAppContainer@1
      displayName: 'Update Web App Container Image Reference' 
      inputs:
        azureSubscription: ''
        appName: $(APPNAME)
        containers: $(CONTAINERREGISTRY)/$(IMAGEREPOSITORY):$(TAG)
        deployToSlotOrASE: true
        resourceGroupName: $(RGNAME)
        slotName: test




Azure Migrate – Additional Firewall Rules

When deploying Azure Migrate Appliances to discovery servers, the appliance needs outbound internet access. In many IT environments, servers are disallowed internet access unless prescribed to certain URL sets. Gratefully Microsoft have given us a list of what they think is the list of URLs that the appliance will need to have whitelisted. This can be found here:

https://docs.microsoft.com/en-us/azure/migrate/migrate-appliance#public-cloud-urls

Issue

Once the appliance has booted up and your onto the GUI, you must ener your Azure Migrate Project key from your subscription and then authenticate to your subscription. We entailed the following error when attempting to resolve the initial key:

Azure Migrate Error

Failed to connect to the Azure Migrate project. Check the errors details, follow the remediation steps and click on ‘Retry’ button

The Azure Migrate Key doesn’t have an expiration on it so this wasn’t the issue. We had whitelisted the URL‘s but on the firewall we were seeing dropped packets:

13:40:41Default DROPTCP10.0.0.10:50860204.79.197.219:443
13:40:41Default DROPTCP10.0.0.10:50861204.79.197.219:80
13:40:41Default DROPTCP10.0.0.10:50857152.199.39.242:443
13:40:42Default DROPTCP10.0.0.10:50862204.79.197.219:80
13:40:42Default DROPTCP10.0.0.10:50858104.74.50.201:80
13:40:43Default DROPTCP10.0.0.10:5086352.152.110.14:443
13:40:44Default DROPTCP10.0.0.10:50860204.79.197.219:443
13:40:44Default DROPTCP10.0.0.10:50861204.79.197.219:80
13:40:45Default DROPTCP10.0.0.10:50862204.79.197.219:80
13:40:46Default DROPTCP10.0.0.10:5086352.152.110.14:443
13:40:46Default DROPTCP10.0.0.10:50859204.79.197.219:443
13:40:47Default DROPTCP10.0.0.10:5086440.90.189.152:443
13:40:47Default DROPTCP10.0.0.10:5086552.114.36.3:443
13:40:49Default DROPTCP10.0.0.10:5086440.90.189.152:443
13:40:50Default DROPTCP10.0.0.10:5086552.114.36.3:443
13:40:50Default DROPTCP10.0.0.10:50860204.79.197.219:443
13:40:50Default DROPTCP10.0.0.10:50861204.79.197.219:80
13:40:51Default DROPTCP10.0.0.10:50862204.79.197.219:80
13:40:52Default DROPTCP10.0.0.10:5086352.152.110.14:443
Subset of the dropped packets based on IP destination during connection failure

Reviewing the SSL certificates on these IP addresses, they are all Microsoft services with multiple SAN entries. We also had a look at the traffic from the developer tools in the browser:

We can see that the browser is trying to start a AAD workflow for device login, which is articulated in the onboarding documentation. Our issue was that the JavaScript for inside the browser session wasn’t located in the whitelist URLs. Reviewing the SAN entries in the certificates presented in the IP destination table we looked for ‘CDN’ or ‘Edge’ URLs.

The fix

The following URLs were added to the whitelist group for the appliance and problems went away.

204.79.197.219*.azureedge.net
152.199.39.242*.azureedge.net
152.199.39.242*.wpc.azureedge.net
152.199.39.242*.wac.azureedge.net
152.199.39.242*.adn.azureedge.net
152.199.39.242*.fms.azureedge.net
152.199.39.242*.azureedge-test.net
152.199.39.242*.ec.azureedge.net
152.199.39.242*.wpc.ec.azureedge.net
152.199.39.242*.wac.ec.azureedge.net
152.199.39.242*.adn.ec.azureedge.net
152.199.39.242*.fms.ec.azureedge.net
152.199.39.242*.aspnetcdn.com
152.199.39.242*.azurecomcdn.net
152.199.39.242cdnads.msads.net


Pimp my VS Code

Those who know me, know that I have a keen interest in software tools and exploring the various different ways that people use them. I take great joy in exploring custom or 3rd party plugins and add-ons to get the most out of the tools I use every day. From OS automation tools (like BetterTouchTool) to custom screen savers (Brooklyn is my current favourite), I love it all.

On a good day, I spend quite a bit of time in Visual Studio Code, my IDE of choice. VS Code has all that you need right out of the box, but why stop there? Heres a list of some of my favourite VS Code Extensions that I now consider essential when doing a fresh install.

Indent-Rainbow and Bracket Pair Colorizer 2 are must installs for me. Both really simple, change colours of indents and brackets so you can easily see them at a glance. Always useful when working with ident heavy languages like YAML.

GitLense is another essential if you are working with Git repositories. GitLense integrates lots of various Git tools and information into the editor. My favourite feature of GitLense is the current line blame, you can see it in the screenshot above which shows an unobtrusive annotation at the end of each line as you select it. The annotation shows commit information for that piece of code.

Beautify helps you make your code beautiful. Beautify can automatically indent Javascript, JSON, CSS, and HTML.

Better Comments makes your comments human readable by changing the colour of comments based on an opening tag. You can even define your own.

Source: Better Comments Documentation in Visual Studio Code

Next up, some extensions that I install to match the work I’m doing. In my day to day work, I’m regularly authoring infrastructure templates for Azure and AWS (ARM and CloudFormation). To assist with making this as simple as possible I install some specific extensions for syntax highlighting, autocompletion and even do some code snippet referencing.

Azure Resource Manager (ARM) Tools is a collection of extensions for working with Azure made by Microsoft. This one has lots of features so I’ll just pick a few. You can use the ‘arm!’ shortcut to create a blank ARM template with all the property you need – this one makes life so much better, spend less time lining up brackets in JSON and more time defining resources!

Image showing the arm template scaffolding snippet
Source: Azure Resource Manager (ARM) Tools Documentation in Visual Studio Code

Each time you use a snippet, you can also use tab complete to go through commonly modified configurations, again, less time reading documentation more time writing code!

Image showing snippet tab stops with pre-populated parameter values
Source: Azure Resource Manager (ARM) Tools Documentation in Visual Studio Code

CloudFormation Template Linter and CloudFormation Resource Snippets add some similar functionality for working with AWS CloudFormation templates. While neither of these are created by Amazon, they both do a good job at implementing similar functionality to the above ARM tools.

Next up is one of my new favourites, Dash, sorry Windows guru’s this one’s only on Mac. Dash is an API documentation browser which can hook into your VS code to quickly search documentation (from their 200+ built in doc sets, or add your own GitHub doc sets). Sounds boring, but I think it’s far from it. I’ve loaded mine up with lots of Microsoft Azure Documentation and AWS documentation. It’s really handy to be able to highlight a resource type or PowerShell Command, hit control + H and have the document reference instantly pop up, each time it saves me minutes.

Dash - Visual Studio Marketplace
Source: Dash Documentation in Visual Studio Code

Finally, my icon and colour theme I use VSCode Icons and Atom One Dark. This really comes down to personal preference. I like the syntax colour coding included in the Atom One Dark theme, I find it useful especially when writing PowerShell. VSCode icons is the most popular icons extension, and I’ve had no issues since installation.

Source: Atom One Dark Theme Documentation in Visual Studio Code

Thats my round up for my must have extensions. Are there any missing off this list that you think should be here? – Comment below with your must have extensions.

Cheers, Joel


Understanding Undocumented ARM Oddities

Over the past year I’ve been working pretty heavily with Azure Resource Manager (ARM) templates to create safe, reusable and consistent deployments of virtual infrastructure. When producing ARM templates, it’s important to understand what resource types are available, and what values to use in your template. I always use the Azure Template Reference to understand exactly how to define a certain resource type. However, sometimes you will run into situations where the Azure Template Reference is missing something that can be done in the Azure portal. So, how do we figure out how to template it if it’s not in the reference documentation?

Export Templates – Perhaps the quickest way to solve this problem is to use the native ‘Export Template’ blade in the Azure portal. For this, you will need to deploy your resource and configure it as you would like, using the Azure Portal. Once you have your resource ready, open the Export Template blade on your resource. This will create an automatically generated ARM template based on the current running state of your resource. From here, you can inspect the generated template and see if your undocumented settings or configuration has been captured in the generated template.

Download template

Azure Resource Explorer – Next stop is the Azure Resource Explorer which provides a visual interface for you to examine the Azure API’s. With the Azure Resource Explorer, you can explore the current running state of an Azure environment in JSON format. This can be very useful when attempting to reverse engineer an existing resource or environment. While Azure Resource Explorer isn’t returning data that can be directly used in an ARM template, it can be used as a mechanism to learn the syntax of resource properties that are missing from the Azure Template Reference.

This image has an empty alt attribute; its file name is 13nov1.png

When issues are encountered with undocumented resources, typically the fastest way to resolve the issue is by manually deploying the resources using the Azure Portal (clicky clicky) then reverse engineering the ARM template with a combination of using the Export Templates function and Azure resource explorer. Going down the route of doing everything in ARM templates, can lead to a lot of trial and error before getting a fully automated deployment, for now at-least.

Cheers,

Joel