尝试通过lambda实现AWS某instance出问题时路由自动切换实现故障转移

尝试通过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更友好一些。

上面, 我们已经完成了核心功能代码的验证。

下一步计划:

  1. 触发lambda的CloudWatch的Event还需要配置;
  2. 代码中通过CloudWatch Event读取instanceID的部分还需要验证;
  3. 未来eni vpcid region这些参数最好放到环境变量里,而不是直接hard code到代码中;
  4. 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:

触发了,点进去看下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,理论上,这个操作会:

  1. 触发CloudWatch Events发送到lambda,
  2. lambda读取event,发现instanceB的状态不是running,
  3. 读取instanceB的ENI,和现有路由表匹配,
  4. 发现路由表中那条测试路由,并把它的target由instanceB的ENI改为instanceA的ENI。

好的,开始停掉instanceB!~

直接去检查路由表,看看是否改过来了
改过来了!成功!~

现在去看下lambda的log,确认下细节

没什么问题,stopping的状态就第一时间触发了路由切换。

目前看功能已经可以正常触发了。已经可以使用了。

目前进度/计划:

  1. 【已完成】触发lambda的CloudWatch的Event还需要配置;
  2. 【已完成】代码中通过CloudWatch Event读取instanceID的部分还需要验证;
  3. 【已完成】代码增加了一些print的log,让log更可读
  4. 【待完成】未来eni vpcid region这些参数最好放到环境变量里,而不是直接hard code到代码中;
  5. 【待完成】role权限应该可以再小一点;
  6. 还有个事儿,上面实际代码跑通了,当时我跑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按钮可以让格式更好看。

然后,test就可以跑了。

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有问题,这样两边倒换都测试下。

测试通过,没有问题,这里就不截图了。

目前进度:

  1. 【已完成】触发lambda的CloudWatch的Event还需要配置;
  2. 【已完成】代码中通过CloudWatch Event读取instanceID的部分还需要验证;
  3. 【已完成】代码增加了一些print的log,让log更可读;
  4. 【已完成】未来eni vpcid region这些参数最好放到环境变量里,而不是直接hard code到代码中;
  5. 【已完成】role权限应该可以再小一点;
  6. 【已完成】解决Test Event报错问题。

总结

至此,计划的功能已经全部实现了。

整个流程如下:

  1. CloudWatch监控主备sdwan实例的状态(不是running则触发);
  2. 某sdwan实例(作为路由表的target,也就是下一跳网关)的状态不是running;
  3. 触发CloudWatch发送event给lambda;
  4. lambda开始运行;
  5. lambda读取event中出问题实例的instanceID,并获取该问题实例的ENI;
  6. lambda读取环境变量中主备sdwan实例的ENI,VPC,region;
  7. lambda比较出问题实例的ENI和主备sdwan实例的ENI,若出问题的为二者之一,则开始准备在路由表中用另一个ENI代替出问题的ENI;
  8. lambda读取现有VPC的路由表,并逐条路由检查,若有路由的target为出问题的ENI,则替换为另一个ENI;
  9. 结束程序。

未来的计划

我之后有时间的话,会出一个教程,让用户/客户,可以参考教程一步一步完成这部分配置。

这部分自动化的路由倒换,其实最好可以触发一个邮件告知相关的管理员,这个通过lambda应该不难完成,等有时间我可能会一并写到教程中。

2 COMMENTS

comments user
wey

?,感觉这个内容很适合做一个同样内核的视频呢哈!

    comments user
    YuShuoxin

    哈哈 感谢评论!~ 等我先多看看你的视频学习下~~

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注