Bootstrapping Linux VMs with DSC in Azure

Part 3: Automating it all together

Don Mills
SingleStone
11 min readMay 20, 2018

--

This is part three of a three part series on using DSC and Azure Automation to configure Linux.

You can find part one: “Getting started” here.

Part two: “Doing it in Azure” is located here.

In our last installment, we created an Azure Automation account, uploaded and compiled some Powershell DSC configuration code, stood up a Linux VM, and used Azure Automation DSC to configure it via the Portal.

But this time, it’s command line all the way.

Adding some sparkle to our configuration

So we’ve seen adding files to /tmp twice now, and it’s boring now, right? So let’s see if we can do something a bit more complicated.

I’m going to head back over to my Linux test box, and edit our configuration file…

root@ubuntuservtest:~# vi testlinuxdsc.ps1
root@ubuntuservtest:~# more testlinuxdsc.ps1
Configuration dsctestlinux{
Import-DscResource -ModuleName PSDesiredStateConfiguration,nxNode "TestDSCLinuxfile"{
nxFile ExampleFile
{
DestinationPath = "/tmp/example"
Contents = "hello world `n"
Ensure = "Present"
Type = "File"
}
}Node "dscwebserver"{ nxPackage apache2
{
Name = "apache2"
Ensure = "Present"
PackageManager = "apt"
}
$IndexPage = @'
<html>
<head>
<title>My DSC Page</title>
</head>
<body>
<H1>Awesome DSC Test Page in the house!</H1>
</body>
</html>
'@
nxFile index_html
{
DestinationPath = "/var/www/html/index.html"
Type = "file"
Contents = $IndexPage
DependsOn = "[nxPackage]apache2"
}
nxService apache2service
{
Name = "apache2"
State = "running"
Enabled = $true
Controller = "systemd"
DependsOn = "[nxFile]index_html"
}
}
}
root@ubuntuservtest:~#

As you can see, I’ve added another Node to the configuration, named dscwebserver. You probably recognize the nxFile from our previous installments as the nx module resource that creates files. But what’s the rest of it?

The nx module has ten types of built in resources. The two new ones are:

  1. nxPackage, which controls Linux package manager actions; and
  2. nxService, which controls Linux service manager actions.

What we are doing here is describing three configuration actions that happen in a series. First, we’ll use nxPackage to install the Apache webserver. Then we use nxFile to create an awesome test index.html page. Finally, we’ll make sure the Apache service is setup to start on boot time with nxService.

We use the DependsOn attribute to make sure our actions happen in the order we want. The test page won’t be created until the Apache package has been installed…and the service won’t be started until the test page is created. Using DependsOn allows you to set an order in which actions occur — otherwise the LCM (Local Configuration Manager) on the target node gets to pick. Usually it will try and do things in the order they appear in the configuration, but it’s good not to leave these things to chance. (Unless you like troubleshooting…)

Now that we’ve jazzed up our configuration, let’s get it into Azure Automation DSC.

Installing Azure Powershell on Linux

OK…if you remember our previous installments then you know our Powershell configuration won’t do anything for us until we compile it into the MOF files. Last time we did this via the Azure Portal, but as I said at the beginning: No More!

On my trusty Linux test box I’ll open Powershell and use the Azure Powershell cmdlets to accomplish the same task. This is how you would typically perform these actions in a pipeline or other automated fashion (there are Ruby and Python SDKs for Azure, but they don’t seem to have DSC or Azure Automation capabilities yet).

First, I’ll have to install the Azure Powershell cmdlets. However, since I’m on a Linux host, what I’m running is actually Powershell Core. So I need to do a different install method to get it working.

PS /root> Install-Module AzureRM.NetCore
PS /root> Import-Module AzureRM.Netcore
PS /root> Import-Module AzureRM.Profile.Netcore

Then I’ll need to log into Azure with my Powershell session:

PS /root> Connect-AzureRmAccount
WARNING: To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code ######### to authenticate.

I visit the URL there, and enter in the code provided. Now my Linux Powershell session is authenticated with Azure.

A sad and sorry tale

