时隔16年Jeff Barr重返10.23-25 QCon上海站,带你看透AI如何重塑软件开发! 了解详情
写点什么

基于 Tag 驱动的 EBS 类型优化 CloudFormation 模板

  • 2019-10-12
  • 本文字数:5219 字

    阅读完需:约 17 分钟

基于 Tag 驱动的 EBS 类型优化 CloudFormation 模板

需求背景

企业中用于做报表的数据库服务器只是在每天的某个时间点需要大规模的磁盘 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"
}]
}
}
复制代码

安装和运行

  1. 在浏览器中输入: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.假设输入参数如下:


  1. 选择 Next,直至 AWS 资源开始创建

  2. 最终运行结果如下



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/


2019-10-12 13:29716
用户头像

发布了 1933 篇内容, 共 160.9 次阅读, 收获喜欢 81 次。

关注

评论

发布
暂无评论
发现更多内容

PyFlink + 区块链?揭秘行业领头企业 BTC.com 如何实现实时计算

Apache Flink

flink

字节跳动 Flink 单点恢复功能实践

Apache Flink

flink

Web前后端:如何分离,如何解耦?

华为云开发者联盟

大前端 后端 开发

Java程序员月薪多少K才能在北上广买得起房?

Java架构之路

Java 程序员 编程语言

spring-boot-route(十)多数据源切换

Java旅途

Java Spring Boot

程序员去外包真的不可取吗?

Java架构师迁哥

开源数据库这么香,为什么我们还要下功夫自研?

华为云开发者联盟

数据库 开源 数据

SpringBoot-技术专题-启动原理

码界西柚

MySQL-技术专题-查询速度性能

码界西柚

互联网应用系统技术方案主要解决什么问题?

博古通今小虾米

开源=免费?

Learun

EffectiveJava读书笔记-01-对象创建与销毁

wander

读书笔记 编程开发

来不及解释了,快上车!力软快速开发平台,助力企业搭乘万物互联的顺风车

Learun

2020年秋招阿里136道Java高级岗面试题(含答案及复习资源)

Java架构之路

Java 程序员 面试 算法 编程语言

深入分析CRM系统对现代企业的作用

Learun

从构建小系统到架构分布式大系统,Spring Boot2的精髓全在这里了

Java架构之路

Java 程序员 面试 Spring Boot 编程语言

技术实操丨SoundNet迁移学习之由声音分类到语音情感识别

华为云开发者联盟

AI 数据 语音识别

lldb常用命令与调试技巧

iOSer

ios lldb常用命令 lldb调试技巧

架构师训练营第四周作业

郎哲158

区块链教育 丨 首批区块链专业新生正式入学

CECBC

区块链技术 区块链教育

MySQL-技术专题-问题分析

码界西柚

国庆期间,我造了台计算机

yes

计算机 底层

Python时间序列分析简介(2)

计算机与AI

Python 时间序列

光大银行刘淼:基于华为云GaussDB(DWS) 数据仓库创新实践

华为云开发者联盟

数据仓库 数据 huawei

31道Java核心面试题,一次性打包送给你

小Q

Java 学习 程序员 架构 面试

建筑行业区块链应用场景是怎样的

CECBC

区块链 行业资讯

Spring Cloud 微服务实践(7) - 日志

xiaoboey

kafka 微服务 Spring Cloud 日志 spring cloud stream

架构师训练营1期第三周作业

木头发芽

Nacos-技术专题-配置中心实现

码界西柚

区块链是一个不知道要解决什么问题的解决方案吗?

CECBC

比特币 区块链 银行

架构师训练营第四章 系统架构总结

郎哲158

基于 Tag 驱动的 EBS 类型优化 CloudFormation 模板_语言 & 开发_亚马逊云科技 (Amazon Web Services)_InfoQ精选文章