Desktop and Application Streaming

Automated Alerting on Amazon WorkSpaces Service Limits

Amazon Web Services (AWS) recently announced the addition of Amazon WorkSpaces Service Limits within Service Quotas. This now allows customers to have visibility into what their current limits are for the various types of WorkSpaces in a given AWS Region. Combined with the API operations to query deployed WorkSpaces, customers now have the tools at their disposal to proactively monitor their WorkSpaces deployments.

Overview

In this article, we walk through the setup of an automation that will alert you via email when you reach a defined percentage of your WorkSpaces quota. Each of the three types of WorkSpaces, Graphics, GraphicsPro, and Non-GPU, are monitored and alerted on independently. This solution leverages an AWS Lambda function to gather the total number of WorkSpaces deployed in each selected region. It then checks this against the current service limits for the given region. Finally, it sends an Amazon Simple Notification Service alert if any quota threshold is breached. The Lambda function is triggered by an Amazon EventBridge rule at a determined time of day. This solution can either be deployed using the provided AWS CloudFormation template or manually by creating each component yourself by following the below steps.

Solution Diagram

Time to read 15 minutes
Time to complete 30 minutes
Cost to complete (estimated) $0
Learning level Advanced (300)
Services used Amazon EventBridge
Amazon Simple Notification service
Amazon Workspaces
AWS CloudFormation
AWS Lambda
Service Quotas

Walkthrough

In this walkthrough, you complete the following tasks:

  1. Create an Amazon Simple Notification Service (Amazon SNS) topic and subscription.
  2. Create an AWS Identity and Access Management (IAM) policy and role.
  3. Create an AWS Lambda function.
  4. Create an Amazon EventBridge rule.

Prerequisites

This article assumes that you have the following already in place:

  • An AWS account.
  • Amazon WorkSpaces deployed into at least one AWS Region.
  • Permissions to create EventBridge rules, Lambda functions, SNS topics and subscriptions, IAM Roles and Policies, and optionally CloudFormation stacks.

Option 1: CloudFormation Deployment

The provided CloudFormation template can be used to quickly deploy all of the parts of this solution with a few clicks and after entering a few required parameters. Once deployed, you have the option to modify various aspects to meet your needs.

  1. Copy the contents of the below YAML CloudFormation template, paste into the text editor of your choice, and save.
  2. Open the CloudFormation console.
  3. In the navigation pane, choose Stacks.
  4. Choose Create stack, then select With new resources (standard).
  5. Keep Template is ready selected, under Specify template select Upload a template file.
  6. Choose Choose file and select the file created in step 1, and choose Next.
  7. Enter a name for the stack and fill out the parameters:
    • AlertThreshold – at what percentage of a limit to send a notification email; 0-100.
    • AlertTime – at what time of day to run the limit check in UTC format; 0-23.
    • MonitorRegions – a comma-separated list of which AWS Regions to check WorkSpace limits.
    • NotificationEmail – email address that alerts are sent to.
  8. Choose Next.
  9. Optionally add tags that will be applied to the resources deployed with the template as required by your environment, then choose Next.
  10. On the Review page, check the box I acknowledge that AWS CloudFormation might create IAM resources with custom names and then click Create stack.

The CloudFormation template will now begin building the resources for this solution. Once finished the status will change from the CREATE_IN_PROGRESS to CREATE_COMPLETE. At this point the solution is fully deployed and will run once per day at the time set by AlertTime parameter.

AWSTemplateFormatVersion: '2010-09-09'
Description: >-
  This template creates the components to setup automatic alerting on Amazon WorkSpaces Service Limits. By default, this solution will monitor the limits once per day at the configured time. The schedule can be modified in the Amazon EventBridge console if a different interval is required (such as hourly or monthly).
Parameters:
  NotificationEmail:
    Type: String
    Description: The email address that receives the alarm notifications. Additional email addresses can be added by subscribing them to the topic created with the template.
    AllowedPattern: '[^\s@]+@[^\s@]+\.[^\s@]+'
    ConstraintDescription: "NotificationEmail must be a valid email address."
    Default: youruser@yourdomain.com
  AlertThreshold:
    Type: String
    Description: At what percentage of the quota used will an alert be triggered.
    AllowedPattern: '^([0-9]{1,2}|100){1}(\.[0-9]{1,2})?$'
    ConstraintDescription: "AlertThredhold should be between 0 and 100."
    Default: 80
  AlertTime:
    Type: String
    Description: Time to run the service limit check each day (UTC).
    AllowedPattern: '^\d$|^1\d$|^2[0-3]$'
    ConstraintDescription: "AlertTime must be between 0 and 23 UTC."
    Default: 12
  MonitorRegions:
    Type: String
    AllowedPattern: '\"[a-zA-Z0-9-]+\"+(,\"[a-zA-Z0-9-]+\"+)*'
    ConstraintDescription: 'Must be a comma separated and quoted list of regions with no spaces between entries. ("us-east-1","us-west-2" or "us-east-1").'
    Description: Comma separated and quoted list of regions to monitor WorkSpaces service limits. ("us-east-1","us-west-2" or "us-east-1")
