Back to Blog
7 min read

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.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.