And here, dear readers, I must make a confession. I had completely planned to do this whole blog series without ever needing a Windows machine (even said I only needed them for games!).

But, in the process of doing the below steps on Linux, I rapidly crashed into this bug. (And here, first comment). Evidently the Linux Powershell Core Azure is not exactly in feature parity with the Windows mainline version. Now the bug report I linked there says that a fix should be available soon (in the month I am writing this) but currently it’s a no-go.

I thought about scrubbing the above section, and just pretending it never happened. But if a fix shows up soon, by the time you read this it may be possible to do the below Powershell sections in Linux. So I decided to leave it in, and kindly ask you to pretend that the Windows Powershell sessions below are actually being run in Linux. Or not.

Getting our configuration uploaded and compiled

Now I can use the Import-AzureRmAutomationDscConfiguration cmdlet to upload the configuration.

PS C:\Users\Don> Import-AzureRmAutomationDscConfiguration -AutomationAccountName "DSCTestAutomate" -ResourceGroupName "D
SCTest" -SourcePath "C:\Users\Don\demo\dsctestlinux.ps1" -Force -Published
ResourceGroupName : DSCTest
AutomationAccountName : DSCTestAutomate
Location : eastus2
State : Published
Name : dsctestlinux
Tags : {}
CreationTime : 5/11/2018 5:40:40 PM -04:00
LastModifiedTime : 5/17/2018 10:17:29 PM -04:00
Description :
Parameters : {}
LogVerbose : False

As you can see, we created the configuration about six days ago, but we just modified it by uploading it again with the -Force tag. That causes any configurations with the same name to be overwritten.

So let’s compile this thing! For that we’ll use the Start-AzureRmAutomationDscCompilationJob cmdlet.

PS C:\Users\Don> Start-AzureRmAutomationDscCompilationJob -ConfigurationName "dsctestlinux" -ResourceGroupName "DSCTest"
-AutomationAccountName "DSCTestAutomate"
ResourceGroupName : DSCTest
AutomationAccountName : DSCTestAutomate
Id : de3affc9-49c7-467b-b081-682c6ba2029f
CreationTime : 5/17/2018 10:23:28 PM -04:00
Status : New
StatusDetails : None
StartTime :
EndTime :
Exception :
LastModifiedTime : 5/17/2018 10:23:28 PM -04:00
LastStatusModifiedTime : 5/17/2018 10:23:28 PM -04:00
JobParameters : {}
ConfigurationName : dsctestlinux

Hmm…OK well we sent it off at least. How did it go? For that we need — you guessed it — another cmdlet. This time we’ll use Get-AzureRmAutomationDscCompilationJob.

PS C:\Users\Don> Get-AzureRmAutomationDscCompilationJob -ConfigurationName "dsctestlinux" -ResourceGroupName "DSCTest" -
AutomationAccountName "DSCTestAutomate" -Status "Completed"
ResourceGroupName : DSCTest
AutomationAccountName : DSCTestAutomate
Id : e3288ef3-5c8d-4e77-8668-92618d1c01f5
CreationTime : 5/11/2018 6:05:57 PM -04:00
Status : Completed
StatusDetails :
StartTime : 5/11/2018 6:06:44 PM -04:00
EndTime : 5/11/2018 6:06:58 PM -04:00
Exception :
LastModifiedTime : 5/11/2018 6:06:58 PM -04:00
LastStatusModifiedTime : 1/1/0001 12:00:00 AM +00:00
JobParameters : {}
ConfigurationName : dsctestlinux
ResourceGroupName : DSCTest
AutomationAccountName : DSCTestAutomate
Id : de3affc9-49c7-467b-b081-682c6ba2029f
CreationTime : 5/17/2018 10:23:28 PM -04:00
Status : Completed
StatusDetails :
StartTime : 5/17/2018 10:24:07 PM -04:00
EndTime : 5/17/2018 10:24:21 PM -04:00
Exception :
LastModifiedTime : 5/17/2018 10:24:21 PM -04:00
LastStatusModifiedTime : 1/1/0001 12:00:00 AM +00:00
JobParameters : {}
ConfigurationName : dsctestlinux

