Desktop and Application Streaming

User Issue Reporter for Amazon AppStream 2.0

The ephemeral nature of an Amazon AppStream 2.0 fleet instances can pose a unique challenge to administrators when trying to troubleshooting application issues. My previous blog post focused on automating log generation and alerting. In this blog post I will show how you can enable users to report issues themselves when they are currently in an AppStream 2.0 session. This ensures that relevant data is submitted to your helpdesk before the user session ends.

Overview

The Issue Reporter application relies on Amazon SES, Amazon S3, Amazon API Gateway, and AWS Lambda. When a user reports an issue from an AppStream 2.0 fleet instance, the problematic application’s logs are stored in Amazon S3. Then the user environment data is sent through API Gateway to a Lambda function. The Lambda function parses through the data and triggers Amazon SES, which sends the data to your helpdesk via email.

 

IR Architecture Diagram

This post walks you through the following steps:

  • Create the supporting infrastructure for the Issue Reporter application
  • Configure an AppStream 2.0 image builder with Issue Reporter application
  • Testing your setup on an AppStream 2.0 fleet instance

Prerequisites

Create the supporting infrastructure

Use the one the following AWS CloudFormation template links to create the supporting infrastructure required for the Issue Reporter implementation.

Fill in the Parameters, then choose Create stack.

CloudFormation stack parameters

The CloudFormation template creates a new Amazon S3 bucket, an API Gateway RESTful API, and a Lambda function. The template also creates the required IAM role and policy for the Lambda function.

Once the CloudFormation stack has completed successfully, navigate to the Outputs tab and copy down the API Gateway invoke URL and S3 bucket name. They are used when configuring the AppStream 2.0 image builder.

CloudFormation output tab

Optionally the resources created by the CloudFormation template can be created manually. At a high level you need to create the following:

The code for the Lambda function and the IAM role policy can be found here.

Configure an AppStream 2.0 Image Builder

With the supporting infrastructure for the Issue Reporter created, the Issue Reporter application script can now be configured on an AppStream 2.0 image builder.

  • In the AppStream 2.0 console, choose Images, Image Builder, and Launch Image Builder.
  • When the image builder is ready, connect to the instance as the administrator.
  • In the directory C:\Scripts, create a new PowerShell script named ir_app.ps1 with the following content:
