亚马逊AWS官方博客

通过预热 Amazon WorkSpaces 提升用户操作体验

1. 前言

Amazon WorkSpaces 提供了灵活的付费方式,使得用户可以按月或按小时付费。按月计费适合需要全天使用 Amazon WorkSpaces 或将其用作主要桌面的工作人员。对于并非需要长时间运行的工作场景,比如兼职工作人员、临时性工作分担、频繁出差的人员、短期项目、在线培训和教育等,使用按小时付费是一种能够很好节约成本的方式。Amazon WorkSpaces 计费模式能够非常灵活的进行选择和切换,只需要通过配置相应实例的运行模式(Running Mode)即可。Amazon WorkSpaces 提供两种运行模式以适配不同的计价模式:

  • AlwaysOn — 按月计费,WorkSpace 将始终处于运行状态,用户可随时访问。
  • AutoStop — 按小时计费,WorkSpace 将在指定的不活动时间(小时,最少为 1 最大为 48) 之后自动停止,并在用户下次登录时恢复。

Amazon WorkSpaces 在自动停止后,当用户重新连接到已停止的 WorkSpace 时,它会恢复到其上次停止时的状态,这个恢复过程通常在 90 秒内。对于大多数用户来说,WorkSpace 客户端等待 90 秒时间才能进入是难以接受的,因此我们可以考虑通过“预热”方式提前为用户启动恢复过程,让等待时间变得更短或直接能“立即访问”,这样将会大大提高用户的操作体验。

本文按照用户的操作需要将“预热”操作分为两种模式,即按需预热和定时预热。以下将分别介绍两种预热模式的实现方式。

2. 按需预热

2.1 架构说明

按需预热模式,即终端用户对于WorkSpaces可访问时间有比较明确的预期,可以通过自助提交请求来实现预热,适用于如出差、定时会议或启动培训计划等场景。

按需预热的本质是接收到用户请求后,生成一个定时任务,此任务可在用户请求的时间点之前完成WorkSpace的恢复启动操作。定时任务通常可以使用CloudWatch来实现,但默认情况下CloudWatch 规则上限为 100 条,虽然该上限限制可以通过提交后台申请来进行调整,但为所有请求各自创建一个 CloudWatch定时规则来执行业务逻辑并不是一个非常合适的方式。因此,我们可以通过使用一个固定频率执行的CloudWatch规则执行 Lambda来读取持久化保存的按需请求数据来触发预热操作。

本文使用DynamoDB保存按需预热请求,整体架构如下:

 

2.2 配置 IAM 及 DynamoDB

2.2.1 DynamoDB 配置

在 DynamoDB 里中创建一个表为Jobs的表,用于保存按需预热请求。Jobs 表使用id 作为主键,并使用on_at字段作为二级索引。DynamoDB 将保存来自用户的按需预热请求,完整字段定义如下:

字段名 用途
id 任务唯一编号(自动生成),主键
username WorkSpaces 用户名
spaceid WorkSpaces ID(可选)
on_at 计划使用时间,DynamoDB 将以 ISO 格式保存,形如2020-03-03T12:12:12

 

 

2.2.2 IAM 策略配置

在 IAM 中为按需预热 Lambda 函数配置相应角色 OnDemandRequestRole,角色对应的策略定义如下:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "state1",
            "Effect": "Allow",
            "Action": [
                "dynamodb:DeleteItem",
                "dynamodb:GetItem",
                "dynamodb:PutItem",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Resource": "arn:<partition>:dynamodb:<region>:<account-id>:table/Jobs"
        },
        {
            "Sid": "state2",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:<partition>:logs:<region>:<account-id>:log-group:/aws/lambda/OnDemandRequest:*"
        },
        {
            "Sid": "state3",
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:<partition>:logs:<region>:<account-id>:*"
        }
    ]
}

 

 

2.3 创建按需请求 Lambda

接收请求并创建定时任务的 Lambda 代码(OnDemandRequest)如下:

from datetime import datetime

import json
import uuid

import boto3


def lambda_handler(event, context):
    """
    Lambda function export to API Gateway to recieve a json formatted post job.
    JSON parameters description:
        'username': The username of WorkSpace. Required.
        'spaceid': The WorkSpace ID request to warm-up. 
                   Optional. If not assign, will warm up all WorkSpace of the user.
        'on_at': The will to be used time of the WorkSpace. Required.

        For example:
        {
            "username": "xxxxx",
            "spaceid": "ws-1234abcd",
            "on_at": "2020-03-03 12:12:12"
        }
    """
    print('Recieved on demand warm-up request - [%s].' % json.dumps(event))

    jobID = str(uuid.uuid1())
    on_at = datetime.strptime(event['on_at'], '%Y-%m-%d %H:%M:%S').isoformat()

    try:
        dynamodb = boto3.resource('dynamodb', region_name='cn-northwest-1')
        table = dynamodb.Table('Jobs')

        table.put_item(
            Item={
                'id': jobID,
                'username': event['username'],
                'spaceid': event.get('spaceid', '-'),
                'on_at': on_at
            }
        )

        print('On demand warm-up request has created with jobID [%s].' % jobID)

        return {
            'statusCode': 200,
            'body': {
                'id': jobID
            }
        }
    except Exception as ex:
        return {
            'statusCode': 500,
            'body': {
                'error': ex
            }
        }

在 Lambda 控制台中使用 OnDemandRequestRole 角色将 OnDemandRequest 部署为基于 Python3.7 的 Lambda 函数。

 

2.4 配置 API Gateway

配置 API Gateway,新建 REST API 以用于接收 HTTP Post 请求并调用 OnDemandRequest Lambda。

 

创建 POST操作方法调用之前部署的OnDemandRequest Lambda函数

 