Resources:
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: "WorkSpaces_Service_Limit_Alerting"
  SNSSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint:
        Ref: NotificationEmail
      Protocol: email
      TopicArn:
        Ref: SNSTopic
  LambdaFunctionIAMRole:
    Type: 'AWS::IAM::Role'
    Properties: 
      RoleName: WorkSpaces_Service_Limit_Alerting_Role
      Description: IAM role for WorkSpaces Service Limit Alerting Lambda Function
      AssumeRolePolicyDocument: # What service can assume this role
        Version: '2012-10-17'
        Statement: 
          - 
            Effect: Allow
            Principal: 
              Service: 
                - 'lambda.amazonaws.com'
            Action: 
              - 'sts:AssumeRole'
  LambdaFunctionIAMPolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      Description: "Permissions needed by the WorkSpaces Service Limits Lambda function to read the required information from Service Quotas and WorkSpaces."
      ManagedPolicyName: WorkSpaces_Service_Limit_Alerting_Policy
      PolicyDocument: 
        Version: '2012-10-17'
        Statement: 
          - Effect: Allow
            Action:
              - servicequotas:GetServiceQuota
              - workspaces:DescribeWorkspaces
            Resource: '*'
          - Effect: Allow
            Action:
              - sns:Publish
            Resource:
              - Ref: SNSTopic
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'
      Roles:
        - !Ref LambdaFunctionIAMRole
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: WorkSpaces_Service_Limit_Alerting
      Handler: index.lambda_handler
      Code:
        ZipFile: |
          import json
          import boto3
          import logging
          import os
          import textwrap
          from botocore import config

          LOGGER = logging.getLogger()
          LOGGER.setLevel(logging.INFO)

          CONFIG = config.Config(
            retries = {
                'max_attempts': 10,
                'mode': 'standard'
            })

          SNS = boto3.client('sns')

          def send_notification():
              #Publish image information to SNS Topic    
              try :
                  SNS_RESPONSE = SNS.publish(
                      TopicArn=ALERT_TOPIC_ARN,
                      Message=MESSAGE,
                      Subject=SUBJECT
                  )
              except Exception as e:
                  LOGGER.error(e)
              return()

          def lambda_handler(event, context):

              #Notification email variables
              global SUBJECT, MESSAGE, ALERT_TOPIC_ARN     

              #Get alert threshold from event data and convert to percentage, or default to 80% if not found
              if 'AlertThreshold' in event :
                  ALERT_THRESHOLD = float(event['AlertThreshold']) / 100
              else :
                  ALERT_THRESHOLD = .80
              ALERT_THRESHOLD_PERCENT = ALERT_THRESHOLD * 100

              LOGGER.info(f"Alert threshold set to {ALERT_THRESHOLD_PERCENT}%")

              #Get list of regions from event data to check WorkSpaces Service Quotas in, or default to current region if not found
              if 'AlertRegions' in event :
                  REGION_LIST = event['AlertRegions']
              else :
                  REGION_LIST = [os.environ['AWS_REGION']]
                  
              ALERT_TOPIC_ARN = event['AlertTopicArn']

              #Trackers for quota alarms
              WORKSPACE_ALARM_STATUS = False
              WORKSPACE_GLOBAL_COUNT = 0
              WORKSPACE_ALARM_LIST = []


              for ALERT_REGION in REGION_LIST :
                  LOGGER.info(f"Begin WorkSpaces quota check in {ALERT_REGION}")
                  SQ = boto3.client('service-quotas',region_name=ALERT_REGION)
                  WS = boto3.client('workspaces',region_name=ALERT_REGION,config=CONFIG)
              
                  #Retreive current quota limit for standard WorkSpaces
                  RESPONSE = SQ.get_service_quota(
                      ServiceCode='workspaces',
                      QuotaCode='L-34278094'
                  )
                  STANDARD_WORKSPACE_QUOTA = int(RESPONSE['Quota']['Value'])
              
              
                  #Retreive current quota limit for graphics g4dn WorkSpaces
                  RESPONSE = SQ.get_service_quota(
                      ServiceCode='workspaces',
                      QuotaCode='L-BCACAEBC'
                  )
                  GRAPHICS_G4DN_WORKSPACE_QUOTA = int(RESPONSE['Quota']['Value'])
              
              
                  #Retreive current quota limit for graphics pro WorkSpaces
                  RESPONSE = SQ.get_service_quota(
                      ServiceCode='workspaces',
                      QuotaCode='L-254B485B'
                  )
                  GRAPHICSPRO_WORKSPACE_QUOTA = int(RESPONSE['Quota']['Value'])
                  
                  #Retreive current quota limit for graphicspro.g4dn WorkSpaces
                  RESPONSE = SQ.get_service_quota(
                      ServiceCode='workspaces',
                      QuotaCode='L-BE9A8466'
                  )
                  GRAPHICSPRO_G4DN_WORKSPACE_QUOTA = int(RESPONSE['Quota']['Value'])        


                  #Return all WorkSpaces in current region      
                  WS_PAGINATOR = WS.get_paginator('describe_workspaces')
                  WS_RESPONSE_ITERATOR = WS_PAGINATOR.paginate()


                  #Counter for the number of WorkSpaces by compute type
                  GRAPHICS_G4DN_COUNT = 0
                  GRAPHICSPRO_COUNT = 0
                  GRAPHICSPRO_G4DN_COUNT = 0
                  STANDARD_COUNT = 0


                  #WorkSpaces API returns pages of 25 WorkSpaces, loop through all pages and count all WorkSpaces by quota type
                  for WS_PAGE in WS_RESPONSE_ITERATOR:
                      for WORKSPACE in WS_PAGE.get('Workspaces'):
                          if (WORKSPACE['WorkspaceProperties']['ComputeTypeName'] == 'GRAPHICS_G4DN'):
                              GRAPHICS_G4DN_COUNT += 1
                          elif (WORKSPACE['WorkspaceProperties']['ComputeTypeName'] == 'GRAPHICSPRO'):
                              GRAPHICSPRO_COUNT += 1
                          elif (WORKSPACE['WorkspaceProperties']['ComputeTypeName'] == 'GRAPHICSPRO_G4DN'):
                              GRAPHICSPRO_G4DN_COUNT += 1                    
                          else:
                              STANDARD_COUNT += 1

            
                  #Check each WorkSpace type count against alarm threshold    
                  if (STANDARD_COUNT > 0):
                      if (STANDARD_COUNT >= (ALERT_THRESHOLD * STANDARD_WORKSPACE_QUOTA)):
                          LOGGER.info(f"ALERT: Approaching regular WorkSpace quota in {ALERT_REGION}")
                          PERCENT = (STANDARD_COUNT / STANDARD_WORKSPACE_QUOTA) * 100
                          WORKSPACE_ALARM_LIST.append([ALERT_REGION,"Standard",STANDARD_WORKSPACE_QUOTA,STANDARD_COUNT,PERCENT]) 
                          WORKSPACE_ALARM_STATUS = True
              
                  if (GRAPHICS_G4DN_COUNT > 0):
                      if (GRAPHICS_G4DN_COUNT >= (ALERT_THRESHOLD * GRAPHICS_G4DN_WORKSPACE_QUOTA)):
                          LOGGER.info(f"ALERT: Approaching graphics.g4dn WorkSpace quota in {ALERT_REGION}")
                          PERCENT = (GRAPHICS_G4DN_COUNT / GRAPHICS_G4DN_WORKSPACE_QUOTA) * 100
                          WORKSPACE_ALARM_LIST.append([ALERT_REGION,"Graphics.g4dnboto3",GRAPHICS_G4DN_WORKSPACE_QUOTA,GRAPHICS_G4DN_COUNT,PERCENT]) 
                          WORKSPACE_ALARM_STATUS = True
                    
                  if (GRAPHICSPRO_COUNT > 0):
                      if (GRAPHICSPRO_COUNT >= (ALERT_THRESHOLD * GRAPHICSPRO_WORKSPACE_QUOTA)):
                          LOGGER.info(f"ALERT: Approaching graphics pro WorkSpace quota in {ALERT_REGION}")
                          PERCENT = (GRAPHICSPRO_COUNT / GRAPHICSPRO_WORKSPACE_QUOTA) * 100
                          WORKSPACE_ALARM_LIST.append([ALERT_REGION,"GraphicsPro",GRAPHICSPRO_WORKSPACE_QUOTA,GRAPHICSPRO_COUNT,PERCENT]) 
                          WORKSPACE_ALARM_STATUS = True

                  if (GRAPHICSPRO_G4DN_COUNT > 0):
                      if (GRAPHICSPRO_G4DN_COUNT >= (ALERT_THRESHOLD * GRAPHICSPRO_G4DN_WORKSPACE_QUOTA)):
                          LOGGER.info(f"ALERT: Approaching graphicspro.g4dn WorkSpace quota in {ALERT_REGION}")
                          PERCENT = (GRAPHICSPRO_G4DN_COUNT / GRAPHICSPRO_G4DN_WORKSPACE_QUOTA) * 100
                          WORKSPACE_ALARM_LIST.append([ALERT_REGION,"GraphicsPro.g4dn",GRAPHICSPRO_G4DN_WORKSPACE_QUOTA,GRAPHICSPRO_G4DN_COUNT,PERCENT]) 
                          WORKSPACE_ALARM_STATUS = True

                  WORKSPACE_COUNT = GRAPHICS_G4DN_COUNT + GRAPHICSPRO_COUNT + GRAPHICSPRO_G4DN_COUNT + STANDARD_COUNT

                  #Tally of all WorkSpaces across all regions checked
                  WORKSPACE_GLOBAL_COUNT = WORKSPACE_GLOBAL_COUNT + WORKSPACE_COUNT

                  #Print data to retain in CloudWatch log of Lambda execution    
                  print("########################################")
                  print(f"#     Report for {ALERT_REGION}")
                  print("########################################")
                  print(f"Graphics.g4dn WorkSpace limit: {GRAPHICS_G4DN_WORKSPACE_QUOTA}")
                  print(f"Graphics.g4dn WorkSpace count: {GRAPHICS_G4DN_COUNT}")
                  print("------------------------------------")
                  print(f"GraphicsPro.g4dn WorkSpace limit: {GRAPHICSPRO_G4DN_WORKSPACE_QUOTA}")
                  print(f"GraphicsPro.g4dn WorkSpace count: {GRAPHICSPRO_G4DN_COUNT}")
                  print("------------------------------------")        
                  print(f"GraphicsPro WorkSpace limit: {GRAPHICSPRO_WORKSPACE_QUOTA}")
                  print(f"GraphicsPro WorkSpace count: {GRAPHICSPRO_COUNT}")
                  print("------------------------------------")
                  print(f"WorkSpace limit: {STANDARD_WORKSPACE_QUOTA}")
                  print(f"WorkSpace count: {STANDARD_COUNT}")
                  print("------------------------------------")
                  print(f"Total WorkSpaces in region: {WORKSPACE_COUNT}")
                  print("########################################")
                  
                  LOGGER.info(f"End WorkSpaces quota check in {ALERT_REGION}")

              LOGGER.info(f"Total WorkSpaces across all regions: {WORKSPACE_GLOBAL_COUNT}")


              #Only generate and send SNS notification if there are any quotas breaching the alarm threshold
              if (WORKSPACE_ALARM_STATUS) :
                  LOGGER.info("Due to alarm, Service Limit Notification published to SNS.")
                  SUBJECT = "ALERT: Amazon WorkSpace Service Limits"
                  
                  MESSAGE = 'The below Amazon WorkSpaces Service Limits are beyond the configured threshold of {}% and are in an alarm state:\n\n'.format(ALERT_THRESHOLD_PERCENT)
                  MESSAGE = MESSAGE + '{: <21} {: <20} {}/{}\n'.format('Region','Desktop Type','Count','Quota')     
                  
                  for ALARM in WORKSPACE_ALARM_LIST:
                      REGION, TYPE, QUOTA, COUNT, PERCENT = ALARM
                      MESSAGE = MESSAGE + '{: <21} {: <23} {}/{} ({}%)\n'.format(REGION,TYPE,COUNT,QUOTA,PERCENT)    
                      
                  send_notification()
              else :
                  LOGGER.info("No service limits are in alarm state. No notification published.")

              return {
                  'WorkSpaceAlarm': WORKSPACE_ALARM_STATUS,
              }
      Runtime: python3.9
      Role: !GetAtt 'LambdaFunctionIAMRole.Arn'
      Timeout: 30
  EventBridgeScheduledRule: 
    Type: AWS::Events::Rule
    Properties: 
      Description: "Trigger Lambda function for WorkSpace Service Limit alerting."
      Name: "WorkSpaces_Service_Limit_Alerting_Trigger"
      ScheduleExpression: !Sub "cron(0 ${AlertTime} * * ? *)"
      State: "ENABLED"
      Targets: 
        - 
          Arn: 
            Fn::GetAtt: 
              - "LambdaFunction"
              - "Arn"
          Id: "TargetLambdaFunction"
          Input: !Sub "{ \"AlertThreshold\": ${AlertThreshold}, \"AlertRegions\": [ ${MonitorRegions} ], \"AlertTopicArn\": \"${SNSTopic}\" }"
  PermissionForEventsToInvokeLambda: 
    Type: AWS::Lambda::Permission
    Properties: 
      FunctionName: !Ref "LambdaFunction"
      Action: "lambda:InvokeFunction"
      Principal: "events.amazonaws.com"
      SourceArn: 
        Fn::GetAtt: 
          - "EventBridgeScheduledRule"
          - "Arn"

