写点什么

基于 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:29616
用户头像

发布了 1853 篇内容, 共 119.8 次阅读, 收获喜欢 78 次。

关注

评论

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

Week12

一叶知秋

新时代背景下的Java语法特性

九叔(高翔龙)

Java java 14 java 14 新特性 Java 分布式

Jenkins持续集成「编译打包、代码检查、单元测试、环境部署、软件测试​」

清菡软件测试

jenkins

数字人民币钱包短暂露面 金融诈骗伺机而起

CECBC

数字货币 钱包 货币

oeasy教您玩转linux-010110内容回顾

o

人民版权 获2020中国产业区块链创新奖

CECBC

区块链 产业发展 版权

Python 到底是强类型语言,还是弱类型语言?

Python猫

Java c++ Python 编程

MySQL复杂where条件分析

程序员历小冰

MySQL

Keepass+Synology 打造私人密码管理器

zj坚果

week 12 学习总结

Geek_2e7dd7

甲方日常4

句子

工作 随笔杂谈 日常

自己做的 PPT 总被批「缺少干货」?试试先回答这三个问题

Tony Wu

效率工具 方法论 PPT

揭开链表的真面目

Java旅途

Java 数据结构 链表

java安全编码指南之:对象构建

程序那些事

Java 安全 安全编码指南 对象构建

面试是一张窄窄的船票

escray

学习 面试

week 12 作业

Geek_2e7dd7

DockerHub 镜像仓库的使用

哈喽沃德先生

Docker 容器 微服务 镜像

USDT承兑商软件开发,区块链支付系统源码搭建

13530558032

数字货币交易平台搭建,去中心化交易所开发方案

13530558032

数字资产钱包开发,深圳区块链理财钱包服务商

13530558032

在面试中成长

escray

学习 面试

产品经理的架构思维

吴世亮

架构 产品经理 电商

拖延症竟然是自己给自己的一种奖励?如何干掉它?

非著名程序员

个人成长 拖延症 番茄土豆工作法

合约跟单软件开发,合约跟单交易所系统开发搭建

13530558032

性能相关 磁盘I/O子系统

Linuxer

SpreadJS 纯前端表格控件应用案例:表格数据管理平台

葡萄城技术团队

面试必备知识点:悲观锁和乐观锁的那些事儿

鄙人薛某

面试 乐观锁 悲观锁 CAS 并发控制

文件系统

Linuxer

管理时间还是挥霍时间?

钰湚—付晓岩

学习 时间管理 工作体会 工作哲学

你也许还不懂静态方法和实例方法

架构师修行之路

Golang写算法

卒迹

算法 Go 语言

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