尝试通过lambda实现AWS某instance出问题时路由自动切换实现故障转移
背景
我们sdwan部署在公有云上的虚拟镜像,为了避免单点故障,会建议客户起两个instance作为主备。但是,传统的VPC路由表,只能将特定网段的下一跳设置为其中一个instance。
如果instance A出问题,需要客户手动修改路由表,把下一跳设置为instance B。
这样做当然可以,但不是自动切换,不够智能,理论上会减少网络可用性,也会给客户增加运维成本。
因此,我们想为客户推荐一个能实现自动切换路由的解决方案。
这里以AWS为例开始研究,研究过程中,会尽量考虑各个平台的通用性。如果AWS上可以实现,从技术上讲,在其他公有云厂家也可以实现相同的功能。
调研
先谷歌下,看看网上是否有类似的现成的方案,这里随便翻了几页,看到两个文章比较有用:
文章1: https://aws.amazon.com/cn/blogs/china/highly-available-design-snares-in-sd-wan-and-private-line-hybrid-networks/
这个是AWS官方博客的文章,里面实现了专线和SDWAN混合组网的高可用。文章中脚本是放在EC2上执行的,总体思路是创建两套路由表,然后通过脚本检测进行切换;
触发切换的机制是检查专线的BGPSTATUS是否为UP。
这个脚本得在后台一直跑,以保证平时为”– Nothing goes wrong, keep monitoring”的状态。
这里有值得借鉴的内容,先留着。
文章里还有个方法二,需要其中一个路由表网段范围更大,方法不够优雅,这里就不考虑了。
文章2: https://www.codenong.com/5da2a8669dba196f8dd5/
这个文章介绍的就是一个实例停止后,通过脚本将路由表中eni-1改为eni-2。
好消息是这个基本就是我想要的功能,坏消息是文章写得不够细,但是好消息是功能核心代码是有的。
那就按照这个代码开始研究吧!!
测试环境
在同一个VPC的两个Available Zone(分别对应两个子网)中,起两个EC2,每个EC2会自动生成各自的eni。
两个子网共用一张默认的路由表。
然后在路由表中,添加一条路由,作为测试的目标路由:
1.1.1.1/32的下一跳设置为instance A。
两个实例的具体instanceID和ENI参数如下:
instance A:instanceID = i-05a7780f2af7c126c ; eni = eni-00fea71a1c9255bef
instance B:instanceID = i-045ebd27124159b6d ; eni = eni-071b4d6cbf57de651
我们的目标就是,让1.1.1.1的路由的target,由 eni-00fea71a1c9255bef 自动改为 eni-071b4d6cbf57de651
第一轮尝试
先把lambda配上,把代码放进去,把代码中涉及到VPC的参数改成我的测试环境
host1eni = 'eni-00fea71a1c9255bef'
host2eni = 'eni-071b4d6cbf57de651'
vpcid = 'vpc-0bf5741eb0ef56a41'
region = 'us-west-1'
这里涉及到lambda的IAM role,因为要修改路由表,默认的lambda权限肯定是不够的。
我新建了一条policy,把文章1中的policy先都加上了
好,权限应该足够执行修改路由表的代码了,下面先跑个代码试试~~
Test的Event先用默认的
说的是代码第21行
my_instance_id = event['detail']['instance-id']
这行不对,定睛一看,这行是要从CloudWatch的event中,读取出问题实例的instanceID呀!~
CloudWatch的Event还没配置,Test的Event也没改,目前还没研究event的格式,这里先把这个参数写死,看看代码能否跑通,所以这里改为
my_instance_id = 'i-05a7780f2af7c126c'
执行代码,printout如下
这里路由status是blackhole是因为这个instance我没有启动,这在目前的测试阶段不是问题。
综上,上面lambda的核心代码已经可以实现我们想要的功能了。当然后面还有很多细节要完善。
今天先到这里,结束前给代码增加了几行printout,对读log和troubleshooting更友好一些。
上面, 我们已经完成了核心功能代码的验证。
下一步计划:
- 触发lambda的CloudWatch的Event还需要配置;
- 代码中通过CloudWatch Event读取instanceID的部分还需要验证;
- 未来eni vpcid region这些参数最好放到环境变量里,而不是直接hard code到代码中;
- role权限应该可以再小一点;
第二轮尝试
CloudWatch Event配置和验证
我们现在配置下触发lambda的CloudWatch Event,要实现的效果,最好是:
两个测试instance,无论其中任何一个的instance状态,不是running的,都触发。
下面试一试~~
这里填完上面的配置后,aws会自动生成Event pattern如上图,这里文字贴出来如下
{
"source": ["aws.ec2"],
"detail-type": ["EC2 Instance State-change Notification"],
"detail": {
"state": ["pending", "shutting-down", "stopped", "stopping", "terminated"],
"instance-id": ["i-05a7780f2af7c126c", "i-045ebd27124159b6d"]
}
}
配置完了~ 下面把测试的instanceB running再stop试下,看看能不能触发。
如果触发了,代码应该会print出来event的信息,然后咱们就知道具体CloudWatch Event发给lambda的内容的格式了。
下面试一下~~
我现在启动instanceB,正常的话,instance状态会由stop变为pending再变为running,理论上pending会触发Event。
启动完了,去看下lambda的log:
{'version': '0', 'id': '153bbf21-05cb-aa5f-f22d-c602cd2dbee4', 'detail-type': 'EC2 Instance State-change Notification', 'source': 'aws.ec2', 'account': '731262942057', 'time': '2021-12-11T12:59:41Z', 'region': 'us-west-1', 'resources': ['arn:aws:ec2:us-west-1:731262942057:instance/i-045ebd27124159b6d'], 'detail': {'instance-id': 'i-045ebd27124159b6d', 'state': 'pending'}}
lambda配置通过CloudWatch Event读取instanceID
在第一轮尝试中,我们把代码中第21行
my_instance_id = event['detail']['instance-id']
从CloudWatch的event中,读取出问题实例的instanceID,这个参数写死了,如下
my_instance_id = 'i-05a7780f2af7c126c'
现在我们改成原来的自动从event中读取
然后我们做如下准备,开始测试真正的触发:
- instanceB让他保持running状态,为之后测试stop做准备
- 测试路由的target设置为instanceB的ENI
妥了,准备好了!!下面我们把instanceB stop,理论上,这个操作会:
- 触发CloudWatch Events发送到lambda,
- lambda读取event,发现instanceB的状态不是running,
- 读取instanceB的ENI,和现有路由表匹配,
- 发现路由表中那条测试路由,并把它的target由instanceB的ENI改为instanceA的ENI。
好的,开始停掉instanceB!~
现在去看下lambda的log,确认下细节
目前看功能已经可以正常触发了。已经可以使用了。
目前进度/计划:
- 【已完成】触发lambda的CloudWatch的Event还需要配置;
- 【已完成】代码中通过CloudWatch Event读取instanceID的部分还需要验证;
- 【已完成】代码增加了一些print的log,让log更可读
- 【待完成】未来eni vpcid region这些参数最好放到环境变量里,而不是直接hard code到代码中;
- 【待完成】role权限应该可以再小一点;
- 还有个事儿,上面实际代码跑通了,当时我跑lambda的Test Event的时候,要么就是报格式不对,要么就是报运行错误,真是奇了怪了,我再研究下,应该很快就能搞清楚。
第三轮尝试(优化)
Test Event的问题
搞明白了,CloudWatch发给lambda的event log,在CloudWatch贴出来是这样的:
{'version': '0', 'id': '153bbf21-05cb-aa5f-f22d-c602cd2dbee4', 'detail-type': 'EC2 Instance State-change Notification', 'source': 'aws.ec2', 'account': '731262942057', 'time': '2021-12-11T12:59:41Z', 'region': 'us-west-1', 'resources': ['arn:aws:ec2:us-west-1:731262942057:instance/i-045ebd27124159b6d'], 'detail': {'instance-id': 'i-045ebd27124159b6d', 'state': 'pending'}}
这个如果直接贴到Test event里,会报错“bad string”
我这一顿试啊 最后发现把单引号 ‘ 都替换成双引号 “ 就好了。。。
贴过来建议点下Format按钮可以让格式更好看。
Item6解决。
将hard code的instance相关参数改为环境变量
把代码中
host1eni = 'eni-00fea71a1c9255bef'
改为
host1eni = os.environ['host1eni']
代码最前面加上import os,然后我们在环境变量中增加这个 host1eni 试一下
Test Event可以跑通!说明方法没问题,我们用这个方法把其他参数改一下。
环境变量也都设置上
Test Event再次测试,也可以跑通。
现在代码中已经没有hard code的、跟VPC相关的具体实际参数了,完美~~
lambda role权限优化
我们回头去修改下当初给lambda的policy
我们首先删除里面的: “directconnect:DescribeVirtualInterfaces”, 这个是直连服务的,咱肯定用不上。
其他的权限,的确都是跟路由表相关的。
我们回头看下主程序,里面路由相关的操作主要是:
- describe_route_tables
- delete_route
- create_route
改动的操作都是路由层面的,不是路由表层面的。
因此我删除如下policy试下:
- “ec2:ReplaceRouteTableAssociation”
- “ec2:CreateRouteTable”
- “ec2:DisassociateRouteTable”
- “ec2:AssociateRouteTable”
上述路由表层面的修改权限这个操作,都是高危操作,而且咱们程序中没有用到,还是删掉比较好。毕竟,给用户/程序足够使用的最小权限,也是最佳实践的一部分。
目前保留的权限如下(也调整了下条目的顺序,describe都放到前面了)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeTags",
"ec2:DescribeVpcs",
"ec2:DescribeRouteTables",
"ec2:CreateRoute",
"ec2:DeleteRoute",
"ec2:ReplaceRoute"
],
"Resource": "*"
}
]
}
这里Describe的权限不会做实际改动,保留这几条没有风险也比较合理。
剩下的涉及到改动的权限,就只有 DeleteRoute 和 CreateRoute 还有 ReplaceRoute 。这里 ReplaceRoute 虽然目前程序没有用到,但是考虑到程序未来的功能更新的扩展性,再考虑到delete和create都有了,replace从效果上将基本一样,同等地位可以保留。
好的,policy改完了。
分析一大顿,不行就尴尬了,试一下~~
我这里建了两个Test Event,一个是instanceA有问题,一个是instanceB有问题,这样两边倒换都测试下。
测试通过,没有问题,这里就不截图了。
目前进度:
- 【已完成】触发lambda的CloudWatch的Event还需要配置;
- 【已完成】代码中通过CloudWatch Event读取instanceID的部分还需要验证;
- 【已完成】代码增加了一些print的log,让log更可读;
- 【已完成】未来eni vpcid region这些参数最好放到环境变量里,而不是直接hard code到代码中;
- 【已完成】role权限应该可以再小一点;
- 【已完成】解决Test Event报错问题。
总结
至此,计划的功能已经全部实现了。
整个流程如下:
- CloudWatch监控主备sdwan实例的状态(不是running则触发);
- 某sdwan实例(作为路由表的target,也就是下一跳网关)的状态不是running;
- 触发CloudWatch发送event给lambda;
- lambda开始运行;
- lambda读取event中出问题实例的instanceID,并获取该问题实例的ENI;
- lambda读取环境变量中主备sdwan实例的ENI,VPC,region;
- lambda比较出问题实例的ENI和主备sdwan实例的ENI,若出问题的为二者之一,则开始准备在路由表中用另一个ENI代替出问题的ENI;
- lambda读取现有VPC的路由表,并逐条路由检查,若有路由的target为出问题的ENI,则替换为另一个ENI;
- 结束程序。
未来的计划
我之后有时间的话,会出一个教程,让用户/客户,可以参考教程一步一步完成这部分配置。
这部分自动化的路由倒换,其实最好可以触发一个邮件告知相关的管理员,这个通过lambda应该不难完成,等有时间我可能会一并写到教程中。
2 COMMENTS