亚马逊AWS官方博客

如何优雅的删除一个 VPC

前言

写下这篇博客的动机来自于昨天早上收到的一封邮件。那封邮件的标题是

 

而邮件的正文是这样的  –
“ This is a basic feature everyone needs. It took me days to implement a VPC resource crawler to remove everything.

+100000000000”

看到这些我就知道了邮件所为何事,以及让我回忆起三年前的旧事。那时候我正在尝试在AWS上配置、部署一个较大规模的集群。因为是实验的性质,就需要在AWS上连续不断的的部署、测试、删除,然后重新来过。为了提升效率,我就考虑如何可以简化这个过程,用一组脚本实现自动化的部署、删除等手工完成的操作。搜索之下就看到有人在已经在我之前于Github 的aws-cli项目提出了这样的一个PR(pull request)- “add –all-dependencies option to ec2 delete-vpc #1721” (https://github.com/aws/aws-cli/issues/1721)。因为这个需求与我的想法比较符合,就在这个提议的回复中+1,以示支持。没有想到的是,这个虽然因为许久没有解决而超期被关闭,但是这个问题迟迟没有解决,影响至今。

 

VPC 是什么?

所谓的VPC是英文Virtual Private Cloud的缩写。这个概念的历史可以上溯到2009年,在那一年的8月26日Amazon Web Services 发布了一个重要的服务,服务的名称就是这里要谈到的VPC。随着云计算的普及,VPC这个叫法也就流行起来。例如IBM在2019 年发布的IBM Cloud™ VPC以及Google Cloud Platform 的VPC ,甚至阿里云的产品也使用了这个名字。

不论是由哪一家云计算平台提供的VPC,这个服务的本质都是大致相同的。那就是一个云计算中设置的一个在网络上逻辑隔离的部分,并可以定义IP 地址范围、路由表、安全策略和网关等。于是云计算的使用者就可以在这个虚拟网络中部署、使用各种云计算的资源了。

由此可见,VPC事实上成为了云计算的基础网络环境,对于云计算的使用者可谓是至关重要了。正因为其重要性,于是在VPC之上承载了许多的功能。在AWS 的VPC服务中就包含了Subnet、Route Table、Internet Gateway、Egress Only Internet Gateways、Endpoints、NAT Gateway、Peering Connections、Network ACLs、Security Groups 等等功能。从我的经验来看,学习AWS最难的两个知识点之一就是VPC(另一个是IAM)。

为什么删除VPC 会成为一个问题?

在云计算的环境中,无论是实现一个架构、部署软件或者进行日常的运维管理,都需要管理云计算上的网络基础设施-VPC,也就要涉及到VPC的创建、管理、修改以及删除等常见的操作。当打算删除一个废弃的VPC的时候,我们通常的第一个念头就是看看命令行工具AWS CLI 是否提供了这样的功能?于是我们就找到了这个命令的说明文档

 

但是当我们使用delete-vpc 试图删除一个VPC的时候就会看到这样的错误信息:

因为有依赖项的存在,无法删除这个VPC。


换一个思路,我们还会想到可以使用浏览器在AWS管理控制台上尝试一下删除的操作,结果却是 –

 

提示信息告诉我们需要先删除所有的依赖对象之后才能够删除这个VPC。于是我们只能手动的遍历各个项目逐一删除,然后再去一次次的尝试删除这个VPC,直到成功完成。

在我看来,这样的处理过程不仅低效还极易出错。尽管这个需求已经存在了若干年,但是官方的工具并没有提供一个特性来加以解决。究其原因,恐怕还是与VPC的复杂性有关。因为VPC提供的网络基础设施与各种云计算的资源与服务相互依赖,删除一个VPC就势必要解决所有的依赖关系。随着云计算技术的不断发展,VPC的功能特性在不断的增加,其复杂性也会越来越高。所以,我们相信如果你是一个AWS的重度用户,那就和我一样都需要一个优雅的删除VPC的方法。

我的脚本 – delete_vpc.sh