So we can see our compilation job from our previous installment, plus the new one we just kicked off. It looks like the status is “Completed”. I feel like we’re getting somewhere here…

Now let’s see what our Node Configurations look like. We should have two now, our original dsctestlinux.TestDSCLinuxfile and a new dsctestlinux.dscwebserver. To do that, we’ll use Get-AzureRmAutomationDscNodeConfiguration.

PS C:\Users\Don> Get-AzureRmAutomationDscNodeConfiguration -ResourceGroupName "DSCTest" -AutomationAccountName "DSCTestA
utomate" -ConfigurationName "dsctestlinux"
ResourceGroupName : DSCTest
AutomationAccountName : DSCTestAutomate
Name : dsctestlinux.dscwebserver
CreationTime : 5/17/2018 10:24:21 PM -04:00
LastModifiedTime : 5/17/2018 10:24:21 PM -04:00
ConfigurationName : dsctestlinux
RollupStatus : Good
ResourceGroupName : DSCTest
AutomationAccountName : DSCTestAutomate
Name : dsctestlinux.TestDSCLinuxfile
CreationTime : 5/17/2018 10:24:21 PM -04:00
LastModifiedTime : 5/17/2018 10:24:21 PM -04:00
ConfigurationName : dsctestlinux
RollupStatus : Bad

This all looks pretty good! Next we need to get a new virtual machine in Azure to test it.

(Note: if you’re like me, you are probably saying “RollupStatus: Bad”??? — I tried to find an explanation of this, but all the docs say are “Specifies the rollup status of DSC node configurations that this cmdlet gets.” Nice. If anybody knows then please, fill me in…but I suspect it has something to do with the TestDSCLinuxfile node configuration not changing on this compile.)

Using an Azure Resource Manager template

But to stand up our next virtual machine, we’re going to go even more automated and use an Azure Resource Manager template to deploy it. The key part here (and the reason for the title of this series), is that we’re going to have the template bootstrap the new host to Azure Automation DSC! The template will create the machine, and then register it with Automation DSC as a node and even inform Azure what node configuration it should be given. One deployment, and we’ll have a new machine configured to our specifications.

The template (as well as all the example DSC configurations we’ve used thus far) is hosted in a Github repository located here. So let’s clone this repo onto our Linux test host.

root@ubuntuservtest:~/# git clone https://github.com/DonMills/LinuxDSCBlogfiles.git
Cloning into 'LinuxDSCBlogfiles'...
remote: Counting objects: 22, done.
remote: Compressing objects: 100% (16/16), done.
remote: Total 22 (delta 5), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (22/22), done.
Checking connectivity... done.
root@ubuntuservtest:~/#

It’s designed to create a virtual machine with a network security group that allows SSH and HTTP in from the internet, install the “DSCForLinux Extension”, register the node, and apply our dsctestlinux.dscwebserver configuration to it.

Getting the registration information

There’s a little step we need to take to get ready for our deployment. When our host registers with Azure Automation DSC it needs the keys and endpoint URL of our Automation account.

You can find this in the Portal under the Automation account at “Account Settings -> Keys”, but I said we weren’t using the Portal anymore. So here it is in Powershell:

PS C:\Users\Don> Get-AzureRmAutomationRegistrationInfo -ResourceGroup "DSCTest" -AutomationAccountName "DSCTestAutomate"ResourceGroupName     : DSCTest
AutomationAccountName : DSCTestAutomate
PrimaryKey : nDTvzZZcYdRdA08GVFvYEgaJW30mI3havwJyt1W3V+STw8rXBb1EG1BS8C35Jxx64pwOqH3iMnzhebs1yR4U0w==
SecondaryKey : Z0EDT9D9BIshMLhmM1j+faWmg97PsQ1ZJrAUSAfxs2+JA9Xr/sY36aRMqWMu66yj+m7oqlr9JorqjJcrAq98ZQ==
Endpoint : https://eus2-agentservice-prod-1.azure-automation.net/accounts/cc5ed905-a196-425e-8adf-aa3f5c81
8dd1

