Azure Desired State Configuration for Configuration Management
Introduction
Azure Automation State Configuration (DSC) provides a way to define and enforce the desired configuration of your Windows and Linux machines. Using PowerShell DSC, you can ensure servers maintain consistent configurations, automatically remediating any drift from the defined state.
In this post, we will explore how to implement Azure Automation DSC for configuration management.
Understanding Desired State Configuration
DSC works with three key components:
- Configurations: PowerShell scripts defining desired state
- Resources: Building blocks for configurations (files, services, registry, etc.)
- Local Configuration Manager (LCM): Agent that applies configurations
Creating DSC Configurations
Write a DSC configuration for a web server:
# WebServerConfig.ps1
Configuration WebServerConfig {
param(
[Parameter(Mandatory=$false)]
[string]$Environment = "Production"
)
Import-DscResource -ModuleName PSDesiredStateConfiguration
Import-DscResource -ModuleName xWebAdministration
Node $AllNodes.NodeName {
# Ensure IIS is installed
WindowsFeature IIS {
Ensure = "Present"
Name = "Web-Server"
}
WindowsFeature IISManagement {
Ensure = "Present"
Name = "Web-Mgmt-Tools"
DependsOn = "[WindowsFeature]IIS"
}
WindowsFeature ASPNet45 {
Ensure = "Present"
Name = "Web-Asp-Net45"
DependsOn = "[WindowsFeature]IIS"
}
# Ensure default website is stopped
xWebsite DefaultSite {
Ensure = "Present"
Name = "Default Web Site"
State = "Stopped"
PhysicalPath = "C:\inetpub\wwwroot"
DependsOn = "[WindowsFeature]IIS"
}
# Create application directory
File WebAppDirectory {
Ensure = "Present"
Type = "Directory"
DestinationPath = "C:\WebApps\MyApplication"
}
# Create application pool
xWebAppPool AppPool {
Ensure = "Present"
Name = "MyAppPool"
State = "Started"
ManagedRuntimeVersion = "v4.0"
ManagedPipelineMode = "Integrated"
IdentityType = "ApplicationPoolIdentity"
IdleTimeout = (New-TimeSpan -Minutes 20).ToString()
MaxProcesses = 1
DependsOn = "[WindowsFeature]IIS"
}
# Create website
xWebsite MyWebsite {
Ensure = "Present"
Name = "MyApplication"
State = "Started"
PhysicalPath = "C:\WebApps\MyApplication"
ApplicationPool = "MyAppPool"
BindingInfo = @(
MSFT_xWebBindingInformation {
Protocol = "HTTP"
Port = 80
HostName = ""
IPAddress = "*"
}
)
DependsOn = "[xWebAppPool]AppPool", "[File]WebAppDirectory"
}
# Configure Windows Firewall
WindowsFeature Firewall {
Ensure = "Present"
Name = "Windows-Defender-Features"
}
# Ensure specific services are running
Service W3SVC {
Name = "W3SVC"
State = "Running"
StartupType = "Automatic"
DependsOn = "[WindowsFeature]IIS"
}
# Configure environment-specific settings
if ($Environment -eq "Production") {
Registry DisableDebug {
Ensure = "Present"
Key = "HKLM:\SOFTWARE\MyApplication"
ValueName = "DebugMode"
ValueData = "0"
ValueType = "Dword"
}
}
# Configure logging directory
File LogDirectory {
Ensure = "Present"
Type = "Directory"
DestinationPath = "C:\Logs\MyApplication"
}
# Set NTFS permissions
Script SetLogPermissions {
GetScript = {
$acl = Get-Acl "C:\Logs\MyApplication"
return @{ Result = $acl.Access | Out-String }
}
SetScript = {
$acl = Get-Acl "C:\Logs\MyApplication"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"IIS AppPool\MyAppPool",
"Modify",
"ContainerInherit,ObjectInherit",
"None",
"Allow"
)
$acl.AddAccessRule($rule)
Set-Acl "C:\Logs\MyApplication" $acl
}
TestScript = {
$acl = Get-Acl "C:\Logs\MyApplication"
$hasPermission = $acl.Access | Where-Object {
$_.IdentityReference -like "*MyAppPool*" -and $_.FileSystemRights -match "Modify"
}
return $null -ne $hasPermission
}
DependsOn = "[File]LogDirectory"
}
}
}
# Configuration data
$ConfigData = @{
AllNodes = @(
@{
NodeName = "localhost"
PSDscAllowPlainTextPassword = $false
}
)
}
Compiling and Uploading Configurations
Upload configurations to Azure Automation:
from azure.mgmt.automation import AutomationClient
from azure.identity import DefaultAzureCredential
import base64
credential = DefaultAzureCredential()
automation_client = AutomationClient(credential, subscription_id)
def upload_dsc_configuration(automation_account, resource_group, config_name, config_content):
"""Upload a DSC configuration to Azure Automation."""
# Base64 encode the content
encoded_content = base64.b64encode(config_content.encode()).decode()
config = automation_client.dsc_configuration.create_or_update(
resource_group_name=resource_group,
automation_account_name=automation_account,
configuration_name=config_name,
parameters={
"name": config_name,
"location": "eastus",
"properties": {
"source": {
"type": "embeddedContent",
"value": config_content
},
"description": f"DSC Configuration: {config_name}"
}
}
)
return config
def compile_configuration(automation_account, resource_group, config_name, parameters=None):
"""Compile a DSC configuration."""
compile_job = automation_client.dsc_compilation_job.create(
resource_group_name=resource_group,
automation_account_name=automation_account,
compilation_job_name=f"{config_name}-compile-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}",
parameters={
"properties": {
"configuration": {"name": config_name},
"parameters": parameters or {}
}
}
)
# Wait for compilation to complete
while True:
job = automation_client.dsc_compilation_job.get(
resource_group_name=resource_group,
automation_account_name=automation_account,
compilation_job_name=compile_job.name
)
if job.status in ["Completed", "Failed", "Suspended"]:
break
time.sleep(10)
return job
# Read and upload configuration
with open("WebServerConfig.ps1", "r") as f:
config_content = f.read()
upload_dsc_configuration(
"automation-dsc",
"rg-automation",
"WebServerConfig",
config_content
)
# Compile the configuration
compile_job = compile_configuration(
"automation-dsc",
"rg-automation",
"WebServerConfig",
parameters={"Environment": "Production"}
)
print(f"Compilation status: {compile_job.status}")
Onboarding Nodes
Register VMs with Azure Automation DSC:
# Script to onboard VM to Azure Automation DSC
param(
[Parameter(Mandatory=$true)]
[string]$AutomationAccountName,
[Parameter(Mandatory=$true)]
[string]$ResourceGroupName,
[Parameter(Mandatory=$true)]
[string]$NodeConfigurationName,
[Parameter(Mandatory=$false)]
[string]$ConfigurationMode = "ApplyAndAutoCorrect",
[Parameter(Mandatory=$false)]
[int]$RefreshFrequencyMins = 30,
[Parameter(Mandatory=$false)]
[int]$ConfigurationModeFrequencyMins = 15
)
# Get automation account registration info
$automationAccount = Get-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AutomationAccountName
$registrationInfo = Get-AzAutomationRegistrationInfo -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName
# Configure LCM meta-configuration
$metaConfig = @"
[DscLocalConfigurationManager()]
Configuration LCMConfig {
Node localhost {
Settings {
RefreshMode = 'Pull'
RefreshFrequencyMins = $RefreshFrequencyMins
ConfigurationMode = '$ConfigurationMode'
ConfigurationModeFrequencyMins = $ConfigurationModeFrequencyMins
RebootNodeIfNeeded = `$true
ActionAfterReboot = 'ContinueConfiguration'
AllowModuleOverwrite = `$true
}
ConfigurationRepositoryWeb AzureAutomationDSC {
ServerUrl = '$($registrationInfo.Endpoint)'
RegistrationKey = '$($registrationInfo.PrimaryKey)'
ConfigurationNames = @('$NodeConfigurationName')
}
ReportServerWeb AzureAutomationDSC {
ServerUrl = '$($registrationInfo.Endpoint)'
RegistrationKey = '$($registrationInfo.PrimaryKey)'
}
}
}
"@
# Create and apply meta-configuration
$metaConfigPath = "$env:TEMP\LCMConfig.ps1"
$metaConfig | Out-File $metaConfigPath
# Generate MOF
& $metaConfigPath
Set-DscLocalConfigurationManager -Path "$env:TEMP\LCMConfig" -Force
Write-Output "Node registered with Azure Automation DSC"
Write-Output "Configuration: $NodeConfigurationName"
Write-Output "Mode: $ConfigurationMode"
Linux DSC Configuration
Configure Linux machines with DSC:
# LinuxWebServerConfig.ps1
Configuration LinuxWebServerConfig {
Import-DscResource -ModuleName nx
Node $AllNodes.NodeName {
# Ensure nginx is installed
nxPackage nginx {
Ensure = "Present"
Name = "nginx"
PackageManager = "apt"
}
# Ensure nginx service is running
nxService nginx {
Name = "nginx"
State = "running"
Enabled = $true
Controller = "systemd"
DependsOn = "[nxPackage]nginx"
}
# Create web directory
nxFile WebDirectory {
Ensure = "Present"
DestinationPath = "/var/www/myapp"
Type = "Directory"
Mode = "755"
Owner = "www-data"
Group = "www-data"
}
# Configure nginx site
nxFile NginxConfig {
Ensure = "Present"
DestinationPath = "/etc/nginx/sites-available/myapp"
Contents = @"
server {
listen 80;
server_name _;
root /var/www/myapp;
index index.html;
location / {
try_files `$uri `$uri/ =404;
}
access_log /var/log/nginx/myapp.access.log;
error_log /var/log/nginx/myapp.error.log;
}
"@
Mode = "644"
Owner = "root"
Group = "root"
DependsOn = "[nxPackage]nginx"
}
# Enable site
nxFile EnableSite {
Ensure = "Present"
DestinationPath = "/etc/nginx/sites-enabled/myapp"
Type = "Link"
SourcePath = "/etc/nginx/sites-available/myapp"
DependsOn = "[nxFile]NginxConfig"
}
# Ensure firewall allows HTTP
nxScript ConfigureFirewall {
GetScript = {
$result = ufw status | grep "80/tcp"
return @{ Result = $result }
}
SetScript = {
ufw allow 80/tcp
}
TestScript = {
$result = ufw status | grep "80/tcp.*ALLOW"
return $result -ne $null
}
}
}
}
Monitoring DSC Compliance
Query DSC compliance status:
def get_dsc_node_status(automation_account, resource_group):
"""Get status of all DSC nodes."""
nodes = automation_client.dsc_node.list_by_automation_account(
resource_group_name=resource_group,
automation_account_name=automation_account
)
node_list = []
for node in nodes:
node_list.append({
"name": node.name,
"node_id": node.node_id,
"status": node.status,
"last_seen": node.last_seen,
"configuration": node.node_configuration.name if node.node_configuration else None,
"ip": node.ip
})
return node_list
def get_dsc_node_report(automation_account, resource_group, node_id):
"""Get detailed report for a DSC node."""
reports = automation_client.dsc_node_report.list_by_node(
resource_group_name=resource_group,
automation_account_name=automation_account,
node_id=node_id
)
latest_report = None
for report in reports:
if latest_report is None or report.start_time > latest_report.start_time:
latest_report = report
if latest_report:
return {
"report_id": latest_report.id,
"status": latest_report.status,
"start_time": latest_report.start_time,
"end_time": latest_report.end_time,
"reboot_requested": latest_report.reboot_requested,
"refresh_mode": latest_report.refresh_mode,
"errors": latest_report.errors,
"resources": latest_report.number_of_resources
}
return None
# Get compliance status
nodes = get_dsc_node_status("automation-dsc", "rg-automation")
print("DSC Node Status:")
print("=" * 60)
for node in nodes:
print(f"\n{node['name']}")
print(f" Status: {node['status']}")
print(f" Configuration: {node['configuration']}")
print(f" Last Seen: {node['last_seen']}")
report = get_dsc_node_report("automation-dsc", "rg-automation", node['node_id'])
if report:
print(f" Latest Report: {report['status']}")
if report['errors']:
print(f" Errors: {len(report['errors'])}")
Conclusion
Azure Automation DSC provides a declarative approach to configuration management. By defining desired states rather than procedural scripts, you ensure consistent, repeatable configurations across your infrastructure.
Key benefits include automatic drift detection and remediation, centralized configuration management, and support for both Windows and Linux. Combined with Azure Automation’s scheduling and monitoring capabilities, DSC helps maintain compliant, well-configured infrastructure at scale.