我曾经使用过CloudFormation、CDK等AWS基础设施部署、管理的工具,也尝试过通过ptython+Boto3 的方式解决自动化部署的问题。不过在过去的一段时间我越来越习惯于使用AWS CLI + Shell 脚本的组合去解决这一类问题。我的考虑是,随着云计算上基础设施的复杂性的不断增加,很多工具的功能特性都存在滞后的现象;此外,出现问题的时候,高级的、抽象的工具往往不能够准确定位问题的原因,使得我们很难解决异常的问题。而相比较起来,AWS CLI 的更新速度应该是最快的,而且这个工具的部署比较简单,脚本运行的依赖项也是比较少的。这些无疑会让我们更简单的达成我们的目标。

就像上文提到的,优雅的删除VPC 最大的挑战就在于解决VPC的依赖项。在我的尝试过程中,逐步明确了这些依赖的项目有-

  1. EC2 实例 –   AWS 云计算虚拟服务器
  2. NAT Gateway – 允许私有子网中的实例连接到 Internet 或其他 AWS 服务
  3. VPN connection –  通过VPN连接 Amazon VPC 与远程网络和用户
  4. VPN Gateway – VPN 网关
  5. VPC Peering  – VPC 对等连接
  6. Endpoint  – VPC 终端节点
  7. Egress only internet gateway – 仅出口互联网网关
  8. ACL s – 网络访问控制列表
  9. Security Group – 网络安全组,起着虚拟防火墙的作用
  10. Elastics IP – 弹性IP 地址,专为动态云计算设计的静态 IPv4 地址
  11. Internet Gateway – Internet 网关, 支持在 VPC Internet 之间进行通信
  12. Elastic Network Interface – 弹性网络接口,可理解为虚拟网卡
  13. Subnet – 子网,VPC中的独立的网络单元
  14. Route tables –  路由表,定义路由规则

目前看来,上述这14个项目就是VPC的依赖项。删除了全部依赖项的资源,也就可以实现对VPC的删除。需要注意的是,这些依赖项之间也存在微妙的依赖关系。这就意味着,我们需要按照一定的顺序删除这些项目,后续的资源项才可能被删除。而上述项目的顺序就是我摸索出来的一个依赖顺序。我们需要按这个顺序逐次处理即可。当然,随着VPC功能的增加,一定会有新的依赖项加入进来,我们只有不断的更新这个脚本才有可能适应未来的变化。

解决了依赖链条的问题,我们还要处理一个新的挑战 – 状态变化的检测。例如,当我删除NAT Gateway之后,NAT Gateway 的状态需要较长的处理时间才能够变为“deleted”。如果我们没有等到这个状态发生变化而去处理其它的项目,例如Elastics IP 或者Internet Gateway 就会遇到因为依赖项的存在而删除失败的问题。检测状态的方法目前有两种:

  • Wait 命令
    这个方法可以让我们等待检测的资源状态的变化。例如:
    aws ec2 wait instance-stopped –instance-ids i-1234567890abcdef0

就可以用来等待 这个EC2 实例的状态改变为”stopped”。目前Wait 命令支持的资源有这样一些

  • 不幸的是,不是所有的资源都提供了Wait 命令,例如NAT Gateway。这时候就需要我们轮训这个资源的状态,确信在其被完全删除之后再去处理下一个资源。例如-
1.	echo "    waiting for state of deleted"  
2.	while :  
3.	do  
4.	    state=$(aws ec2 describe-nat-gateways \  
5.	        --region us-east-1 \  
6.	        --filter 'Name=vpc-id,Values='${VPC_ID} \  
7.	        --query 'NatGateways[].State' \  
8.	        --output text --region ${AWS_REGION})  
9.	    if [ "$state" = 'deleted' ]; then  
10.	        break  
11.	    fi  
12.	    sleep 3  
13.	done  

对于代码最好的解释莫过于代码本身,实现的细节相信通过代码能够一目了然。希望这个脚本不仅适用于我也同样对你有所帮助。我在Github 建立了一个新的项目,免得下一次需要的时候却遍寻不到。项目的地址是 https://github.com/lianghong/delete_vpc

