前言
写下这篇博客的动机来自于昨天早上收到的一封邮件。那封邮件的标题是
而邮件的正文是这样的 –
“ 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的依赖项。在我的尝试过程中,逐步明确了这些依赖的项目有-
- EC2 实例 – AWS 云计算虚拟服务器
- NAT Gateway – 允许私有子网中的实例连接到 Internet 或其他 AWS 服务
- VPN connection – 通过VPN连接 Amazon VPC 与远程网络和用户
- VPN Gateway – VPN 网关
- VPC Peering – VPC 对等连接
- Endpoint – VPC 终端节点
- Egress only internet gateway – 仅出口互联网网关
- ACL s – 网络访问控制列表
- Security Group – 网络安全组,起着虚拟防火墙的作用
- Elastics IP – 弹性IP 地址,专为动态云计算设计的静态 IPv4 地址
- Internet Gateway – Internet 网关, 支持在 VPC 和 Internet 之间进行通信
- Elastic Network Interface – 弹性网络接口,可理解为虚拟网卡
- Subnet – 子网,VPC中的独立的网络单元
- 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
本篇作者