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。
打开 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 函数。
在 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。
 
 
       本篇作者