Option 2: Manual Solution Deployment

The following steps describe how to create the solution manually. You will create each of the resources from the CloudFormation template using the AWS console.

Step 1. Create the SNS Topic and Subscription(s)

In this step, you create the Amazon SNS topic and subscription. The Lambda function will use the topic to send alert emails when your configured service limit threshold has been breached.

  1. Open the Amazon SNS console.
  2. In the navigation pane, choose Topics.
  3. Choose Create topic.
  4. Select Standard type.
  5. Give the topic a name, for example, WorkSpaces-Limits-Alerting.
  6. Choose Create topic.
  7. Copy the ARN for use in later steps.
  8. Under Subscriptions, choose Create subscription.
  9. For Protocol, select Email.
  10. For Endpoint, enter the email address that will receive the limit alerts.
  11. Choose Create subscription.
  12. Repeat steps 7-10 to add any additional addresses that will receive the notifications.
  13. Each subscription will receive an AWS Notification email that has a link to Confirm subscription. The address will not receive the alert emails from this solution until that confirm link is opened.

Step 2. Create the IAM Policy and Role

In this step, you create an IAM policy and role that the Lambda function will assume. This provides Lambda the permissions needed to read the required information from Service Quotas and WorkSpaces. The policy also contains the basic Lambda permissions required for logging and to publish to SNS.

  1. Open the IAM console.
  2. In the navigation pane, choose Policies.
  3. Choose Create Policy.
  4. Choose the JSON tab.
  5. Copy and paste the following JSON policy.
  6. Replace the following values in the policy to match your environment:
    • Replace <aws-region> with the AWS Region code in which the solution is being built.
    • Replace <account-id> with the account ID number of the account in which the solution is being built.
    • Replace <sns-topic-arn> with the ARN from the SNS Topic created in the previous step.
  7. Once finished, choose Next: Tags.
  8. Add any required tags for your environment, then choose Next: Review.
  9. Enter a name of your choosing, for example, WorkSpaces_Service_Limit_Alerting_Policy.
  10. Choose Create policy.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "servicequotas:GetServiceQuota",
                "workspaces:DescribeWorkspaces"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "sns:Publish"
            ],
            "Resource": [
                "<sns-topic-arn>"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:<aws-region>:<account-id>:*",
            "Effect": "Allow"
        }
    ]
}
  1. In the navigation pane, choose Roles.
  2. Click Create role.
  3. For Select type of trusted entity, keep AWS service selected.
  4. Choose Lambda, and then click Next: Permissions.
  5. Browse for or search in the filter policies search box for the name of the policy created in the previous step. Select the check box next to the policy name.
  6. Choose Next: Tags.
  7. Add any required tags for your environment, then choose Next: Review.
  8. Enter a name for your Role to help you identify it, for example, WorkSpaces_Service_Limit_Alerting_Role.
  9. Choose Create role.

