@@ -223,6 +223,126 @@ def remove_unused_ssh_key_pairs(client, executor_name_part):
223223 }))
224224
225225
226+ def cleanup_orphaned_eips (ec2_client , executor_name_part ):
227+ """
228+ Clean up orphaned EIPs from terminated instances.
229+ :param ec2_client: the boto3 EC2 client
230+ :param executor_name_part: used to filter EIPs by Environment tag to match this value
231+ """
232+ print (json .dumps ({
233+ "Level" : "info" ,
234+ "Message" : f"Checking for orphaned EIPs for agent { executor_name_part } "
235+ }))
236+
237+ try :
238+ # Find all EIPs (we'll filter by tag content below)
239+ eips_response = ec2_client .describe_addresses ()
240+
241+ eips_to_cleanup = []
242+
243+ for eip in eips_response .get ("Addresses" , []):
244+ allocation_id = eip ["AllocationId" ]
245+ instance_id = eip .get ("InstanceId" )
246+
247+ # First check if this EIP belongs to our environment
248+ eip_tags = {tag ["Key" ]: tag ["Value" ] for tag in eip .get ("Tags" , [])}
249+ if not ("Environment" in eip_tags and executor_name_part in eip_tags ["Environment" ]):
250+ continue # Skip EIPs not belonging to our environment
251+
252+ if instance_id :
253+ # Check if the associated instance still exists and is terminated
254+ try :
255+ instance_response = ec2_client .describe_instances (InstanceIds = [instance_id ])
256+ instance_state = instance_response ["Reservations" ][0 ]["Instances" ][0 ]["State" ]["Name" ]
257+
258+ if instance_state == "terminated" :
259+ eips_to_cleanup .append ({
260+ "allocation_id" : allocation_id ,
261+ "instance_id" : instance_id ,
262+ "public_ip" : eip .get ("PublicIp" , "unknown" ),
263+ "reason" : f"associated instance { instance_id } is terminated"
264+ })
265+ except ClientError as error :
266+ if 'InvalidInstanceID.NotFound' in str (error ):
267+ # Instance no longer exists
268+ eips_to_cleanup .append ({
269+ "allocation_id" : allocation_id ,
270+ "instance_id" : instance_id ,
271+ "public_ip" : eip .get ("PublicIp" , "unknown" ),
272+ "reason" : f"associated instance { instance_id } no longer exists"
273+ })
274+ else :
275+ print (json .dumps ({
276+ "Level" : "warning" ,
277+ "Message" : f"Could not check instance { instance_id } for EIP { allocation_id } " ,
278+ "Exception" : str (error )
279+ }))
280+ else :
281+ # EIP is not associated with any instance and belongs to our environment
282+ eips_to_cleanup .append ({
283+ "allocation_id" : allocation_id ,
284+ "instance_id" : "none" ,
285+ "public_ip" : eip .get ("PublicIp" , "unknown" ),
286+ "reason" : "unassociated EIP with matching Environment tag"
287+ })
288+
289+ # Clean up identified orphaned EIPs
290+ for eip_info in eips_to_cleanup :
291+ try :
292+ print (json .dumps ({
293+ "Level" : "info" ,
294+ "AllocationId" : eip_info ["allocation_id" ],
295+ "PublicIp" : eip_info ["public_ip" ],
296+ "Message" : f"Releasing orphaned EIP: { eip_info ['reason' ]} "
297+ }))
298+
299+ # Disassociate first if still associated
300+ if eip_info ["instance_id" ] != "none" :
301+ try :
302+ ec2_client .disassociate_address (AllocationId = eip_info ["allocation_id" ])
303+ except ClientError as disassociate_error :
304+ print (json .dumps ({
305+ "Level" : "warning" ,
306+ "Message" : f"Failed to disassociate EIP { eip_info ['allocation_id' ]} " ,
307+ "Exception" : str (disassociate_error )
308+ }))
309+
310+ # Release the EIP
311+ ec2_client .release_address (AllocationId = eip_info ["allocation_id" ])
312+
313+ print (json .dumps ({
314+ "Level" : "info" ,
315+ "AllocationId" : eip_info ["allocation_id" ],
316+ "Message" : "Successfully released orphaned EIP"
317+ }))
318+
319+ except ClientError as error :
320+ print (json .dumps ({
321+ "Level" : "error" ,
322+ "AllocationId" : eip_info ["allocation_id" ],
323+ "Message" : f"Failed to release orphaned EIP" ,
324+ "Exception" : str (error )
325+ }))
326+
327+ if not eips_to_cleanup :
328+ print (json .dumps ({
329+ "Level" : "info" ,
330+ "Message" : "No orphaned EIPs found to clean up"
331+ }))
332+ else :
333+ print (json .dumps ({
334+ "Level" : "info" ,
335+ "Message" : f"Cleaned up { len (eips_to_cleanup )} orphaned EIP(s)"
336+ }))
337+
338+ except ClientError as error :
339+ print (json .dumps ({
340+ "Level" : "error" ,
341+ "Message" : "Failed to describe EIPs for cleanup" ,
342+ "Exception" : str (error )
343+ }))
344+
345+
226346# context not used: this is the interface for a AWS Lambda function defined by AWS
227347# pylint: disable=unused-argument
228348def handler (event , context ):
@@ -269,6 +389,9 @@ def handler(event, context):
269389
270390 remove_unused_ssh_key_pairs (client = client , executor_name_part = os .environ ['NAME_EXECUTOR_INSTANCE' ])
271391
392+ # Clean up orphaned EIPs from terminated instances
393+ cleanup_orphaned_eips (ec2_client = client , executor_name_part = os .environ ['NAME_EXECUTOR_INSTANCE' ])
394+
272395 return "Housekeeping done"
273396
274397
0 commit comments