注意:若使用宁夏或北京区域的 API Gateway,需要提供 ISP 备案或配置方法请求的身份验证为 AWS_IAM。

 

部署 API,并记录相应 Endpoint。

 

 

2.5 配置 CloudWatch 定时任务

配置 CloudWatch,创建固定频率规则用于执行定时任务检查 DynamoDB 是否有需要执行的任务。

 

 

2.6 使用 awscurl 测试

现在我们可以通过 Endpoint 提交按需预热请求。使用基于 AWS_IAM 方式进行授权的 Endpoint,需要在对调用请求进行预签名。我们可以使用 awscurl 工具来执行预签名的URL 的调用。

awscurl 工具可以通过以下命令来安装和使用。

pip install awscurl
export AWS_ACCESS_KEY_ID="<your-account-access-key-id>"
export AWS_SECRET_ACCESS_KEY="<your-account-secret-access-key>"
awscurl --region <your-api-region> "<your-api-endpoint>" -X POST -d "{\"username\": \"<workspace-username>\", \"spaceid\": \"<workspace-id> \", \"on_at\": \"<request-on-datetime, format as: 2020-03-03 12:12:12>\"}"


 

按需操作接收的 Post 格式如下:

{
  "username": "xxxxx",
  "spaceid": "ws-1234abcd",
  "on_at": "2020-03-03 12:12:12"
}


打开 WorkSpaces 控制台,可以看到该用户所对应的 WorkSpaces 在指定时间前 180 秒时已将状态从 STOPPED 切换为STARTING。

 

3. 定时预热

3.1 架构说明

定时预热相对与按需预热的实现相对简单,可以通过配置CloudWatch 规则执行定时Lambda任务来启动 WorkSpaces 实例,本文中的定时任务未对待启动的Workspaces进行过滤,实际使用过程中可以通过额外的配置(如 S3文件、RDS、DynamoDB等持久化服务)来自定义具体任务执行的条件。

 

定时预热整体架构如下:

3.2 配置 IAM

在 IAM 中为定时预热 Lambda 函数配置相应角色 OnScheduleScannerRole,角色对应的策略定义如下:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "state1",
            "Effect": "Allow",
            "Action": [
                "workspaces:StartWorkspaces",
                "workspaces:DescribeWorkspaces"
            ],
            "Resource": "arn:<aws-partitionname>:workspaces:<region>:<account-id>:workspace/*"
        },
        {
            "Sid": "state2",
            "Effect": "Allow",
            "Action": [
                "workspaces:DescribeWorkspaceDirectories"
            ],
            "Resource": " arn:<aws-partitionname>:workspaces:<region>:<account-id>:*"
        }
    ]
}

 

 

3.3 创建定时预热 Lambda

定时预热 Lambda 代码(OnScheduleScanner) 如下:

from datetime import datetime

import boto3


def lambda_handler(event, context):
    """
    Lambda function used with CloudWatch Rule Event to execute scheduled WorkSpaces warm-up job.
    Can pass in a DirectoryId as parameter, for example:
        event['directory'] = 'd-123456'
    if no DirectoryId specified, the job will scan all available Directoies and warm-up WorkSpaces.
    """

    print('Start scheduled warm-up at %s.' % datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    directory = event.get('directory', None)

    try:
        directories = []
        workspaces = []

        client = boto3.client('workspaces')

        check = {} if directory is None else {'DirectoryIds': [directory]}

        resp = client.describe_workspace_directories(**check)

        while resp:
            directories += resp['Directories']

            resp = client.describe_workspace_directories(
                **check,
                NextToken=resp['NextToken']
            ) if 'NextToken' in resp else None
        
        for dir in directories:
            dir_id = dir['DirectoryId']
            resp = client.describe_workspaces(DirectoryId=dir_id)

            while resp:
                workspaces += resp['Workspaces']

                resp = client.describe_workspaces(
                    DirectoryId=dir_id, NextToken=resp['NextToken']
                ) if 'NextToken' in resp else None
        
        for workspace in workspaces:
            if workspace['State'] == 'STOPPED':
                client.start_workspaces(
                    StartWorkspaceRequests=[{
                        'WorkspaceId': workspace['WorkspaceId']
                    }]
                )

                print('Starting WorkSpace for id - [%s].' % workspace['WorkspaceId'])
    except Exception as ex:
        print('Error to run scheduled warm-up job - [%s].' % ex)

在 Lambda 控制台中使用 OnScheduleScannerRole 角色将 OnScheduleScanner 部署为基于 Python3.7 的 Lambda 函数。


在 Lambda 控制台中使用 OnScheduleScannerRole 角色将 OnScheduleScanner 部署为基于 Python3.7 的 Lambda 函数。

 

3.4 配置 CloudWatch 定时规则

在 CloudWatch中配置一个定时任务并选择OnScheduleScanner为目标。

 

打开 WorkSpaces控制台,可以看到在定时任务指定的时间点之后,所有状态为STOPPED的实例状态已改变。

 

 

附录

本文中代码及 IAM 策略配置参数如下:

  • <partition>:资源所处分区。对于标准 AWS 区域,分区是 aws。如果资源位于其他分区,则分区是 aws-partitionname。例如,位于 中国(北京) 区域的资源的分区为 aws-cn。
  • <region>:区域标识。如cn-northwest-1。
  • <account-id>:资源的 AWS 账户 ID(不含连字符)。如123456789012。

 

本篇作者

陈昊

AWS 合作伙伴解决方案架构师,有将近 20 年的 IT 从业经验,在企业应用开发、架构设计及建设方面具有丰富的实践经验。目前主要负责 AWS (中国)合作伙伴的方案架构咨询和设计工作,致力于AWS 云服务在国内的应用推广以及帮助合作伙伴构建更高效的AWS云服务解决方案。