AWS UserData Multiple Run Framework Part II

My first post on the topic of using AWS UserData (via CloudFormation and PowerShell), was posted recently (http://tommymaynard.com/aws-userdata-multiple-run-framework-2017). While it didn’t light up Twitter and Facebook, where it was shared, there was still some value to it for me, and at least one other person. Therefore, I’ve decided to do part two. This is one of these using-PowerShell-to-create-PowerShell posts. I just love these.

A quick recap on the first post. I use the below PowerShell function in all the UserData sections of all my CloudFormation documents (when building Windows systems). Additionally, I also include an If-ElseIf language construct to do specific actions against an EC2 instance, as it’s coming online. The best part here is that I can do instance restarts between configuration steps. While I’ll include the full example (actually put together), from Part I below, it might be best to read that first post (link above).

Function Set-SystemForNextRun {
    Param (
        [string]$PassFile,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($PassFile) {
        [System.Void](New-Item -Path "$env:SystemDrive\passfile$PassFile.txt" -ItemType File)
    }
    If ($UserData) {
        $Path = "$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        $ConfigXml = Get-Content -Path $Path
        ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        $ConfigXml.Save($Path)
    }
    If ($Restart) {
        Restart-Computer -Force
    }
}

If (-Not(Test-Path -Path "$env:SystemDrive\passfile1.txt")) {
 
    # Place code here (1).
 
    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '1' -UserData -Restart
 
} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile2.txt")) {
 
    # Place code here (2).
 
    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '2' -UserData -Restart
 
} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile3.txt")) {
 
    # Place code here (3).
 
    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '3' -UserData -Restart
 
} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile4.txt")) {
 
    # Place code here (4).
 
    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '4'
 
}

Here’s the thing, the above example is going to restart my computer three times: after the If portion, after the first ElseIf, and after the second ElseIf portion. These three restarts effectively give me four areas in which to configure an EC2 instance in sequence and again, between restarts.

There’s no way for me to predict how many code runs you or I are going to need with each new project, whether or not we’ll want to enable UserData after the final code run, or even if we’ll need a final, final restart. Therefore, I’ve written a PowerShell function to help with this process. Why manually edit this If-ElseIf statement if you don’t have to, right? With this newer function, you can decide how many code runs you need and whether you need to enable UserData or issue a restart after your final code section executes. Here are a few examples with the full function, New-AWSMultiRunTemplateat the end of today’s post.

