Part 3: Deploying CIS Benchmarks at Scale with Systems Manager

Introduction and Recap

Now that your environment is ready, let's deploy CIS benchmarks to your Windows servers and learn how to scale from one to hundreds of instances. If you're like me, you probably tested that MOF file we created in Part 1 on your test instance and watched 300+ security settings magically apply themselves. Pretty satisfying, right? But the real power comes when we can do this across our entire fleet without touching a single RDP session.

Quick Recap from Our Journey So Far

In Part 1, we:

  • Set up IAM roles and S3 buckets to store our configurations
  • Installed the CisDsc module and explored what it can do
  • Created our first DSC configuration targeting CIS Level 1
  • Uploaded that MOF file to S3 (remember localhost.mof?)
  • Understood how Systems Manager, DSC, and S3 work together

In Part 2 (you did read that, right?), we:

  • Verified Systems Manager can actually see and manage our instances
  • Tested network connectivity to AWS endpoints
  • Ensured SSM Agent is running and registered
  • Ran test commands to confirm everything works

If you skipped Part 2 and your instances aren't showing up in Systems Manager Fleet Manager with an "Online" status, stop here and go back. Seriously. The rest of this won't work without that foundation.

What You'll Learn Today

Today we're going operational. You'll learn how to:

  • Deploy configurations using Systems Manager State Manager
  • Monitor compliance in real-time (and actually understand what failed)
  • Scale your deployment strategy without melting your servers
  • Optimize for performance and cost (because cloud bills are real)

Fair warning: we're going to hit some bumps. DSC is powerful but quirky, and Systems Manager adds its own personality to the mix. I'll share the gotchas I've discovered through trial and error (emphasis on error).

Pre-flight Check

Before we dive in, let's make sure you're ready. If you're using the AWS Console, navigate to Systems Manager > Node Tools > Fleet Manager to verify your instances show as "Online".

From your local machine, you can also run this quick check:

# Verify your instance is visible to Systems Manager
aws ssm describe-instance-information \
    --filters "Key=InstanceIds,Values=i-YOUR-INSTANCE-ID" \
    --query 'InstanceInformationList[0].[InstanceId,PingStatus,AgentVersion]' \
    --output table

You should see:

  • Your instance ID
  • PingStatus: Online
  • A version number for the agent

If you see "Connection Lost" or nothing at all, go back to Part 2. Don't worry, we'll wait.

Creating Production-Ready DSC Configurations

Let's level up from our basic configuration. Production environments need logging, error handling, and flexibility.

Complex DSC Configuration with Logging

First, let's create a more robust configuration that actually tells us what it's doing. Also, not all servers are created equal. Your domain controllers have different security requirements than your web servers. Finally, CIS benchmarks aren't enough, oftentimes you need something additional. Here's a configuration that applies logging for cloudwatch, different server roles, and modifies the registry direct.

# Load all required DSC modules FIRST
Write-Host "Loading DSC modules..." -ForegroundColor Cyan
Import-Module PSDesiredStateConfiguration -Force
Import-Module CisDsc -Force -ErrorAction SilentlyContinue
Import-Module SecurityPolicyDsc -Force -ErrorAction SilentlyContinue
Import-Module AuditPolicyDsc -Force -ErrorAction SilentlyContinue
Write-Host "✓ All DSC modules loaded" -ForegroundColor Green

Configuration ComprehensiveSecurity {
    param(
        [string]$NodeName = 'localhost',
        [string]$ServerRole = 'MemberServer',
        [string]$LogPath = 'C:\Logs\DSC'
    )

    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Import-DscResource -ModuleName CisDsc
    Import-DscResource -ModuleName SecurityPolicyDsc
    Import-DscResource -ModuleName AuditPolicyDsc

    Node $NodeName {
        # Ensure log directory exists
        File DSCLogDirectory {
            Ensure = 'Present'
            Type = 'Directory'
            DestinationPath = $LogPath
            Force = $true
        }

        # Configure LCM consistently
        LocalConfigurationManager {
            RefreshMode = 'Push'
            ConfigurationMode = 'ApplyAndMonitor'
            RebootNodeIfNeeded = $false
            ActionAfterReboot = 'ContinueConfiguration'
        }

        # DSC Execution Logger - Start
        Script DSCExecutionStart {
            SetScript = {
                function Write-DSCLog {
                    param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                    $logEntry = "[$timestamp] [$Level] $Message"
                    $logDir = Split-Path $LogPath
                    if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                    Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                    Write-Host $logEntry
                }
                
                Write-DSCLog "=== Starting DSC Configuration Application ===" -Level "INFO"
                Write-DSCLog "Server Role: $using:ServerRole" -Level "INFO"
                Write-DSCLog "Node Name: $using:NodeName" -Level "INFO"
                Write-DSCLog "Log Path: $using:LogPath" -Level "INFO"
                Write-DSCLog "Execution Time: $(Get-Date)" -Level "INFO"
                
                # Log system information
                $osInfo = Get-CimInstance -ClassName Win32_OperatingSystem
                Write-DSCLog "OS: $($osInfo.Caption) $($osInfo.Version)" -Level "INFO"
                Write-DSCLog "Computer Name: $($env:COMPUTERNAME)" -Level "INFO"
            }
            TestScript = { $false }
            GetScript = { @{ Result = "DSC Start Logger" } }
            DependsOn = '[File]DSCLogDirectory'
        }

        # Common CIS parameters
        $CommonParams = @{
            Cis2316AccountsRenameGuestaccount = "GuestRenamed"
            Cis2374LegalNoticeText = "Authorized access only - All activity monitored"
            Cis2375LegalNoticeCaption = "Security Warning"
            Cis2376CachedLogonsCount = "2"
            Cis1849ScreensaverGracePeriod = "5"
        }

        # Role-specific CIS baseline with account policy exclusions
        switch ($ServerRole) {
            'DomainController' {
                Script DCConfigStart {
                    SetScript = {
                        function Write-DSCLog {
                            param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                            $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                            $logEntry = "[$timestamp] [$Level] $Message"
                            $logDir = Split-Path $LogPath
                            if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                            Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                            Write-Host $logEntry
                        }
                        
                        Write-DSCLog "Applying Domain Controller CIS baseline" -Level "INFO"
                        Write-DSCLog "Excluding controls for DC-specific requirements" -Level "INFO"
                    }
                    TestScript = { $false }
                    GetScript = { @{ Result = "DC Config Start" } }
                    DependsOn = '[Script]DSCExecutionStart'
                }

                CIS_Microsoft_Windows_Server_2022_Member_Server_Release_21H2 DCBaseline {
                    Cis2316AccountsRenameGuestaccount = $CommonParams.Cis2316AccountsRenameGuestaccount
                    Cis2374LegalNoticeText = $CommonParams.Cis2374LegalNoticeText
                    Cis2375LegalNoticeCaption = $CommonParams.Cis2375LegalNoticeCaption
                    Cis2376CachedLogonsCount = $CommonParams.Cis2376CachedLogonsCount
                    Cis1849ScreensaverGracePeriod = $CommonParams.Cis1849ScreensaverGracePeriod
                    ExcludeList = @(
                        '2.2.21',  # Deny access from network (DCs need this)
                        '2.2.26',  # Deny logon as service (some DC services)
                        '2.3.1.5', # Administrator account status (needed for break-glass)
                        '2.3.1.1', # Administrator account status (alternative ID)
                        '1.1.1',   # Account lockout duration
                        '1.1.2',   # Account lockout threshold
                        '1.1.3',   # Reset account lockout counter
                        '1.1.4',   # Account lockout duration (alternative)
                        '1.1.5',   # Account lockout threshold (alternative)
                        '1.1.6',   # Reset account lockout counter (alternative)
                        '1.2.1',   # Account lockout duration (variant)
                        '1.2.2',   # Account lockout threshold (variant)
                        '1.2.3'    # Reset account lockout counter (variant)
                    )
                    DependsOn = '[Script]DCConfigStart'
                }

                # Remove the AccountPolicy resource that's causing conflicts
                Script DCConfigComplete {
                    SetScript = {
                        function Write-DSCLog {
                            param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                            $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                            $logEntry = "[$timestamp] [$Level] $Message"
                            $logDir = Split-Path $LogPath
                            if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                            Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                            Write-Host $logEntry
                        }
                        
                        Write-DSCLog "Domain Controller CIS baseline application completed" -Level "INFO"
                        Write-DSCLog "Account policy excluded to avoid conflicts" -Level "INFO"
                    }
                    TestScript = { $false }
                    GetScript = { @{ Result = "DC Config Complete" } }
                    DependsOn = '[CIS_Microsoft_Windows_Server_2022_Member_Server_Release_21H2]DCBaseline'
                }
            }
            'WebServer' {
                Script WebConfigStart {
                    SetScript = {
                        function Write-DSCLog {
                            param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                            $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                            $logEntry = "[$timestamp] [$Level] $Message"
                            $logDir = Split-Path $LogPath
                            if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                            Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                            Write-Host $logEntry
                        }
                        
                        Write-DSCLog "Applying Web Server CIS baseline" -Level "INFO"
                        Write-DSCLog "Excluding controls for IIS requirements" -Level "INFO"
                    }
                    TestScript = { $false }
                    GetScript = { @{ Result = "Web Config Start" } }
                    DependsOn = '[Script]DSCExecutionStart'
                }

                CIS_Microsoft_Windows_Server_2022_Member_Server_Release_21H2 WebBaseline {
                    Cis2316AccountsRenameGuestaccount = $CommonParams.Cis2316AccountsRenameGuestaccount
                    Cis2374LegalNoticeText = $CommonParams.Cis2374LegalNoticeText
                    Cis2375LegalNoticeCaption = $CommonParams.Cis2375LegalNoticeCaption
                    Cis2376CachedLogonsCount = $CommonParams.Cis2376CachedLogonsCount
                    Cis1849ScreensaverGracePeriod = $CommonParams.Cis1849ScreensaverGracePeriod
                    ExcludeList = @(
                        '2.3.1.5',    # Administrator account status (needed for break-glass)
                        '2.3.1.1',    # Administrator account status (alternative ID)
                        '5.1',        # Windows Firewall (IIS manages its own)
                        '18.9.47.5.1', # WinRM for remote management
                        '1.1.1',      # Account lockout duration
                        '1.1.2',      # Account lockout threshold
                        '1.1.3',      # Reset account lockout counter
                        '1.1.4',      # Account lockout duration (alternative)
                        '1.1.5',      # Account lockout threshold (alternative)
                        '1.1.6',      # Reset account lockout counter (alternative)
                        '1.2.1',      # Account lockout duration (variant)
                        '1.2.2',      # Account lockout threshold (variant)
                        '1.2.3'       # Reset account lockout counter (variant)
                    )
                    DependsOn = '[Script]WebConfigStart'
                }

                # Remove the AccountPolicy resource that's causing conflicts
                Script WebConfigComplete {
                    SetScript = {
                        function Write-DSCLog {
                            param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                            $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                            $logEntry = "[$timestamp] [$Level] $Message"
                            $logDir = Split-Path $LogPath
                            if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                            Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                            Write-Host $logEntry
                        }
                        
                        Write-DSCLog "Web Server CIS baseline application completed" -Level "INFO"
                        Write-DSCLog "Account policy excluded to avoid conflicts" -Level "INFO"
                    }
                    TestScript = { $false }
                    GetScript = { @{ Result = "Web Config Complete" } }
                    DependsOn = '[CIS_Microsoft_Windows_Server_2022_Member_Server_Release_21H2]WebBaseline'
                }
            }
            Default {
                Script MemberConfigStart {
                    SetScript = {
                        function Write-DSCLog {
                            param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                            $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                            $logEntry = "[$timestamp] [$Level] $Message"
                            $logDir = Split-Path $LogPath
                            if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                            Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                            Write-Host $logEntry
                        }
                        
                        Write-DSCLog "Applying Member Server CIS baseline" -Level "INFO"
                        Write-DSCLog "Standard member server configuration" -Level "INFO"
                    }
                    TestScript = { $false }
                    GetScript = { @{ Result = "Member Config Start" } }
                    DependsOn = '[Script]DSCExecutionStart'
                }

                CIS_Microsoft_Windows_Server_2022_Member_Server_Release_21H2 MemberBaseline {
                    Cis2316AccountsRenameGuestaccount = $CommonParams.Cis2316AccountsRenameGuestaccount
                    Cis2374LegalNoticeText = $CommonParams.Cis2374LegalNoticeText
                    Cis2375LegalNoticeCaption = $CommonParams.Cis2375LegalNoticeCaption
                    Cis2376CachedLogonsCount = $CommonParams.Cis2376CachedLogonsCount
                    Cis1849ScreensaverGracePeriod = $CommonParams.Cis1849ScreensaverGracePeriod
                    ExcludeList = @(
                        '2.3.1.5',  # Administrator account status (needed for break-glass)
                        '2.3.1.1',  # Administrator account status (alternative ID)
                        '18.9.4.1', # AutoAdminLogon (some automation requires this)
                        '1.1.1',    # Account lockout duration
                        '1.1.2',    # Account lockout threshold
                        '1.1.3',    # Reset account lockout counter
                        '1.1.4',    # Account lockout duration (alternative)
                        '1.1.5',    # Account lockout threshold (alternative)
                        '1.1.6',    # Reset account lockout counter (alternative)
                        '1.2.1',    # Account lockout duration (variant)
                        '1.2.2',    # Account lockout threshold (variant)
                        '1.2.3'     # Reset account lockout counter (variant)
                    )
                    DependsOn = '[Script]MemberConfigStart'
                }

                # Remove the AccountPolicy resource that's causing conflicts
                Script MemberConfigComplete {
                    SetScript = {
                        function Write-DSCLog {
                            param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                            $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                            $logEntry = "[$timestamp] [$Level] $Message"
                            $logDir = Split-Path $LogPath
                            if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                            Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                            Write-Host $logEntry
                        }
                        
                        Write-DSCLog "Member Server CIS baseline application completed" -Level "INFO"
                        Write-DSCLog "Account policy excluded to avoid conflicts" -Level "INFO"
                    }
                    TestScript = { $false }
                    GetScript = { @{ Result = "Member Config Complete" } }
                    DependsOn = '[CIS_Microsoft_Windows_Server_2022_Member_Server_Release_21H2]MemberBaseline'
                }
            }
        }

        # Custom registry settings that complement CIS
        Script CustomRegistryStart {
            SetScript = {
                function Write-DSCLog {
                    param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                    $logEntry = "[$timestamp] [$Level] $Message"
                    $logDir = Split-Path $LogPath
                    if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                    Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                    Write-Host $logEntry
                }
                
                Write-DSCLog "Applying custom registry security settings" -Level "INFO"
            }
            TestScript = { $false }
            GetScript = { @{ Result = "Custom Registry Start" } }
            DependsOn = '[File]DSCLogDirectory'
        }

        Registry DisableAutorun {
            Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer'
            ValueName = 'NoDriveTypeAutoRun'
            ValueData = 255
            ValueType = 'Dword'
            Ensure = 'Present'
            DependsOn = '[Script]CustomRegistryStart'
        }

        Registry DisableLLMNR {
            Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient'
            ValueName = 'EnableMulticast'
            ValueData = 0
            ValueType = 'Dword'
            Ensure = 'Present'
            DependsOn = '[Script]CustomRegistryStart'
        }

        Script CustomRegistryComplete {
            SetScript = {
                function Write-DSCLog {
                    param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                    $logEntry = "[$timestamp] [$Level] $Message"
                    $logDir = Split-Path $LogPath
                    if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                    Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                    Write-Host $logEntry
                }
                
                Write-DSCLog "Custom registry settings applied successfully" -Level "INFO"
                Write-DSCLog "- Disabled autorun for all drive types" -Level "INFO"
                Write-DSCLog "- Disabled LLMNR for security" -Level "INFO"
            }
            TestScript = { $false }
            GetScript = { @{ Result = "Custom Registry Complete" } }
            DependsOn = '[Registry]DisableAutorun', '[Registry]DisableLLMNR'
        }

        # Additional audit policies
        Script AuditPolicyStart {
            SetScript = {
                function Write-DSCLog {
                    param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                    $logEntry = "[$timestamp] [$Level] $Message"
                    $logDir = Split-Path $LogPath
                    if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                    Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                    Write-Host $logEntry
                }
                
                Write-DSCLog "Configuring additional audit policies" -Level "INFO"
            }
            TestScript = { $false }
            GetScript = { @{ Result = "Audit Policy Start" } }
            DependsOn = '[File]DSCLogDirectory'
        }

        AuditPolicySubcategory AccountLockout {
            Name = 'Account Lockout'
            AuditFlag = 'Failure'
            Ensure = 'Present'
            DependsOn = '[Script]AuditPolicyStart'
        }

        AuditPolicySubcategory LogonEvents {
            Name = 'Logon'
            AuditFlag = 'Failure'
            Ensure = 'Present'
            DependsOn = '[Script]AuditPolicyStart'
        }

        if ($ServerRole -eq 'DomainController') {
            AuditPolicySubcategory DirectoryServiceAccess {
                Name = 'Directory Service Access'
                AuditFlag = 'Failure'
                Ensure = 'Present'
                DependsOn = '[Script]AuditPolicyStart'
            }

            Script DCAuditComplete {
                SetScript = {
                    function Write-DSCLog {
                        param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                        $logEntry = "[$timestamp] [$Level] $Message"
                        $logDir = Split-Path $LogPath
                        if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                        Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                        Write-Host $logEntry
                    }
                    
                    Write-DSCLog "Domain Controller audit policies configured" -Level "INFO"
                    Write-DSCLog "- Account lockout failures" -Level "INFO"
                    Write-DSCLog "- Logon failures" -Level "INFO"
                    Write-DSCLog "- Directory service access failures" -Level "INFO"
                }
                TestScript = { $false }
                GetScript = { @{ Result = "DC Audit Complete" } }
                DependsOn = '[AuditPolicySubcategory]DirectoryServiceAccess'
            }
        } else {
            Script StandardAuditComplete {
                SetScript = {
                    function Write-DSCLog {
                        param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                        $logEntry = "[$timestamp] [$Level] $Message"
                        $logDir = Split-Path $LogPath
                        if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                        Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                        Write-Host $logEntry
                    }
                    
                    Write-DSCLog "Standard audit policies configured" -Level "INFO"
                    Write-DSCLog "- Account lockout failures" -Level "INFO"
                    Write-DSCLog "- Logon failures" -Level "INFO"
                }
                TestScript = { $false }
                GetScript = { @{ Result = "Standard Audit Complete" } }
                DependsOn = '[AuditPolicySubcategory]LogonEvents'
            }
        }

        # Final completion logging
        Script DSCExecutionComplete {
            SetScript = {
                function Write-DSCLog {
                    param([string]$Message, [string]$Level = "INFO", [string]$LogPath = "C:\Logs\DSC\dsc-execution.log")
                    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
                    $logEntry = "[$timestamp] [$Level] $Message"
                    $logDir = Split-Path $LogPath
                    if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
                    Add-Content -Path $LogPath -Value $logEntry -ErrorAction SilentlyContinue
                    Write-Host $logEntry
                }
                
                Write-DSCLog "=== DSC Configuration Application Completed ===" -Level "INFO"
                Write-DSCLog "Server Role: $using:ServerRole" -Level "INFO"
                Write-DSCLog "Completion Time: $(Get-Date)" -Level "INFO"
                Write-DSCLog "Configuration applied successfully" -Level "INFO"
                
                $summaryPath = "$using:LogPath\dsc-summary-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
                $summary = "DSC Configuration Summary`nServer Role: $using:ServerRole`nNode Name: $using:NodeName`nCompletion Time: $(Get-Date)`nStatus: SUCCESS"
                $summary | Out-File -FilePath $summaryPath -Encoding UTF8
                Write-DSCLog "Summary report generated: $summaryPath" -Level "INFO"
            }
            TestScript = { $false }
            GetScript = { @{ Result = "DSC Execution Complete" } }
            DependsOn = '[Script]CustomRegistryComplete'
        }
    }
}

# Generate MOF files for all roles
Write-Host "Starting MOF generation process..." -ForegroundColor Cyan

@('WebServer', 'DomainController', 'MemberServer') | ForEach-Object {
    $Role = $_
    $OutputPath = ".\MOF\Comprehensive\$Role"
    
    Write-Host "Generating MOF for $Role role..." -ForegroundColor Yellow
    
    try {
        if (!(Test-Path $OutputPath)) {
            New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
        }
        
        ComprehensiveSecurity -ServerRole $Role -NodeName 'localhost' -OutputPath $OutputPath
        
        Write-Host "✓ Generated comprehensive security configuration for $Role role" -ForegroundColor Green
        
        $mofFile = Join-Path $OutputPath "localhost.mof"
        if (Test-Path $mofFile) {
            $mofSize = (Get-Item $mofFile).Length
            Write-Host "  MOF Size: $mofSize bytes" -ForegroundColor Gray
        } else {
            Write-Host "  WARNING: MOF file not found!" -ForegroundColor Red
        }
        
    } catch {
        Write-Host "✗ Failed to generate MOF for $Role role" -ForegroundColor Red
        Write-Host "  Error: $($_.Exception.Message)" -ForegroundColor Red
    }
}

Write-Host "MOF generation process completed!" -ForegroundColor Cyan
Write-Host "Upload the MOF files to S3 and update your Systems Manager associations." -ForegroundColor White