Step 3. Create the Lambda function

In this step, we create the Lambda function that is responsible for reading the Service Quota limits and currently deployed WorkSpace counts in each of the AWS Regions defined in a later section.

  1. Open the Lambda console.
  2. Choose Create function.
  3. Keep Author from scratch selected and enter a Function name, for example, WorkSpaces_Service_Limit_Alerting.
  4. Select Python 3.9 as the Runtime.
  5. Expand the Permissions section, select Use an existing role, and from the list choose the role created in step 2.
  6. Choose Create function.
  7. Within the Code source section, replace the placeholder code with the below Python script.
  8. Once replaced, click Deploy to save the changes.
  9. Choose Configuration.
  10. Choose General configuration, then choose Edit.
  11. Set the Timeout to 30 seconds.
  12. Choose Save.
import json
import boto3
import logging
import os
import textwrap
from botocore import config

LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)

CONFIG = config.Config(
  retries = {
      'max_attempts': 10,
      'mode': 'standard'
  })

SNS = boto3.client('sns')

def send_notification():
    #Publish image information to SNS Topic    
    try :
        SNS_RESPONSE = SNS.publish(
            TopicArn=ALERT_TOPIC_ARN,
            Message=MESSAGE,
            Subject=SUBJECT
        )
    except Exception as e:
        LOGGER.error(e)
    return()