In this first example, we’ll just run the function with its default settings. This will create the Set-SystemForNextRun function and an If-ElseIf statement (with two places to configure the instance and a single restart in between them. If this is what you needed, you’d take the output of the New-AWSMultiRunTemplate function invocation and paste it into your AWS CloudFormation’s UserData section, between an open and close PowerShell tag: <powershell> and </powershell>.

New-AWSMultiRunTemplate
Function Set-SystemForNextRun {
    Param (
        [string]$PassFile,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($PassFile) {
        [System.Void](New-Item -Path "$env:SystemDrive\passfile$PassFile.txt" -ItemType File)
    }
    If ($UserData) {
        $Path = "$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]$ConfigXml = Get-Content -Path $Path
        ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        $ConfigXml.Save($Path)
    }
    If ($Restart) {
        Restart-Computer -Force
    }
}

If (-Not(Test-Path -Path "$env:SystemDrive\passfile1.txt")) {

    # Place code here (1).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '1' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile2.txt")) {

    # Place code here (2).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '2'

}

This next run will create a single If statement. I suppose if you’re only going to do a single configuration pass, you don’t need this function at all. Again, I said I use it for all my CloudFormation templates. Say you edit your CloudFormation document later and add more configuration and restarts, you’ll already have most of what you need to be included in your CloudFormation document.

New-AWSMultiRunTemplate -CodeSectionCount 1
Function Set-SystemForNextRun {
    Param (
        [string]$PassFile,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($PassFile) {
        [System.Void](New-Item -Path "$env:SystemDrive\passfile$PassFile.txt" -ItemType File)
    }
    If ($UserData) {
        $Path = "$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]$ConfigXml = Get-Content -Path $Path
        ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        $ConfigXml.Save($Path)
    }
    If ($Restart) {
        Restart-Computer -Force
    }
}

If (-Not(Test-Path -Path "$env:SystemDrive\passfile1.txt")) {

    # Place code here (1).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '1'

}

Now, let’s get crazy! We’re going to add a bunch of ElseIf statements in this example. Additionally, we’re going to highlight a couple of other features. There are parameters to use if you wish to include a final UserData reset and a final restart. Take a look at the below parameters used with the New-AWSMultiRunTemplate.

New-AWSMultiRunTemplate -CodeSectionCount 6 -EnableUserData All -EnableRestart All
Function Set-SystemForNextRun {
    Param (
        [string]$PassFile,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($PassFile) {
        [System.Void](New-Item -Path "$env:SystemDrive\passfile$PassFile.txt" -ItemType File)
    }
    If ($UserData) {
        $Path = "$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]$ConfigXml = Get-Content -Path $Path
        ($ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        $ConfigXml.Save($Path)
    }
    If ($Restart) {
        Restart-Computer -Force
    }
}

If (-Not(Test-Path -Path "$env:SystemDrive\passfile1.txt")) {

    # Place code here (1).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '1' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile2.txt")) {

    # Place code here (2).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '2' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile3.txt")) {

    # Place code here (3).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '3' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile4.txt")) {

    # Place code here (4).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '4' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile5.txt")) {

    # Place code here (5).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '5' -UserData -Restart

} ElseIf (-Not(Test-Path -Path "$env:SystemDrive\passfile6.txt")) {

    # Place code here (6).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '6' -UserData -Restart

}

Notice that in the final ElseIf above, we have the UserData and Restart parameters. These were added by including the EnableUserData and EnableRestart parameters, and All as the parameters’ value. By default, they’re both set to the AllButLast values. And that it’s! Here’s the New-AWSMultiRunTemplate function, in case it’s something you might find helpful! Enjoy!!

Function New-AWSMultiRunTemplate {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateRange(1,10)]
        [int]$CodeSectionCount = 2,

        [Parameter()]
        [ValidateSet('All','AllButLast')]
        [string]$EnableUserData = 'AllButLast',

        [Parameter()]
        [ValidateSet('All','AllButLast')]
        [string]$EnableRestart = 'AllButLast'
    )

    DynamicParam {
        # Create dynamic, Log parameter.
        If ($PSBoundParameters['Verbose']) {
            $SingleAttribute = New-Object System.Management.Automation.ParameterAttribute
            $SingleAttribute.Position = 1

            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $AttributeCollection.Add($SingleAttribute)

            $LogParameter = New-Object 
System.Management.Automation.RuntimeDefinedParameter('Log',[switch],$AttributeCollection)
            $LogParameter.Value = $true
 
            $ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            $ParamDictionary.Add('Log',$LogParameter)
            return $ParamDictionary
        } # End If.
    } # End DynamicParam.

    Begin {
        #region Create logs directory and Write-Verbose function.
        If ($PSBoundParameters['Verbose'] -and $PSBoundParameters['Log']) {
            $LogDirectory = "$($MyInvocation.MyCommand.Name)"
            $LogPath = "$env:SystemDrive\support\Logs\$LogDirectory"
            If (-Not(Test-Path -Path $LogPath)) {
                [System.Void](New-Item -Path $LogPath -ItemType Directory)
            }
            $LogFilePath = "$LogPath\$(Get-Date -Format 'DyyyyMMddThhmmsstt').txt"

            Function Write-Verbose {
                Param ($Message)
                Microsoft.PowerShell.Utility\Write-Verbose -Message $Message
                Microsoft.PowerShell.Utility\Write-Verbose -Message "[$(Get-Date -Format G)]: $Message" 4&amp;gt;&amp;gt; 
$LogFilePath
            }
        }

        # Set Write-Verbose block location.
        $BlockLocation = '[BEGIN  ]'
        Write-Verbose -Message "$BlockLocation Entering the Begin block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Storing the template's function to memory."
        $TemplateFunction = @"
Function Set-SystemForNextRun {
    Param (
        [string]`$PassFile,
        [switch]`$UserData,
        [switch]`$Restart
    )
    If (`$PassFile) {
        [System.Void](New-Item -Path "`$env:SystemDrive\passfile`$PassFile.txt" -ItemType File)
    }
    If (`$UserData) {
        `$Path = "`$env:ProgramFiles\Amazon\Ec2ConfigService\Settings\config.xml"
        [xml]`$ConfigXml = Get-Content -Path `$Path
        (`$ConfigXml.Ec2ConfigurationSettings.Plugins.Plugin |
            Where-Object -Property Name -eq 'Ec2HandleUserData').State = 'Enabled'
        `$ConfigXml.Save(`$Path)
    }
    If (`$Restart) {
        Restart-Computer -Force
    }
}


"@
    } # End Begin.

    Process {
        #region Set Write-Verbose block location.
        $BlockLocation = '[PROCESS]'
        Write-Verbose -Message "$BlockLocation Entering the Process block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Beginning to create the If-ElseIf code for the template."
        1..$CodeSectionCount | ForEach-Object {
            If ($_ -eq 1) {
                $Start = 'If'
            } Else {
                $Start = 'ElseIf'
            }

            If ($EnableUserData -eq 'All') {
                $UserData = '-UserData '
            } ElseIf ($_ -eq $CodeSectionCount) {
                $UserData = $null
            } Else {
                $UserData = '-UserData '
            }

            If ($EnableRestart -eq 'All') {
                $Restart = '-Restart'
            } ElseIf ($_ -eq $CodeSectionCount) {
                $Restart = $null
            } Else {
                $Restart = '-Restart'
            }

            $TemplateIfElseIf += @"
$Start (-Not(Test-Path -Path "`$env:SystemDrive\passfile$_.txt")) {
    
    # Place code here ($_).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -PassFile '$_' $UserData$Restart

} $End
"@
        } # End ForEach-Object.
    } # End Process.

    End {
        #region Set Write-Verbose block location.
        $BlockLocation = '[END    ]'
        Write-Verbose -Message "$BlockLocation Entering the End block [Function: $($MyInvocation.MyCommand.Name)]."
        #endregion

        Write-Verbose -Message "$BlockLocation Displaying the AWS mulitple run code with $CodeSectionCount code 
section(s)."
        "$TemplateFunction$TemplateIfElseIf"
    } # End End.
} # End New-AWSMultiRunTemplate function.

Read III

Leave a Reply

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