背景
为推进远程办公,我们向员工分发了 Amazon WorkSpaces 并配备了 iPad,使他们可以随时随地工作。员工在远程办公时,会通过 iPad 连接 Amazon WorkSpaces 进行操作。
然而,在 iPad 上使用 Amazon WorkSpaces 时,出现了箭头键无法正常工作的情况。尽管这一问题尚未解决,我们暂时使用 AWS Client VPN 的替代架构来进行运维。
有关此架构的详细信息,请参考以下文章:
目前,子网始终与 AWS Client VPN 端点关联,因此始终会产生费用。
由于从 iPad 以外的设备连接时并不需要 AWS Client VPN,我们需要一种方法,通过仅在必要时进行关联来降低成本。
方法:实现子网关联
我们决定,当需要使用 AWS Client VPN 时,让员工自行进行子网关联。
但通过管理控制台或 AWS CLI 命令进行关联,对于部分员工来说技术上有一定难度。
因此,我们开发了一种系统,让员工可以通过浏览器轻松完成关联。
具体步骤如下:
员工通过浏览器访问 ALB。ALB 的目标是一个 Lambda 函数,该函数执行 AWS Client VPN 端点的子网关联。
为了确保只有员工可以进行操作,我们为 ALB 配置了 Amazon Cognito 身份验证。
这种方法无需复杂的操作流程,只需向员工分发域名即可。
配置如下所示:
事实上,这种配置已经在另一篇文章中介绍过。有关详细说明,请参考以下文章:
方法:子网解除关联
为了实现子网的解除关联,我们开发了一种自动化机制。
具体来说,通过 CloudWatch 警报监控 方法:子网关联 中执行的 Lambda 函数。
如果在警报触发后的一段时间内未检测到任何客户端连接到 Client VPN 端点,则会触发另一个 Lambda 函数以解除子网关联。
尽管这不是复杂的架构,其示意图如下:
管理员步骤
在此部分中,我们假设已经创建了用于子网关联的 Lambda 函数以及 Client VPN 端点。
创建 Lambda 函数
创建一个用于解除 Client VPN 端点子网关联的 Lambda 函数。
- 执行以下 AWS CLI 命令以为 Lambda 函数创建 IAM 角色:
# Create IAM role
aws iam create-role \
--role-name LambdaRole-ClientVPN \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}'
# Attach the IAM policy required for Client VPN
aws iam attach-role-policy \
--role-name LambdaRole-ClientVPN \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess
# Attach the IAM policy required for logging
aws iam attach-role-policy \
--role-name LambdaRole-ClientVPN \
--policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
- 使用以下 URL 创建 Lambda 函数:
https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/create/function - 按以下设置配置函数:
- 函数名称:
AWSClientVPN-DisassociatingSubnets
- 运行时:Python 3.13
- 执行角色:
LambdaRole-ClientVPN
- 函数名称:
图片
- 使用以下 URL 将超时时间更改为 1 分钟:
https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions/AWSClientVPN-DisassociatingSubnets/edit/basic-settings?tab=configure- 超时:1 分钟
图片
- 为 Lambda 函数添加代码。
输入代码后,需要单击 Deploy 或使用快捷键 Ctrl+Shift+U 部署。
大部分代码是使用 ChatGPT 生成的,代码如下:
import boto3
import os
import json
def lambda_handler(event, context):
# Initialize the EC2 client
ec2 = boto3.client('ec2')
# Input parameters
client_vpn_endpoint_id = '<Your Client VPN endpoint ID>'
try:
# Describe the Client VPN target network associations
associations_response = ec2.describe_client_vpn_target_networks(
ClientVpnEndpointId=client_vpn_endpoint_id
)
# Collect all Association IDs
association_ids = [association['AssociationId'] for association in associations_response['ClientVpnTargetNetworks']]
if not association_ids:
return {
'statusCode': 404,
'body': json.dumps({
'Message': 'No associations found for the given Client VPN endpoint.',
'ClientVpnEndpointId': client_vpn_endpoint_id
})
}
# Loop through and disassociate each target network
results = []
for association_id in association_ids:
try:
response = ec2.disassociate_client_vpn_target_network(
ClientVpnEndpointId=client_vpn_endpoint_id,
AssociationId=association_id
)
results.append({
'AssociationId': association_id,
'Status': response['Status']
})
except Exception as e:
results.append({
'AssociationId': association_id,
'Error': str(e)
})
# Return response details
return {
'statusCode': 200,
'body': json.dumps({
'Message': 'Processed all associations for disassociation.',
'Results': results
})
}
except Exception as e:
print(f"Error disassociating Client VPN endpoint: {str(e)}")
return {
'statusCode': 500,
'body': json.dumps({
'Message': 'Failed to process disassociations for Client VPN endpoint.',
'Error': str(e)
})
}
创建 CloudWatch 警报
- 可以通过以下 URL 设置包含必要指标的 CloudWatch 警报。使用此 URL 可创建触发 Lambda 函数的 CloudWatch 警报:
https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#alarmsV2:create?~(Page~'Preview~AlarmType~'MetricAlarm~AlarmData~(Metrics~(~(Id~'e1~Expression~'m1*2bm2~ReturnData~true~Label~'UsingClientVPN)~(MetricStat~(Metric~(MetricName~'Invocations~Namespace~'AWS*2fLambda~Dimensions~(~(Name~'FunctionName~Value~'AWSClientVPN-AssociatingSubnets)))~Period~300~Stat~'Average)~Id~'m1~ReturnData~false)~(MetricStat~(Metric~(MetricName~'ActiveConnectionsCount~Namespace~'AWS*2fClientVPN~Dimensions~(~(Name~'Endpoint~Value~'<Your Client VPN endpoint ID>)))~Period~300~Stat~'Average)~Id~'m2~ReturnData~false))~AlarmName~'AWSClientVPN-DisassociatingSubnets~AlarmDescription~'~ActionsEnabled~true~ComparisonOperator~'LessThanThreshold~Threshold~1~DatapointsToAlarm~6~EvaluationPeriods~6~TreatMissingData~'missing~AlarmActions~(~'arn*3aaws*3alambda*3aus-east-1*3a<Your AWS account ID>*3afunction*3aAWSClientVPN-DisassociatingSubnets)~InsufficientDataActions~(~)~OKActions~(~))~AlarmRecommendation~false~MultiTimeSeries~false)- 请将 URL 中的占位符替换为您的区域和资源 ID:
<Your Client VPN endpoint ID>
→ 您的 Client VPN 端点 ID<Your AWS account ID>
→ 您的 AWS 账户 ID
- 描述: 此 CloudWatch 警报在关联 Lambda 函数触发后,如果 Client VPN 端点的活动连接数在约 30 分钟内保持为 0,则触发解除关联 Lambda 函数。
- 监控指标: 关联 Lambda 函数的执行总数以及与客户端 VPN 终端节点的连接数
- 警报条件: 如果监控指标在 30 分钟内的六个数据点中小于 1,则警报触发。
- 警报来源: 还可以通过以下 AWS CLI 命令创建警报:
- 请将 URL 中的占位符替换为您的区域和资源 ID:
AWS CLI Command
ACCOUNT_ID="<Your AWS account ID>"
aws cloudwatch put-metric-alarm \
--alarm-name 'AWSClientVPN-DisassociatingSubnets' \
--actions-enabled \
--alarm-actions 'arn:aws:lambda:us-east-1:$ACCOUNT_ID:function:AWSClientVPN-DisassociatingSubnets' \
--evaluation-periods 6 \
--datapoints-to-alarm 6 \
--threshold 1 \
--comparison-operator 'LessThanThreshold' \
--treat-missing-data 'missing' \
--metrics '[{"Id":"e1","Label":"UsingClientVPN","ReturnData":true,"Expression":"m1+m2"},{"Id":"m1","ReturnData":false,"MetricStat":{"Metric":{"Namespace":"AWS/ClientVPN","MetricName":"ActiveConnectionsCount","Dimensions":[{"Name":"Endpoint","Value":"cvpn-endpoint-01234example56789"}]},"Period":300,"Stat":"Average"}},{"Id":"m2","ReturnData":false,"MetricStat":{"Metric":{"Namespace":"AWS/Lambda","MetricName":"Invocations","Dimensions":[{"Name":"FunctionName","Value":"AWSClientVPN-AssociatingSubnets"}]},"Period":300,"Stat":"Average"}}]'
图片
向 Lambda 函数添加基于资源的策略
- 执行以下 AWS CLI 命令,为 Lambda 函数添加一个基于资源的策略,允许它由 CloudWatch 警报触发。
LAMBDA_FUNCTION_NAME="AWSClientVPN-DisassociatingSubnets"
# Get the ARN of CloudWatch alarm
CLOUDWATCH_ALARM_ARN=$(aws cloudwatch describe-alarms \
--alarm-names "AWSClientVPN-DisassociatingSubnets" \
--query "MetricAlarms[0].AlarmArn" \
--output text)
# Add permission for the CloudWatch alarm to invoke the Lambda function
aws lambda add-permission \
--function-name $LAMBDA_FUNCTION_NAME \
--statement-id "AWS-CW_Invoke-alarm" \
--action lambda:InvokeFunction \
--principal lambda.alarms.cloudwatch.amazonaws.com \
--source-arn $CLOUDWATCH_ALARM_ARN
通过此配置,当在一定时间内没有连接时,Lambda 函数将被触发,自动解除子网与 AWS Client VPN 端点的关联。
通过结合 方法:子网关联 和 方法:子网解除关联,可以将子网关联的操作最小化,从而进一步优化成本。
希望此示例对有类似用例的人有所帮助。