Notice the ConfigurationMode = 'ApplyAndMonitor'? That's intentional. In production, you often want to detect drift without automatically fixing it, especially during business hours. We'll handle auto-remediation through Systems Manager scheduling.

Finally, add a MOF that's lightweight to test your logic before actually throwing a big configuration at things. Save this as test-lightweight.mof in the same .\MOF folder you put the others.

/*
@TargetNode='localhost'
@GeneratedBy=TestUser
@GenerationDate=01/01/2024 12:00:00
@GenerationHost=TestHost
*/

instance of MSFT_RoleResource as $MSFT_RoleResource1ref
{
ResourceID = "[WindowsFeature]TelnetClient";
Ensure = "Absent";
Name = "Telnet-Client";
ModuleName = "PSDesiredStateConfiguration";
ModuleVersion = "1.0";
ConfigurationName = "TestLightweightConfiguration";
};

instance of MSFT_RegistryResource as $MSFT_RegistryResource1ref
{
ResourceID = "[Registry]TestRegKey";
ValueName = "DSCTestValue";
ValueType = "String";
Key = "HKEY_LOCAL_MACHINE\\SOFTWARE\\DSCTest";
ValueData = {"TestConfiguration"};
Ensure = "Present";
ModuleName = "PSDesiredStateConfiguration";
ModuleVersion = "1.0";
ConfigurationName = "TestLightweightConfiguration";
};

instance of MSFT_ServiceResource as $MSFT_ServiceResource1ref
{
ResourceID = "[Service]Spooler";
Name = "Spooler";
State = "Running";
StartupType = "Automatic";
ModuleName = "PSDesiredStateConfiguration";
ModuleVersion = "1.0";
ConfigurationName = "TestLightweightConfiguration";
};

instance of OMI_ConfigurationDocument
{
Version="2.0.0";
MinimumCompatibleVersion = "1.0.0";
CompatibleVersionAdditionalProperties= {"Omi_BaseResource:ConfigurationName"};
Author="TestUser";
GenerationDate="01/01/2024 12:00:00";
GenerationHost="TestHost";
Name="TestLightweightConfiguration";
};

Upload these new MOFs to S3:

# Upload all MOFs to organized folders
Get-ChildItem -Path .\MOF -Recurse -Filter "*.mof" | ForEach-Object {
    $key = "configurations/$($_.Directory.Name)/$($_.Name)"
    Write-S3Object -BucketName "systems-manager-windows-server-dsc-configurations" `
                   -File $_.FullName `
                   -Key $key
}

# Verify they are all there
Get-S3Object -BucketName "systems-manager-windows-server-dsc-configurations" -Prefix "configurations/" | 
    Select-Object Key, Size, LastModified | 
    Format-Table -AutoSize

Deploying via AWS Systems Manager

Now for the fun part - actually deploying these configurations at scale. But first, let's verify Systems Manager can execute commands on your instance:

# Quick test - this should return "Success" status
$InstanceID = "i-YOUR-INSTANCE-ID"
aws ssm send-command `
    --document-name "AWS-RunPowerShellScript" `
    --targets "Key=instanceids,Values=$InstanceID" `
    --parameters 'commands=["echo Hello from Systems Manager"]' `
    --query 'Command.CommandId' `
    --output text | ForEach-Object {
        $cmdId = $_
        do {
            Start-Sleep 2
            $status = aws ssm get-command-invocation --command-id $cmdId --instance-id $InstanceID --query 'Status' --output text
            Write-Host "Status: $status"
        } while ($status -eq "Pending" -or $status -eq "InProgress")
        
        # Get final output
        aws ssm get-command-invocation --command-id $cmdId --instance-id $InstanceID --query '[Status,StandardOutputContent]' --output text
    }

If that doesn't work, you know what I'm going to say... Part 2 is calling your name.

Creating the Systems Manager Document

First, we need a Systems Manager document that knows how to apply our DSC configurations. There is a pre-made AWS-ApplyDSCMofs that you can use out of the box, but I tweaked it a bit:

{
  "schemaVersion": "2.2",
  "description": "Apply CIS Level 1 Baseline using DSC with enhanced error handling - State Manager Compatible",
  "parameters": {
    "configurationName": {
      "type": "String",
      "description": "Name of the DSC configuration",
      "default": "CISLevel1Production",
      "allowedPattern": "^[a-zA-Z0-9_-]+$"
    },
    "configurationS3Bucket": {
      "type": "String",
      "description": "S3 bucket containing the MOF file",
      "allowedPattern": "^[a-z0-9.-]+$"
    },
    "configurationS3Key": {
      "type": "String",
      "description": "S3 key for the MOF file",
      "allowedPattern": "^[a-zA-Z0-9!_.*'()/-]+$"
    },
    "requiredModules": {
      "type": "String",
      "description": "Comma-separated list of DSC modules to install",
      "default": "CisDsc,SecurityPolicyDsc,AuditPolicyDsc,NetworkingDsc,ComputerManagementDsc"
    },
    "testMode": {
      "type": "String",
      "description": "Run in test mode with enhanced logging and validation only",
      "default": "false",
      "allowedValues": ["true", "false"]
    },
    "complianceCheck": {
      "type": "String",
      "description": "Run compliance check after applying",
      "default": "true",
      "allowedValues": ["true", "false"]
    },
    "EnableVerboseLogging": {
      "type": "String",
      "description": "Enable verbose logging for troubleshooting",
      "default": "false",
      "allowedValues": ["true", "false"]
    },
    "EnableDebugLogging": {
      "type": "String",
      "description": "Enable debug logging for detailed troubleshooting",
      "default": "false",
      "allowedValues": ["true", "false"]
    },
    "ProxyUri": {
      "type": "String",
      "description": "Optional proxy server URI",
      "default": "",
      "allowedPattern": "[a-zA-Z0-9\\:\\-_/\\.]*"
    },
    "RebootBehavior": {
      "type": "String",
      "description": "Reboot behavior after configuration application",
      "default": "Never",
      "allowedValues": ["Never", "IfRequired", "Immediately"]
    },
    "MaxRetryAttempts": {
      "type": "String",
      "description": "Maximum retry attempts for DSC application",
      "default": "3",
      "allowedPattern": "^[1-5]$"
    }
  },
  "mainSteps": [
    {
      "action": "aws:runPowerShellScript",
      "name": "applyDSCConfiguration",
      "inputs": {
        "timeoutSeconds": "10800",
        "runCommand": [
          "##################################################################################",
          "# Optimized DSC Configuration Application Script",
          "# Optimizations: Memory management, parallel processing, improved error handling",
          "##################################################################################",
          "",
          "# Optimize PowerShell session configuration",
          "$ProgressPreference = 'SilentlyContinue'",
          "$ConfirmPreference = 'None'",
          "$ErrorActionPreference = 'Stop'",
          "[System.GC]::Collect()  # Initial garbage collection",
          "",
          "# Network optimization",
          "if (-not [Net.ServicePointManager]::SecurityProtocol.HasFlag([Net.SecurityProtocolType]::Tls12)) {",
          "    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12",
          "}",
          "[Net.ServicePointManager]::DefaultConnectionLimit = 100",
          "[Net.ServicePointManager]::Expect100Continue = $false",
          "",
          "# Global variables for performance",
          "$global:ProxyUri = '{{ProxyUri}}'",
          "$global:EnableVerboseLogging = $false",
          "$global:EnableDebugLogging = $false",
          "$global:LogBuffer = [System.Collections.Generic.List[string]]::new()",
          "",
          "# Optimized stack trace with size limit",
          "$global:stackTrace = New-Object 'System.Collections.Generic.Stack[string]'",
          "Set-Variable -Name 'MaxStackDepth' -Value 50 -Option ReadOnly",
          "",
          "# Enhanced trap with memory management",
          "trap {",
          "    try {",
          "        $separator = '#' * 70",
          "        $errorInfo = @(",
          "            $separator",
          "            '# DSC Configuration Error'",
          "            $separator",
          "            \"Error: $($_.Exception.Message) (line $($_.InvocationInfo.ScriptLineNumber))\"",
          "            \"Command: $($_.InvocationInfo.Line.Trim())\"",
          "        )",
          "        ",
          "        if ($global:stackTrace -and $global:stackTrace.GetType().Name -eq 'Stack`1' -and $global:stackTrace.Count -gt 0) {",
          "            $errorInfo += @($separator, '# Stack Trace', $separator)",
          "            # Convert stack to array properly with error handling",
          "            $stackArray = @()",
          "            try {",
          "                while ($global:stackTrace.Count -gt 0) {",
          "                    $stackArray += $global:stackTrace.Pop()",
          "                }",
          "                $errorInfo += $stackArray",
          "            } catch {",
          "                $errorInfo += 'Stack trace unavailable due to error'",
          "            }",
          "        }",
          "        ",
          "        $errorMessage = $errorInfo -join [Environment]::NewLine",
          "        Write-Error $errorMessage -ErrorAction Continue",
          "        ",
          "        # Log to file if logging enabled",
          "        if ($env:DSCLogFileName -and (Test-Path (Split-Path $env:DSCLogFileName -ErrorAction SilentlyContinue))) {",
          "            try { $errorMessage | Out-File -FilePath $env:DSCLogFileName -Append -Encoding UTF8 } catch { }",
          "        }",
          "    } catch {",
          "        Write-Error \"Critical error in error handler: $_\" -ErrorAction Continue",
          "    } finally {",
          "        $exitCode = if ($_.InvocationInfo.ScriptLineNumber -gt 0) { $_.InvocationInfo.ScriptLineNumber } else { 1 }",
          "        [Environment]::Exit($exitCode)",
          "    }",
          "}",

          "",
          "# Optimized logging function",
          "Function Write-DSCLog {",
          "    [CmdletBinding()]",
          "    param(",
          "        [Parameter(Mandatory)]",
          "        [string]$Message,",
          "        [ValidateSet('Info', 'Warning', 'Error', 'Debug', 'Verbose')]",
          "        [string]$Level = 'Info'",
          "    )",
          "    ",
          "    # Early return for disabled logging levels",
          "    if (($Level -eq 'Verbose' -and -not $global:EnableVerboseLogging) -or",
          "        ($Level -eq 'Debug' -and -not $global:EnableDebugLogging)) {",
          "        return",
          "    }",
          "    ",
          "    $timestamp = [DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss.fff')",
          "    $logEntry = \"[$timestamp] [$Level] $Message\"",
          "    ",
          "    # Output to appropriate stream",
          "    switch ($Level) {",
          "        'Error' { Write-Error $logEntry -ErrorAction Continue }",
          "        'Warning' { Write-Warning $logEntry }",
          "        'Debug' { if ($global:EnableDebugLogging) { Write-Debug $logEntry } }",
          "        'Verbose' { if ($global:EnableVerboseLogging) { Write-Verbose $logEntry -Verbose } }",
          "        default { Write-Output $logEntry }",
          "    }",
          "    ",
          "    # Buffer logging for performance",
          "    if ($env:DSCLogFileName) {",
          "        $global:LogBuffer.Add($logEntry)",
          "        # Flush buffer when it gets large",
          "        if ($global:LogBuffer.Count -ge 100) {",
          "            try {",
          "                $global:LogBuffer | Out-File -FilePath $env:DSCLogFileName -Append -Encoding UTF8",
          "                $global:LogBuffer.Clear()",
          "            } catch { <# Ignore logging errors #> }",
          "        }",
          "    }",
          "}",
          "",
          "# Stack management with depth control and error handling",
          "Function Push-StackTrace {",
          "    param([string]$Operation)",
          "    try {",
          "        if ($global:stackTrace -and $global:stackTrace.GetType().Name -eq 'Stack`1' -and $global:stackTrace.Count -lt $MaxStackDepth) {",
          "            $global:stackTrace.Push(\"$([DateTime]::Now.ToString('HH:mm:ss')) - $Operation\")",
          "        }",
          "    } catch {",
          "        # Silently ignore stack errors",
          "    }",
          "}",
          "",
          "Function Pop-StackTrace {",
          "    try {",
          "        if ($global:stackTrace -and $global:stackTrace.GetType().Name -eq 'Stack`1' -and $global:stackTrace.Count -gt 0) {",
          "            [void]$global:stackTrace.Pop()",
          "        }",
          "    } catch {",
          "        # Silently ignore stack errors",
          "    }",
          "}",
          "",
          "# Initialize logging with optimizations",
          "try {",
          "    [bool]$debugEnabled = [bool]::Parse('{{EnableDebugLogging}}')",
          "    [bool]$verboseEnabled = [bool]::Parse('{{EnableVerboseLogging}}')",
          "    ",
          "    if ($debugEnabled) {",
          "        $global:EnableDebugLogging = $true",
          "        $global:EnableVerboseLogging = $true",
          "        $DebugPreference = 'Continue'",
          "        $VerbosePreference = 'Continue'",
          "        ",
          "        $logDir = \"$env:ProgramData\\DSCLogs\"",
          "        if (-not (Test-Path $logDir)) {",
          "            [void](New-Item -Path $logDir -ItemType Directory -Force)",
          "        }",
          "        $env:DSCLogFileName = Join-Path $logDir 'DSC-Apply-Configuration.log'",
          "    } elseif ($verboseEnabled) {",
          "        $global:EnableVerboseLogging = $true",
          "        $VerbosePreference = 'Continue'",
          "    }",
          "} catch {",
          "    Write-Warning \"Failed to initialize logging: $_\"",
          "}",
          "",
          "# Fixed result object as hashtable for dynamic property support",
          "$result = @{",
          "    Status = 'InProgress'",
          "    ConfigurationName = '{{configurationName}}'",
          "    StartTime = [DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss')",
          "    EndTime = $null",
          "    Messages = [System.Collections.Generic.List[string]]::new()",
          "    TestMode = ([bool]::Parse('{{testMode}}'))",
          "    Performance = @{",
          "        ModuleInstallTime = 0.0",
          "        DownloadTime = 0.0",
          "        ValidationTime = 0.0",
          "        ApplicationTime = 0.0",
          "        ComplianceTime = 0.0",
          "    }",
          "    Environment = @{",
          "        PowerShellVersion = $PSVersionTable.PSVersion.ToString()",
          "        OSVersion = [System.Environment]::OSVersion.Version.ToString()",
          "        MachineName = $env:COMPUTERNAME",
          "        InstanceId = $env:AWS_SSM_INSTANCE_ID",
          "        Region = $env:AWS_SSM_REGION_NAME",
          "        ProcessorCount = $env:NUMBER_OF_PROCESSORS",
          "        AvailableMemoryGB = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)",
          "    }",
          "    ComplianceCheck = $null",
          "    Error = $null",
          "    TotalDuration = 0.0",
          "}",
          "",

          "Write-DSCLog \"Starting optimized DSC configuration: $($result.ConfigurationName)\"",
          "Write-DSCLog \"Environment: PS $($result.Environment.PowerShellVersion), $($result.Environment.ProcessorCount) cores, $($result.Environment.AvailableMemoryGB)GB RAM\" -Level Verbose",
          "",
          "try {",
          "    # Reinitialize stack trace to ensure it's properly created",
          "    $global:stackTrace = New-Object 'System.Collections.Generic.Stack[string]'",
          "    Push-StackTrace 'Main execution'",
          "    ",
          "    # Optimized AWS module handling",
          "    Push-StackTrace 'AWS module setup'",
          "    Write-DSCLog 'Setting up AWS PowerShell dependencies'",
          "    ",
          "    # Check if AWS CLI is available for faster S3 operations",
          "    $useAWSCLI = $false",
          "    try {",
          "        $awsVersion = & aws --version 2>$null",
          "        if ($LASTEXITCODE -eq 0 -and $awsVersion) {",
          "            $useAWSCLI = $true",
          "            Write-DSCLog 'AWS CLI detected - will use for S3 operations' -Level Verbose",
          "        }",
          "    } catch { ",
          "        # AWS CLI not available, will use PowerShell modules",
          "    }",
          "    ",
          "    if (-not $useAWSCLI) {",
          "        # Minimal AWS module installation",
          "        $awsModules = @('AWS.Tools.S3')",
          "        foreach ($module in $awsModules) {",
          "            if (-not (Get-Module -ListAvailable -Name $module)) {",
          "                Write-DSCLog \"Installing $module\"",
          "                Install-Module -Name $module -Force -AllowClobber -Scope AllUsers -Repository PSGallery",
          "            }",
          "            Import-Module -Name $module -Force",
          "        }",
          "    }",
          "    ",
          "    # Proxy setup",
          "    if ($global:ProxyUri -and $global:ProxyUri -ne '') {",
          "        try {",
          "            $proxyUri = [Uri]$global:ProxyUri",
          "            if (-not $useAWSCLI) {",
          "                Set-AWSProxy -Hostname $proxyUri.Host -Port $proxyUri.Port",
          "            }",
          "            [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy($global:ProxyUri)",
          "            $result.Messages.Add(\"Proxy configured: $global:ProxyUri\")",
          "        } catch {",
          "            Write-DSCLog \"Proxy setup failed: $($_.Exception.Message)\" -Level Warning",
          "        }",
          "    }",
          "    Pop-StackTrace",
          "    ",
          "    # Optimized PowerShell Gallery setup",
          "    Push-StackTrace 'PowerShell Gallery setup'",
          "    try {",
          "        # Check if NuGet is needed",
          "        if (-not (Get-PackageProvider -Name NuGet -ListAvailable | Where-Object Version -ge '2.8.5.201')) {",
          "            Write-DSCLog 'Installing NuGet package provider'",
          "            Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers",
          "            $result.Messages.Add('Installed NuGet package provider')",
          "        }",
          "        ",
          "        # Set PSGallery as trusted if needed",
          "        $gallery = Get-PSRepository -Name PSGallery",
          "        if ($gallery.InstallationPolicy -ne 'Trusted') {",
          "            Set-PSRepository -Name PSGallery -InstallationPolicy Trusted",
          "            $result.Messages.Add('Set PowerShell Gallery as trusted')",
          "        }",
          "    } catch {",
          "        Write-DSCLog \"PowerShell Gallery setup failed: $_\" -Level Warning",
          "        throw",
          "    }",
          "    Pop-StackTrace",
          "    ",
          "    # Optimized module installation with sequential processing (not parallel for PowerShell 5.1 compatibility)",
          "    $moduleTimer = [System.Diagnostics.Stopwatch]::StartNew()",
          "    Push-StackTrace 'Module installation'",
          "    ",
          "    $modules = '{{requiredModules}}' -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }",
          "    Write-DSCLog \"Processing $($modules.Count) DSC modules\"",
          "    ",
          "    # Install modules sequentially for better compatibility",
          "    foreach ($moduleName in $modules) {",
          "        if ($moduleName) {",
          "            Write-DSCLog \"Processing DSC module: $moduleName\" -Level Verbose",
          "            Push-StackTrace \"Installing module: $moduleName\"",
          "            ",
          "            $existingModule = Get-Module -ListAvailable -Name $moduleName",
          "            if (-not $existingModule) {",
          "                try {",
          "                    $installStart = Get-Date",
          "                    Install-Module -Name $moduleName -Force -AllowClobber -Scope AllUsers -Repository PSGallery",
          "                    $installDuration = ((Get-Date) - $installStart).TotalSeconds",
          "                    Write-DSCLog \"Module $moduleName installed in $installDuration seconds\"",
          "                    $result.Messages.Add(\"Installed: $moduleName ($installDuration sec)\")",
          "                } catch {",
          "                    Write-DSCLog \"Failed to install module $moduleName`: $($_.Exception.Message)\" -Level Warning",
          "                    $result.Messages.Add(\"WARNING: Failed to install $moduleName\")",
          "                }",
          "            } else {",
          "                Write-DSCLog \"Module $moduleName already available (v$($existingModule.Version))\" -Level Verbose",
          "            }",
          "            ",
          "            # Import with verification",
          "            try {",
          "                Import-Module -Name $moduleName -Force",
          "                $imported = Get-Module -Name $moduleName",
          "                if ($imported) {",
          "                    Write-DSCLog \"Successfully imported: $moduleName (v$($imported.Version))\" -Level Verbose",
          "                } else {",
          "                    Write-DSCLog \"Module $moduleName not found after import\" -Level Warning",
          "                }",
          "            } catch {",
          "                Write-DSCLog \"Failed to import module $moduleName`: $($_.Exception.Message)\" -Level Warning",
          "            }",
          "            ",
          "            Pop-StackTrace",
          "        }",
          "    }",
          "",
          "    # Enhanced Working Directory Management",
          "    $workDir = 'C:\\SSM-DSC-Temp'",
          "    Write-DSCLog \"Setting up working directory: $workDir\"",
          "    if (Test-Path $workDir) {",
          "        Remove-Item $workDir -Recurse -Force",
          "        Write-DSCLog 'Cleaned existing working directory' -Level Verbose",
          "    }",
          "    New-Item -Path $workDir -ItemType Directory -Force | Out-Null",
          "",
          "    # Optimized S3 download",
          "    $downloadTimer = [System.Diagnostics.Stopwatch]::StartNew()",
          "    Push-StackTrace 'S3 download'",
          "    ",
          "    $mofPath = Join-Path $workDir 'localhost.mof'",
          "    Write-DSCLog \"Downloading MOF: s3://{{configurationS3Bucket}}/{{configurationS3Key}}\"",
          "    ",
          "    $maxRetries = [int]::Parse('{{MaxRetryAttempts}}')",
          "    $downloadSuccess = $false",
          "    ",
          "    for ($attempt = 1; $attempt -le $maxRetries -and -not $downloadSuccess; $attempt++) {",
          "        try {",
          "            if ($useAWSCLI) {",
          "                # Use AWS CLI for potentially faster downloads",
          "                $awsCommand = \"aws s3 cp s3://{{configurationS3Bucket}}/{{configurationS3Key}} `\"$mofPath`\"\"",
          "                $awsResult = Invoke-Expression $awsCommand",
          "                if ($LASTEXITCODE -eq 0) { $downloadSuccess = $true }",
          "            } else {",
          "                # Use PowerShell AWS tools",
          "                Read-S3Object -BucketName '{{configurationS3Bucket}}' -Key '{{configurationS3Key}}' -File $mofPath",
          "                $downloadSuccess = $true",
          "            }",
          "            ",
          "            if ($downloadSuccess) {",
          "                Write-DSCLog \"Download successful on attempt $attempt\"",
          "            }",
          "        } catch {",
          "            Write-DSCLog \"Download attempt $attempt failed: $($_.Exception.Message)\" -Level Warning",
          "            if ($attempt -lt $maxRetries) {",
          "                Start-Sleep -Seconds (2 * $attempt)  # Exponential backoff",
          "            }",
          "        }",
          "    }",
          "    ",
          "    if (-not $downloadSuccess -or -not (Test-Path $mofPath)) {",
          "        throw \"Failed to download MOF file after $maxRetries attempts\"",
          "    }",
          "    ",
          "    $downloadTimer.Stop()",
          "    $result.Performance.DownloadTime = $downloadTimer.Elapsed.TotalSeconds",
          "    $mofSize = (Get-Item $mofPath).Length",
          "    Write-DSCLog \"Download completed: $mofSize bytes in $($result.Performance.DownloadTime) seconds\"",
          "    Pop-StackTrace",
          "    ",
          "    # Enhanced MOF Validation with proper timer",
          "    $validationTimer = [System.Diagnostics.Stopwatch]::StartNew()",
          "    Write-DSCLog 'Validating MOF file content...'",

          "    try {",
          "        $mofContent = Get-Content $mofPath -Raw",
          "        if ($mofContent.Length -eq 0) {",
          "            throw 'MOF file is empty'",
          "        }",
          "        ",
          "        if ($mofContent -match '@TargetNode') {",
          "            Write-DSCLog 'MOF file contains valid DSC configuration syntax'",
          "        } else {",
          "            Write-Warning 'MOF file may not contain valid DSC configuration'",
          "        }",
          "        ",
          "        # Check for common MOF syntax issues",
          "        if ($mofContent -match 'ValueData\\s*=\\s*\"[^\"]*\";' -and $mofContent -match 'ValueType\\s*=\\s*\"String\\[\\]\";') {",
          "            Write-Warning 'Potential type mismatch detected: String value assigned to String[] property'",
          "            $result.Messages.Add('WARNING: Potential ValueData type mismatch in MOF')",
          "        }",
          "        ",
          "        $mofLines = ($mofContent -split '\\r?\\n').Length",
          "        Write-DSCLog \"MOF file contains $mofLines lines\"",
          "        $result.Messages.Add(\"MOF file validation passed ($mofLines lines)\")",
          "        ",
          "        # Additional MOF validation",
          "        Write-DSCLog 'Performing advanced MOF validation...'",

          "        try {",
          "            $tempConfigPath = Join-Path $workDir 'temp-validation'",
          "            New-Item -Path $tempConfigPath -ItemType Directory -Force | Out-Null",
          "            Copy-Item $mofPath (Join-Path $tempConfigPath 'localhost.mof')",
          "            ",
          "            $null = Test-DscConfiguration -Path $tempConfigPath -ErrorAction Stop",
          "            Write-DSCLog 'MOF syntax validation passed'",
          "            Remove-Item $tempConfigPath -Recurse -Force -ErrorAction SilentlyContinue",
          "        } catch {",
          "            Write-Warning \"MOF syntax validation failed: $($_.Exception.Message)\"",
          "            $result.Messages.Add(\"WARNING: MOF syntax issue detected: $($_.Exception.Message)\")",
          "            Remove-Item $tempConfigPath -Recurse -Force -ErrorAction SilentlyContinue",
          "        }",
          "    } catch {",
          "        throw \"MOF file validation failed: $($_.Exception.Message)\"",
          "    }",
          "    ",
          "    $validationTimer.Stop()",
          "    $result.Performance.ValidationTime = $validationTimer.Elapsed.TotalSeconds",
          "",
          "    # Test mode or application",
          "    if ([bool]::Parse('{{testMode}}')) {",
          "        Write-DSCLog '=== TEST MODE: All validations completed ===' -Level Warning",
          "        $result.Status = 'TestModeSuccess'",
          "        $result.Messages.Add('Test mode completed successfully')",
          "    } else {",
          "        $appTimer = [System.Diagnostics.Stopwatch]::StartNew()",
          "        Push-StackTrace 'DSC application'",
          "        ",
          "        $maxRetries = [int]::Parse('{{MaxRetryAttempts}}')",
          "        $applied = $false",
          "        for ($attempt = 1; $attempt -le $maxRetries -and -not $applied; $attempt++) {",
          "            try {",
          "                Write-DSCLog \"DSC application attempt $attempt/$maxRetries\"",
          "                Push-StackTrace \"DSC Application attempt $attempt\"",
          "                ",
          "                Start-DscConfiguration -Path $workDir -Wait -Force -Verbose:$global:EnableVerboseLogging",
          "                $applied = $true",
          "                Write-DSCLog \"Configuration applied successfully\"",
          "                Pop-StackTrace",
          "                ",
          "            } catch [Microsoft.Management.Infrastructure.CimException] {",
          "                $cimMsg = $_.Exception.Message",
          "                Write-DSCLog \"CIM Exception: $cimMsg\" -Level Warning",
          "                ",
          "                if ($cimMsg -match 'Convert property.*failed' -and $cimMsg -match 'ValueData') {",
          "                    Write-DSCLog 'Type conversion error - likely MOF ValueData format issue' -Level Error",
          "                    Write-DSCLog 'Suggestion: Ensure ValueData uses array format: ValueData = {\"value\"}' -Level Error",
          "                }",
          "                ",
          "                Pop-StackTrace",
          "                if ($attempt -eq $maxRetries) { throw }",
          "                Start-Sleep -Seconds (10 * $attempt)",
          "                ",
          "            } catch {",
          "                Write-DSCLog \"Attempt $attempt failed: $($_.Exception.Message)\" -Level Warning",
          "                Pop-StackTrace",
          "                if ($attempt -eq $maxRetries) { throw }",
          "                Start-Sleep -Seconds (10 * $attempt)",
          "            }",
          "        }",
          "        ",
          "        $appTimer.Stop()",
          "        $result.Performance.ApplicationTime = $appTimer.Elapsed.TotalSeconds",
          "        ",
          "        # Enhanced Compliance Checking",
          "        if ([bool]::Parse('{{complianceCheck}}') -and $applied) {",
          "            $compTimer = [System.Diagnostics.Stopwatch]::StartNew()",
          "            Write-DSCLog 'Running compliance validation'",
          "            ",
          "            $testResult = Test-DscConfiguration -Detailed",
          "            $compTimer.Stop()",
          "            $result.Performance.ComplianceTime = $compTimer.Elapsed.TotalSeconds",
          "            ",
          "            $result.ComplianceCheck = @{",
          "                InDesiredState = $testResult.InDesiredState",
          "                TestDate = [DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss')",
          "                Duration = $result.Performance.ComplianceTime",
          "                ResourcesTotal = ($testResult.ResourcesInDesiredState.Count + $testResult.ResourcesNotInDesiredState.Count)",
          "                ResourcesCompliant = $testResult.ResourcesInDesiredState.Count",
          "                ResourcesNonCompliant = $testResult.ResourcesNotInDesiredState.Count",
          "            }",
          "            ",
          "            if ($testResult.InDesiredState) {",
          "                Write-DSCLog 'SUCCESS: All resources in desired state'",
          "                $result.Status = 'Success'",
          "            } else {",
          "                Write-DSCLog \"WARNING: $($testResult.ResourcesNotInDesiredState.Count) resources not compliant\" -Level Warning",
          "                $result.Status = 'SuccessWithWarnings'",
          "            }",
          "        } else {",
          "            $result.Status = 'Success'",
          "        }",
          "        ",
          "        Pop-StackTrace",
          "    }",
          "    ",
          "    Pop-StackTrace",
          "",
          "} catch {",
          "    $result.Status = 'Failed'",
          "    $result.Error = $_.Exception.Message",
          "    $result.Messages.Add(\"FAILED: $($_.Exception.Message)\")",
          "    Write-DSCLog \"DSC operation failed: $($_.Exception.Message)\" -Level Error",
          "    ",
          "    # Enhanced error details for troubleshooting",
          "    $errorDetails = @{",
          "        Message = $_.Exception.Message",
          "        Type = $_.Exception.GetType().FullName",
          "        LineNumber = $_.InvocationInfo.ScriptLineNumber",
          "        Command = $_.InvocationInfo.Line.Trim()",
          "        StackTraceType = if ($global:stackTrace) { $global:stackTrace.GetType().FullName } else { 'null' }",
          "    }",
          "    Write-DSCLog \"Error details: $($errorDetails | ConvertTo-Json -Compress)\" -Level Debug",
          "    exit 1",
          "",
          "} finally {",
          "    # Performance summary and cleanup",
          "    $result.EndTime = [DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss')",
          "    $totalDuration = ([DateTime]::Now - [DateTime]::Parse($result.StartTime)).TotalSeconds",
          "    $result.TotalDuration = [math]::Round($totalDuration, 2)",
          "    ",
          "    Write-DSCLog \"=== EXECUTION SUMMARY ===\"",
          "    Write-DSCLog \"Status: $($result.Status)\"",
          "    Write-DSCLog \"Total Duration: $($result.TotalDuration) seconds\"",
          "    Write-DSCLog \"Modules: $($result.Performance.ModuleInstallTime)s | Download: $($result.Performance.DownloadTime)s | App: $($result.Performance.ApplicationTime)s\" -Level Verbose",
          "    ",
          "    # Flush any remaining logs",
          "    if ($global:LogBuffer -and $global:LogBuffer.Count -gt 0 -and $env:DSCLogFileName) {",
          "        try { $global:LogBuffer | Out-File -FilePath $env:DSCLogFileName -Append -Encoding UTF8 } catch { }",
          "    }",
          "    ",
          "    # Output final result",
          "    try {",
          "        $result | ConvertTo-Json -Depth 5 -Compress | Write-Output",
          "    } catch {",
          "        Write-Output \"Status: $($result.Status), Duration: $($result.TotalDuration)s\"",
          "    }",
          "    ",
          "    # Cleanup",
          "    if (Test-Path $workDir -ErrorAction SilentlyContinue) {",
          "        Remove-Item $workDir -Recurse -Force -ErrorAction SilentlyContinue",
          "    }",
          "    ",
          "    # Memory cleanup",
          "    if ($global:LogBuffer -and $global:LogBuffer.GetType().Name -eq 'List`1') { ",
          "        try { $global:LogBuffer.Clear() } catch { } ",
          "    }",
          "    if ($global:stackTrace -and $global:stackTrace.GetType().Name -eq 'Stack`1') { ",
          "        try { $global:stackTrace.Clear() } catch { } ",
          "    }",
          "    [System.GC]::Collect()",
          "    ",
          "    Write-DSCLog \"DSC operation completed: $($result.Status)\"",
          "}"
        ]
      },
      "outputs": [
        {
          "Name": "ConfigurationStatus",
          "Selector": "$",
          "Type": "StringMap"
        }
      ]
    }
  ],
  "outputs": [
    "applyDSCConfiguration.ConfigurationStatus"
  ]
}