def lambda_handler(event, context):

    #Notification email variables
    global SUBJECT, MESSAGE, ALERT_TOPIC_ARN     

    #Get alert threshold from event data and convert to percentage, or default to 80% if not found
    if 'AlertThreshold' in event :
        ALERT_THRESHOLD = float(event['AlertThreshold']) / 100
    else :
        ALERT_THRESHOLD = .80
    ALERT_THRESHOLD_PERCENT = ALERT_THRESHOLD * 100

    LOGGER.info(f"Alert threshold set to {ALERT_THRESHOLD_PERCENT}%")

    #Get list of regions from event data to check WorkSpaces Service Quotas in, or default to current region if not found
    if 'AlertRegions' in event :
        REGION_LIST = event['AlertRegions']
    else :
        REGION_LIST = [os.environ['AWS_REGION']]
        
    ALERT_TOPIC_ARN = event['AlertTopicArn']

    #Trackers for quota alarms
    WORKSPACE_ALARM_STATUS = False
    WORKSPACE_GLOBAL_COUNT = 0
    WORKSPACE_ALARM_LIST = []


    for ALERT_REGION in REGION_LIST :
        LOGGER.info(f"Begin WorkSpaces quota check in {ALERT_REGION}")
        SQ = boto3.client('service-quotas',region_name=ALERT_REGION)
        WS = boto3.client('workspaces',region_name=ALERT_REGION,config=CONFIG)
    
        #Retreive current quota limit for standard WorkSpaces
        RESPONSE = SQ.get_service_quota(
            ServiceCode='workspaces',
            QuotaCode='L-34278094'
        )
        STANDARD_WORKSPACE_QUOTA = int(RESPONSE['Quota']['Value'])
    
    
        #Retreive current quota limit for graphics g4dn WorkSpaces
        RESPONSE = SQ.get_service_quota(
            ServiceCode='workspaces',
            QuotaCode='L-BCACAEBC'
        )
        GRAPHICS_G4DN_WORKSPACE_QUOTA = int(RESPONSE['Quota']['Value'])
    
    
        #Retreive current quota limit for graphics pro WorkSpaces
        RESPONSE = SQ.get_service_quota(
            ServiceCode='workspaces',
            QuotaCode='L-254B485B'
        )
        GRAPHICSPRO_WORKSPACE_QUOTA = int(RESPONSE['Quota']['Value'])
        
        #Retreive current quota limit for graphicspro.g4dn WorkSpaces
        RESPONSE = SQ.get_service_quota(
            ServiceCode='workspaces',
            QuotaCode='L-BE9A8466'
        )
        GRAPHICSPRO_G4DN_WORKSPACE_QUOTA = int(RESPONSE['Quota']['Value'])        


        #Return all WorkSpaces in current region      
        WS_PAGINATOR = WS.get_paginator('describe_workspaces')
        WS_RESPONSE_ITERATOR = WS_PAGINATOR.paginate()


        #Counter for the number of WorkSpaces by compute type
        GRAPHICS_G4DN_COUNT = 0
        GRAPHICSPRO_COUNT = 0
        GRAPHICSPRO_G4DN_COUNT = 0
        STANDARD_COUNT = 0


        #WorkSpaces API returns pages of 25 WorkSpaces, loop through all pages and count all WorkSpaces by quota type
        for WS_PAGE in WS_RESPONSE_ITERATOR:
            for WORKSPACE in WS_PAGE.get('Workspaces'):
                if (WORKSPACE['WorkspaceProperties']['ComputeTypeName'] == 'GRAPHICS_G4DN'):
                    GRAPHICS_G4DN_COUNT += 1
                elif (WORKSPACE['WorkspaceProperties']['ComputeTypeName'] == 'GRAPHICSPRO'):
                    GRAPHICSPRO_COUNT += 1
                elif (WORKSPACE['WorkspaceProperties']['ComputeTypeName'] == 'GRAPHICSPRO_G4DN'):
                    GRAPHICSPRO_G4DN_COUNT += 1                    
                else:
                    STANDARD_COUNT += 1

  
        #Check each WorkSpace type count against alarm threshold    
        if (STANDARD_COUNT > 0):
            if (STANDARD_COUNT >= (ALERT_THRESHOLD * STANDARD_WORKSPACE_QUOTA)):
                LOGGER.info(f"ALERT: Approaching regular WorkSpace quota in {ALERT_REGION}")
                PERCENT = (STANDARD_COUNT / STANDARD_WORKSPACE_QUOTA) * 100
                WORKSPACE_ALARM_LIST.append([ALERT_REGION,"Standard",STANDARD_WORKSPACE_QUOTA,STANDARD_COUNT,PERCENT]) 
                WORKSPACE_ALARM_STATUS = True
    
        if (GRAPHICS_G4DN_COUNT > 0):
            if (GRAPHICS_G4DN_COUNT >= (ALERT_THRESHOLD * GRAPHICS_G4DN_WORKSPACE_QUOTA)):
                LOGGER.info(f"ALERT: Approaching graphics.g4dn WorkSpace quota in {ALERT_REGION}")
                PERCENT = (GRAPHICS_G4DN_COUNT / GRAPHICS_G4DN_WORKSPACE_QUOTA) * 100
                WORKSPACE_ALARM_LIST.append([ALERT_REGION,"Graphics.g4dnboto3",GRAPHICS_G4DN_WORKSPACE_QUOTA,GRAPHICS_G4DN_COUNT,PERCENT]) 
                WORKSPACE_ALARM_STATUS = True
          
        if (GRAPHICSPRO_COUNT > 0):
            if (GRAPHICSPRO_COUNT >= (ALERT_THRESHOLD * GRAPHICSPRO_WORKSPACE_QUOTA)):
                LOGGER.info(f"ALERT: Approaching graphics pro WorkSpace quota in {ALERT_REGION}")
                PERCENT = (GRAPHICSPRO_COUNT / GRAPHICSPRO_WORKSPACE_QUOTA) * 100
                WORKSPACE_ALARM_LIST.append([ALERT_REGION,"GraphicsPro",GRAPHICSPRO_WORKSPACE_QUOTA,GRAPHICSPRO_COUNT,PERCENT]) 
                WORKSPACE_ALARM_STATUS = True

        if (GRAPHICSPRO_G4DN_COUNT > 0):
            if (GRAPHICSPRO_G4DN_COUNT >= (ALERT_THRESHOLD * GRAPHICSPRO_G4DN_WORKSPACE_QUOTA)):
                LOGGER.info(f"ALERT: Approaching graphicspro.g4dn WorkSpace quota in {ALERT_REGION}")
                PERCENT = (GRAPHICSPRO_G4DN_COUNT / GRAPHICSPRO_G4DN_WORKSPACE_QUOTA) * 100
                WORKSPACE_ALARM_LIST.append([ALERT_REGION,"GraphicsPro.g4dn",GRAPHICSPRO_G4DN_WORKSPACE_QUOTA,GRAPHICSPRO_G4DN_COUNT,PERCENT]) 
                WORKSPACE_ALARM_STATUS = True

        WORKSPACE_COUNT = GRAPHICS_G4DN_COUNT + GRAPHICSPRO_COUNT + GRAPHICSPRO_G4DN_COUNT + STANDARD_COUNT

        #Tally of all WorkSpaces across all regions checked
        WORKSPACE_GLOBAL_COUNT = WORKSPACE_GLOBAL_COUNT + WORKSPACE_COUNT

        #Print data to retain in CloudWatch log of Lambda execution    
        print("########################################")
        print(f"#     Report for {ALERT_REGION}")
        print("########################################")
        print(f"Graphics.g4dn WorkSpace limit: {GRAPHICS_G4DN_WORKSPACE_QUOTA}")
        print(f"Graphics.g4dn WorkSpace count: {GRAPHICS_G4DN_COUNT}")
        print("------------------------------------")
        print(f"GraphicsPro.g4dn WorkSpace limit: {GRAPHICSPRO_G4DN_WORKSPACE_QUOTA}")
        print(f"GraphicsPro.g4dn WorkSpace count: {GRAPHICSPRO_G4DN_COUNT}")
        print("------------------------------------")        
        print(f"GraphicsPro WorkSpace limit: {GRAPHICSPRO_WORKSPACE_QUOTA}")
        print(f"GraphicsPro WorkSpace count: {GRAPHICSPRO_COUNT}")
        print("------------------------------------")
        print(f"WorkSpace limit: {STANDARD_WORKSPACE_QUOTA}")
        print(f"WorkSpace count: {STANDARD_COUNT}")
        print("------------------------------------")
        print(f"Total WorkSpaces in region: {WORKSPACE_COUNT}")
        print("########################################")
        
        LOGGER.info(f"End WorkSpaces quota check in {ALERT_REGION}")

    LOGGER.info(f"Total WorkSpaces across all regions: {WORKSPACE_GLOBAL_COUNT}")


    #Only generate and send SNS notification if there are any quotas breaching the alarm threshold
    if (WORKSPACE_ALARM_STATUS) :
        LOGGER.info("Due to alarm, Service Limit Notification published to SNS.")
        SUBJECT = "ALERT: Amazon WorkSpace Service Limits"
        
        MESSAGE = 'The below Amazon WorkSpaces Service Limits are beyond the configured threshold of {}% and are in an alarm state:\n\n'.format(ALERT_THRESHOLD_PERCENT)
        MESSAGE = MESSAGE + '{: <21} {: <20} {}/{}\n'.format('Region','Desktop Type','Count','Quota')     
        
        for ALARM in WORKSPACE_ALARM_LIST:
            REGION, TYPE, QUOTA, COUNT, PERCENT = ALARM
            MESSAGE = MESSAGE + '{: <21} {: <23} {}/{} ({}%)\n'.format(REGION,TYPE,COUNT,QUOTA,PERCENT)    
            
        send_notification()
    else :
        LOGGER.info("No service limits are in alarm state. No notification published.")

    return {
        'WorkSpaceAlarm': WORKSPACE_ALARM_STATUS,
    }