Add-Type -AssemblyName System.Windows.Forms | Out-Null
Add-Type -AssemblyName System.Drawing | Out-Null
Add-Type -AssemblyName PresentationCore, PresentationFramework | Out-Null
$logFile = "$Env:TEMP\IR\Logging\IssueReporter.log"
New-Item -path $logfile -ItemType File -Force | Out-Null
Function Write-Log {
    Param ([string]$message)
    $stamp = Get-Date -Format "yyyy/MM/dd HH:mm:ss"
    $logoutput = "$stamp $message"
    Add-content $logfile -value $logoutput
}
function Write-IRLogtoS3 {
    try {
        Write-Log "Attempting to upload Issue Reporter log file to S3."
        Write-S3Object -BucketName $BucketName -File $logFile -Key $s3LogPathIR -Region $BucketRegion -ProfileName appstream_machine_role -ErrorAction Stop
    }
    catch {
        write-log "Could not upload the Issue Reporter log file to S3. $_"
        return
    }
    
}
function New-IssueSubmission {
    $form.Controls.Add($progressBar)
    $email = $emailBox.Text
    if ($textBox.Text -eq '') {
        $desc = "User did not provide a description of the issue."
    }
    else {
        $desc = $textBox.Text
    }
    $ComboSelection = $ComboBox.SelectedIndex
    try {
        switch ($ComboSelection) { #Change the $appname variable to match the name of the application you are gathering logs for; update the paths to the path(s) of the log files for the appplication
            1 { $appname = "Application1"; $uploadpath = "$env:TEMP\$AS2UserName-$appname.zip"; Compress-Archive -path "Path\to\Application\File1.log", "Path\to\Application\File2.log" -DestinationPath $uploadpath -Force -ErrorAction Stop; }
            2 { $appname = "Application2"; $uploadpath = "$env:TEMP\$AS2UserName-$appname.zip"; Compress-Archive -path "Path\to\Application\File1.log", "Path\to\Application\File2.log" -DestinationPath $uploadpath -Force -ErrorAction Stop; }
            3 { $appname = "AppStreamEnv"; $uploadpath = "$env:TEMP\$AS2UserName-$appname.zip"; Compress-Archive -path "Path\to\Application\File1.log", "Path\to\Application\File2.log" -DestinationPath $uploadpath -Force -ErrorAction Stop; }
            default { write-log "Unknown combobox selection" }
                
        }
        $progressBar.PerformStep()
            
    }
    catch {
        write-log "Could not archive the application logs. $_"
        Write-IRLogtoS3
        [System.Windows.MessageBox]::Show("There was an when trying to archive the application logs.`n`nPlease try again.", 'Error', 'OK', 'Error')
        $form.DialogResult = [System.Windows.Forms.DialogResult]::Abort
        return $SubmitCount
            
    }
        
    If ($ComboSelection -ge 1) {
        $s3LogPathApp = "$($S3ObjectPath)$AS2UserName-$SessionID-$appname.zip"
        $envdata["App"] = "$appname"
        $envdata["IRLogPath"] = "$s3LogPathIR"
        $envdata["AppLogPath"] = "$s3LogPathApp"
        try {
            Write-Log "Uploading application logs to S3."
            Write-S3Object -BucketName $BucketName -File $uploadpath -Key "$($S3ObjectPath)$AS2UserName-$appname.zip" -Region $BucketRegion -ProfileName appstream_machine_role -ErrorAction Stop
            $progressBar.PerformStep()
    
            try {
                $InvokeUrl = "<APIGateway Invoke URL>" #replace with invoke URL created by the CloudFormation template 
                $body = @{"email" = "$email"; "description" = "$desc"; "env" = $envdata; } | ConvertTo-Json
                $rest = Invoke-RestMethod -UseBasicParsing -Uri $InvokeUrl  -ContentType "application/json" -Method POST -Body $body -ErrorAction Stop
                if ($rest.Message | Select-String -SimpleMatch "Success") {
                    write-log "API Gateway invoked sucessfully"
                    $progressBar.PerformStep()
                    $SubmitCount = $SubmitCount + 1
                    write-log "Submit Count is $SubmitCount."
                    [System.Windows.MessageBox]::Show("You're issue has been reported to the Example Corp helpdesk sucessfully.`n`nThank you.", 'Issue Reported Successfully', 'OK', 'Info') | Out-Null #Update the Example Corp lanaguage with your company's name
                    $emailBox.clear(); $ComboBox.SelectedIndex = 0; $textBox.clear(); $form.Controls.Remove($progressBar); $progressBar.Value = 0;                                    
                }
                return $SubmitCount
            }
            catch {
                Write-Log "There was an error submitting data to API Gateway. $_" #Update the Example Corp lanaguage with your company's name and email address
                [System.Windows.MessageBox]::Show("Issue Reporter was unable to trigger an email to the Example Corp helpdesk.`n`nPlease email the helpdesk directly (help@examplecorp.com) detailing the issue(s) you are having.", 'Unable to Send Email', 'OK', 'Warning') | Out-Null
                $form.Controls.Remove($progressBar)
                $SubmitCount = $SubmitCount + 1
                write-log "Submit Count is $SubmitCount."
                $progressBar.Value = 0
                return $SubmitCount
                
            }
            
            
        }
        catch {
            write-log "Could not upload the application logs to S3. $_"
            Write-IRLogtoS3
            [System.Windows.MessageBox]::Show("There was an error uploading files to S3.`n`nPlease try again.", 'Error', 'OK', 'Error') | Out-Null
            $form.DialogResult = [System.Windows.Forms.DialogResult]::Abort
            return $SubmitCount
            
    
        }

    }
    else {
        [System.Windows.MessageBox]::Show("The input submitted is invalid. Please try again.", 'Invalid Input', 'OK', 'Warning') | Out-Null
        $form.Controls.Remove($progressBar)
        $progressBar.Value = 0
    }

}
$AS2UserName = (Get-Item Env:AppStream_UserName).Value
$SessionID = (Get-Item Env:AppStream_Session_ID).Value
$InstanceAWSRegion = (Get-Item Env:AWS_Region).Value
$StackName = (Get-Item Env:AppStream_Stack_Name).Value
$FleetName = (Get-Item Env:AppStream_Resource_Name).Value
$AccessMode = (Get-Item Env:AppStream_User_Access_Mode).Value
$HostName = $ENV:COMPUTERNAME
$InstanceID = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/dynamic/instance-identity/document") | ConvertFrom-Json | Select-Object -ExpandProperty instanceID
$SubmitCount = 0
$BucketName = "<Bucket-Name>" #Replace with the S3 bucket name created by the CloudFormation template
$BucketRegion = "<Region>" #Replace with the Region the S3 bucket is in
$S3ObjectPath = "$($InstanceAWSRegion)/$($StackName)/$($AS2UserName)/$($SessionID)/"
$s3LogPathIR = "$($S3ObjectPath)IssueReporterLogs/IssueReporter-$(Get-Date -Format "yyyy_MM_dd").log"
Add-content $logfile -value "AppStream Environment Information`n"
$envdata = [ordered]@{"HostName" = "$HostName"; "AS2UserName" = "$AS2UserName "; "SessionID" = "$SessionID"; "InstanceID" = "$InstanceID"; "StackName" = "$StackName"; "FleetName" = "$FleetName"; "AccessMode" = "$AccessMode"; "Region" = "$InstanceAWSRegion"; }
$envdata.GetEnumerator() | ForEach-Object { Add-Content -Path $logFile -Value "$($_.Key): $($_.Value)" }
Add-content $logfile -value "`nIssue Reporter Log`n"
$envdata.Add("IRLogPath", "")
$envdata.Add("AppLogPath", "")
$envdata.Add("App", "")