Note that I’ll display my keys for the purpose of this discussion, because I can regenerate the keys (and invalidate the old ones) at any time.

Creating a parameter file

You can always append a ton of parameters to the Powershell template deployment command, but what we will do for this run is create a parameter file to hold all that information for us.

If we look at the template, we can see the 11 parameters needed for the deployment. So I’ll create a parameter file that contains values for each one, and I’ll use the registration URL and one of the keys I obtained in the previous step in the proper places.

{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"username": {
"value": "dmills"
},
"password": {
"value": "Th1sIs4utom4tion!"
},
"vmName": {
"value": "DSCLinuxTestNode2"
},
"NodeConfigName": {
"value": "dsctestlinux.dscwebserver"
},
"ubuntuOSVersion": {
"value": "16.04-LTS"
},
"registrationUrl": {
"value": "https://eus2-agentservice-prod-1.azure-automation.net/accounts/cc5ed905-a196-425e-8adf-aa3f5c818dd1"
},
"registrationKey": {
"value": "nDTvzZZcYdRdA08GVFvYEgaJW30mI3havwJyt1W3V+STw8rXBb1EG1BS8C35Jxx64pwOqH3iMnzhebs1yR4U0w=="
},
"virtualNetworkName": {
"value": "DRMTest-vnet"
},
"subnetName": {
"value": "default-drm"
},
"storageAcct": {
"value": "drmtestdiskstorage"
},
"virtualNetworkResourceGroup":{
"value": "DSCTest"
}
}
}

I’ll save this file as dscbootstrapdeploy.parameters.json to match my dscbootstrapdeploy.jsontemplate. You don’t have to make them match, but easy to remember is usually good.

Some discussion of the DSCForLinux Extension

The real magic here is being accomplished by the Azure VM DSCForLinux Extension. It’s the element that handles registering the node with Azure Automation DSC, as well as telling DSC what configuration should be applied to it. But it can do a lot more…

Let’s look at the extension section of the template:

{
"apiVersion": "2018-04-01",
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(parameters('vmName'),'/enabledsc')]",
"location": "[resourceGroup().location]",
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', parameters('vmName'))]"
],
"properties": {
"publisher": "Microsoft.OSTCExtensions",
"type": "DSCForLinux",
"typeHandlerVersion": "2.70",
"settings": {
"ExtensionAction": "Register",
"NodeConfigurationName" : "[parameters('NodeConfigName')]"
},
"protectedSettings": {
"RegistrationUrl": "[parameters('registrationUrl')]",
"RegistrationKey": "[parameters('registrationKey')]"
}
}
}

First, let’s talk about ExtensionAction.

One of the prevailing thoughts in DevOps is that in an “immutable infrastructure” environment the hosts should only be configured a single time — at creation. If changes ever need to be made, you should rebuild the host instead of modifying it in place.

For that reason, often configuration management tools are used without a centralized server so that the configuration is retrieved from a storage location and performed only once, at first boot. This is the model used for things like chef local mode (chef zero).

So to meet these needs, the DSCForLinux extension can operate in DSC push mode (remember that from the first installment?). It can fetch MOF files from Azure Storage accounts, or any publicly accessible storage that has a URL. To accomplish this we’d set the ExtensionAction to Pull and provide the details of the storage location.

You can also use it to connect to an old-style DSC pull server, or even install and remove custom DSC modules from the node.

But for our purposes, we are going for register mode so that our node gets connected to our Azure Automation Account. To accomplish this, we set the ExtensionAction to Register. And how will we tell Azure Automation DSC what configuration we want for this node? Via the NodeConfigurationName setting.

The last thing I want to say about the extension concerns secrets management.

When the extension is started, it saves most of the information about how it is configured in plain text files on the node. But some of that information, like our Azure Automation account registrationURL and key should not be readable by users of the system. For this purpose, the extension includes the protectedSettings properties. These will still be saved in a configuration file, but will be encrypted with a certificate first.

Let’s do this

It looks like we are set…we’ve got our template and our parameter file composed and now we just need to deploy it.

First we’ll create a new Resource Group to hold the deployment.