Step 4. Create the EventBridge rule

In this final step, you create an EventBridge rule to invoke the Lambda function created in the previous step. This rule defines the schedule for when the Lambda function will run and check for WorkSpaces service limit breaches. This rule also contains the parameters that define the service limit threshold, the AWS Regions to monitor WorkSpaces limits, and the SNS top.

  1. Open the EventBridge console.
  2. Choose Create rule.
  3. Enter a rule Name, for example WorkSpaces_Service_Limit_Alerting_Trigger.
  4. Under Define pattern select Schedule.
  5. Define either a Fixed rate or a Cron expression to match the frequency you desire for the service limit checks.
  6. Under Select targets keep the Target set to Lambda function.
  7. In the Function dropdown, select the Lambda function created in step 3.
  8. Expand Configure input, then select Constant (JSON text).
  9. Copy and paste the following JSON statement into the box.
  10. Replace the following values in the statement to match your environment:
      • Replace <alert-threshold> a number between 0 and 100. This value determines at what percentage of your service limit will an alert notification be sent, for example 80.
      • Replace <alert-regions> with a comma separated list of which AWS Regions to check WorkSpace limits, for example us-east-1, us-west-2.
      • Replace <sns-topic-arn> with the ARN from the SNS Topic created in step 1.

    { “AlertThreshold”: <alert-threshold>, “AlertRegions”: [ “<alert-regions>” ], “AlertTopicArn”: “<sns-topic-arn>“}

  11. Choose Create.