$form = New-Object System.Windows.Forms.Form
$form.Text = 'ExampleCorp Issue Reporter' #Update the Example Corp lanaguage with your company's name
$form.AutoSize = $true
$form.MinimumSize = New-Object System.Drawing.Size(350, 300)
$form.FormBorderStyle = "Sizable"
$Form.StartPosition = "WindowsDefaultLocation"
$Form.Topmost = $true
$Form.ShowInTaskbar = $true
$Form.AutoSizeMode = "GrowAndShrink"
$Form.SizeGripStyle = "auto"
$Form.AutoScroll = $true
$iconPath = "${PSScriptRoot}\examplecorp.ico" #Update the Example Corp icon with your company's icon
$icon = [system.drawing.icon]::ExtractAssociatedIcon($iconPath)
$Form.Icon = $icon

$SubmitButton = New-Object System.Windows.Forms.Button
$SubmitButton.Location = New-Object System.Drawing.Point(10, 330)
$SubmitButton.Size = New-Object System.Drawing.Size(75, 23)
$SubmitButton.Text = 'Submit'
$SubmitButton.Enabled = $false
$form.Controls.Add($SubmitButton)

$ExitButton = New-Object System.Windows.Forms.Button
$ExitButton.Location = New-Object System.Drawing.Point(160, 330)
$ExitButton.Size = New-Object System.Drawing.Size(75, 23)
$ExitButton.Text = 'Exit'
$ExitButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$form.CancelButton = $ExitButton
$form.Controls.Add($ExitButton)

$RestButton = New-Object System.Windows.Forms.Button
$RestButton.Location = New-Object System.Drawing.Point(85, 330)
$RestButton.Size = New-Object System.Drawing.Size(75, 23)
$RestButton.Text = 'Reset'
$RestButton.Enabled = $true
$form.Controls.Add($RestButton)

