CloudFormation’s Update-CfnStack and “No Updates Are To Be Performed”

If you use Cloudformation with Powershell – and I do – you’ve probably run into this error message more than once.

If you’re like me, you’ll have tried to suppress it in several ways. You’ve tried to check your own template for changes, avoiding the call if there are none. And you’ve probably fallen foul of tiny, insignificant differences that CloudFormation doesn’t care about. You’ve probably done the same with your parameters and hit similar problems.

You might even have taken to adding a timestamp to a harmless field of your template so that at least something has updated. At an unnamed previous company, we’d just add a comment to the end of a launchconfiguration’s powershell script, which was enough to force an update and suppress the error. You might even be fine with just ignoring the rain of red text.

In my current project, we can’t just ignore it, as we’re driving our templates out of Octopus Deploy. “No updates are to be performed” throws and aborts our pipeline, which is annoying. But we want to catch other, genuine error messages from Update-CfnStack.

So what to do?

Well, turns out this is an object lesson in correct use of try/catch.

Consider the following example code

Update-CFNStack -StackName $StackName `
    -TemplateBody (Get-TemplateContent) `
    -verbose `
    -Parameter $cfparams `
    -Region $region `
    -Capability CAPABILITY_IAM `
    -ErrorAction SilentlyContinue

That ErrorAction parameter? Does nothing. What you need instead is to wrap it in a try/catch

try
{
    Update-CFNStack -StackName $StackName `
        -TemplateBody (Get-TemplateContent) `
        -verbose `
        -Parameter $cfparams `
        -Region $region `
        -Capability CAPABILITY_IAM 
}
catch
{}

Lovely. That’ll be quiet now. But… it won’t throw when, for example, the Get-TemplateContent function goes haywire and returns you invalid JSON. Or when the stack is stuck in a ROLLBACK state and can’t be updated. This is clearly most undesirable.

So, let’s see if we can catch just the specific exception

try
{
    Update-CFNStack -StackName $StackName `
        -TemplateBody (Get-TemplateContent) `
        -verbose `
        -Parameter $cfparams `
        -Region $region `
        -Capability CAPABILITY_IAM 
}
catch [Amazon.CloudFormation.AmazonCloudFormationException]
{}

Ah yes, lovely. Except that everything this Cmdlet throws is an AmazonCloudFormationException. So this is actually no use at all. What we need to do is look within the exception and figure out which subspecies of problem we’re looking at. Which brings us to the complete snippet.

try {
    Update-CFNStack -StackName $StackName `
        -TemplateBody (Get-TemplateContent) `
        -verbose `
        -Parameter $cfparams `
        -Region $region `
        -Capability CAPABILITY_IAM 
} catch [Amazon.CloudFormation.AmazonCloudFormationException] {
    if($_.Exception.Message -notlike "*No updates are to be performed*")
    {
        throw $_  # rethrow if it's not the thing we want to suppress
    }
    else {
        Write-Output "No updates are to be performed."
    }
}

This looks much better. You can – as I have – wrap this up into a nice idempotent function called Invoke-CfnStackOperation, have it determine automatically if a New-CfnStack or an Update-CfnStack is required, and keep our params all nice and tidy.

But how do we go about proving it works? Well, these days, I actually feel insecure if I don’t have Pester tests against my PowerShell code. Who even writes PowerShell without automated tests these days?*

The trick lies in a mock that will throw the error you wish to suppress, on demand, like so:

Describe "Catching 'No updates are to be performed'" {
    Mock New-CfnStack {} # mocked to stop this accidentally creating a stack
    Mock Update-CfnStack {  # throws
        $message = "Update-CFNStack : No updates are to be performed."
        throw [Amazon.CloudFormation.AmazonCloudFormationException]::new($message)
    } -verifiable
    Mock Get-CFNStack { 
        return [pscustomObject]@{
            StackName = 'My-Test-Stack'; 
            StackStatus = "MOCKED"; 
            CreationTime = (Get-Date);
            Parameters = [pscustomobject]@(
                @{ParameterKey = 'Justaparam'; ParameterValue = 'Justavalue'}
            )
        } 
    } # should return a result, invoking the trigger

    It "Should suppress the 'No updates' exception" {
        { 
          Invoke-CloudFormationOperation -stackName 'My-Test-Stack' `
            -params $params `
            -template (Get-GeneratedTemplate)
        } | Should Not Throw
        Assert-VerifiableMocks
    }
}

And you can prove it still throws on other exceptions by tweaking the $message variable, and testing for Should Throw rather than Should Not Throw.

And there’s that annoying message suppressed and dealt with appropriately.

 

 

* the answer to this may be more worrying than you expect. There’s a LOT of untested PowerShell out there

 

Leave a Reply

Your email address will not be published. Required fields are marked *