需求背景
企业中用于做报表的数据库服务器只是在每天的某个时间点需要大规模的磁盘 IO 操作,持续时间大约几个小时,如果按照高峰时期的磁盘 IO 需求来设定 EBS 卷的类型,需要 IOPS 为 10000 的 io1 卷。而按照平时大多数时候的磁盘 IO 水平来看,gp2 类型的磁盘已经可以满足要求。为了能够做到物尽其用,节省成本,在 IO 高峰时候使用 io1 卷,而平时使用 gp2 卷会是一个理想的安排。
方案概述
为了能够简化实际操作过程的复杂度,方案采用通过定义 EBS 卷的 Tag 来触发对应的 Lambda 功能从而实现定期更改 EBS 卷的类型。
首先,用户需要启动 CloudTrail, 并将 CloudTrail 与 CloudWatch 集成,本文假设 CloudWatch Log Group 的名字是 CloudTrail/DefaultLogGroup。
我们将通过 CloudTrail 来捕捉用户对 EBS 卷的操作,如果用户创建、更改或者删除名字为 ChangeEBSType 的 Tag,就会触发一个 Lambda 功能调用。判断触发条件是通过定义 Log Group 的 Filter 实现的,Filter 的定义如下:
"SubscriptionFilter": {
"Type": "AWS::Logs::SubscriptionFilter",
"Properties": {
"LogGroupName": {
"Ref": "CloudTrailLogGroup"
},
"FilterPattern": "{($.requestParameters.tagSet.items[0].key = \"ChangeEBSType\") && ($.eventName = *Tags) && ($.requestParameters.tagSet.items[0].value!= \"\" ) && ($.requestParameters.resourcesSet.items[0].resourceId = \"vol*\")}",
"DestinationArn": {
"Fn::GetAtt": ["EBSChangeScheduler", "Arn"]
}
}
}
复制代码
当 Filter 的条件满足后,就会触发名字为 EBSChangeScheduler 的 Lambda 功能,这个 Lambda 程序将根据 Tag 的输入值,调用另一个 CloudFormation 模板以部署对应的 CloudWatch Event Rules(规定什么时间对 EBS 卷的类型进行修改)和 Lambda 功能(change-ebs-type 完成 EBS 卷的类型修改)。
整个方案的流程如下:
调用现有的 CloudFormation 模板并创建对应 Stack 的 Lambda(EBSChangeScheduler.py)功能的示例代码如下:
import json
import zlib
import boto3
import botocore
import base64
import string
TemplateURL = ""
def get_cloudtrail_event(event):
data = base64.b64decode(event['awslogs']['data'])
data = zlib.decompress(data, 16 + zlib.MAX_WBITS)
cloudtrail_event = json.loads(data)
return cloudtrail_event
def get_message_from_cloudtrail_event(log_event):
old_str = '\\"'
new_str = '"'
message = log_event['message']
message = message.replace(old_str, new_str)
return json.loads(message)
def create_cloudformation(stack_name, parameter1, parameter2, volume_id, client):
print ("Create cloudformation stack: %s" % stack_name)
try:
response = client.create_stack(StackName=stack_name, TemplateURL=TemplateURL, Parameters=[
{'ParameterKey': 'TargetEBSVolumeInfo', 'ParameterValue': parameter1}, {'ParameterKey': 'ScheduleExpression', 'ParameterValue': parameter2}, ], Capabilities=['CAPABILITY_IAM'])
except Exception as ex:
print ex.message
def update_cloudformation(stack_name, parameter1, parameter2, volume_id, client):
print ("Update cloudformation stack: %s" % stack_name)
try:
response = client.update_stack(StackName=stack_name, UsePreviousTemplate=True, Parameters=[
{'ParameterKey': 'TargetEBSVolumeInfo', 'ParameterValue': parameter1}, {'ParameterKey': 'ScheduleExpression', 'ParameterValue': parameter2}, ], Capabilities=['CAPABILITY_IAM'])
except botocore.exceptions.ClientError as ex:
error_message = ex.response['Error']['Message']
if error_message == 'No updates are to be performed.':
print("No changes")
else:
raise
def check_valid_stack(stack_name, client):
try:
response = client.describe_stacks()
except Exception as ex:
print ex.message
for stack in response['Stacks']:
if stack_name in stack['StackName']:
return True
def build_ebs_volume_change_schedule(stack_name, target_schedule, volume_id, client):
target_type = target_schedule.split(':')
parameter1 = volume_id + ":" + target_type[0] + ":" + target_type[1]
parameter2 = "cron" + target_type[2]
print ("CloudForamtion template parameters:{},{}".format(
parameter1, parameter2))
print ("Volume %s will be changed to %s, IOPS is %s" %
(volume_id, target_type[0], target_type[1]))
print ("This task will be executed based on %s" % target_type[2])
if check_valid_stack(stack_name, client):
try:
cloudformation = boto3.resource('cloudformation')
try:
stack = cloudformation.Stack(stack_name)
except Exception as ex:
print ex.message
stack_status = stack.stack_status
print ("Stack (%s) status: %s" % (stack_name, stack_status))
if stack_status == "ROLLBACK_COMPLETE" or stack_status == "ROLLBACK_FAILED" or stack_status == "DELETE_FAILED":
try:
response = client.delete_stack(StackName=stack_name)
waiter = client.get_waiter('stack_delete_complete')
waiter.wait(StackName=stack_name)
except Exception as ex:
print ex.message
if stack_status == "CREATE_IN_PROGRESS":
waiter = client.get_waiter('stack_create_complete')
waiter.wait(StackName=stack_name)
if stack_status == "DELETE_IN_PROGRESS":
waiter = client.get_waiter('stack_delete_complete')
waiter.wait(StackName=stack_name)
if stack_status == "UPDATE_IN_PROGRESS":
waiter = client.get_waiter('stack_update_complete')
waiter.wait(StackName=stack_name)
except Exception as ex:
print ex.message
if check_valid_stack(stack_name, client):
update_cloudformation(stack_name, parameter1, parameter2,
volume_id, client)
waiter = client.get_waiter('stack_update_complete')
else:
create_cloudformation(stack_name, parameter1, parameter2,
volume_id, client)
waiter = client.get_waiter('stack_create_complete')
def delete_ebs_volume_change_schedule(volume_id, client):
response = client.describe_stacks()
for stack in response['Stacks']:
if volume_id in stack['StackName']:
try:
print("Delete cloudformation stack: %s" %
stack['StackName'])
response = client.delete_stack(
StackName=stack['StackName'])
except Exception as ex:
print ex.message
def lambda_handler(event, context):
volume_id = []
global TemplateURL
export = {}
client = boto3.client('cloudformation')
print (event)
export = client.list_exports()
for item in export['Exports']:
if item['Name'] == 'CFUrl':
TemplateURL = item['Value']
print ("CF URL: %s" % TemplateURL)
cloudtrail_event = get_cloudtrail_event(event)
for log_event in cloudtrail_event['logEvents']:
trail_message = get_message_from_cloudtrail_event(log_event)
volume_id = trail_message['requestParameters']['resourcesSet']['items'][0]['resourceId']
if trail_message['eventName'] == "CreateTags":
for item in trail_message['requestParameters']['tagSet']['items']:
if item['key'] == 'ChangeEBSType':
cf_parameter = item['value']
break
if trail_message['eventName'] == "CreateTags":
start_stop = cf_parameter.split(',')
i = 0
for schedule in start_stop:
stack_name = "change-ebs-type-" + str(i) + "-" + volume_id
build_ebs_volume_change_schedule(
stack_name, schedule, volume_id, client)
i = i + 1
if trail_message['eventName'] == "DeleteTags":
delete_ebs_volume_change_schedule(volume_id, client)
复制代码
特殊处理: 由于中国区的 Lambda 尚不支持环境变量,修改 EBS 卷的 CloudFormation 模板 URL 无法传给 Python 程序,所以利用了 CloudFormation 的 Export 功能,通过将 URL 变成 Export 的变量,在 Python 里面读取这个变量完成参数传递。 CloudFormation: “Outputs”: { “CFPath”: { “Description”: “The URL of CF”, “Value”: { “Ref”: “CFUrl” }, “Export”: { “Name”: “CFUrl” } } }
对应的 Python 语句:
global TemplateURL
export = {}
client = boto3.client('cloudformation')
export = client.list_exports()
for item in export['Exports']:
if item['Name'] == 'CFUrl':
TemplateURL = item['Value']
print ("CF URL: %s" % TemplateURL)
复制代码
EBS 卷的 Tag (ChangeEBSType)的格式有如下约定:
卷类型:IOPS 值:变更起始计划, 卷类型:IOPS 值:变更起始计划
例如如下设置:
io1:30000:(0 11 * * ? *),gp2:100:(0 19 * * ? *)
可以解释为:每天 11 点(UTC 时间)将当前的卷变更为 IOPS 为 30000 的 io1 类型,同日 19 点(UTC 时间)将当前卷恢复成 gp2 类型。(注意 gp2 类型的 EBS 卷忽略 IOPS 的值,所有 IOPS 值可以随意写,但不能为空值)
因为 EBS 卷的变更最小间隔时间为 6 小时,所以要确保 6 个小时内仅有一次磁盘类型的变更。
CloudWatch Event Rule可以通过如下CloudFormation的JSON语句创建:
"MyEventsRule": {
"Type": "AWS::Events::Rule",
"Properties": {
"Description": "Events Rule Invoke Lambda",
"Name": {
"Fn::Sub": "${AWS::StackName}-ChangeEBSEvent"
},
"ScheduleExpression": {
"Ref": "ScheduleExpression"
},
"State": "ENABLED",
"Targets": [{
"Arn": {
"Fn::GetAtt": [
"ModifyEbs",
"Arn"
]
},
"Id": "ModifyEbs"
}]
}
}
复制代码
安装和运行
在浏览器中输入:https://github.com/shaneliuyx/ChangeEBS, 下载:
ebs_change_scheduler_v2.json
ebs_change_scheduler_v2.zip
change_ebs_type.json
2.将 json 上载到 S3 (上载 URL 假设为https://s3.cn-north-1.amazonaws.com.cn/shane/change_ebs_type.json)
3.打开 AWS 控制台,并选择 CloudFormation,选择存储在本地的 json 文件 json,运行 CloudFormation 模板。
4.假设输入参数如下:
选择 Next,直至 AWS 资源开始创建
最终运行结果如下
7.选择 Volume ID 为 vol-0e3625c0f2f14e30d 的 EBS 卷,创建新的 tag,名称:ChangeEBSType,值:io1:30000:(0 12 * * ? *),gp2:100:(0 19 * * ? *)
8.我们可以在 CloudTrail 上查到如下记录:
9.几分钟后,我们可以在 CloudFormation 的控制台上看到 2 个新的 Stack 已经建立完成了:
10.同时检查 CloudWatch 控制台,选择 Events-Rules,发现建立了 2 个新的 Rules:
至此,我们就已经设置好了一个针对 EBS 卷的类型调度计划,此计划规定在 1 天中该 EBS 卷使用 io1 类型运行 7 个小时,使用 gp2 类型运行 17 个小时。在满足了服务器性能需求的同时,每天节省了 17 个小时的 io1 卷使用费用。
需要注意的是,由于磁盘类型转换的时间与磁盘的容量相关,在指定调度计划的时候一定要预估磁盘转换完成需要预留的时间,以免影响正常系统的使用效率。
作者介绍:
刘育新
AWS 专业服务部资深顾问,专注于企业客户的云迁移项目,长期从事 IT 基础设施的设计和实施工作。
本文转载自 AWS 技术博客。
原文链接:
https://amazonaws-china.com/cn/blogs/china/based-tag-drive-ebs-cloudformation-model/
评论