$emailboxlabel = New-Object System.Windows.Forms.Label
$emailboxlabel.AutoSize = $true
$emailboxlabel.Location = New-Object System.Drawing.Point(10, 20)
$emailboxlabel.Text = 'Please enter your email address:'
$form.Controls.Add($emailboxlabel)
$emailBox = New-Object System.Windows.Forms.TextBox
$emailBox.Location = New-Object System.Drawing.Point(10, 40)
$emailBox.AutoSize = $true
$emailBox.MinimumSize = New-Object System.Drawing.Size(240, 20)
$form.Controls.Add($emailBox)
$emailBox.add_textChanged( { if ($emailbox.text -match '^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$' -and $ComboBox.SelectedIndex -gt 0) { $SubmitButton.Enabled = $true }else { $SubmitButton.Enabled = $false } })
$selectlabel = New-Object System.Windows.Forms.Label
$selectlabel.Location = New-Object System.Drawing.Point(10, 70)
$selectlabel.AutoSize = $true
$selectlabel.Text = 'Please select the application having issues:'
$form.Controls.Add($selectlabel)
$ComboBox = New-Object System.Windows.Forms.ComboBox
$ComboBox.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
$ComboBox.Location = New-Object System.Drawing.Point(10, 90)
$ComboBox.Size = New-Object System.Drawing.Size(240, 20)
[void] $ComboBox.Items.Add('--') #Update with name of the applications you are collecting logs for, this should correspond to the names of the applications in the archving section of the script above
[void] $ComboBox.Items.Add('Issue with Application-001') 
[void] $ComboBox.Items.Add('Issue with Application-002')
[void] $ComboBox.Items.Add('Issue with the AppStream 2.0 environment')
$ComboBox.SelectedIndex = 0
$ComboBox.add_SelectedIndexChanged( { if ($ComboBox.SelectedIndex -gt 0 -and $emailbox.text -match '^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$') { $SubmitButton.Enabled = $true }else { $SubmitButton.Enabled = $false } })
$form.Controls.Add($ComboBox)
$textboxlabel = New-Object System.Windows.Forms.Label
$textboxlabel.Location = New-Object System.Drawing.Point(10, 120)
$textboxlabel.AutoSize = $true
$textboxlabel.Text = 'Please describe the issue you are having:'
$form.Controls.Add($textboxlabel)
$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = New-Object System.Drawing.Point(10, 140)
$textBox.AutoSize = $true
$textBox.MinimumSize = New-Object System.Drawing.Size(290, 140)
$textBox.Multiline = $true
$textBox.ScrollBars = "Vertical"
$textBox.MaxLength = 750
$form.Controls.Add($textBox)
$form.Add_Shown( { $textBox.Select() })
$HelpURL = New-Object System.Windows.Forms.LinkLabel #Change the text and URL to match your companmy's helpdesk website
$HelpURL.Location = New-Object System.Drawing.Point 10 , 285
$HelpURL.AutoSize = $True
$HelpURL.LinkColor = "BLUE"
$HelpURL.ActiveLinkColor = "PURPLE"
$HelpURL.Text = "Example Corp HelpDesk Website"
$HelpURL.TextAlign = "BottomLeft"
$HelpURL.add_Click( { [system.Diagnostics.Process]::start("https://www.example.com") })
$form.Controls.Add($HelpURL)
$KBURL = New-Object System.Windows.Forms.LinkLabel #Change the text and URL to match your companmy's KB website
$KBURL.Location = New-Object System.Drawing.Point 10 , 305
$KBURL.AutoSize = $True
$KBURL.LinkColor = "BLUE"
$KBURL.ActiveLinkColor = "PURPLE"
$KBURL.Text = "Example Corp AppStream KB"
$KBURL.TextAlign = "BottomLeft"
$KBURL.add_Click( { [system.Diagnostics.Process]::start("https://www.example.com") })
$form.Controls.Add($KBURL)
$progressBar = New-Object System.Windows.Forms.ProgressBar
$progressBar.Name = 'ProgressBar'
$progressBar.Value = 0
$progressBar.Step = 33.33333
$progressBar.Style = "Continuous"
$progressBar.Location = New-Object System.Drawing.Size (10, 370)
$progressBar.Size = New-Object System.Drawing.Size (225, 20)

$RestButton.Add_Click( { $emailBox.clear(); $ComboBox.SelectedIndex = 0; $textBox.clear(); })
$SubmitButton.Add_Click( {$Script:SubmitCount = New-IssueSubmission})

$result = $form.ShowDialog()
write-log "Submit Count is $SubmitCount."
if (($result -eq [System.Windows.Forms.DialogResult]::Cancel) -and ($SubmitCount -gt 0)) {
    Write-IRLogtoS3
}
$form.Dispose()
exit
  • Make the following edits within the script:
    • Application names and paths should be updated to the names and log locations of the applications that you want to gather logs for.
    • Replace <APIGateway Invoke URL> with the API Gateway invoke URL from the CloudFormation template output.
    • Replace any language referencing Example Corp with your own company’s name.
    • The placeholders <Bucket-Name> and <Region> should be replaced with the name and bucket Region of the S3 bucket from the CloudFormation template output.
    • Update the $iconPath variable with path to your company’s icon.
    • Update the combobox dropdown menu with name of your applications.
    • Update the website link URLs and description text.
    • By default the Issue Reporter application captures the following data from the AppStream 2.0 fleet instance environment:
      • Access Mode
      • Streaming session’s user name
      • Session ID
      • Instance ID
      • Stack name
      • Fleet name
      • User’s email address
      • Instance hostname
      • Region
    • If you want to add other variables that are collected by the application, update the $envdata array with new entries.
  • After all the changes have been made, save and close the script.
  • Verify that the script has been updated correctly by running the script in PowerShell.