Clean up

In this blog post, you created several components that may generate costs based on usage. To avoid incurring future charges, remove the following resources.

  1. If the CloudFormation template was used:
    1. Navigate to the CloudFormation console.
    2. Select the stack created above.
    3. Choose Delete. This will automatically delete the other resources used in the solution.
  2. To prevent future executions of the solution:
    1. Navigate to the EventBridge console.
    2. Click Rules.
    3. Select the rule create above and click Delete. This will prevent future executions of the solution.
  3. Lambda functions do not incur a charge unless invoked. To remove this component completely:
    1. Open the Lambda console.
    2. Select your function.
    3. Choose Actions, then Delete.
  4. SNS charges for the number of messages sent. To remove the topic created as part of this solution:
    1. Open the SNS console.
    2. Click Topics.
    3. Select your topic, then Delete.
  5. There are no charges for IAM Policies and Roles. To remove the IAM entities created in this article:
    1. Navigate to the IAM console.
    2. Click Roles.
    3. Select the role created above, then Delete.
    4. Click Policies.
    5. Select the policy for this article, then choose Actions, then Delete.

Conclusion

You now have solution to proactively monitor your WorkSpaces usage and alert you as your approach your service limits. At a scheduled interval, an EventBridge rule triggers a Lambda function to gather the WorkSpaces Service Limits and currently deployed WorkSpaces counts, and automatically send an alert email via an Amazon SNS subscription to your administrators.

If changes are needed, you can go to the following locations to update or change different aspects of the solution:

  • Change alerting time or frequency:
    • In the EventBridge console, navigate to the Rules section, and edit the rule created in step 4.
    • Define a new CRON expression or fixed rate.
  • Modify or add additional email addresses for the notification:
    • In the SNS console, navigate to the Topics section, then choose the topic created in step 1.
    • Under Subscriptions, choose Create subscription, the in the dropdown Protocol select Email.
    • Enter the new email address for Endpoint.
  • Change or add the monitored AWS Regions or switch the alert threshold:
    • In the EventBridge console, navigate to the Rules section, and edit the rule created in step 4.
    • Under the Select targets section, expand Configure input.
    • Change the JSON values for AlertThreshold or AlertRegions as required.
Justin Grego is a Senior End User Computing Specialist Solutions Architect. As part of the EUC Service Aligned SA Team, he helps enable both customers and fellow SAs get up to speed on and be successful with new AWS EUC features and services.