Public/CloudFormation/Compare-ATCFNStackResourceDrift.ps1
function Compare-ATCFNStackResourceDrift { <# .SYNOPSIS Get resource drift for given stack .DESCRIPTION Optionally run a drift check on the stack and, depending on whether -PassThru was given either bring up the drift differences in the configured diff viewer, or output the drifted resource information to the pipeline. .PARAMETER StackName Name of stack to check. .PARAMETER PassThru If set, then emit the drift information to the pipeline .PARAMETER NoReCheck If set, use the current drift information unless the stack has never been checked in which case a check will be run. If not set, a check is always run. .EXAMPLE Compare-ATCFNStackResourceDrift -StackName my-stack Run a drift check, and display any drift in the configured diff tool. .EXAMPLE $drifts = Compare-ATCFNStackResourceDrift -StackName my-stack -PassThru Run a drift check and return any drifts in the pipeline. .EXAMPLE Compare-ATCFNStackResourceDrift -StackName my-stack -NoReCheck Use results from last run drift check, and display any drift in the configured diff tool. .EXAMPLE Get-CFNStack | Compare-ATCFNStackResourceDrift -PassThru Get a check on all stacks in the account for current region. Advisable to use -PassThru or you may have a lot of diff windows opened! .OUTPUTS List of modified resources if -PassThru switch was present. .LINK Set-ATConfigurationItem #> [CmdletBinding()] param ( [Parameter(Mandatory, ParameterSetName = 'ByName', ValueFromPipelineByPropertyName )] [string]$StackName, [switch]$PassThru, [switch]$NoReCheck ) begin { } process { try { $stack = Get-CFNStack -StackName $StackName } catch { Write-Warning "Stack not found: $StackName" return } $driftStatus = $stack.DriftInformation.StackDriftStatus # Detect last CF update $event = Get-CFNStackEvent -StackName $stack.StackId | Select-Object -First 1 Write-Host "$($StackName): Last CloudFormation Update: $($event.Timestamp.ToString('dd MMM yyyy, HH:mm:ss'))" if (-not $NoReCheck -or @('UNKNOWN', 'NOT_CHECKED') -contains $driftStatus) { # Initiate drift detection $driftStatus = Get-StackDrift -Stack $stack if ($null -eq $driftStatus) { return } } else { Write-Host "Last drift check: $($stack.DriftInformation.LastCheckTimestamp.ToString('dd MMM yyyy, HH:mm:ss'))" } if ($driftStatus -eq 'IN_SYNC') { Write-Host "IN_SYNC" return } Write-Warning "Stack has drifted..." $newLine = [Environment]::NewLine # Get drifts and format into two files that can be compared in a diff tool $allExpected = New-Object System.Text.StringBuilder $allActual = New-Object System.Text.StringBuilder $drifts = Get-CFNDetectedStackResourceDrift -StackName $stack.StackId -StackResourceDriftStatusFilter @('DELETED', 'MODIFIED') if ($PassThru) { return [PSCustomObject][ordered] @{ StackName = $stack.StackName DriftInformation = $drifts } } $drifts | ForEach-Object { $drift = $_ $expected = ($drift.ExpectedProperties | ConvertFrom-Json | ConvertTo-Json -Depth 10 | Format-Json) -split $newLine switch ($_.StackResourceDriftStatus) { 'MODIFIED' { $actual = ($drift.ActualProperties | ConvertFrom-Json | ConvertTo-Json -Depth 10 | Format-Json) -split $newLine # Whichever is longer, bulk the other up with newlines to align output if ($actual.Length -ne $expected.Length) { $blankLines = [Environment]::NewLine * [Math]::Abs($actual.Length - $expected.Length) if ($actual.Length -gt $expected.Length) { $expected += $blankLines } else { $actual += $blankLines } } } 'DELETED' { $actual = $newLine * $expected.Length } } # Write headings $allExpected.AppendLine("$($drift.LogicalResourceId) ($($drift.ResourceType))").AppendLine() | Out-Null $allActual.AppendLine("$($drift.LogicalResourceId) ($($drift.ResourceType)): $($drift.StackResourceDriftStatus)").AppendLine() | Out-Null # Make expected and actual the same number of lines so everything lines up in diff viewer $allExpected.AppendLine(($expected -join $newLine)).AppendLine() | Out-Null $allActual.AppendLine(($actual -join $newLine)).AppendLine() | Out-Null } # Write out and invoke diff tool $expectedOutFile = Join-Path ([IO.Path]::GetTempPath()) "DriftResults-$($StackName)-expected.json" $actualOutFile = Join-Path ([IO.Path]::GetTempPath()) "DriftResults-$($StackName)-actual.json" $encoding = New-Object System.Text.UTF8Encoding($false, $false) [IO.File]::WriteAllText($expectedOutFile, $allExpected.ToString(), $encoding) [IO.File]::WriteAllText($actualOutFile, $allActual.ToString(), $encoding) Invoke-ATDiffTool -LeftPath $expectedOutFile -RightPath $actualOutFile -LeftTitle ($StackName + " - Expected") -RightTitle ($StackName + " - Actual") -Wait } } |