You will see a form appear, similar to the following:

IR application GUI

Create your AppStream 2.0 image

  • From the image builder instance’s desktop, start Image Assistant.
  • When Image Assistant is open, add PowerShell as an application.
    • Change the Name and Display Name of the application to Issue Reporter
    • Optionally, update the Icon Path to a PNG of your choice.
    • Edit the Launch Parameters to the following: -windowstyle hidden -NoProfile -NonInteractive -noexit -file "ir_app.ps1"
    • Update the Working Directory to C:\Scripts
  • Add any other applications required for your image.
  • Proceed with the normal image creation process. For more information, see Create a Custom AppStream 2.0 Image by Using the AppStream 2.0 Console.

NOTE: The execution policy in PowerShell does not run unsigned scripts by default. You may need to modify the execution policy for your script to run. Please see About Execution Policies for more information on PowerShell execution policies.

Create and configure an IAM role

While your AppStream 2.0 image is being created, create the IAM role and policy for your fleet instances to use when uploading logs to Amazon S3.

  • In the IAM console, and choose Policies, Create policy.
  • Choose the JSON tab. Copy and paste the following JSON policy into the policy document box:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::<S3-BUCKET-NAME>/*"
        }
    ]
}
  • Update the policy with the name of your S3 bucket.
  • Choose Review policy. Name your policy, and choose Create policy.
  • In the navigation pane, choose Roles, Create Role, and configure the following boxes:
    • For Select type of trusted entity, choose AWS Service.
    • For the service that will use this role, choose AppStream 2.0.
  • Choose Next: Permissions.
  • In the Filter policies search box, search for the policy that you previously created. When the policy appears in the list, select the check box next to the policy name.
  • Choose Next: Tags. Although you can specify a tag for the policy, a tag is not required.
  • Choose Next: Review. Name your role, and choose Create role.

Configure your AppStream 2.0 fleet

With the IAM role created, and image creation process finished, you are ready to create the AppStream 2.0 fleet.

  • From the AppStream 2.0 console, choose Fleets, Create Fleet.
  • Provide a name for your new fleet, and configure the fleet to use the image that you previously created. On Step 3, make sure to configure the IAM role setting to use the IAM role that you created. Proceed with the fleet creation process.
  • After the fleet has been created, make sure that it’s in the Starting state, and then assign it to a stack.

Testing your setup

After your fleet has started, launch an AppStream 2.0 session. When you get to the catalog page, launch the Issue Reporter application.

  • Enter a valid email address in the email address text.
  • Select an application from the issue drop-down menu.
  • Optionally, provide a test description of an issue that a user might encounter.
  • Submit the issue.

If the test is successful, you should see the log files uploaded to your S3 bucket. You should also receive an email with the user’s environment details and description of the issue they had.

Clean Up

Delete your CloudFormation Stack

  • On the Stacks page in the CloudFormation console, select the stack that you previously created.
  • In the stack details pane, choose Delete.
  • Select Delete stack when prompted. This will delete all the resources that were created by the stack.

NOTE: Your S3 bucket must be empty before CloudFormation can delete it

Stop and Delete your AppStream 2.0 image builder

  • Open the AppStream 2.0 console.
  • In the navigation pane, choose Images, Image Builder.
  • Confirm whether the image builder that you created is in a stopped state. If not, select the image builder and choose Actions, Stop.
  • After the image builder has stopped, choose Actions, Delete.

Disassociate your AppStream 2.0 fleet from your stack and delete your stack

  • In the AppStream 2.0 console, in the navigation pane, choose Stacks.
  • Select the stack you created and choose Actions, Disassociate Fleet. This action disassociates the fleet from the stack.
  • To delete the stack, choose Actions, Delete.

Delete your AppStream 2.0 fleet

  • In the AppStream 2.0 console, in the navigation pane, choose Fleets.
  • Confirm whether the fleet that you created is in a stopped state. If not, select the fleet and choose Actions, Stop.
  • After the fleet has stopped, choose Actions, Delete.

Security considerations

Here are some security implementations that should be considered before deploying this solution to production.

Conclusion

That’s it! You now have a custom AppStream 2.0 image configured with Issue Reporter application along with the required supporting AWS infrastructure. Your users can now report issues they experience while on AppStream 2.0 fleet instances .