Bootstrapping Linux VMs with DSC in Azure
Part 3: Automating it all together
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:
nxPackage
, which controls Linux package manager actions; andnxService
, 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 -PublishedResourceGroupName : 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 : dsctestlinuxResourceGroupName : 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 : GoodResourceGroupName : 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.json
template. 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.146DeploymentDebugLogLevel :
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.
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.