PS /root/LinuxDSCBlogfiles> New-AzureRmResourceGroup -Name "DSCTestDeployment" -Location "East US"ResourceGroupName : DSCTestDeployment
Location : eastus
ProvisioningState : Succeeded
Tags :
ResourceId : /subscriptions/#########/resourceGroups/DSCTestDeployment

Back on our Linux host…the bug only affects the Automation commands.

Then we’ll do a sanity check by running a deployment check first using Test-AzureRmResourceGroupDeployment...

PS /root/LinuxDSCBlogfiles> Test-AzureRmResourceGroupDeployment -ResourceGroupName "DSCTestDeployment" -TemplateFile "/root/LinuxDSCBlogfiles/dscbootstrapdeploy.json" 
-TemplateParameterFile "/root/LinuxDSCBlogfiles/dscbootstrapdeploy.parameters.json"
PS /root/dscdeploy/LinuxDSCBlogfiles>

No errors, so let’s actually deploy the template. To do that, we’ll use theNew-AzureRmResourceGroupDeployment cmdlet.

PS /root/dscdeploy/LinuxDSCBlogfiles>  New-AzureRmResourceGroupDeployment -ResourceGroupName "DSCTestDeployment" -TemplateFile "/root/dscdeploy/LinuxDSCBlogfiles/dscbootstrapdeploy.json" -TemplateParameterFile "/root/dscdeploy/LinuxDSCBlogfiles/dscbootstrapdeploy.parameters.json"DeploymentName          : dscbootstrapdeploy
ResourceGroupName : DSCTestDeployment
ProvisioningState : Succeeded
Timestamp : 5/20/18 12:20:19 AM
Mode : Incremental
TemplateLink :
Parameters :
Name Type
...
Outputs :
Name Type Value
=============== =========================
==========
ipaddress String
23.100.29.146
DeploymentDebugLogLevel :

OK! The deployment went through…is the node added to our Automation Account?

PS C:\Users\Don\demo> Get-AzureRmAutomationDscNode -ResourceGroupName "DSCTest" -AutomationAccountName "DSCTestAutomate"
-Name "DSCLinuxTestNode2"
ResourceGroupName : DSCTest
AutomationAccountName : DSCTestAutomate
Name : DSCLinuxTestNode2
RegistrationTime : 5/19/2018 8:13:15 PM -04:00
LastSeen : 5/19/2018 8:13:18 PM -04:00
IpAddress : 127.0.0.1;::1;10.110.0.6;fe80::20d:3aff:fe13:415;
Id : 2bb3ccd6-d2f1-4787-ab2d-632e86107b09
NodeConfigurationName : dsctestlinux.dscwebserver
Status : Compliant

There it is…and as you can see the NodeConfigurationName is dsctestlinux.dscwebserver. Sweet!

So at this point, you might as well go make a sandwich or get a cold drink. It’s going to take a bit for the LSM on the new node to apply the configuration.

When you’re ready, get the IP Address from the output of the deployment. According to the one above, we’re looking at 23.100.29.146. So first I’ll SSH into that host and then I’ll look for our index page in the /var/www/html directory.

root@ubuntuservtest:~/dscdeploy/LinuxDSCBlogfiles# ssh 23.100.29.146 -l dmills
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.13.0-1016-azure x86_64)
....
dmills@DSCLinuxTestNode2:~$ ls -l /var/www/html
total 4
-rw-r--r-- 1 root root 116 May 20 00:30 index.html

The file’s there and in the right place. Let’s just skip straight to hitting the IP with a web browser.

Boom.

And that, as they say, is that.

Whew! It’s been a long haul together exploring this topic, but I hope you’ve found it helpful. We started with a introduction to DSC and how it could be used on Linux, then we moved on to setting up Azure Automation DSC and configuring Linux virtual machines in the Azure cloud, and now we’ve finished up with automating the bootstrapping of Linux VMs to DSC in Azure (just like the title promised).

I’ll sign off for now, but feel free to share any feedback or comments below.

--

--

Don Mills
SingleStone

A hacker at heart, Don Mills has spent a long time designing and securing things with bits in them.