Public/CloudFormation/Compare-ATCFNStackResourceDrift.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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
    {
        if ($null -eq (Get-Command -Name Start-CFNStackDriftDetection -ErrorAction SilentlyContinue))
        {
            Write-Warning 'Upgrade your version of AWSPowerShell to see drift information'
            return $null
        }
    }

    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
    }
}