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。
本篇作者