Save this as DSC-Apply-Configuration.json and create the document:

aws ssm create-document `
    --name "DSC-Apply-CIS-Configuration" `
    --document-type "Command" `
    --content file://DSC-Apply-Configuration.json `
    --document-format "JSON"

Creating State Manager Association

Now let's create associations that automatically apply our configurations. In the console, you can do this via Systems Manager > Node Tools > State Manager > Create association.

Throw a name in there if you'd like and then search for the document you just uploaded. We're going to use the test-lightweight.mof as our guinea pig first and run it once.

Or the same information via CLI:

aws ssm create-association `
    --name "DSC_Apply_CIS_Configuration" `
    --association-name "TEST-Lightweight-SingleServer-Association"
    --targets "Key=InstanceIds,Values=i-YOUR-INSTANCE-ID" `
    --parameters '{
        "configurationName": ["TestLightweightConfiguration"],
        "configurationS3Bucket": ["systems-manager-windows-server-dsc-configurations"],
        "configurationS3Key": ["configurations/MOF/test-lightweight.mof"],
        "requiredModules": ["Cim;SecurityPolicy;DSC.AuditPolicy;DSC.NetworkingDsc;ComputerManagementDsc"],
        "testMode": ["true"],
        "complianceCheck": ["true"],
        "EnableVerboseLogging": ["false"],
        "EnableDebugLogging": ["false"],
        "ProxyUri": [""],
        "RebootBehavior": ["IfRequired"],
        "MaxRetryAttempts": [""]
    }' `
    --output-location '{
        "S3Location": {
            "OutputS3BucketName": "systems-manager-windows-server-dsc-configurations",
            "OutputS3KeyPrefix": "associations/dsc/"
        }
    }' `
    --compliance-severity "HIGH" `
    --max-errors "0" `
    --region "us-east-1"

You will be redirected to the Association's page and see it is in a grey Pending state.

Wait a couple minutes and it should change to a green Success on refresh. Once it's green, click on it and go the Execution History and you should see a success as well.

You can drill down to the output, maybe seeing it install some other DSC modules that we haven't covered yet. We also send it to the S3 bucket we've been using for MOFs, so you can check there as well but note the GUID there is your Run Command ID, not your Execution History ID.

If you look at the registry or settings nothing was changed. Why? Because we set the test mode = true flag, let's modify the association again and change that value.

You can just edit via console or send another cli.

aws ssm update-association `
    --association-id "YOUR-ASSOCIATION-ID" `
    --parameters '{"TestMode": ["false"]}' `
    --region "us-east-1"

After the update we once again we wait for Systems Manager to reach out to our instance and apply the configurations. Once it shows Success, let's RDP and check the registry, you should see a HKEY_LOCAL_MACHINE\SOFTWARE\DSCTest folder with a DSCTestValue set to TestConfiguration now. You can also check the telnet and print spooler settings to verify as well.

Deployment Strategies

Now that you've successfully deployed to a single test instance, let's talk about scaling up safely. The jump from one server to many is where things get interesting (and potentially dangerous).

Start with Tags

The key to safe deployments is using EC2 tags to control your rollout. If you haven't already when we made the template, tag your instances based on their role and environment:

# Tag your test instances
aws ec2 create-tags --resources i-YOUR-INSTANCE-ID, i-YOU-INSTANCE-ID2 --tags Key=Environment,Value=Test

# Tag instance by role
aws ec2 create-tags --resources i-YOUR-INSTANCE-ID --tags Key=Environment,Value=Test Key=DSCRole,Value=MemberServer

# Tag production servers by criticality
aws ec2 create-tags --resources i-PROD-INSTANCE-1 --tags Key=Environment,Value=Production Key=DeploymentGroup,Value=Canary
aws ec2 create-tags --resources i-PROD-INSTANCE-2,i-PROD-INSTANCE-3 --tags Key=Environment,Value=Production Key=DeploymentGroup,Value=Wave1

The Safe Scaling Approach

Here's how to go from one server to many without breaking everything:

1. Expand to Your Test Environment

First, deploy to all test servers you just tagged to catch any server-specific issues:

# Create association for test environment
aws ssm create-association `
    --name "DSC-Apply-CIS-Configuration" `
    --association-name "DSC-CIS-Test-Association" `
    --targets "Key=tag:Environment,Values=Test" `
    --parameters '{
        "configurationName": ["DSC-CIS-TEST"],
        "configurationS3Bucket": ["systems-manager-windows-server-dsc-configurations"],
        "configurationS3Key": ["configurations/MemberServer/localhost.mof"],
        "requiredModules": ["Cim;SecurityPolicy;DSC.AuditPolicy;DSC.NetworkingDsc;ComputerManagementDsc"],
        "testMode": ["false"],
        "complianceCheck": ["true"],
        "EnableVerboseLogging": ["false"],
        "EnableDebugLogging": ["false"],
        "ProxyUri": [""],
        "RebootBehavior": ["IfRequired"],
        "MaxRetryAttempts": ["1"]
    }' `
    --output-location '{
        "S3Location": {
            "OutputS3BucketName": "systems-manager-windows-server-dsc-configurations",
            "OutputS3KeyPrefix": "associations/dsc/test"
        }
    }' `
    --schedule-expression "rate(12 hours)" `
    --max-concurrency "2" `
    --max-errors "1" `
    --region "us-east-1"

Key settings explained:

  • max-concurrency "2" - Only 2 servers at a time
  • max-errors "1" - Stop if any server fails
  • schedule-expression "rate(12 hours)" - Re-apply twice daily

2. Production Canary Deployment

Pick 1-2 production servers as your "canaries" - these brave servers get changes first:

# Deploy to canary servers only
aws ssm create-association `
    --name "DSC-Apply-CIS-Configuration" `
    --association-name "DSC-CIS-Canary-Association" `
    --targets "Key=tag:Environment,Values=Canary" `
    --parameters '{
        "configurationName": ["DSC-CIS-CANARY"],
        "configurationS3Bucket": ["systems-manager-windows-server-dsc-configurations"],
        "configurationS3Key": ["configurations/MemberServer/localhost.mof"],
        "requiredModules": ["Cim;SecurityPolicy;DSC.AuditPolicy;DSC.NetworkingDsc;ComputerManagementDsc"],
        "testMode": ["false"],
        "complianceCheck": ["true"],
        "EnableVerboseLogging": ["false"],
        "EnableDebugLogging": ["false"],
        "ProxyUri": [""],
        "RebootBehavior": ["IfRequired"],
        "MaxRetryAttempts": ["1"]
    }' `
    --output-location '{
        "S3Location": {
            "OutputS3BucketName": "systems-manager-windows-server-dsc-configurations",
            "OutputS3KeyPrefix": "associations/dsc/canary"
        }
    }' `
    --schedule-expression "rate(24 hours)" `
    --region "us-east-1"

Monitor for 24-48 hours before proceeding. Check:

  • Performance metrics (CPU, memory)
  • Event logs for errors
  • Application functionality
  • User complaints

3. Gradual Production Rollout

If your canaries survive, gradually increase the deployment scope:

# Phase 1: 10% of production
aws ssm create-association `
    --name "DSC-Apply-CIS-Configuration" `
    --association-name "DSC-CIS-Prod-Association"
    --targets "Key=tag:Environment,Values=Prod" `
    --parameters '{
        "configurationName": ["DSC-CIS-PROD"],
        "configurationS3Bucket": ["systems-manager-windows-server-dsc-configurations"],
        "configurationS3Key": ["configurations/MemberServer/localhost.mof"],
        "requiredModules": ["Cim;SecurityPolicy;DSC.AuditPolicy;DSC.NetworkingDsc;ComputerManagementDsc"],
        "testMode": ["false"],
        "complianceCheck": ["true"],
        "EnableVerboseLogging": ["false"],
        "EnableDebugLogging": ["false"],
        "ProxyUri": [""],
        "RebootBehavior": ["IfRequired"],
        "MaxRetryAttempts": ["1"]
    }' `
    --output-location '{
        "S3Location": {
            "OutputS3BucketName": "systems-manager-windows-server-dsc-configurations",
            "OutputS3KeyPrefix": "associations/dsc/prod"
        }
    }' `
    --max-concurrency "10%" `
    --max-errors "5" ``
    --region "us-east-1"

# After validation, Phase 2: 50% concurrency
aws ssm update-association `
    --association-id "YOUR-ASSOCIATION-ID" `
    --max-concurrency "50%"

# Finally: All servers (but still with limits)
aws ssm update-association `
    --association-id "YOUR-ASSOCIATION-ID" `
    --max-concurrency "20" `
    --max-errors "10"