脚本的使用比较简单-

1、./delete_vpc.sh [AWS 区域] ,枚举出来所制定的区域的全部VPC

 

2、./delete_vpc.sh [AWS 区域]  [VPC ID],删除指定区域下的VPC

 

项目的代码在这里

1.	#!/bin/bash  
2.	#description : Delete a specific AWS VPC  
3.	#author      : lianghong Fei  
4.	#e-mail      : feilianghong@gmail.com  
5.	#create date : May 23,2020  
6.	#modify date : May 23,2020  
7.	  
8.	set -e  
9.	  
10.	if [ -z "$1" ]; then  
11.	   echo "Usage      : $0 <aws region> <vpc id>"  
12.	   echo "For example: $0 us-east-1 vpc-xxxxxxxxxx"  
13.	   echo ""  
14.	   exit 1  
15.	fi  
16.	  
17.	if [ -z "$2" ]; then  
18.	    # list all VPCs in specific Region  
19.	    aws ec2 describe-vpcs \  
20.	        --query 'Vpcs[].{vpcid:VpcId,name:Tags[?Key==`Name`].Value[]}' \  
21.	        --output table --region $1  
22.	    exit 1  
23.	else  
24.	    AWS_REGION=$1  
25.	    VPC_ID=$2  
26.	fi  
27.	  
28.	# Check VPC state , avaiable or not  
29.	state=$(aws ec2 describe-vpcs \  
30.	    --vpc-ids ${VPC_ID} \  
31.	    --query 'Vpcs[].State' \  
32.	    --output text --region ${AWS_REGION})  
33.	  
34.	if [ ${state} != 'available' ]; then  
35.	    echo "The VPC of ${VPC_ID} is NOT available now! "  
36.	    exit 1  
37.	fi  
38.	  
39.	# Stop instance  
40.	echo "Process of EC2 instance ..."  
41.	for instance in $(aws ec2 describe-instances \  
42.	    --filters "Name=vpc-id,Values=${VPC_ID}" "Name=instance-state-name,Values=running" \  
43.	    --query 'Reservations[].Instances[].InstanceId' \  
44.	    --output text --region ${AWS_REGION})  
45.	do  
46.	    echo "    stop instance of $instance"  
47.	    aws ec2 stop-instances \  
48.	        --instance-ids ${instance} \  
49.	        --region ${AWS_REGION} > /dev/null  
50.	  
51.	    #wait until instance stoped  
52.	    aws ec2 wait instance-stopped \  
53.	        --instance-ids ${instance} \  
54.	        --region ${AWS_REGION}  
55.	done  
56.	  
57.	# Terminate instancie  
58.	for instance in $(aws ec2 describe-instances \  
59.	    --filters 'Name=vpc-id,Values='${VPC_ID} \  
60.	    --query 'Reservations[].Instances[].InstanceId' \  
61.	    --output text --region ${AWS_REGION})  
62.	do  
63.	    echo "    terminate EC2 instance of $instance"  
64.	    aws ec2 terminate-instances \  
65.	        --instance-ids ${instance} \  
66.	        --region ${AWS_REGION} > /dev/null  
67.	  
68.	    # Wait until instance terminated  
69.	    aws ec2 wait instance-terminated \  
70.	        --instance-ids ${instance} \  
71.	        --region ${AWS_REGION}  
72.	done  
73.	  
74.	#Delete NAT Gateway  
75.	echo "Process of NAT Gateway ..."  
76.	for natgateway in $(aws ec2 describe-nat-gateways \  
77.	    --filter 'Name=vpc-id,Values='${VPC_ID} \  
78.	    --query 'NatGateways[].NatGatewayId' \  
79.	    --output text --region ${AWS_REGION})  
80.	do  
81.	    echo "    delete NAT Gateway of $natgateway"  
82.	    aws ec2 delete-nat-gateway \  
83.	        --nat-gateway-id ${natgateway} \  
84.	        --region ${AWS_REGION}  > /dev/null  
85.	done  
86.	  
87.	echo "    waiting for state of deleted"  
88.	while :  
89.	do  
90.	    state=$(aws ec2 describe-nat-gateways \  
91.	        --region us-east-1 \  
92.	        --filter 'Name=vpc-id,Values='${VPC_ID} \  
93.	        --query 'NatGateways[].State' \  
94.	        --output text --region ${AWS_REGION})  
95.	    if [ "$state" = 'deleted' ]; then  
96.	        break  
97.	    fi  
98.	    sleep 3  
99.	done  
100.	  
101.	#Delete VPN connection  
102.	echo "Process of VPN connection ..."  
103.	for vpn in $(aws ec2 describe-vpn-connections \  
104.	    --filters 'Name=vpc-id,Values='${VPC_ID} \  
105.	    --query 'VpnConnections[].VpnConnectionId' \  
106.	    --output text --region ${AWS_REGION})  
107.	do  
108.	    echo "    delete VPN Connection of $vpn"  
109.	    aws ec2 delete-vpn-connection \  
110.	        --vpn-connection-id ${vpn} \  
111.	        --region ${AWS_REGION}  > /dev/null  
112.	    #Wait until deleted  
113.	    aws ec2 wait vpn-connection-deleted \  
114.	        --vpn-connection-ids ${vpn} \  
115.	        --region ${AWS_REGION}  
116.	done  
117.	  
118.	#Delete VPN Gateway  
119.	echo "Process of VPN Gateway ..."  
120.	for vpngateway in $(aws ec2 describe-vpn-gateways \  
121.	    --filters 'Name=attachment.vpc-id,Values='${VPC_ID} \  
122.	    --query 'VpnGateways[].VpnGatewayId' \  
123.	    --output text --region ${AWS_REGION})  
124.	do  
125.	    echo "    delete VPN Gateway of $vpngateway"  
126.	    aws ec2 delete-vpn-gateway \  
127.	        --vpn-gateway-id ${vpngateway} \  
128.	        --region ${AWS_REGION}  > /dev/null  
129.	done  
130.	  
131.	#Delete Peering  
132.	echo "Process of VPC Peering ..."  
133.	for peering in $(aws ec2 describe-vpc-peering-connections \  
134.	    --filters 'Name=requester-vpc-info.vpc-id,Values='${VPC_ID} \  
135.	    --query 'VpcPeeringConnections[].VpcPeeringConnectionId' \  
136.	    --output text --region ${AWS_REGION})  
137.	do  
138.	    echo "    delete VPC Peering of $peering"  
139.	    aws ec2 delete-vpc-peering-connection \  
140.	        --vpc-peering-connection-id ${peering} \  
141.	        --region ${AWS_REGION}  > /dev/null  
142.	    #Wait until deleted  
143.	    aws ec2 wait vpc-peering-connection-deleted \  
144.	        --vpc-peering-connection-ids ${peering} \  
145.	        --region ${AWS_REGION}  
146.	done  
147.	  
148.	#Delete Endpoints  
149.	echo "Process of VPC endpoints ..."  
150.	for endpoints in $(aws ec2 describe-vpc-endpoints \  
151.	    --filters 'Name=vpc-id,Values='${VPC_ID} \  
152.	    --query 'VpcEndpoints[].VpcEndpointId' \  
153.	    --output text --region ${AWS_REGION})  
154.	do  
155.	    echo "    delete endpoint of $endpoints"  
156.	    aws ec2 delete-vpc-endpoints \  
157.	        --vpc-endpoint-ids ${endpoints} \  
158.	        --region ${AWS_REGION}  > /dev/null  
159.	done  
160.	  
161.	#Delete egress only internet gateway  
162.	echo "Process of Egress only internet gateway ..."  
163.	for egress in $(aws ec2 describe-egress-only-internet-gateways \  
164.	    --filters 'Name=attachment.vpc-id,Values='${VPC_ID} \  
165.	    --query 'EgressOnlyInternetGateways[].EgressOnlyInternetGatewayId' \  
166.	    --output text --region ${AWS_REGION})  
167.	do  
168.	    echo "    delete egress only internet gateway of $egress"  
169.	    aws ec2 delete-egress-only-internet-gateway \  
170.	        --egress-only-internet-gateway-id ${egress} \  
171.	        --region ${AWS_REGION}  > /dev/null  
172.	done  
173.	  
174.	#Delete ACLs  
175.	echo "Process of Network ACLs ..."  
176.	for acl in $(aws ec2 describe-network-acls \  
177.	    --filters 'Name=vpc-id,Values='${VPC_ID} \  
178.	    --query 'NetworkAcls[].NetworkAclId' \  
179.	    --output text --region ${AWS_REGION})  
180.	do  
181.	    #Check it's default acl  
182.	    acl_default=$(aws ec2 describe-network-acls \  
183.	        --network-acl-ids ${acl} \  
184.	        --query 'NetworkAcls[].IsDefault' \  
185.	        --output text --region ${AWS_REGION})  
186.	  
187.	    #Ignore default acl  
188.	    if [ "$acl_default" = 'true' ] || [ "$acl_default" = 'True' ]; then  
189.	        continue  
190.	    fi  
191.	  
192.	    echo "    delete ACL of $acl"  
193.	    aws ec2 delete-network-acl \  
194.	        --network-acl-id ${acl} \  
195.	        --region ${AWS_REGION}  > /dev/null  
196.	done  
197.	  
198.	#Delete Security Group  
199.	echo "Process of Security Group ..."  
200.	for sg in $(aws ec2 describe-security-groups \  
201.	    --filters 'Name=vpc-id,Values='${VPC_ID} \  
202.	    --query 'SecurityGroups[].GroupId' \  
203.	    --output text --region ${AWS_REGION})  
204.	do  
205.	    #Check it's default security group  
206.	    sg_name=$(aws ec2 describe-security-groups \  
207.	        --group-ids ${sg} --query 'SecurityGroups[].GroupName' \  
208.	        --output text --region ${AWS_REGION})  
209.	    #Ignore default security group  
210.	    if [ "$sg_name" = 'default' ] || [ "$sg_name" = 'Default' ]; then  
211.	        continue  
212.	    fi  
213.	  
214.	    echo "    delete Security group of $sg"  
215.	    aws ec2 delete-security-group \  
216.	        --group-id ${sg} \  
217.	        --region ${AWS_REGION}  > /dev/null  
218.	done  
219.	  
220.	#Delete EIP  
221.	echo "Process of Elastic IP ..."  
222.	for associationid in $(aws ec2 describe-network-interfaces \  
223.	    --filters 'Name=vpc-id,Values='${VPC_ID} \  
224.	    --query 'NetworkInterfaces[].Association[].AssociationId' \  
225.	    --output text --region ${AWS_REGION})  
226.	do  
227.	    echo "    disassociate EIP association-id of $associationid"  
228.	    aws ec2 disassociate-address \  
229.	        --association-id ${associationid} \  
230.	        --region ${AWS_REGION} > /dev/null  
231.	done  
232.	  
233.	#Delete IGW  
234.	echo "Process of Internet Gateway ..."  
235.	for igw in $(aws ec2 describe-internet-gateways \  
236.	    --filters 'Name=attachment.vpc-id,Values='${VPC_ID} \  
237.	    --query 'InternetGateways[].InternetGatewayId' \  
238.	    --output text --region ${AWS_REGION})  
239.	do  
240.	    echo "    detach IGW of $igw"  
241.	    aws ec2 detach-internet-gateway \  
242.	        --internet-gateway-id ${igw} \  
243.	        --vpc-id ${VPC_ID} \  
244.	        --region ${AWS_REGION}  > /dev/null  
245.	  
246.	    #We need a waiter here  
247.	    sleep 1  
248.	  
249.	    echo "    delete IGW of $igw"  
250.	    aws ec2 delete-internet-gateway \  
251.	        --internet-gateway-id ${igw} \  
252.	        --region ${AWS_REGION}  > /dev/null  
253.	done  
254.	  
255.	#Delete NIC  
256.	echo "Process of Network Interface ..."  
257.	for nic in $(aws ec2 describe-network-interfaces \  
258.	    --filters 'Name=vpc-id,Values='${VPC_ID} \  
259.	    --query 'NetworkInterfaces[].NetworkInterfaceId' \  
260.	    --output text --region ${AWS_REGION})  
261.	do  
262.	    echo "    detach Network Interface of $nic"  
263.	    aws ec2 detach-network-interface \  
264.	        --attachment-id ${nic} \  
265.	        --region ${AWS_REGION}  > /dev/null  
266.	  
267.	    #We need a waiter here  
268.	    sleep 1  
269.	  
270.	    echo "    delete Network Interface of $nic"  
271.	    aws ec2 delete-network-interface \  
272.	        --network-interface-id ${nic} \  
273.	        --region ${AWS_REGION}  > /dev/null  
274.	done  
275.	  
276.	#Delete Subnet  
277.	echo "Process of Subnet ..."  
278.	for subnet in $(aws ec2 describe-subnets \  
279.	    --filters 'Name=vpc-id,Values='${VPC_ID} \  
280.	    --query 'Subnets[].SubnetId'  \  
281.	    --output text --region ${AWS_REGION})  
282.	do  
283.	    echo "    delete Subnet of $subnet"  
284.	    aws ec2 delete-subnet \  
285.	        --subnet-id ${subnet} \  
286.	        --region ${AWS_REGION}  > /dev/null  
287.	done  
288.	  
289.	#Delete RouteTable  
290.	echo "Process of Route table ..."  
291.	for routetable in $(aws ec2 describe-route-tables \  
292.	    --filters 'Name=vpc-id,Values='${VPC_ID} \  
293.	    --query 'RouteTables[].RouteTableId' \  
294.	    --output text --region ${AWS_REGION})  
295.	do  
296.	    #Check it's main route table  
297.	    main_table=$(aws ec2 describe-route-tables  \  
298.	            --route-table-ids ${routetable} \  
299.	            --query 'RouteTables[].Associations[].Main' \  
300.	            --output text --region ${AWS_REGION})  
301.	  
302.	    #Ignore main route table  
303.	    if [ "$main_table" = 'True' ] || [ "$main_table" = 'true' ]; then  
304.	        continue  
305.	    fi  
306.	  
307.	    echo "    delete Route tabls of $routetable"  
308.	    aws ec2 delete-route-table \  
309.	        --route-table-id ${routetable} \  
310.	        --region ${AWS_REGION}  > /dev/null  
311.	done  
312.	  
313.	#Delete VPC  
314.	echo "Finally delete VPC of ${VPC_ID}"  
315.	aws ec2 delete-vpc \  
316.	    --vpc-id ${VPC_ID} \  
317.	    --region ${AWS_REGION}  
318.	  
319.	echo "Done."  

好了,现在我可以说终于可以用一种优雅的方式删除废弃的VPC了。或许我测试的场景还不能够覆盖各种可能性,如果你有新的问题或者建议请发送邮件( feilianghong@gmail.com )或者在Github 的项目上留言。

后记

因为是要分享出来的内容,这个脚本我格外的用心。但是昨天我收到的一个PR 却让我不禁脸红,325行的脚本居然还有不少的错误!修订以后的脚本可以从以下链接获得,也希望得到你们的反馈即使是会让我羞愧的PR。
https://github.com/lianghong/delete_vpc

 

本篇作者

费良宏

费良宏,AWS Principal Developer Advocate。在过去的20多年一直从事软件架构、程序开发以及技术推广等领域的工作。他经常在各类技术会议上发表演讲进行分享,他还是多个技术社区的热心参与者。他擅长Web领域应用、移动应用以及机器学习等的开发,也从事过多个大型软件项目的设计、开发与项目管理。目前他专注与云计算以及互联网等技术领域,致力于帮助中国的 开发者构建基于云计算的新一代的互联网应用。