AWS UserData Multiple Run Framework Part III

Update: There’s a fourth version of this article series. See the link at the bottom of this post.

At the bottom of the below post (Part II), was a function that created code for what I called the AWS UserData Multiple Run Framework. The code produced by this function can be added to the AWS CloudFormation UserData section allowing for Windows EC2 instance configuration between a controlled number of restarts. You know… configure, restart, configure, restart, configure, restart, etc. Without this in place, or some other configuration tool, you only get one time to utilize the UserData section, and that’s when an EC2 instance launches for the first time.

That version, however, required the use of text files at the root of the C:\ drive. These were used so that the UserData code would know which section of the code to run after each restart. I always worried that someone wouldn’t know the importance of the text files at the root of the C:\ drive and remove them. Therefore, I wrote a newer version. I believe I previously mentioned that as a possibility, in one of the two other related posts.

AWS UserData Multiple Run Framework Part II

This version—the newest version—makes use of the Windows Registry to make the determination of what code to run next. There are no more scattered files on the root of the C:\, and instead, everything is better protected, and hidden, in the Windows Registry. Let’s briefly discuss each section first. Then, use the function at the bottom of this post. It creates the code that you would add to the AWS CloudFormation UserData section. It isn’t the code you’d add to the UserData section. It’s still PowerShell creating PowerShell. For reference, think of the Microsoft function New-IseSnippet of old, and the New-ModuleManifest cmdlet. They’re both PowerShell, that create PowerShell.

Function Set-SystemForNextRun {
    Param (
        [string]$Pass,
        [switch]$UserData,
        [switch]$Restart
    )
    If ($Pass) {
        [System.Void](New-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT' -Name "Pass$Pass" -Value 'Complete')
    }
    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
    }
}

The Set-SystemForNextRun function—the first part of the code that’s created—has some minor changes. First, the PassFile parameter is now just Pass. This was changed because, well, we’re not using files anymore. Second, the code inside the If ($Pass) section no longer creates text files and instead, creates registry values. Remember, as this code runs inside the AWS CloudFormation UserData section, this function is written to memory before it’s ever used. The two upcoming sections are actually run, or rather, do things we can see, first.

# Check if Registry Subkey does not exist.
If (-Not(Get-Item -Path 'HKLM:\SOFTWARE\DEPT' -ErrorAction SilentlyContinue)) {
    # Create Registry Subkey.
    [System.Void](New-Item -Path 'HKLM:\SOFTWARE\' -Name 'DEPT')
}

The above code is the first run code in UserData, as again our Set-SystemForNextRun function is written to memory and yet to be invoked. Its purpose is to create a Windows Registry subkey DEPT if it doesn’t already exist. You can change DEPT to whatever makes the most sense for your use. This is the location where we’ll store our values that indicate which section in the upcoming If-ElseIf (ElseIf, ElseIf, etc.) statement we’ll run.

If (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT').Pass1 -eq 'Complete')) {

    # Place code here (1).

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

} ElseIf (-Not((Get-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT').Pass2 -eq 'Complete')) {

    # Place code here (2).

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

}

This is the meat of what’s produced by the upcoming function. It’s the If-ElseIf statement. On the first run of the UserData, the If statement will fire. This is because we won’t have a value called Pass1 set to the string “Complete” in the HKLM:\SOFTWARE\DEPT subkey. After its code is done, it’ll invoke the Set-SystemForNextRun function which will add the Pass1 value to the Registry, set UserData to enabled, and restart the instance. On the next run, it’ll execute the code in the first (and only) ElseIf section because the value Pass1 will have been created, but the value Pass2 will not have been created. It gets created after the code in this section is complete.

That’s it. Hopefully, this can be helpful for more than just me and those around me at work.

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>> $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]`$Pass,
        [switch]`$UserData,
        [switch]`$Restart
    )
    If (`$Pass) {
        [System.Void](New-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT' -Name "Pass`$Pass" -Value 'Complete')
    }
    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
    }
}

# Check if Registry Subkey does not exist.
If (-Not(Get-Item -Path 'HKLM:\SOFTWARE\DEPT' -ErrorAction SilentlyContinue)) {
    # Create Registry Subkey.
    [System.Void](New-Item -Path 'HKLM:\SOFTWARE\' -Name 'DEPT')
}


"@
    } # 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((Get-ItemProperty -Path 'HKLM:\SOFTWARE\DEPT').Pass$_ -eq 'Complete')) {
    
    # Place code here ($_).

    # Invoke Set-SystemForNextRun function.
    Set-SystemForNextRun -Pass '$_' $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 DEPT AWS mulitple run code with $CodeSectionCount code section(s)."
        "$TemplateFunction$TemplateIfElseIf"
    } # End End.
} # End Function: New-AWSMultiRunTemplate.

Read IV

Leave a Reply

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