Understanding Concurrency Settings

  • Percentage: "10%" - Deploys to 10% of targeted instances at once
  • Fixed number: "20" - Deploys to exactly 20 instances at once
  • Which to use: Percentages for small fleets, fixed numbers for large fleets (you don't want 10% of 1000 servers = 100 simultaneous deployments)

Quick Rollback Plan

If something goes wrong, here's your emergency brake:

# Option 1: Stop the association immediately (prevents future runs)
aws ssm delete-association --association-id "YOUR-ASSOCIATION-ID"

# Option 2: Disable the association (keeps it but stops execution)
aws ssm update-association `
    --association-id "YOUR-ASSOCIATION-ID" `
    --schedule-expression "rate(999 days)" `
    --region "us-east-1"

# Option 3: Revert to test mode (safer than deletion)
aws ssm update-association `
    --association-id "YOUR-ASSOCIATION-ID" `
    --parameters '{
        "ConfigurationName": ["YOUR-CONFIGURATION-NAME"],
        "configurationS3Bucket": ["systems-manager-windows-server-dsc-configurations"],
        "configurationS3Key": ["configurations/MemberServer/localhost.mof"],
        "TestMode": ["true"],
        "ComplianceCheck": ["false"]
    }' `
    --region "us-east-1"

Deployment Checklist

Before each phase:

  • [ ] Review compliance reports from previous phase
  • [ ] Check performance metrics
  • [ ] Verify no critical alerts fired
  • [ ] Confirm rollback plan is ready
  • [ ] Document any new exclusions needed

For now, this manual phased approach works well for fleets up to a few hundred servers. In Part 4, we'll automate this entire process with PowerShell functions that handle the monitoring and phase progression automatically.

Monitoring and Compliance

Deploying is only half the battle. You might have been wondering how you're supposed to check for metrics. Now we need visibility into what's actually happening.

Real-time Compliance Monitoring

In the console, you can view compliance data under Systems Manager > Node Tools > Compliance.

Or if you're like me and prefer scripts, let's build a compliance report using PowerShell.

<#
.SYNOPSIS
    Generates comprehensive AWS Systems Manager compliance reports for managed instances.

.DESCRIPTION
    This script retrieves and analyzes compliance status across AWS Systems Manager managed Windows instances.
    It provides detailed reporting on all compliance items (patches, associations, custom compliance),
    execution history, and generates exportable reports for trending and analysis.
    The script uses AWS CLI for all API interactions.

.PARAMETER InstanceIds
    Optional array of specific EC2 instance IDs to check. If not provided, all online Windows managed instances will be checked.

.PARAMETER DetailedReport
    Switch parameter to include detailed information about non-compliant items in the console output.

.PARAMETER TimeoutSeconds
    Timeout value for AWS CLI operations. Default is 30 seconds.

.NOTES
    File Name     : Get-SSMCompliance.ps1
    Author        : Jeffrey Stuhr
    Prerequisites : AWS CLI must be installed and configured with appropriate IAM permissions
    Required IAM Permissions:
    - ssm:DescribeInstanceInformation
    - ssm:ListComplianceItems
    - ssm:ListCommandInvocations
    - sts:GetCallerIdentity

    Blog Reference: This script supports Systems Manager compliance monitoring as discussed at:
                   https://www.techbyjeff.net/

.LINK
    https://www.techbyjeff.net/

.EXAMPLE
    .\Get-SSMCompliance.ps1

    Generates a compliance report for all online Windows managed instances with basic output.

.EXAMPLE
    .\Get-SSMCompliance.ps1 -DetailedReport

    Generates a compliance report with detailed non-compliant items displayed in the console.

.EXAMPLE
    .\Get-SSMCompliance.ps1 -InstanceIds @("i-0123456789abcdef0", "i-0987654321fedcba0") -DetailedReport

    Generates a detailed compliance report for specific instances only.
#>

# Get comprehensive compliance status
function Get-SSMComplianceReport {
    <#
    .SYNOPSIS
        Core function that retrieves Systems Manager compliance data from AWS.

    .DESCRIPTION
        This function queries AWS Systems Manager for all compliance information across managed instances,
        including patch compliance, association compliance, and custom compliance items.
        Processes the data and returns a structured report object containing compliance metrics.
    #>
    param(
        [Parameter(Mandatory=$false, HelpMessage="Array of EC2 instance IDs to check for compliance")]
        [string[]]$InstanceIds = @(),

        [Parameter(Mandatory=$false, HelpMessage="Include detailed non-compliant items in console output")]
        [switch]$DetailedReport,

        [Parameter(Mandatory=$false, HelpMessage="Timeout for AWS CLI operations in seconds")]
        [int]$TimeoutSeconds = 30
    )

    # Verify AWS CLI availability and authentication
    Write-Host "Verifying AWS CLI prerequisites..." -ForegroundColor Green

    # Check if AWS CLI is available in the system PATH
    if (-not (Get-Command aws -ErrorAction SilentlyContinue)) {
        throw "AWS CLI is not installed or not in PATH. Please install AWS CLI first."
    }

    # Test AWS CLI connectivity and authentication
    Write-Host "Testing AWS CLI connectivity..." -ForegroundColor Green
    try {
        # Attempt to get caller identity to verify authentication
        $testResult = aws sts get-caller-identity --output json 2>$null
        if ($LASTEXITCODE -ne 0) {
            throw "AWS CLI authentication failed. Please run 'aws configure' first."
        }
        Write-Host "AWS CLI connectivity confirmed." -ForegroundColor Green
    }
    catch {
        throw "AWS CLI test failed: $_"
    }

    # Retrieve target instances if none specified
    if ($InstanceIds.Count -eq 0) {
        Write-Host "Retrieving managed instances..." -ForegroundColor Green

        # Query for all online Windows instances managed by Systems Manager
        # Uses JMESPath query to filter for online Windows instances only
        $instancesJson = aws ssm describe-instance-information --query "InstanceInformationList[?PingStatus=='Online' && PlatformType=='Windows'].InstanceId" --output json 2>$null

        if ($LASTEXITCODE -ne 0) {
            throw "Failed to retrieve instance information from AWS"
        }

        # Parse JSON response and convert to PowerShell array
        $InstanceIds = $instancesJson | ConvertFrom-Json
        Write-Host "Found $($InstanceIds.Count) managed instances." -ForegroundColor Green
    }

    # Initialize data collection array
    $complianceData = @()
    $instanceCount = 0

    # Process each instance for compliance data
    foreach ($instanceId in $InstanceIds) {
        $instanceCount++

        # Update progress bar with current status
        Write-Progress -Activity "Checking compliance" -Status "$instanceId ($instanceCount of $($InstanceIds.Count))" -PercentComplete (($instanceCount / $InstanceIds.Count) * 100)
        Write-Host "Processing instance: $instanceId" -ForegroundColor Cyan

        try {
            # Retrieve instance metadata
            Write-Host "  Getting instance details..." -ForegroundColor Gray

            # Query specific instance information using instance ID filter
            $instanceJson = aws ssm describe-instance-information --instance-information-filter-list "key=InstanceIds,valueSet=$instanceId" --output json 2>$null

            if ($LASTEXITCODE -ne 0) {
                Write-Warning "Failed to get instance information for $instanceId"
                continue
            }

            # Parse and validate instance response
            $instanceResult = $instanceJson | ConvertFrom-Json
            if (-not $instanceResult.InstanceInformationList -or $instanceResult.InstanceInformationList.Count -eq 0) {
                Write-Warning "No instance information found for $instanceId"
                continue
            }
            $instance = $instanceResult.InstanceInformationList[0]

            # Retrieve compliance items for this instance
            Write-Host "  Getting compliance items..." -ForegroundColor Gray

            # Query all compliance items for the managed instance
            # This includes patch compliance, association compliance, and any custom compliance items
            $complianceJson = aws ssm list-compliance-items --resource-id $instanceId --resource-type "ManagedInstance" --output json 2>$null
            $compliance = @()

            if ($LASTEXITCODE -eq 0) {
                $complianceResult = $complianceJson | ConvertFrom-Json
                if ($complianceResult.ComplianceItems) {
                    $compliance = $complianceResult.ComplianceItems
                }
            }

            # Retrieve command execution history to find recent document executions
            Write-Host "  Getting command history..." -ForegroundColor Gray

            # Query recent command invocations to find DSC or other document executions
            $commandsJson = aws ssm list-command-invocations --instance-id $instanceId --max-items 5 --output json 2>$null
            $lastExecution = $null

            if ($LASTEXITCODE -eq 0) {
                $commandsResult = $commandsJson | ConvertFrom-Json
                if ($commandsResult.CommandInvocations) {
                    # Get the most recent command execution (prioritize DSC if available)
                    $dscExecution = $commandsResult.CommandInvocations | Where-Object {$_.DocumentName -like "*DSC*"} | Select-Object -First 1
                    $lastExecution = if ($dscExecution) { $dscExecution } else { $commandsResult.CommandInvocations | Select-Object -First 1 }
                }
            }

            # Build compliance data object for this instance
            $complianceData += [PSCustomObject]@{
                InstanceId = $instanceId
                Name = if ($instance.ComputerName) { $instance.ComputerName } else { "Unknown" }
                LastExecution = if ($lastExecution) { $lastExecution.RequestedDateTime } else { $null }
                Status = if ($lastExecution) { $lastExecution.Status } else { "Unknown" }
                CompliantItems = ($compliance | Where-Object {$_.Status -eq "COMPLIANT"}).Count
                NonCompliantItems = ($compliance | Where-Object {$_.Status -eq "NON_COMPLIANT"}).Count
                CompliancePercentage = if ($compliance.Count -gt 0) {
                    # Calculate compliance percentage rounded to 2 decimal places
                    [math]::Round((($compliance | Where-Object {$_.Status -eq "COMPLIANT"}).Count / $compliance.Count) * 100, 2)
                } else { 0 }
            }

            # Display detailed non-compliant items if requested
            if ($DetailedReport -and ($compliance | Where-Object {$_.Status -eq "NON_COMPLIANT"})) {
                Write-Host "`nNon-compliant items for $($instance.ComputerName):" -ForegroundColor Yellow
                $compliance | Where-Object {$_.Status -eq "NON_COMPLIANT"} |
                    Select-Object Title, Severity |
                    Format-Table -AutoSize
            }
        }
        catch {
            # Log any errors encountered during instance processing
            Write-Warning "Error processing instance $instanceId : $_"
            continue
        }
    }

    # Clear progress bar
    Write-Progress -Activity "Checking compliance" -Completed
    return $complianceData
}

# Main execution block
Write-Host "Starting Systems Manager Compliance Report..." -ForegroundColor Green

# Generate the compliance report
$report = Get-SSMComplianceReport -DetailedReport

# Display results in formatted table
$report | Format-Table -AutoSize

# Export results to CSV file with timestamp
$csvPath = ".\SSM-Compliance-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv"
$report | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host "Report exported to: $csvPath" -ForegroundColor Green

Creating CloudWatch Dashboard

Let's create a basic dashboard to visualize compliance:

{
    "widgets": [
        {
            "type": "metric",
            "x": 0,
            "y": 0,
            "width": 12,
            "height": 6,
            "properties": {
                "metrics": [
                    [ "AWS/SSM-RunCommand", "CommandsSucceeded", { "stat": "Sum" } ],
                    [ ".", "CommandsFailed", { "stat": "Sum" } ]
                ],
                "period": 300,
                "region": "us-east-1",
                "title": "DSC Deployment Status"
            }
        },
        {
            "type": "log",
            "x": 12,
            "y": 0,
            "width": 12,
            "height": 6,
            "properties": {
                "query": "SOURCE '/aws/systemsmanager/ssm-errors' | fields @timestamp, @message\n| filter @message like /ERROR/ or @message like /failed/\n| parse @message /(?<instance>i-[a-z0-9]+)/\n| parse @message /ERROR\\s+(?<error_summary>.{1,100})/\n| sort @timestamp desc\n| limit 10",
                "region": "us-east-1",
                "title": "SSM Agent Errors",
                "view": "table"
            }
        },
        {
            "type": "log",
            "x": 0,
            "y": 6,
            "width": 12,
            "height": 6,
            "properties": {
                "query": "SOURCE '/aws/systemsmanager/dsc-files' | fields @timestamp, @message\n| parse @message /\\[(?<level>\\w+)\\]/\n| parse @message /Server Role: (?<role>\\w+)/\n| parse @message /(?<summary>[^\\n]{1,80})/\n| sort @timestamp desc\n| limit 15",
                "region": "us-east-1",
                "title": "DSC Execution Details",
                "view": "table"
            }
        },
        {
            "type": "log",
            "x": 12,
            "y": 6,
            "width": 12,
            "height": 6,
            "properties": {
                "query": "SOURCE '/aws/systemsmanager/dsc-files' | fields @timestamp, @message, @logStream\n| filter @message like /ERROR/ or @message like /failed/ or @message like /Starting/ or @message like /Completed/\n| parse @logStream /(?<instance_id>i-[a-z0-9]+)/\n| sort @timestamp desc\n| limit 10",
                "region": "us-east-1",
                "title": "Critical Events by Instance (DSC Files)",
                "view": "table"
            }
        },
        {
            "type": "log",
            "x": 0,
            "y": 12,
            "width": 12,
            "height": 6,
            "properties": {
                "query": "SOURCE '/aws/systemsmanager/ssm-errors' | fields @timestamp, @message, @logStream\n| filter @message like /ERROR/ or @message like /failed/\n| parse @logStream /(?<instance_id>i-[a-z0-9]+)/\n| sort @timestamp desc\n| limit 10",
                "region": "us-east-1",
                "title": "Critical Events by Instance (SSM Errors)",
                "view": "table"
            }
        }
    ]
}

Save the above as dashboard.json and then we'll create the dashboard:

aws cloudwatch put-dashboard `
    --dashboard-name "DSC-CIS-Compliance" `
    --dashboard-body file://dashboard.json `
    --region "us-east-1"

Setting Up Alerts

Don't wait for someone to check the dashboard - get notified when things go wrong:

# Setup-DSC-Monitoring.ps1
# Run this ONCE to set up CloudWatch monitoring for your DSC configurations

Write-Host "Setting up DSC CloudWatch monitoring..." -ForegroundColor Cyan

# Step 1: Create SNS topic for alerts
Write-Host "Creating SNS topic for alerts..." -ForegroundColor Yellow
try {
    $topic = aws sns create-topic --name "DSC-Compliance-Alerts" --query 'TopicArn' --output text
    Write-Host "✓ SNS topic created: $topic" -ForegroundColor Green
    
    # Subscribe your email (CHANGE THIS EMAIL!)
    $email = "your-email@company.com"  # <-- CHANGE THIS TO YOUR EMAIL
    aws sns subscribe `
        --topic-arn $topic `
        --protocol email `
        --notification-endpoint $email
    
    Write-Host "✓ Email subscription added for: $email" -ForegroundColor Green
    Write-Host "  Check your email and confirm the subscription!" -ForegroundColor Yellow
    
} catch {
    Write-Host "✗ Failed to create SNS topic" -ForegroundColor Red
    Write-Host "  Error: $($_.Exception.Message)" -ForegroundColor Red
    return
}

# Step 2: Create metric filters for log analysis
Write-Host "Creating CloudWatch log metric filters..." -ForegroundColor Yellow

try {
    # Filter for successful DSC executions
    aws logs put-metric-filter `
        --log-group-name "/aws/systemsmanager/dsc-files" `
        --filter-name "DSC-Success-Count" `
        --filter-pattern "[timestamp, level=INFO, message*completed*successfully*]" `
        --metric-transformations `
            metricName=DSC_Success_Count,metricNamespace=CWAgent,metricValue=1

    # Filter for total DSC executions
    aws logs put-metric-filter `
        --log-group-name "/aws/systemsmanager/dsc-files" `
        --filter-name "DSC-Total-Count" `
        --filter-pattern "[timestamp, level=INFO, message*Starting*DSC*Configuration*]" `
        --metric-transformations `
            metricName=DSC_Total_Count,metricNamespace=CWAgent,metricValue=1

    # Filter for DSC errors
    aws logs put-metric-filter `
        --log-group-name "/aws/systemsmanager/dsc-files" `
        --filter-name "DSC-Error-Count" `
        --filter-pattern "[timestamp, level=ERROR, ...]" `
        --metric-transformations `
            metricName=DSC_Error_Count,metricNamespace=CWAgent,metricValue=1

    # Filter for SSM errors
    aws logs put-metric-filter `
        --log-group-name "/aws/systemsmanager/ssm-errors" `
        --filter-name "SSM-Error-Count" `
        --filter-pattern "[timestamp, level=ERROR, ...]" `
        --metric-transformations `
            metricName=SSM_Error_Count,metricNamespace=CWAgent,metricValue=1

    Write-Host "✓ All metric filters created successfully" -ForegroundColor Green
    
} catch {
    Write-Host "✗ Failed to create metric filters" -ForegroundColor Red
    Write-Host "  Error: $($_.Exception.Message)" -ForegroundColor Red
}

# Step 3: Create CloudWatch alarms
Write-Host "Creating CloudWatch alarms..." -ForegroundColor Yellow

try {
    # Alarm for failed deployments (using existing SSM metrics)
    aws cloudwatch put-metric-alarm `
        --alarm-name "DSC-Deployment-Failures" `
        --alarm-description "Alert on DSC deployment failures" `
        --metric-name "CommandsFailed" `
        --namespace "AWS/SSM-RunCommand" `
        --statistic Sum `
        --period 300 `
        --evaluation-periods 1 `
        --threshold 5 `
        --comparison-operator GreaterThanThreshold `
        --alarm-actions $topic

    # Alarm for DSC errors in logs
    aws cloudwatch put-metric-alarm `
        --alarm-name "DSC-Critical-Errors" `
        --alarm-description "Alert on ERROR level messages in DSC logs" `
        --metric-name "DSC_Error_Count" `
        --namespace "CWAgent" `
        --statistic Sum `
        --period 300 `
        --evaluation-periods 1 `
        --threshold 1 `
        --comparison-operator GreaterThanOrEqualToThreshold `
        --alarm-actions $topic

    # Alarm for SSM errors in logs
    aws cloudwatch put-metric-alarm `
        --alarm-name "SSM-Critical-Errors" `
        --alarm-description "Alert on ERROR level messages in SSM logs" `
        --metric-name "SSM_Error_Count" `
        --namespace "CWAgent" `
        --statistic Sum `
        --period 300 `
        --evaluation-periods 1 `
        --threshold 1 `
        --comparison-operator GreaterThanOrEqualToThreshold `
        --alarm-actions $topic

    Write-Host "✓ All CloudWatch alarms created successfully" -ForegroundColor Green
    
} catch {
    Write-Host "✗ Failed to create CloudWatch alarms" -ForegroundColor Red
    Write-Host "  Error: $($_.Exception.Message)" -ForegroundColor Red
}

Write-Host ""
Write-Host "=== DSC Monitoring Setup Complete! ===" -ForegroundColor Cyan
Write-Host "✓ SNS topic created for alerts" -ForegroundColor Green
Write-Host "✓ Log metric filters created" -ForegroundColor Green
Write-Host "✓ CloudWatch alarms configured" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:" -ForegroundColor White
Write-Host "1. Check your email and confirm the SNS subscription" -ForegroundColor Yellow
Write-Host "2. Wait for some DSC executions to generate metrics" -ForegroundColor Yellow
Write-Host "3. Check the CloudWatch console to verify metrics are appearing" -ForegroundColor Yellow
Write-Host "4. Test alerts by triggering a failure (optional)" -ForegroundColor Yellow

The metric filters will watch your log files and count:

  • How many times DSC starts (total executions)
  • How many times DSC completes successfully
  • How many ERROR messages appear

Then your alarms can use these counts to alert you when things go wrong.

What Gets Created (Once for Your Entire AWS Account):

  1. SNS Topic - One topic that receives alerts from all servers
  2. Log Metric Filters - These watch the CloudWatch log groups that ALL your servers write to
  3. CloudWatch Alarms - These monitor metrics across ALL servers

How It Works:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Server 1      │    │   Server 2      │    │   Server 3      │
│   (Web Server)  │    │   (Domain Ctrl) │    │   (Member Srv)  │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘
                                 │
                                 ▼
                    ┌─────────────────────────┐
                    │  CloudWatch Log Groups  │
                    │  - /aws/systemsmanager/ │
                    │    dsc-files            │
                    │  - /aws/systemsmanager/ │
                    │    ssm-errors           │
                    └─────────────────────────┘
                                 │
                                 ▼
                    ┌─────────────────────────┐
                    │   Metric Filters        │ ← Created ONCE
                    │   (Count errors/success)│
                    └─────────────────────────┘
                                 │
                                 ▼
                    ┌─────────────────────────┐
                    │   CloudWatch Alarms     │ ← Created ONCE
                    │   (Alert on thresholds) │
                    └─────────────────────────┘
                                 │
                                 ▼
                    ┌─────────────────────────┐
                    │      SNS Topic          │ ← Created ONCE
                    │   (Email notifications) │
                    └─────────────────────────┘

Custom Metrics for Deep Insights

For more detailed monitoring, push custom metrics:

# Complete DSC Monitoring Setup using JSON file method
Write-Host "Setting up complete DSC monitoring using JSON file approach..." -ForegroundColor Cyan

# Full monitoring script - no AWS CLI needed on instances, just detailed logging
$fullMonitoringScript = @'
function Write-DSCLog {
    param([string]$Message, [string]$Level = "INFO")
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
    $logEntry = "[$timestamp] [$Level] $Message"
    Write-Host $logEntry

    # Write to log file for CloudWatch Logs
    $logFile = "C:\Logs\DSC\dsc-monitoring.log"
    $logDir = Split-Path $logFile
    if (!(Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force }
    Add-Content -Path $logFile -Value $logEntry -ErrorAction SilentlyContinue
}

try {
    # Get instance info
    try {
        $instanceId = (Invoke-RestMethod -Uri "http://169.254.169.254/latest/meta-data/instance-id" -TimeoutSec 3)
    } catch {
        $instanceId = $env:COMPUTERNAME
        Write-DSCLog "Using computer name as instance ID: $instanceId" -Level "WARN"
    }

    Write-DSCLog "=== Starting DSC Compliance Check ===" -Level "INFO"
    Write-DSCLog "Instance: $instanceId" -Level "INFO"
    Write-DSCLog "Timestamp: $(Get-Date)" -Level "INFO"

    # Get DSC status
    Write-DSCLog "Running Test-DscConfiguration..." -Level "INFO"
    $dscStatus = Test-DscConfiguration -Detailed

    # Calculate compliance
    $totalResources = $dscStatus.ResourcesInDesiredState.Count + $dscStatus.ResourcesNotInDesiredState.Count
    $compliantResources = $dscStatus.ResourcesInDesiredState.Count
    $nonCompliantResources = $dscStatus.ResourcesNotInDesiredState.Count

    $compliancePercentage = if ($totalResources -gt 0) {
        [math]::Round(($compliantResources / $totalResources) * 100, 2)
    } else { 0 }

    Write-DSCLog "=== DSC Compliance Results ===" -Level "INFO"
    Write-DSCLog "Total Resources: $totalResources" -Level "INFO"
    Write-DSCLog "Compliant Resources: $compliantResources" -Level "INFO"
    Write-DSCLog "Non-Compliant Resources: $nonCompliantResources" -Level "INFO"
    Write-DSCLog "Compliance Percentage: $compliancePercentage%" -Level "INFO"

    # Log metric in parseable format for dashboard
    Write-DSCLog "METRIC|DSC_Compliance_Percentage|$compliancePercentage|Percent|InstanceId=$instanceId" -Level "INFO"
    Write-DSCLog "METRIC|DSC_Total_Resources|$totalResources|Count|InstanceId=$instanceId" -Level "INFO"
    Write-DSCLog "METRIC|DSC_NonCompliant_Resources|$nonCompliantResources|Count|InstanceId=$instanceId" -Level "INFO"

    # Log details of non-compliant resources
    if ($nonCompliantResources -gt 0) {
        Write-DSCLog "=== Non-Compliant Resource Details ===" -Level "WARN"
        $dscStatus.ResourcesNotInDesiredState | ForEach-Object {
            Write-DSCLog "Non-Compliant: $($_.ResourceId) - $($_.InDesiredState)" -Level "WARN"
        }
    } else {
        Write-DSCLog "All resources are compliant!" -Level "INFO"
    }

    Write-DSCLog "=== DSC Compliance Check Complete ===" -Level "INFO"

} catch {
    Write-DSCLog "=== DSC Compliance Check Failed ===" -Level "ERROR"
    Write-DSCLog "Error: $($_.Exception.Message)" -Level "ERROR"
    Write-DSCLog "METRIC|DSC_Check_Errors|1|Count|InstanceId=$instanceId" -Level "ERROR"
}
'@

# Create JSON file for the monitoring command
$monitoringJsonFile = "dsc-monitoring-command.json"
$monitoringCommand = @{
    DocumentName = "AWS-RunPowerShellScript"
    Targets = @(
        @{
            Key = "tag:Environment"
            Values = @("test")
        }
    )
    Parameters = @{
        commands = @($fullMonitoringScript)
    }
} | ConvertTo-Json -Depth 4

$monitoringCommand | Out-File -FilePath $monitoringJsonFile -Encoding UTF8

# Test the full monitoring script
Write-Host "Testing full monitoring script..." -ForegroundColor Yellow
$testCommandId = aws ssm send-command --cli-input-json "file://$monitoringJsonFile" --query 'Command.CommandId' --output text

Write-Host "✓ Test command sent: $testCommandId" -ForegroundColor Green

# Wait for execution
Write-Host "Waiting 30 seconds for execution..." -ForegroundColor Yellow
Start-Sleep -Seconds 30

# Check results
Write-Host "Getting results..." -ForegroundColor Yellow
$output = aws ssm get-command-invocation --command-id $testCommandId --instance-id "i-004ff505a11d64441" --query 'StandardOutputContent' --output text

Write-Host ""
Write-Host "=== Monitoring Script Output ===" -ForegroundColor Cyan
Write-Host $output -ForegroundColor White

if ($output -like "*DSC Compliance Check Complete*") {
    Write-Host ""
    Write-Host "✓ Full monitoring script works! Setting up maintenance window..." -ForegroundColor Green

    # Create maintenance window
    Write-Host "Creating maintenance window..." -ForegroundColor Yellow
    $windowId = aws ssm create-maintenance-window `
        --name "DSC-Compliance-Monitoring" `
        --description "Hourly DSC compliance monitoring with detailed logging" `
        --duration 1 `
        --cutoff 0 `
        --schedule "rate(1 hour)" `
        --allow-unassociated-targets `
        --query 'WindowId' `
        --output text

    Write-Host "✓ Maintenance window created: $windowId" -ForegroundColor Green

    # Register targets
    Write-Host "Registering targets..." -ForegroundColor Yellow
    $targetId = aws ssm register-target-with-maintenance-window `
        --window-id $windowId `
        --resource-type "INSTANCE" `
        --targets "Key=tag:Environment,Values=test" `
        --query 'WindowTargetId' `
        --output text

    Write-Host "✓ Targets registered: $targetId" -ForegroundColor Green

    # Create task parameters file
    $taskParamsFile = "dsc-task-params.json"
    $taskParams = @{
        RunCommand = @{
            Parameters = @{
                commands = @($fullMonitoringScript)
            }
        }
    } | ConvertTo-Json -Depth 4

    $taskParams | Out-File -FilePath $taskParamsFile -Encoding UTF8

    # Register the task
    Write-Host "Registering monitoring task..." -ForegroundColor Yellow
    $taskId = aws ssm register-task-with-maintenance-window `
        --window-id $windowId `
        --targets "Key=WindowTargetIds,Values=$targetId" `
        --task-type "RUN_COMMAND" `
        --task-arn "AWS-RunPowerShellScript" `
        --max-concurrency "5" `
        --max-errors "1" `
        --task-invocation-parameters "file://$taskParamsFile" `
        --query 'WindowTaskId' `
        --output text

    Write-Host "✓ Task registered: $taskId" -ForegroundColor Green

    # Clean up temp files
    Remove-Item $monitoringJsonFile -ErrorAction SilentlyContinue
    Remove-Item $taskParamsFile -ErrorAction SilentlyContinue

    Write-Host ""
    Write-Host "=== DSC Monitoring Setup Complete! ===" -ForegroundColor Cyan
    Write-Host "✓ Maintenance Window: $windowId" -ForegroundColor Green
    Write-Host "✓ Target ID: $targetId" -ForegroundColor Green
    Write-Host "✓ Task ID: $taskId" -ForegroundColor Green
    Write-Host "✓ Schedule: Every hour automatically" -ForegroundColor Green
    Write-Host ""
    Write-Host "Your CloudWatch dashboard will now show:" -ForegroundColor White
    Write-Host "• Detailed DSC compliance logs in /aws/systemsmanager log groups" -ForegroundColor Gray
    Write-Host "• Structured METRIC entries you can parse for dashboard widgets" -ForegroundColor Gray
    Write-Host "• Resource-level compliance details" -ForegroundColor Gray
    Write-Host "• Hourly compliance trending data" -ForegroundColor Gray

} else {
    Write-Host "✗ Monitoring script had issues. Output:" -ForegroundColor Red
    Write-Host $output -ForegroundColor Red
}

Write-Host ""
Write-Host "=== Monitor Your Setup ===" -ForegroundColor Yellow
Write-Host "Check maintenance window executions:" -ForegroundColor White
Write-Host "aws ssm describe-maintenance-window-executions --window-id $windowId" -ForegroundColor Gray
Write-Host ""
Write-Host "View logs in CloudWatch Console:" -ForegroundColor White
Write-Host "CloudWatch > Logs > /aws/systemsmanager/* log groups" -ForegroundColor Gray

Step-by-Step Breakdown

Step 1: You run this ONCE from your computer

  • Creates the PowerShell script as a variable
  • Sends it to AWS Systems Manager
  • Sets up an hourly schedule

Step 2: Systems Manager takes over

  • Looks for all VMs with the tag Environment=test
  • Automatically runs the script on each VM every hour
  • Each VM executes the compliance check independently

Step 3: Each VM does the work

  • Runs Test-DscConfiguration -Detailed on itself
  • Calculates its own compliance percentage
  • Sends its own metrics to CloudWatch

You can verify the output of this script with the Maintenance Windows: mw- that is shown and correlating with Maintenance Windows in the console.

Scaling Challenges and Solutions

As you scale beyond a handful of servers, new challenges emerge. Let's tackle them head-on.

Performance Impact Analysis

DSC can be CPU-intensive, especially during initial configuration. Here's how to measure and manage the impact:

# Aggregate DSC Performance Results at Scale
Write-Host "Aggregating DSC performance results from multiple instances..." -ForegroundColor Cyan

# Function to parse text-based performance output
function Parse-TextPerformanceData {
    param(
        [string]$Output,
        [string]$InstanceId
    )

    # Extract key metrics from text output
    $duration = if ($Output -match "Operation Duration: ([\d.]+) seconds") { [decimal]$matches[1] } else { $null }
    $cpuBaseline = if ($Output -match "Baseline CPU: ([\d.]+)%") { [decimal]$matches[1] } else { $null }
    $cpuPeak = if ($Output -match "CPU Impact: [\d.]+% -> ([\d.]+)%") { [decimal]$matches[1] } else { $null }
    $cpuChange = if ($Output -match "Change: \+([\d.]+)%") { [decimal]$matches[1] } else { $null }
    $memoryUsed = if ($Output -match "Memory Impact: Used ([\d.]+) MB") { [decimal]$matches[1] } else { $null }
    $totalResources = if ($Output -match "DSC Resources: (\d+) total") { [int]$matches[1] } else { $null }
    $complianceRate = if ($Output -match "Compliance: ([\d.]+)%") { [decimal]$matches[1] } else { $null }
    $resourcesPerSec = if ($Output -match "\(([\d.]+) resources/second\)") { [decimal]$matches[1] } else { $null }
    $baselineMemory = if ($Output -match "Baseline Free Memory: ([\d.]+) MB") { [decimal]$matches[1] } else { $null }

    return [PSCustomObject]@{
        InstanceId = $InstanceId
        OperationType = "Compliance Check"
        Duration = $duration
        BaselineCPU = $cpuBaseline
        PeakCPU = $cpuPeak
        CPUImpact = $cpuChange
        BaselineMemoryMB = $baselineMemory
        MemoryUsedMB = $memoryUsed
        TotalResources = $totalResources
        ComplianceRate = $complianceRate
        ResourcesPerSecond = $resourcesPerSec
        Status = "Success"
        Timestamp = Get-Date
    }
}

# Function to collect results from all instances
function Get-AggregatedDSCPerformance {
    param(
        [string]$CommandId,
        [string]$TagKey = "Environment",
        [string]$TagValue = "test"
    )

    Write-Host "Getting all instances with tag $TagKey=$TagValue..." -ForegroundColor Yellow

    # Get all instance IDs with the specified tag
    $instanceIds = aws ec2 describe-instances `
        --filters "Name=tag:$TagKey,Values=$TagValue" "Name=instance-state-name,Values=running" `
        --query "Reservations[].Instances[].InstanceId" `
        --output text

    if (!$instanceIds) {
        Write-Host "No instances found with tag $TagKey=$TagValue" -ForegroundColor Red
        return
    }

    $instanceList = $instanceIds -split "`s+"
    Write-Host "Found $($instanceList.Count) instances: $($instanceList -join ', ')" -ForegroundColor Green

    # Collect results from each instance
    $allResults = @()

    foreach ($instanceId in $instanceList) {
        Write-Host "Getting results from instance: $instanceId" -ForegroundColor Yellow

        try {
            # Get command invocation status
            $invocation = aws ssm get-command-invocation `
                --command-id $CommandId `
                --instance-id $instanceId `
                --output json | ConvertFrom-Json

            if ($invocation.Status -eq "Success") {
                Write-Host "✓ $instanceId - Command completed successfully" -ForegroundColor Green

                # Extract performance data from text output (fallback parsing)
                $output = $invocation.StandardOutputContent

                # Try to parse JSON performance data first
                if ($output -match '\{.*"InstanceId".*\}') {
                    try {
                        $perfData = ($matches[0] | ConvertFrom-Json)
                        $allResults += $perfData
                        Write-Host "  Parsed JSON performance data successfully" -ForegroundColor Gray
                    } catch {
                        Write-Host "  JSON parse failed, using text parsing" -ForegroundColor Yellow
                        $perfData = Parse-TextPerformanceData -Output $output -InstanceId $instanceId
                        $allResults += $perfData
                    }
                } else {
                    Write-Host "  No JSON found, parsing text output" -ForegroundColor Gray
                    $perfData = Parse-TextPerformanceData -Output $output -InstanceId $instanceId
                    $allResults += $perfData
                }

            } elseif ($invocation.Status -eq "InProgress") {
                Write-Host "⏳ $instanceId - Command still running" -ForegroundColor Yellow
                $allResults += [PSCustomObject]@{
                    InstanceId = $instanceId
                    Status = "InProgress"
                }

            } else {
                Write-Host "✗ $instanceId - Command failed: $($invocation.Status)" -ForegroundColor Red
                $allResults += [PSCustomObject]@{
                    InstanceId = $instanceId
                    Status = $invocation.Status
                    ErrorOutput = $invocation.StandardErrorContent
                }
            }

        } catch {
            Write-Host "✗ $instanceId - Error getting results: $($_.Exception.Message)" -ForegroundColor Red
            $allResults += [PSCustomObject]@{
                InstanceId = $instanceId
                Status = "QueryError"
                Error = $_.Exception.Message
            }
        }
    }

    return $allResults
}

# Function to analyze aggregated results
function Analyze-AggregatedResults {
    param($Results)

    Write-Host ""
    Write-Host "=== AGGREGATED PERFORMANCE ANALYSIS ===" -ForegroundColor Cyan

    # Separate successful vs failed results
    $successfulResults = $Results | Where-Object { $_.Duration -ne $null }
    $failedResults = $Results | Where-Object { $_.Status -ne "Success" -and $_.Duration -eq $null }

    Write-Host "Total Instances: $($Results.Count)" -ForegroundColor White
    Write-Host "Successful: $($successfulResults.Count)" -ForegroundColor Green
    Write-Host "Failed/Incomplete: $($failedResults.Count)" -ForegroundColor Red

    if ($successfulResults.Count -gt 0) {
        Write-Host ""
        Write-Host "=== PERFORMANCE STATISTICS ===" -ForegroundColor Yellow

        # Calculate aggregate statistics
        $durations = $successfulResults.Duration
        $cpuImpacts = $successfulResults.CPUImpact
        $memoryUsage = $successfulResults.MemoryUsedMB
        $resourceCounts = $successfulResults.TotalResources

        Write-Host "Execution Time:" -ForegroundColor White
        Write-Host "  Average: $([math]::Round(($durations | Measure-Object -Average).Average, 2)) seconds" -ForegroundColor Gray
        Write-Host "  Min: $([math]::Round(($durations | Measure-Object -Minimum).Minimum, 2)) seconds" -ForegroundColor Gray
        Write-Host "  Max: $([math]::Round(($durations | Measure-Object -Maximum).Maximum, 2)) seconds" -ForegroundColor Gray

        Write-Host "CPU Impact:" -ForegroundColor White
        Write-Host "  Average: $([math]::Round(($cpuImpacts | Measure-Object -Average).Average, 2))%" -ForegroundColor Gray
        Write-Host "  Max: $([math]::Round(($cpuImpacts | Measure-Object -Maximum).Maximum, 2))%" -ForegroundColor Gray

        Write-Host "Memory Usage:" -ForegroundColor White
        Write-Host "  Average: $([math]::Round(($memoryUsage | Measure-Object -Average).Average, 2)) MB" -ForegroundColor Gray
        Write-Host "  Max: $([math]::Round(($memoryUsage | Measure-Object -Maximum).Maximum, 2)) MB" -ForegroundColor Gray

        if ($resourceCounts -and ($resourceCounts | Where-Object { $_ -gt 0 }).Count -gt 0) {
            Write-Host "DSC Resources:" -ForegroundColor White
            Write-Host "  Average: $([math]::Round(($resourceCounts | Measure-Object -Average).Average, 0)) resources" -ForegroundColor Gray
            Write-Host "  Total across all instances: $(($resourceCounts | Measure-Object -Sum).Sum) resources" -ForegroundColor Gray
        }

        # Scaling recommendations
        Write-Host ""
        Write-Host "=== SCALING RECOMMENDATIONS ===" -ForegroundColor Yellow

        $maxDuration = ($durations | Measure-Object -Maximum).Maximum
        $avgDuration = ($durations | Measure-Object -Average).Average

        if ($maxDuration -gt 60) {
            Write-Host "⚠️  CRITICAL: Max execution time $maxDuration seconds - stagger deployment windows" -ForegroundColor Red
        } elseif ($avgDuration -gt 30) {
            Write-Host "⚠️  WARNING: Average $avgDuration seconds - consider optimization" -ForegroundColor Yellow
        } else {
            Write-Host "✅ GOOD: Execution times acceptable for scale" -ForegroundColor Green
        }

        $maxCPU = ($cpuImpacts | Measure-Object -Maximum).Maximum
        if ($maxCPU -gt 50) {
            Write-Host "⚠️  HIGH CPU: Peak $maxCPU% impact - limit concurrent executions" -ForegroundColor Red
        } else {
            Write-Host "✅ CPU impact manageable for concurrent execution" -ForegroundColor Green
        }

        # Instance-by-instance breakdown
        Write-Host ""
        Write-Host "=== INSTANCE BREAKDOWN ===" -ForegroundColor Yellow
        $successfulResults | Sort-Object Duration -Descending | ForEach-Object {
            $complianceInfo = if ($_.ComplianceRate) { " ($($_.ComplianceRate)% compliant)" } else { "" }
            Write-Host "$($_.InstanceId): $($_.Duration)s, CPU +$($_.CPUImpact)%, Memory $($_.MemoryUsedMB)MB$complianceInfo" -ForegroundColor Gray
        }
    }

    # Show failures
    if ($failedResults.Count -gt 0) {
        Write-Host ""
        Write-Host "=== FAILED INSTANCES ===" -ForegroundColor Red
        $failedResults | ForEach-Object {
            Write-Host "$($_.InstanceId): $($_.Status)" -ForegroundColor Red
            if ($_.Error) { Write-Host "  Error: $($_.Error)" -ForegroundColor Gray }
        }
    }

    # Export results
    Write-Host ""
    Write-Host "=== EXPORTING RESULTS ===" -ForegroundColor Yellow
    $exportFile = "dsc-performance-results-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
    $Results | ConvertTo-Json -Depth 5 | Out-File -FilePath $exportFile -Encoding UTF8
    Write-Host "Results exported to: $exportFile" -ForegroundColor Green
}

# Actually run it automatically for demonstration
Write-Host ""
Write-Host "=== RUNNING AGGREGATION NOW ===" -ForegroundColor Cyan

$results = Get-AggregatedDSCPerformance -CommandId "e0dbc320-9812-44ad-821a-1f93d2100113"
Analyze-AggregatedResults -Results $results

Write-Host ""
Write-Host "=== AGGREGATION COMPLETE ===" -ForegroundColor Green

This will crawl what tag you specify and return a brief window of usage in a companion json. Useful to send out while you are dialing in your configurations to see if you need to split things up – whether it's splitting up total number of benchmarks or adjusting hours on the server's workload.

Handling Scale Limits

AWS has rate limits, and DSC has resource limits. Here's how to handle both:

function Deploy-CISConfigurationAtScale {
    <#
    .SYNOPSIS
        Main function for deploying DSC configurations at scale with intelligent batching.

    .DESCRIPTION
        Coordinates the entire scale deployment process including instance discovery,
        validation, batching strategy, execution monitoring, and results reporting.
    #>
    param(
        [Parameter(Mandatory, HelpMessage="AWS Systems Manager document name for DSC deployment")]
        [string]$DocumentName,

        [Parameter(Mandatory, HelpMessage="S3 bucket containing the DSC configuration MOF file")]
        [string]$ConfigurationS3Bucket,

        [Parameter(Mandatory, HelpMessage="S3 key path to the DSC configuration MOF file")]
        [string]$ConfigurationS3Key,

        [string]$TagKey = "Environment",
        [string]$TagValue = "Production",
        [string]$ConfigurationName = "CISLevel1Production",
        [string]$RequiredModules = "CisDsc,SecurityPolicyDsc,AuditPolicyDsc,NetworkingDsc,ComputerManagementDsc",

        # Batching and Rate Limiting Parameters
        [int]$BatchSize = 20,                    # AWS SSM concurrent limit consideration
        [int]$DelayBetweenBatchesMinutes = 3,    # Rate limiting delay
        [int]$TimeoutMinutes = 45,               # Per-batch timeout
        [int]$MaxRetryAttempts = 2,              # Retry failed batches

        # DSC Parameters
        [bool]$TestMode = $false,
        [bool]$ComplianceCheck = $true,
        [bool]$EnableVerboseLogging = $false,
        [bool]$EnableDebugLogging = $false,
        [string]$ProxyUri = "",
        [ValidateSet("Never", "IfRequired", "Immediately")]
        [string]$RebootBehavior = "Never"
    )

    Write-Host "=== CIS DSC Scale Deployment Starting ===" -ForegroundColor Cyan
    Write-Host "Document: $DocumentName" -ForegroundColor White
    Write-Host "Target: $TagKey=$TagValue" -ForegroundColor White
    Write-Host "Batch Size: $BatchSize instances" -ForegroundColor White
    Write-Host "S3 Configuration: s3://$ConfigurationS3Bucket/$ConfigurationS3Key" -ForegroundColor White

    try {
        # Step 1: Discover and validate target instances
        Write-Host ""
        Write-Host "=== STEP 1: Instance Discovery ===" -ForegroundColor Yellow

        $instancesJson = aws ec2 describe-instances `
            --filters "Name=tag:$TagKey,Values=$TagValue" "Name=instance-state-name,Values=running" `
            --query "Reservations[].Instances[].[InstanceId,Tags[?Key=='Name'].Value|[0],InstanceType,LaunchTime]" `
            --output json

        if (!$instancesJson) {
            throw "No instances found with tag $TagKey=$TagValue"
        }

        $instances = $instancesJson | ConvertFrom-Json
        $instanceIds = $instances | ForEach-Object { $_[0] }

        Write-Host "Found $($instanceIds.Count) target instances:" -ForegroundColor Green
        $instances | ForEach-Object {
            $id = $_[0]; $name = $_[1]; $type = $_[2]; $launch = $_[3]
            Write-Host "  $id - $name ($type) - Launched: $launch" -ForegroundColor Gray
        }

        # Step 2: Validate SSM connectivity and platform compatibility
        Write-Host ""
        Write-Host "=== STEP 2: SSM Validation ===" -ForegroundColor Yellow

        $ssmInstancesJson = aws ssm describe-instance-information `
            --query "InstanceInformationList[?PingStatus=='Online' && PlatformType=='Windows'].[InstanceId,PlatformName,PlatformVersion,LastPingDateTime]" `
            --output json

        $ssmInstances = ($ssmInstancesJson | ConvertFrom-Json)
        $onlineInstanceIds = $ssmInstances | ForEach-Object { $_[0] }

        $validInstances = $instanceIds | Where-Object { $_ -in $onlineInstanceIds }
        $offlineInstances = $instanceIds | Where-Object { $_ -notin $onlineInstanceIds }

        Write-Host "SSM Online & Windows: $($validInstances.Count)" -ForegroundColor Green
        if ($offlineInstances.Count -gt 0) {
            Write-Host "SSM Offline/Unavailable: $($offlineInstances.Count)" -ForegroundColor Red
            $offlineInstances | ForEach-Object {
                Write-Host "  Offline: $_" -ForegroundColor Red
            }
        }

        if ($validInstances.Count -eq 0) {
            throw "No instances are online and SSM-managed for Windows!"
        }

        # Step 3: Validate S3 configuration exists
        Write-Host ""
        Write-Host "=== STEP 3: S3 Configuration Validation ===" -ForegroundColor Yellow

        try {
            $s3ObjectInfo = aws s3api head-object --bucket $ConfigurationS3Bucket --key $ConfigurationS3Key --output json | ConvertFrom-Json
            $configSizeKB = [math]::Round($s3ObjectInfo.ContentLength / 1024, 2)
            Write-Host "✓ Configuration found: $configSizeKB KB, Last Modified: $($s3ObjectInfo.LastModified)" -ForegroundColor Green

            # Adjust batch size based on configuration size
            if ($configSizeKB -gt 4096) {  # 4MB threshold
                $originalBatchSize = $BatchSize
                $BatchSize = [math]::Max([math]::Floor($BatchSize / 2), 5)  # Reduce batch size for large configs
                Write-Host "⚠️  Large configuration detected ($configSizeKB KB) - reducing batch size from $originalBatchSize to $BatchSize" -ForegroundColor Yellow
            }
        } catch {
            throw "S3 configuration validation failed: Cannot access s3://$ConfigurationS3Bucket/$ConfigurationS3Key - $($_.Exception.Message)"
        }

        # Step 4: Create batches with intelligent sizing
        Write-Host ""
        Write-Host "=== STEP 4: Batch Creation ===" -ForegroundColor Yellow

        # Sort instances by launch time (older instances first - more stable)
        $sortedInstances = $validInstances | ForEach-Object {
            $instanceId = $_
            $instanceInfo = $instances | Where-Object { $_[0] -eq $instanceId }
            [PSCustomObject]@{
                InstanceId = $instanceId
                Name = $instanceInfo[1]
                Type = $instanceInfo[2]
                LaunchTime = [DateTime]$instanceInfo[3]
            }
        } | Sort-Object LaunchTime

        $batches = @()
        for ($i = 0; $i -lt $sortedInstances.Count; $i += $BatchSize) {
            $batchEnd = [Math]::Min($i + $BatchSize - 1, $sortedInstances.Count - 1)
            $batchInstances = $sortedInstances[$i..$batchEnd]
            $batches += ,[PSCustomObject]@{
                Number = ($batches.Count + 1)
                Instances = $batchInstances
                InstanceIds = $batchInstances.InstanceId
                Size = $batchInstances.Count
            }
        }

        Write-Host "Created $($batches.Count) batches for deployment" -ForegroundColor Green
        $batches | ForEach-Object {
            Write-Host "  Batch $($_.Number): $($_.Size) instances - $($_.InstanceIds -join ', ')" -ForegroundColor Gray
        }

        # Step 5: Execute batched deployment
        Write-Host ""
        Write-Host "=== STEP 5: Batched Deployment Execution ===" -ForegroundColor Yellow

        $deploymentResults = @()
        $startTime = Get-Date

        for ($batchIndex = 0; $batchIndex -lt $batches.Count; $batchIndex++) {
            $batch = $batches[$batchIndex]
            $batchStartTime = Get-Date

            Write-Host ""
            Write-Host "--- Batch $($batch.Number) of $($batches.Count) ---" -ForegroundColor Cyan
            Write-Host "Instances: $($batch.Size)" -ForegroundColor White
            Write-Host "Target IDs: $($batch.InstanceIds -join ', ')" -ForegroundColor Gray

            # Construct parameters for your SSM document
            $documentParameters = @{
                configurationName = $ConfigurationName
                configurationS3Bucket = $ConfigurationS3Bucket
                configurationS3Key = $ConfigurationS3Key
                requiredModules = $RequiredModules
                testMode = $TestMode.ToString().ToLower()
                complianceCheck = $ComplianceCheck.ToString().ToLower()
                EnableVerboseLogging = $EnableVerboseLogging.ToString().ToLower()
                EnableDebugLogging = $EnableDebugLogging.ToString().ToLower()
                ProxyUri = $ProxyUri
                RebootBehavior = $RebootBehavior
                MaxRetryAttempts = $MaxRetryAttempts.ToString()
            }

            # Convert parameters to JSON format for AWS CLI
            $parametersJson = $documentParameters.GetEnumerator() | ForEach-Object {
                '"' + $_.Key + '":"' + $_.Value + '"'
            }
            $parametersString = '{' + ($parametersJson -join ',') + '}'

            Write-Host "Executing SSM document with parameters..." -ForegroundColor Yellow
            Write-Host "Document: $DocumentName" -ForegroundColor Gray
            Write-Host "Parameters: $parametersString" -ForegroundColor Gray

            # Execute the SSM document for this batch
            $batchAttempts = 0
            $batchSuccess = $false
            $commandId = $null

            while ($batchAttempts -lt $MaxRetryAttempts -and -not $batchSuccess) {
                $batchAttempts++

                try {
                    Write-Host "Batch attempt $batchAttempts of $MaxRetryAttempts" -ForegroundColor Gray

                    # Send command to batch instances
                    $commandId = aws ssm send-command `
                        --document-name $DocumentName `
                        --instance-ids $batch.InstanceIds `
                        --parameters $parametersString `
                        --timeout-seconds $(($TimeoutMinutes * 60)) `
                        --query 'Command.CommandId' `
                        --output text

                    if (!$commandId -or $commandId -eq "None") {
                        throw "Failed to send command - no command ID returned"
                    }

                    Write-Host "✓ Command sent successfully: $commandId" -ForegroundColor Green
                    $batchSuccess = $true

                } catch {
                    Write-Host "✗ Batch attempt $batchAttempts failed: $($_.Exception.Message)" -ForegroundColor Red

                    if ($batchAttempts -lt $MaxRetryAttempts) {
                        $retryDelay = 30 * $batchAttempts
                        Write-Host "Waiting $retryDelay seconds before retry..." -ForegroundColor Yellow
                        Start-Sleep -Seconds $retryDelay
                    }
                }
            }

            if (-not $batchSuccess) {
                Write-Host "✗ Batch $($batch.Number) failed after $MaxRetryAttempts attempts" -ForegroundColor Red
                $deploymentResults += [PSCustomObject]@{
                    BatchNumber = $batch.Number
                    CommandId = $null
                    Status = "Failed"
                    InstanceCount = $batch.Size
                    FailedInstances = $batch.InstanceIds
                    Duration = 0
                    Error = "Failed to send command after $MaxRetryAttempts attempts"
                }
                continue
            }

            # Monitor batch execution with progress tracking
            Write-Host "Monitoring batch execution..." -ForegroundColor Yellow
            $monitoringStartTime = Get-Date
            $batchComplete = $false

            while (-not $batchComplete) {
                $elapsed = ((Get-Date) - $monitoringStartTime).TotalMinutes

                if ($elapsed -gt $TimeoutMinutes) {
                    Write-Host "⏰ Batch $($batch.Number) timed out after $TimeoutMinutes minutes" -ForegroundColor Red
                    break
                }

                # Check command status for all instances in batch
                $invocationsJson = aws ssm list-command-invocations `
                    --command-id $commandId `
                    --query 'CommandInvocations[*].[InstanceId,Status,StatusDetails]' `
                    --output json

                $invocations = $invocationsJson | ConvertFrom-Json
                $inProgress = $invocations | Where-Object { $_[1] -eq "InProgress" }
                $succeeded = $invocations | Where-Object { $_[1] -eq "Success" }
                $failed = $invocations | Where-Object { $_[1] -eq "Failed" }
                $cancelled = $invocations | Where-Object { $_[1] -eq "Cancelled" }
                $timedOut = $invocations | Where-Object { $_[1] -eq "TimedOut" }

                $completed = $succeeded.Count + $failed.Count + $cancelled.Count + $timedOut.Count
                $total = $invocations.Count

                if ($completed -eq $total) {
                    $batchComplete = $true
                    $batchDuration = ((Get-Date) - $batchStartTime).TotalMinutes

                    Write-Host "✓ Batch $($batch.Number) completed in $([math]::Round($batchDuration, 1)) minutes!" -ForegroundColor Green
                    Write-Host "  Succeeded: $($succeeded.Count)" -ForegroundColor Green
                    Write-Host "  Failed: $($failed.Count)" -ForegroundColor Red
                    Write-Host "  Cancelled: $($cancelled.Count)" -ForegroundColor Yellow
                    Write-Host "  Timed Out: $($timedOut.Count)" -ForegroundColor Red

                    # Show failed instances
                    if ($failed.Count -gt 0) {
                        Write-Host "  Failed instances:" -ForegroundColor Red
                        $failed | ForEach-Object {
                            Write-Host "    - $($_[0]): $($_[2])" -ForegroundColor Red
                        }
                    }

                    # Show timed out instances
                    if ($timedOut.Count -gt 0) {
                        Write-Host "  Timed out instances:" -ForegroundColor Red
                        $timedOut | ForEach-Object {
                            Write-Host "    - $($_[0]): $($_[2])" -ForegroundColor Red
                        }
                    }

                } else {
                    $percentComplete = [math]::Round(($completed / $total) * 100, 1)
                    Write-Host "Progress: $completed/$total ($percentComplete%) - $($inProgress.Count) in progress - Elapsed: $([math]::Round($elapsed, 1))min" -ForegroundColor Gray
                    Start-Sleep -Seconds 30  # Check every 30 seconds
                }
            }

            # Store batch results
            $batchResult = [PSCustomObject]@{
                BatchNumber = $batch.Number
                CommandId = $commandId
                Status = if ($succeeded.Count -eq $total) { "Success" } elseif ($succeeded.Count -gt 0) { "PartialSuccess" } else { "Failed" }
                InstanceCount = $batch.Size
                Succeeded = $succeeded.Count
                Failed = $failed.Count
                Cancelled = $cancelled.Count
                TimedOut = $timedOut.Count
                Duration = [math]::Round(((Get-Date) - $batchStartTime).TotalMinutes, 2)
                SucceededInstances = $succeeded | ForEach-Object { $_[0] }
                FailedInstances = ($failed + $timedOut + $cancelled) | ForEach-Object { $_[0] }
            }

            $deploymentResults += $batchResult

            # Rate limiting delay between batches (except for last batch)
            if ($batchIndex -lt $batches.Count - 1) {
                Write-Host ""
                Write-Host "⏸️  Rate limiting: Waiting $DelayBetweenBatchesMinutes minutes before next batch..." -ForegroundColor Yellow
                Write-Host "   This prevents AWS API throttling and reduces system load" -ForegroundColor Gray
                Start-Sleep -Seconds ($DelayBetweenBatchesMinutes * 60)
            }
        }

        # Step 6: Final deployment summary and analysis
        Write-Host ""
        Write-Host "=== DEPLOYMENT SUMMARY ===" -ForegroundColor Cyan

        $totalDuration = ((Get-Date) - $startTime).TotalMinutes
        $totalSucceeded = ($deploymentResults | Measure-Object -Property Succeeded -Sum).Sum
        $totalFailed = ($deploymentResults | Measure-Object -Property Failed -Sum).Sum
        $totalTimedOut = ($deploymentResults | Measure-Object -Property TimedOut -Sum).Sum
        $totalCancelled = ($deploymentResults | Measure-Object -Property Cancelled -Sum).Sum
        $avgBatchDuration = ($deploymentResults | Measure-Object -Property Duration -Average).Average

        Write-Host "Total Duration: $([math]::Round($totalDuration, 1)) minutes" -ForegroundColor White
        Write-Host "Total Instances: $($validInstances.Count)" -ForegroundColor White
        Write-Host "Succeeded: $totalSucceeded" -ForegroundColor Green
        Write-Host "Failed: $totalFailed" -ForegroundColor Red
        Write-Host "Timed Out: $totalTimedOut" -ForegroundColor Red
        Write-Host "Cancelled: $totalCancelled" -ForegroundColor Yellow
        Write-Host "Success Rate: $([math]::Round(($totalSucceeded / $validInstances.Count) * 100, 1))%" -ForegroundColor White
        Write-Host "Average Batch Duration: $([math]::Round($avgBatchDuration, 1)) minutes" -ForegroundColor White

        # Batch-by-batch breakdown
        Write-Host ""
        Write-Host "=== BATCH BREAKDOWN ===" -ForegroundColor Yellow
        $deploymentResults | ForEach-Object {
            $status = switch ($_.Status) {
                "Success" { "✓" }
                "PartialSuccess" { "⚠" }
                "Failed" { "✗" }
                default { "?" }
            }
            Write-Host "$status Batch $($_.BatchNumber): $($_.Succeeded)/$($_.InstanceCount) succeeded in $($_.Duration)min [Command: $($_.CommandId)]" -ForegroundColor Gray
        }

        # Export detailed results
        $exportFile = "cis-dsc-deployment-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
        $deploymentSummary = [PSCustomObject]@{
            DeploymentStartTime = $startTime.ToString('yyyy-MM-dd HH:mm:ss')
            DeploymentEndTime = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
            TotalDurationMinutes = [math]::Round($totalDuration, 2)
            Configuration = @{
                DocumentName = $DocumentName
                S3Bucket = $ConfigurationS3Bucket
                S3Key = $ConfigurationS3Key
                ConfigurationName = $ConfigurationName
            }
            TargetCriteria = @{
                TagKey = $TagKey
                TagValue = $TagValue
            }
            BatchingStrategy = @{
                BatchSize = $BatchSize
                DelayBetweenBatchesMinutes = $DelayBetweenBatchesMinutes
                TimeoutMinutes = $TimeoutMinutes
            }
            Results = @{
                TotalInstances = $validInstances.Count
                Succeeded = $totalSucceeded
                Failed = $totalFailed
                TimedOut = $totalTimedOut
                Cancelled = $totalCancelled
                SuccessRate = [math]::Round(($totalSucceeded / $validInstances.Count) * 100, 2)
            }
            BatchResults = $deploymentResults
        }

        $deploymentSummary | ConvertTo-Json -Depth 5 | Out-File -FilePath $exportFile -Encoding UTF8
        Write-Host ""
        Write-Host "📊 Detailed results exported to: $exportFile" -ForegroundColor Green

        return $deploymentSummary

    } catch {
        Write-Host ""
        Write-Host "❌ Scale deployment failed: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host "Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
        return $null
    }
}

Optimizing MOF File Distribution

S3 is great, but at scale you need to optimize data transfer. This takes a bit of prep.

# S3 Transfer Acceleration Setup
# Save as: Set-S3Accelerate.ps1
# Run once to enable faster S3 downloads for your DSC configurations

param(
    [Parameter(Mandatory)]
    [string]$BucketName = "systems-manager-windows-server-dsc-configurations"
)

Write-Host "Setting up S3 Transfer Acceleration for bucket: $BucketName" -ForegroundColor Cyan

try {
    # Enable S3 Transfer Acceleration
    aws s3api put-bucket-accelerate-configuration `
        --bucket $BucketName `
        --accelerate-configuration Status=Enabled

    Write-Host "✓ S3 Transfer Acceleration enabled successfully" -ForegroundColor Green

    # Verify it was enabled
    $config = aws s3api get-bucket-accelerate-configuration --bucket $BucketName --output json | ConvertFrom-Json

    if ($config.Status -eq "Enabled") {
        Write-Host "✓ Acceleration confirmed enabled" -ForegroundColor Green
        Write-Host "Your accelerated endpoint will be: https://$BucketName.s3-accelerate.amazonaws.com" -ForegroundColor White
    } else {
        Write-Host "⚠️  Acceleration may not be fully enabled yet" -ForegroundColor Yellow
    }

} catch {
    Write-Host "❌ Failed to enable S3 Transfer Acceleration: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host "Check that:" -ForegroundColor Yellow
    Write-Host "  - You have s3:PutAccelerateConfiguration permission" -ForegroundColor Gray
    Write-Host "  - The bucket name is correct" -ForegroundColor Gray
    Write-Host "  - AWS CLI is configured properly" -ForegroundColor Gray
    exit 1
}

Write-Host ""
Write-Host "S3 Transfer Acceleration setup complete!" -ForegroundColor Green
Write-Host "This will speed up MOF file downloads globally." -ForegroundColor White
<#
.SYNOPSIS
    Calculates SHA256 hash for MOF files from local filesystem or S3.

.DESCRIPTION
    This script provides functions to calculate SHA256 hashes of MOF files from either
    local storage or directly from S3 objects. Essential for AWS Systems Manager State
    Manager caching and validation without requiring local file access.

.PARAMETER MOFFilePath
    The full path to a local MOF file for hash calculation.

.PARAMETER S3Bucket
    The S3 bucket name containing the MOF file.

.PARAMETER S3Key
    The S3 object key (path) to the MOF file.

.PARAMETER S3Uri
    Full S3 URI (s3://bucket/key) to the MOF file.

.EXAMPLE
    Get-MOFHash -MOFFilePath "C:\DSC\MyConfig.mof"
    Calculates hash for local MOF file.

.EXAMPLE
    Get-MOFHash -S3Bucket "my-dsc-configs" -S3Key "production/webserver.mof"
    Calculates hash for MOF file stored in S3.

.EXAMPLE
    Get-MOFHash -S3Uri "s3://my-dsc-configs/production/webserver.mof"
    Calculates hash using full S3 URI.

.NOTES
    Author: Jeffrey Stuhr
    Purpose: AWS Systems Manager MOF file validation and caching
    Requirements: PowerShell 5.0+, AWS CLI or AWS PowerShell module
#>

function Test-AWSConfiguration {
    <#
    .SYNOPSIS
        Validates AWS CLI and PowerShell module availability.
    #>
    $hasAWSCLI = $false
    $hasAWSModule = $false

    # Test AWS CLI
    try {
        $null = aws --version 2>$null
        if ($LASTEXITCODE -eq 0) {
            $hasAWSCLI = $true
        }
    } catch { }

    # Test AWS PowerShell modules
    if ((Get-Module -Name AWS.Tools.S3 -ListAvailable) -or (Get-Module -Name AWSPowerShell -ListAvailable)) {
        $hasAWSModule = $true
    }

    return @{
        HasCLI = $hasAWSCLI
        HasModule = $hasAWSModule
        IsConfigured = $hasAWSCLI -or $hasAWSModule
    }
}

function Get-MOFHash {
    <#
    .SYNOPSIS
        Calculates SHA256 hash for MOF files from local or S3 sources.

    .DESCRIPTION
        Generates SHA256 hash for MOF files from local filesystem or S3, with deployment
        guidance and automatic clipboard copying for easy use in AWS Systems Manager.

    .PARAMETER MOFFilePath
        Full path to local MOF file to analyze.

    .PARAMETER S3Bucket
        S3 bucket name containing the MOF file.

    .PARAMETER S3Key
        S3 object key (path) to the MOF file.

    .PARAMETER S3Uri
        Full S3 URI in format s3://bucket/key.

    .OUTPUTS
        Returns hashtable with file information including path, size, hash, and metadata.
    #>
    [CmdletBinding(DefaultParameterSetName = 'Local')]
    param(
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Local',
            HelpMessage = "Enter the full path to the local MOF file"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$MOFFilePath,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'S3BucketKey',
            HelpMessage = "Enter the S3 bucket name"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$S3Bucket,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'S3BucketKey',
            HelpMessage = "Enter the S3 object key (path)"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$S3Key,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'S3Uri',
            HelpMessage = "Enter the full S3 URI (s3://bucket/key)"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$S3Uri
    )

    try {
        if ($PSCmdlet.ParameterSetName -eq 'Local') {
            # Handle local file processing
            Write-Host "Calculating hash for local MOF file..." -ForegroundColor Cyan
            return Get-LocalMOFHash -FilePath $MOFFilePath
        }
        else {
            # Validate AWS configuration before proceeding
            $awsConfig = Test-AWSConfiguration
            if (-not $awsConfig.IsConfigured) {
                throw "AWS CLI or AWS PowerShell module is required for S3 operations. Please install AWS CLI or AWS PowerShell module."
            }

            # Handle S3 file processing
            if ($PSCmdlet.ParameterSetName -eq 'S3Uri') {
                # Parse S3 URI to extract bucket and key
                if ($S3Uri -match '^s3://([^/]+)/(.+)$') {
                    $S3Bucket = $matches[1]
                    $S3Key = $matches[2]
                } else {
                    throw "Invalid S3 URI format. Expected: s3://bucket/key"
                }
            }

            Write-Host "Calculating hash for S3 MOF file..." -ForegroundColor Cyan
            Write-Host "Bucket: $S3Bucket" -ForegroundColor Gray
            Write-Host "Key: $S3Key" -ForegroundColor Gray

            return Get-S3MOFHash -Bucket $S3Bucket -Key $S3Key
        }
    }
    catch {
        throw "Failed to calculate MOF hash: $($_.Exception.Message)"
    }
}

function Get-LocalMOFHash {
    param([string]$FilePath)

    if (-not (Test-Path $FilePath)) {
        throw "MOF file not found: $FilePath"
    }

    $fileInfo = Get-Item $FilePath
    $hash = (Get-FileHash $FilePath -Algorithm SHA256).Hash
    $sizeKB = [math]::Round($fileInfo.Length / 1024, 2)

    Write-Host ""
    Write-Host "=== Local MOF File Information ===" -ForegroundColor Yellow
    Write-Host "File Path: $FilePath" -ForegroundColor White
    Write-Host "File Size: $sizeKB KB" -ForegroundColor White
    Write-Host "SHA256 Hash: $hash" -ForegroundColor Green

    Show-DeploymentGuidance -SizeKB $sizeKB
    Copy-HashToClipboard -Hash $hash

    return @{
        Source = "Local"
        FilePath = $FilePath
        FileName = $fileInfo.Name
        SizeBytes = $fileInfo.Length
        SizeKB = $sizeKB
        Hash = $hash
        LastModified = $fileInfo.LastWriteTime
        S3Bucket = $null
        S3Key = $null
    }
}

function Get-S3MOFHash {
    param(
        [string]$Bucket,
        [string]$Key
    )

    # Try AWS CLI first (most commonly available)
    $s3Info = Get-S3ObjectInfo -Bucket $Bucket -Key $Key -Method "CLI"

    if (-not $s3Info) {
        # Fallback to AWS PowerShell module
        $s3Info = Get-S3ObjectInfo -Bucket $Bucket -Key $Key -Method "PowerShell"
    }

    if (-not $s3Info) {
        throw "Unable to retrieve S3 object information. Ensure AWS CLI is configured or AWS PowerShell module is installed."
    }

    $sizeKB = [math]::Round($s3Info.Size / 1024, 2)

    Write-Host ""
    Write-Host "=== S3 MOF File Information ===" -ForegroundColor Yellow
    Write-Host "S3 Location: s3://$Bucket/$Key" -ForegroundColor White
    Write-Host "File Size: $sizeKB KB" -ForegroundColor White
    Write-Host "SHA256 Hash: $($s3Info.Hash)" -ForegroundColor Green
    Write-Host "Last Modified: $($s3Info.LastModified)" -ForegroundColor Gray

    Show-DeploymentGuidance -SizeKB $sizeKB
    Copy-HashToClipboard -Hash $s3Info.Hash

    return @{
        Source = "S3"
        FilePath = "s3://$Bucket/$Key"
        FileName = Split-Path $Key -Leaf
        SizeBytes = $s3Info.Size
        SizeKB = $sizeKB
        Hash = $s3Info.Hash
        LastModified = $s3Info.LastModified
        S3Bucket = $Bucket
        S3Key = $Key
    }
}

function Get-S3ObjectInfo {
    param(
        [string]$Bucket,
        [string]$Key,
        [string]$Method
    )

    if ($Method -eq "CLI") {
        try {
            # Method 1: Use AWS CLI to get object metadata
            Write-Host "Using AWS CLI to retrieve S3 object information..." -ForegroundColor Gray

            # First check if object exists
            $s3Output = aws s3api head-object --bucket $Bucket --key $Key --output json 2>$null

            if ($LASTEXITCODE -ne 0) {
                throw "S3 object not found or access denied: s3://$Bucket/$Key"
            }

            $s3Metadata = $s3Output | ConvertFrom-Json

            # AWS S3 provides ETag which is MD5 for simple uploads, but we need SHA256
            # For SHA256, we need to check if it's stored as metadata or calculate it
            $sha256Hash = $null

            # Check if SHA256 is stored as metadata
            if ($s3Metadata.Metadata -and $s3Metadata.Metadata.'sha256') {
                $sha256Hash = $s3Metadata.Metadata.'sha256'
                Write-Host "✓ Found SHA256 in object metadata" -ForegroundColor Green
            }
            elseif ($s3Metadata.Metadata -and $s3Metadata.Metadata.'x-amz-content-sha256') {
                $sha256Hash = $s3Metadata.Metadata.'x-amz-content-sha256'
                Write-Host "✓ Found SHA256 in AMZ content metadata" -ForegroundColor Green
            }
            else {
                # Calculate SHA256 by downloading and hashing (for smaller files)
                Write-Host "SHA256 not in metadata, calculating..." -ForegroundColor Yellow
                $sha256Hash = Get-S3ObjectSHA256 -Bucket $Bucket -Key $Key -Method "CLI"
            }

            return @{
                Size = [long]$s3Metadata.ContentLength
                Hash = $sha256Hash
                LastModified = [DateTime]$s3Metadata.LastModified
                ETag = $s3Metadata.ETag.Trim('"')
            }
        }
        catch {
            Write-Host "AWS CLI method failed: $($_.Exception.Message)" -ForegroundColor Yellow
            return $null
        }
    }
    elseif ($Method -eq "PowerShell") {
        try {
            # Method 2: Use AWS PowerShell module
            Write-Host "Using AWS PowerShell module..." -ForegroundColor Gray

            if (-not (Get-Module -Name AWS.Tools.S3 -ListAvailable)) {
                if (-not (Get-Module -Name AWSPowerShell -ListAvailable)) {
                    throw "Neither AWS.Tools.S3 nor AWSPowerShell module is installed"
                }
            }

            $s3Object = Get-S3Object -BucketName $Bucket -Key $Key -ErrorAction Stop

            # Check for SHA256 in object metadata first
            $sha256Hash = $null
            $s3ObjectMetadata = Get-S3ObjectMetadata -BucketName $Bucket -Key $Key -ErrorAction Stop

            if ($s3ObjectMetadata.Metadata -and $s3ObjectMetadata.Metadata['sha256']) {
                $sha256Hash = $s3ObjectMetadata.Metadata['sha256']
                Write-Host "✓ Found SHA256 in object metadata" -ForegroundColor Green
            }
            else {
                # Calculate SHA256 by downloading and hashing
                Write-Host "SHA256 not in metadata, calculating..." -ForegroundColor Yellow
                $sha256Hash = Get-S3ObjectSHA256 -Bucket $Bucket -Key $Key -Method "PowerShell"
            }

            return @{
                Size = $s3Object.Size
                Hash = $sha256Hash
                LastModified = $s3Object.LastModified
                ETag = $s3Object.ETag
            }
        }
        catch {
            Write-Host "AWS PowerShell method failed: $($_.Exception.Message)" -ForegroundColor Yellow
            return $null
        }
    }

    return $null
}

function Get-S3ObjectSHA256 {
    param(
        [string]$Bucket,
        [string]$Key,
        [string]$Method = "CLI"
    )

    try {
        # For files under 100MB, download temporarily and hash
        # For larger files, this would need streaming or alternative approach
        $tempFile = [System.IO.Path]::GetTempFileName()

        Write-Host "Downloading S3 object to calculate SHA256..." -ForegroundColor Gray

        if ($Method -eq "CLI") {
            # Download using AWS CLI
            aws s3 cp "s3://$Bucket/$Key" $tempFile --quiet 2>$null

            if ($LASTEXITCODE -eq 0) {
                $hash = (Get-FileHash $tempFile -Algorithm SHA256).Hash
                Remove-Item $tempFile -Force
                Write-Host "✓ SHA256 calculated from downloaded file" -ForegroundColor Green
                return $hash
            }
            else {
                throw "Failed to download S3 object using AWS CLI"
            }
        }
        elseif ($Method -eq "PowerShell") {
            # Download using AWS PowerShell module
            Read-S3Object -BucketName $Bucket -Key $Key -File $tempFile -ErrorAction Stop

            $hash = (Get-FileHash $tempFile -Algorithm SHA256).Hash
            Remove-Item $tempFile -Force
            Write-Host "✓ SHA256 calculated from downloaded file" -ForegroundColor Green
            return $hash
        }
        else {
            throw "Invalid download method specified"
        }
    }
    catch {
        if (Test-Path $tempFile) {
            Remove-Item $tempFile -Force
        }
        Write-Host "⚠️  Could not calculate SHA256: $($_.Exception.Message)" -ForegroundColor Yellow
        return "HASH_CALCULATION_FAILED"
    }
}

function Show-DeploymentGuidance {
    param([double]$SizeKB)

    Write-Host ""
    if ($SizeKB -gt 4096) {
        Write-Host "⚠️  WARNING: Large MOF file ($SizeKB KB)" -ForegroundColor Red
        Write-Host "   Consider reducing batch sizes for deployment" -ForegroundColor Yellow
        Write-Host "   Monitor Systems Manager execution timeouts" -ForegroundColor Yellow
    }
    elseif ($SizeKB -gt 2048) {
        Write-Host "⚠️  NOTICE: Medium-sized MOF file ($SizeKB KB)" -ForegroundColor Yellow
        Write-Host "   Monitor deployment performance" -ForegroundColor Gray
    }
    else {
        Write-Host "✓ MOF file size is reasonable for deployment" -ForegroundColor Green
    }
}

function Copy-HashToClipboard {
    param([string]$Hash)

    try {
        $Hash | Set-Clipboard
        Write-Host "✓ Hash copied to clipboard" -ForegroundColor Green
    }
    catch {
        Write-Host "ℹ️  Hash not copied to clipboard (Set-Clipboard not available)" -ForegroundColor Gray
    }
}

# Script execution when run directly
if ($MyInvocation.InvocationName -ne '.' -and $args.Count -gt 0) {
    # Parse command line arguments manually to avoid parameter conflicts
    $paramHash = @{}

    for ($i = 0; $i -lt $args.Count; $i += 2) {
        if ($i + 1 -lt $args.Count) {
            $paramName = $args[$i] -replace '^-', ''
            $paramValue = $args[$i + 1]
            $paramHash[$paramName] = $paramValue
        }
    }

    try {
        if ($paramHash.S3Uri) {
            Get-MOFHash -S3Uri $paramHash.S3Uri
        }
        elseif ($paramHash.S3Bucket -and $paramHash.S3Key) {
            Get-MOFHash -S3Bucket $paramHash.S3Bucket -S3Key $paramHash.S3Key
        }
        elseif ($paramHash.MOFFilePath) {
            Get-MOFHash -MOFFilePath $paramHash.MOFFilePath
        }
        else {
            Write-Host "Usage examples:" -ForegroundColor Yellow
            Write-Host "  .\Get-MOFHash.ps1 -MOFFilePath 'C:\DSC\config.mof'" -ForegroundColor Gray
            Write-Host "  .\Get-MOFHash.ps1 -S3Bucket 'my-bucket' -S3Key 'configs/prod.mof'" -ForegroundColor Gray
            Write-Host "  .\Get-MOFHash.ps1 -S3Uri 's3://my-bucket/configs/prod.mof'" -ForegroundColor Gray
        }
    }
    catch {
        Write-Error "Script execution failed: $($_.Exception.Message)"
        exit 1
    }
}
elseif ($MyInvocation.InvocationName -ne '.' -and $args.Count -eq 0) {
    Write-Host "Usage examples:" -ForegroundColor Yellow
    Write-Host "  .\Get-MOFHash.ps1 -MOFFilePath 'C:\DSC\config.mof'" -ForegroundColor Gray
    Write-Host "  .\Get-MOFHash.ps1 -S3Bucket 'my-bucket' -S3Key 'configs/prod.mof'" -ForegroundColor Gray
    Write-Host "  .\Get-MOFHash.ps1 -S3Uri 's3://my-bucket/configs/prod.mof'" -ForegroundColor Gray
    Write-Host ""
    Write-Host "Or import as module:" -ForegroundColor Yellow
    Write-Host "  . .\Get-MOFHash.ps1" -ForegroundColor Gray
    Write-Host "  Get-MOFHash -MOFFilePath 'C:\DSC\config.mof'" -ForegroundColor Gray
}
# Enable S3 Transfer Acceleration for global deployments
aws s3api put-bucket-accelerate-configuration `
    --bucket systems-manager-windows-server-dsc-configurations `
    --accelerate-configuration Status=Enabled

# Implement intelligent caching
$cachingScript = @'
function Get-DSCConfiguration {
    param(
        [string]$S3Bucket,
        [string]$S3Key,
        [string]$ExpectedHash
    )
    
    $cacheDir = "C:\DSCCache"
    $cachedMof = Join-Path $cacheDir "cached.mof"
    $hashFile = Join-Path $cacheDir "cached.hash"
    
    # Create cache directory
    New-Item -Path $cacheDir -ItemType Directory -Force | Out-Null
    
    # Check if we have a valid cached version
    if ((Test-Path $cachedMof) -and (Test-Path $hashFile)) {
        $cachedHash = Get-Content $hashFile
        if ($cachedHash -eq $ExpectedHash) {
            Write-Output "Using cached MOF file (hash match)"
            return $cachedMof
        }
    }
    
    # Download new version
    Write-Output "Downloading MOF from S3..."
    $tempFile = Join-Path $cacheDir "download.mof"
    
    # Use S3 acceleration endpoint if available
    $acceleratedUrl = "https://$S3Bucket.s3-accelerate.amazonaws.com/$S3Key"
    
    try {
        # Try accelerated endpoint first
        Invoke-WebRequest -Uri $acceleratedUrl -OutFile $tempFile -UseBasicParsing
    } catch {
        # Fall back to standard S3
        Read-S3Object -BucketName $S3Bucket -Key $S3Key -File $tempFile
    }
    
    # Verify download
    $downloadHash = (Get-FileHash $tempFile -Algorithm SHA256).Hash
    if ($downloadHash -ne $ExpectedHash) {
        throw "Downloaded file hash mismatch"
    }
    
    # Update cache
    Move-Item $tempFile $cachedMof -Force
    $downloadHash | Out-File $hashFile -Force
    
    return $cachedMof
}
'@

# Create SSM document with caching
$documentWithCaching = @{
    schemaVersion = "2.2"
    description = "Apply DSC with intelligent caching"
    parameters = @{
        configurationHash = @{
            type = "String"
            description = "SHA256 hash of the MOF file"
        }
    }
    mainSteps = @(
        @{
            action = "aws:runPowerShellScript"
            name = "applyDSCWithCache"
            inputs = @{
                runCommand = @(
                    $cachingScript,
                    "",
                    "# Use the caching function",
                    '$mofPath = Get-DSCConfiguration -S3Bucket "{{configurationS3Bucket}}" -S3Key "{{configurationS3Key}}" -ExpectedHash "{{configurationHash}}"',
                    "Start-DscConfiguration -Path (Split-Path $mofPath) -Wait -Force"
                )
            }
        }
    )
}

# Create the enhanced document
$documentWithCaching | ConvertTo-Json -Depth 10 | 
    Out-File -FilePath "DSC-Apply-With-Caching.json"

aws ssm create-document `
    --name "DSC-Apply-CIS-Cached" `
    --document-type "Command" `
    --content file://DSC-Apply-With-Caching.json

New ssm-document-cached-dsc.json that we'll be uploading. note: I kind of got in the groove of adding unicode outputs and that messed this up so bad lol

{
  "schemaVersion": "2.2",
  "description": "Apply DSC configuration with intelligent MOF caching and S3 acceleration",
  "parameters": {
    "configurationS3Bucket": {
      "type": "String",
      "description": "S3 bucket containing the MOF file",
      "allowedPattern": "^[a-z0-9.-]+$"
    },
    "configurationS3Key": {
      "type": "String",
      "description": "S3 key for the MOF file",
      "allowedPattern": "^[a-zA-Z0-9!_.*'()/-]+$"
    },
    "configurationHash": {
      "type": "String",
      "description": "SHA256 hash of the MOF file for cache validation",
      "allowedPattern": "^[A-Fa-f0-9]{64}$"
    },
    "useAcceleratedEndpoint": {
      "type": "String",
      "description": "Use S3 Transfer Acceleration if available",
      "default": "true",
      "allowedValues": ["true", "false"]
    },
    "maxCacheAgeDays": {
      "type": "String",
      "description": "Maximum age of cached files in days",
      "default": "7",
      "allowedPattern": "^[1-9][0-9]*$"
    }
  },
  "mainSteps": [
    {
      "action": "aws:runPowerShellScript",
      "name": "applyDSCWithCaching",
      "inputs": {
        "timeoutSeconds": "7200",
        "runCommand": [
          "# DSC Configuration with Intelligent Caching",
          "param(",
          "    [string]$S3Bucket = '{{configurationS3Bucket}}',",
          "    [string]$S3Key = '{{configurationS3Key}}',",
          "    [string]$ExpectedHash = '{{configurationHash}}',",
          "    [bool]$UseAccelerated = [bool]::Parse('{{useAcceleratedEndpoint}}'),",
          "    [int]$MaxCacheAge = [int]::Parse('{{maxCacheAgeDays}}')",
          ")",
          "",
          "$ErrorActionPreference = 'Stop'",
          "$startTime = Get-Date",
          "",
          "Write-Output '=== DSC Configuration with Caching Started ==='",
          "Write-Output \"S3 Location: s3://$S3Bucket/$S3Key\"",
          "Write-Output \"Expected Hash: $ExpectedHash\"",
          "Write-Output \"Use Acceleration: $UseAccelerated\"",
          "",
          "try {",
          "    # Setup cache directory",
          "    $cacheDir = 'C:\\DSCCache'",
          "    if (-not (Test-Path $cacheDir)) {",
          "        New-Item -Path $cacheDir -ItemType Directory -Force | Out-Null",
          "        Write-Output '[OK] Cache directory created'",
          "    }",
          "",
          "    # Define cache file paths",
          "    $configId = ($S3Key -replace '[\\\\/:*?\"<>|]', '_').ToLower()",
          "    $cachedMof = Join-Path $cacheDir \"$configId.mof\"",
          "    $hashFile = Join-Path $cacheDir \"$configId.hash\"",
          "",
          "    # Check for valid cached version",
          "    $cacheValid = $false",
          "    if ((Test-Path $cachedMof) -and (Test-Path $hashFile)) {",
          "        try {",
          "            $cachedHash = (Get-Content $hashFile -ErrorAction Stop).Trim()",
          "            if ($cachedHash -eq $ExpectedHash) {",
          "                $actualHash = (Get-FileHash $cachedMof -Algorithm SHA256).Hash",
          "                if ($actualHash -eq $ExpectedHash) {",
          "                    $cacheValid = $true",
          "                    $cacheSize = (Get-Item $cachedMof).Length",
          "                    Write-Output \"[OK] Valid cached MOF found: $cacheSize bytes\"",
          "                }",
          "            }",
          "        } catch {",
          "            Write-Output \"[WARN]  Cache validation failed: $($_.Exception.Message)\"",
          "        }",
          "    }",
          "",
          "    # Download if cache is invalid",
          "    if (-not $cacheValid) {",
          "        Write-Output 'Downloading MOF from S3...'",
          "        $tempFile = Join-Path $cacheDir \"download_$(Get-Random).mof\"",
          "        $downloadSuccess = $false",
          "",
          "        # Try AWS CLI first (supports acceleration)",
          "        try {",
          "            $awsResult = & aws s3 cp \"s3://$S3Bucket/$S3Key\" $tempFile 2>&1",
          "            if ($LASTEXITCODE -eq 0 -and (Test-Path $tempFile)) {",
          "                $downloadSize = (Get-Item $tempFile).Length",
          "                if ($downloadSize -gt 0) {",
          "                    Write-Output \"[OK] Download successful: $downloadSize bytes\"",
          "                    $downloadSuccess = $true",
          "                }",
          "            }",
          "        } catch {",
          "            Write-Output \"AWS CLI download failed: $($_.Exception.Message)\"",
          "        }",
          "",
          "        # Fallback to PowerShell if AWS CLI failed",
          "        if (-not $downloadSuccess) {",
          "            try {",
          "                if (Get-Module -ListAvailable -Name 'AWS.Tools.S3') {",
          "                    Import-Module AWS.Tools.S3 -Force",
          "                    Read-S3Object -BucketName $S3Bucket -Key $S3Key -File $tempFile",
          "                    if (Test-Path $tempFile) {",
          "                        $downloadSize = (Get-Item $tempFile).Length",
          "                        Write-Output \"[OK] PowerShell download successful: $downloadSize bytes\"",
          "                        $downloadSuccess = $true",
          "                    }",
          "                } else {",
          "                    throw 'AWS PowerShell module not available'",
          "                }",
          "            } catch {",
          "                Write-Output \"PowerShell download failed: $($_.Exception.Message)\"",
          "            }",
          "        }",
          "",
          "        if (-not $downloadSuccess) {",
          "            throw 'All download methods failed'",
          "        }",
          "",
          "        # Verify downloaded file hash",
          "        $downloadHash = (Get-FileHash $tempFile -Algorithm SHA256).Hash",
          "        if ($downloadHash -ne $ExpectedHash) {",
          "            Remove-Item $tempFile -Force -ErrorAction SilentlyContinue",
          "            throw \"Hash verification failed. Expected: $ExpectedHash, Got: $downloadHash\"",
          "        }",
          "",
          "        # Update cache atomically",
          "        if (Test-Path $cachedMof) { Remove-Item $cachedMof -Force }",
          "        if (Test-Path $hashFile) { Remove-Item $hashFile -Force }",
          "        Move-Item $tempFile $cachedMof -Force",
          "        $ExpectedHash | Out-File $hashFile -Encoding ASCII -Force",
          "        Write-Output '[OK] Cache updated successfully'",
          "    }",
          "",
          "    # Prepare working directory for DSC",
          "    $workDir = \"C:\\Temp\\DSC-$(Get-Random)\"",
          "    New-Item -Path $workDir -ItemType Directory -Force | Out-Null",
          "    Copy-Item $cachedMof (Join-Path $workDir 'localhost.mof') -Force",
          "",
          "    # Apply DSC configuration",
          "    Write-Output 'Applying DSC configuration...'",
          "    Start-DscConfiguration -Path $workDir -Wait -Force -Verbose",
          "    Write-Output '[OK] DSC configuration applied successfully'",
          "",
          "    # Compliance check",
          "    Write-Output 'Running compliance validation...'",
          "    $testResult = Test-DscConfiguration -Detailed",
          "    $totalResources = $testResult.ResourcesInDesiredState.Count + $testResult.ResourcesNotInDesiredState.Count",
          "",
          "    if ($testResult.InDesiredState) {",
          "        Write-Output \"[OK] All $totalResources resources in desired state\"",
          "        $status = 'Success'",
          "    } else {",
          "        Write-Output \"[WARN]  $($testResult.ResourcesNotInDesiredState.Count) of $totalResources resources not compliant\"",
          "        $status = 'SuccessWithWarnings'",
          "    }",
          "",
          "    # Cleanup",
          "    Remove-Item $workDir -Recurse -Force -ErrorAction SilentlyContinue",
          "",
          "    # Final result",
          "    $duration = ((Get-Date) - $startTime).TotalMinutes",
          "    $result = @{",
          "        Status = $status",
          "        Duration = [math]::Round($duration, 2)",
          "        TotalResources = $totalResources",
          "        CompliantResources = $testResult.ResourcesInDesiredState.Count",
          "        NonCompliantResources = $testResult.ResourcesNotInDesiredState.Count",
          "        CacheUsed = $cacheValid",
          "        ConfigurationHash = $ExpectedHash",
          "    }",
          "",
          "    Write-Output '=== DSC Configuration Complete ==='",
          "    Write-Output \"Status: $($result.Status)\"",
          "    Write-Output \"Duration: $($result.Duration) minutes\"",
          "    Write-Output \"Resources: $($result.CompliantResources)/$($result.TotalResources) compliant\"",
          "    Write-Output \"Cache Used: $($result.CacheUsed)\"",
          "",
          "    # Output JSON result for programmatic consumption",
          "    $result | ConvertTo-Json -Compress",
          "",
          "} catch {",
          "    $errorResult = @{",
          "        Status = 'Failed'",
          "        Error = $_.Exception.Message",
          "        Duration = ((Get-Date) - $startTime).TotalMinutes",
          "    }",
          "",
          "    Write-Output '=== DSC Configuration Failed ==='",
          "    Write-Output \"Error: $($_.Exception.Message)\"",
          "    ",
          "    $errorResult | ConvertTo-Json -Compress",
          "    exit 1",
          "}"
        ]
      },
      "outputs": [
        {
          "Name": "DSCResult",
          "Selector": "$",
          "Type": "StringMap"
        }
      ]
    }
  ],
  "outputs": [
    "applyDSCWithCaching.DSCResult"
  ]
}

And finally run New-SSMCacheDocument.ps1 to get it up and ready for your association.

<#
.SYNOPSIS
    Creates or updates an AWS Systems Manager document for DSC configuration deployment with caching.

.DESCRIPTION
    This script automates the creation and management of AWS SSM documents that enable
    PowerShell DSC configuration deployment with intelligent caching capabilities.
    It validates JSON document content, handles existing document updates, and sets
    appropriate permissions for the SSM document.

.PARAMETER DocumentName
    The name of the SSM document to create or update. Default is "DSC-Apply-With-Caching".

.PARAMETER JsonFilePath
    Path to the JSON file containing the SSM document definition.
    Default is ".\ssm-document-cached-dsc.json".

.PARAMETER Verbose
    Show detailed troubleshooting information and alternative approaches when errors occur.

.EXAMPLE
    .\New-SSMCacheDocument.ps1
    Creates the SSM document with default parameters.

.EXAMPLE
    .\New-SSMCacheDocument.ps1 -DocumentName "MyCustomDSCDoc" -JsonFilePath "C:\configs\my-doc.json" -Verbose
    Creates an SSM document with custom name and JSON file path, showing verbose output.

.NOTES
    Author: Jeffrey Stuhr
    Purpose: AWS Systems Manager DSC deployment automation
    Requirements:
    - AWS CLI configured with appropriate permissions
    - Valid JSON SSM document definition file
    - IAM permissions: ssm:CreateDocument, ssm:UpdateDocument, ssm:DescribeDocument, ssm:ModifyDocumentPermission
#>

[CmdletBinding()]
param(
    [Parameter(HelpMessage = "Name of the SSM document to create or update")]
    [string]$DocumentName = "DSC-Apply-With-Caching",

    [Parameter(HelpMessage = "Path to the JSON file containing SSM document definition")]
    [string]$JsonFilePath = ".\ssm-document-cached-dsc.json"
)

# Display script execution start message
Write-Host "Creating SSM Document: $DocumentName" -ForegroundColor Cyan

# Validate that the required JSON document file exists
if (-not (Test-Path $JsonFilePath)) {
    Write-Host "❌ JSON file not found: $JsonFilePath" -ForegroundColor Red
    Write-Host "Make sure you have saved the SSM Document JSON as: $JsonFilePath" -ForegroundColor Yellow
    exit 1
}

try {
    # Validate JSON file structure and content before attempting AWS operations
    Write-Host "Validating JSON file..." -ForegroundColor Yellow

    # Read the JSON file with explicit UTF-8 encoding to avoid encoding issues
    $jsonContent = Get-Content $JsonFilePath -Raw -Encoding UTF8 | ConvertFrom-Json

    Write-Host "✓ JSON file is valid" -ForegroundColor Green
    Write-Host "  Schema Version: $($jsonContent.schemaVersion)" -ForegroundColor Gray
    Write-Host "  Description: $($jsonContent.description)" -ForegroundColor Gray
    Write-Host "  Parameters: $($jsonContent.parameters.Count)" -ForegroundColor Gray

    # Create a safe temporary file path without spaces or special characters
    Write-Host "Preparing JSON content for AWS CLI..." -ForegroundColor Yellow

    # Use current directory for temp file to avoid path issues
    $tempJsonFile = ".\ssm-temp-$(Get-Random).json"

    # Convert the JSON content back to string with proper formatting
    $jsonString = $jsonContent | ConvertTo-Json -Depth 10

    # Save as UTF-8 without BOM (required by AWS CLI)
    $utf8NoBom = New-Object System.Text.UTF8Encoding $false
    $fullTempPath = Join-Path (Get-Location).Path $tempJsonFile
    [System.IO.File]::WriteAllText($fullTempPath, $jsonString, $utf8NoBom)

    Write-Host "✓ Temporary JSON file created: $tempJsonFile" -ForegroundColor Green

    # Alternative approach: Pass JSON content directly as string instead of file
    Write-Host "Using direct JSON content approach..." -ForegroundColor Yellow

    # Escape the JSON for command line usage
    $escapedJson = $jsonString -replace '"', '\"'

    # Check if an SSM document with the same name already exists
    Write-Host "Checking if document already exists..." -ForegroundColor Yellow
    $existingDoc = aws ssm describe-document --name $DocumentName --output json 2>$null

    if ($LASTEXITCODE -eq 0) {
        # Document exists - prompt user for update confirmation
        Write-Host "⚠️  Document '$DocumentName' already exists" -ForegroundColor Yellow
        $response = Read-Host "Do you want to update it? (y/N)"

        if ($response -eq 'y' -or $response -eq 'Y') {
            # Update existing document with new content - try file approach first
            Write-Host "Updating existing document..." -ForegroundColor Yellow

            # Method 1: Try with temporary file
            $result = aws ssm update-document `
                --name $DocumentName `
                --content "file://$tempJsonFile" `
                --document-format JSON `
                --document-version '$LATEST' `
                --output json 2>&1

            if ($LASTEXITCODE -ne 0) {
                Write-Host "File method failed, trying stdin approach..." -ForegroundColor Yellow
                # Method 2: Use stdin approach
                $result = $jsonString | aws ssm update-document `
                    --name $DocumentName `
                    --content "file://-" `
                    --document-format JSON `
                    --document-version '$LATEST' `
                    --output json
            }

            # Enhanced error handling for duplicate content
            if ($LASTEXITCODE -eq 0) {
                Write-Host "✓ Document updated successfully" -ForegroundColor Green
                try {
                    $updateInfo = $result | ConvertFrom-Json
                    Write-Host "  New Version: $($updateInfo.DocumentDescription.DocumentVersion)" -ForegroundColor Gray
                } catch {
                    Write-Host "  Update completed (version info not available)" -ForegroundColor Gray
                }
            } elseif ($LASTEXITCODE -eq 254) {
                # Handle duplicate content gracefully - this is actually success
                Write-Host "⚠️  No changes detected - document content is identical" -ForegroundColor Yellow
                Write-Host "✓ Document '$DocumentName' is already current" -ForegroundColor Green
                Write-Host "  No update needed - existing document matches your JSON file" -ForegroundColor Gray
            } else {
                Write-Host "Update error output: $result" -ForegroundColor Red
                throw "Failed to update document - AWS CLI error code: $LASTEXITCODE"
            }
        } else {
            # User chose not to update - exit gracefully
            Write-Host "Document update cancelled" -ForegroundColor Yellow
            Remove-Item $tempJsonFile -Force -ErrorAction SilentlyContinue
            exit 0
        }
    } else {
        # Document doesn't exist - create new one
        Write-Host "Creating new document..." -ForegroundColor Yellow

        # Method 1: Try with temporary file
        Write-Host "Attempting file-based creation..." -ForegroundColor Gray
        $result = aws ssm create-document `
            --name $DocumentName `
            --document-type "Command" `
            --content "file://$tempJsonFile" `
            --document-format JSON `
            --output json 2>&1

        if ($LASTEXITCODE -ne 0) {
            Write-Host "File method failed, trying stdin approach..." -ForegroundColor Yellow
            # Method 2: Use stdin approach (pipe JSON directly)
            $result = $jsonString | aws ssm create-document `
                --name $DocumentName `
                --document-type "Command" `
                --content "file://-" `
                --document-format JSON `
                --output json
        }

        if ($LASTEXITCODE -eq 0) {
            Write-Host "✓ Document created successfully" -ForegroundColor Green
            try {
                $createInfo = $result | ConvertFrom-Json
                Write-Host "  Document Name: $($createInfo.DocumentDescription.Name)" -ForegroundColor Gray
                Write-Host "  Version: $($createInfo.DocumentDescription.DocumentVersion)" -ForegroundColor Gray
                Write-Host "  Status: $($createInfo.DocumentDescription.Status)" -ForegroundColor Gray
            } catch {
                Write-Host "  Creation completed (details not available)" -ForegroundColor Gray
            }
        } else {
            # Provide detailed error information
            Write-Host "AWS CLI exit code: $LASTEXITCODE" -ForegroundColor Red
            Write-Host "Error output: $result" -ForegroundColor Red

            # Try one more approach - create a simple batch file to avoid PowerShell escaping issues
            Write-Host "Trying batch file approach as last resort..." -ForegroundColor Yellow

            $batchFile = ".\create-ssm-doc.bat"
            $batchContent = @"
@echo off
aws ssm create-document --name "$DocumentName" --document-type "Command" --content "file://$tempJsonFile" --document-format JSON --output json
"@
            $batchContent | Out-File -FilePath $batchFile -Encoding ASCII

            $batchResult = & cmd /c $batchFile
            $batchExitCode = $LASTEXITCODE

            Remove-Item $batchFile -Force -ErrorAction SilentlyContinue

            if ($batchExitCode -eq 0) {
                Write-Host "✓ Document created successfully using batch approach" -ForegroundColor Green
            } elseif ($batchExitCode -eq 254) {
                # Handle duplicate content even during creation attempts
                Write-Host "⚠️  Document already exists with identical content" -ForegroundColor Yellow
                Write-Host "✓ Document '$DocumentName' is ready to use" -ForegroundColor Green
            } else {
                throw "All creation methods failed - AWS CLI error code: $batchExitCode"
            }
        }
    }

    # Configure document permissions for sharing within the account
    # Only attempt if we had a successful operation (including duplicate content scenarios)
    if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 254) {
        Write-Host "Setting document permissions..." -ForegroundColor Yellow
        $permResult = aws ssm modify-document-permission `
            --name $DocumentName `
            --permission-type Share `
            --account-ids-to-add "self" `
            --output json 2>$null

        if ($LASTEXITCODE -eq 0) {
            Write-Host "✓ Document permissions set" -ForegroundColor Green
        } else {
            # Permission setting failure is not critical - document can still be used
            Write-Host "⚠️  Could not set document permissions (this is usually OK)" -ForegroundColor Yellow
        }

        # Verify the document was created successfully
        Write-Host "Verifying document creation..." -ForegroundColor Yellow
        $verifyResult = aws ssm describe-document --name $DocumentName --output json 2>$null

        if ($LASTEXITCODE -eq 0) {
            Write-Host "✓ Document verification successful" -ForegroundColor Green
            try {
                $docInfo = $verifyResult | ConvertFrom-Json
                Write-Host "  Status: $($docInfo.Document.Status)" -ForegroundColor Gray
                Write-Host "  Document Format: $($docInfo.Document.DocumentFormat)" -ForegroundColor Gray
            } catch {
                Write-Host "  Document exists and is accessible" -ForegroundColor Gray
            }
        } else {
            Write-Host "⚠️  Could not verify document (but creation may have succeeded)" -ForegroundColor Yellow
        }

        # Display success summary and usage instructions
        Write-Host ""
        Write-Host "=== SSM Document Ready ===" -ForegroundColor Green
        Write-Host "Document Name: $DocumentName" -ForegroundColor White
        Write-Host "You can now use this document with your scale deployment function" -ForegroundColor White
        Write-Host ""

        # Provide example usage for the newly created document
        Write-Host "Example usage:" -ForegroundColor Yellow
        Write-Host '$result = Deploy-CISConfigurationAtScale \' -ForegroundColor Gray
        Write-Host "    -DocumentName `"$DocumentName`" \" -ForegroundColor Gray
        Write-Host '    -ConfigurationS3Bucket "your-bucket" \' -ForegroundColor Gray
        Write-Host '    -ConfigurationS3Key "configs/production.mof" \' -ForegroundColor Gray
        Write-Host '    -ConfigurationHash "your-mof-hash"' -ForegroundColor Gray

        # Show how to test the document
        Write-Host ""
        Write-Host "To test the document:" -ForegroundColor Yellow
        Write-Host "aws ssm list-documents --filters Key=Name,Values=$DocumentName" -ForegroundColor Gray
    }

} catch {
    # Handle errors gracefully with helpful troubleshooting information
    Write-Host "❌ Failed to create SSM document: $($_.Exception.Message)" -ForegroundColor Red

    Write-Verbose "Alternative manual approach:"
    Write-Verbose "1. Copy the JSON content manually:"
    Write-Verbose "   Get-Content $JsonFilePath -Raw | Set-Clipboard"
    Write-Verbose ""
    Write-Verbose "2. Create document via AWS Console:"
    Write-Verbose "   - Go to Systems Manager > Documents"
    Write-Verbose "   - Click 'Create document'"
    Write-Verbose "   - Choose 'Command' document type"
    Write-Verbose "   - Paste JSON content"
    Write-Verbose ""
    Write-Verbose "3. Or try AWS CLI with reduced JSON:"
    Write-Verbose "   - The JSON might be too complex for your AWS CLI version"
    Write-Verbose "   - Try updating AWS CLI: pip install --upgrade awscli"

    Write-Host "Run with -Verbose for troubleshooting steps" -ForegroundColor Yellow

    exit 1
} finally {
    # Clean up temporary files
    @(".\ssm-temp-*.json", ".\create-ssm-doc.bat") | ForEach-Object {
        Get-ChildItem -Path $_ -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
    }
}

Next Steps and Preview

What We've Accomplished

Look at what you've built! You've gone from manual server hardening to a fully automated, scalable solution that:

  • Deploys production-ready DSC configurations with proper error handling and logging
  • Scales intelligently from one server to thousands without overwhelming your infrastructure
  • Monitors compliance in real-time with CloudWatch dashboards and alerts
  • Reduce Redundant Downloads through caching

You're no longer just applying security settings - you're running a compliance platform.

Key Takeaways

Before you rush off to implement this everywhere, remember these lessons:

  1. Verify Systems Manager connectivity first - I cannot stress this enough. If you skipped Part 2, issues will haunt you throughout deployment.
  2. Start small, scale gradually - Test on a few non-critical servers first. I learned this the hard way when I accidentally locked myself out of 50 servers (thank goodness for break-glass accounts).
  3. Monitor performance impact closely - DSC can be CPU-intensive. Schedule wisely and watch those CloudWatch metrics.
  4. Automate compliance reporting - Your auditors will love you for those automated reports showing 99%+ compliance.
  5. Plan for exceptions and conflicts - Every environment has quirks. Document your ExcludeList choices.
  6. Test your recovery procedures - Before you need them. Trust me on this one.

Coming in Part 4: Advanced Operations

Ready to take it to the next level? In Part 4, we'll cover:

  • Handles real-world issues like timeouts, WinRM quotas, GPO conflicts, and Systems Manager connectivity
  • Production best practices from organizations running this at scale
  • Advanced automation with Lambda for self-healing infrastructure
  • Multi-account strategies using AWS Organizations and Control Tower
  • CI/CD integration to version control your security configurations
  • Disaster recovery planning when (not if) something goes catastrophically wrong

Homework Before Part 4

Want to be ready for the advanced stuff? Here's your homework:

  1. Deploy to at least 10 instances - You'll start seeing patterns and issues that don't appear with just one or two servers.
  2. Set up your CloudWatch dashboard - Visualize your compliance status. Make it pretty enough to show management.
  3. Document your exclusion list - For each excluded control, document why. Future you will thank present you.
  4. Measure baseline performance metrics - How long does a full configuration take? What's the CPU impact? You'll need these numbers for capacity planning.
  5. Break something and fix it - Seriously. Intentionally cause a configuration drift and watch your automation fix it. It's oddly satisfying.

Resources and References

Here are the official docs you'll want to bookmark:


Ready to Scale Your Security?

You've got the tools, the knowledge, and hopefully the motivation to transform your Windows security posture. The question isn't whether you should automate your security configurations - it's how quickly you can get started.

Share your deployment experiences in the comments:

  • What issues did you hit that I didn't cover?
  • How many servers are you managing with this approach?
  • What creative exclusions did you need for your environment?

And if you successfully deploy this to 100+ servers without any issues on your first try, please let me know your secret. I'll either be incredibly impressed or incredibly suspicious. 😉

Majority of code listed in this article can also be viewed at the companion GitHub.

See you in Part 4 where we'll push the boundaries of what's possible with DSC and Systems Manager!