🚀 Executive Summary

TL;DR: Unattached AWS Elastic IPs (EIPs) can silently accumulate significant, unnecessary costs over time due to forgotten projects and deployments. This solution provides a Python script leveraging boto3 to automatically identify these orphan EIPs and send timely alerts to a Slack channel, enabling proactive cost optimization and saving engineering time.

🎯 Key Takeaways

  • Unattached Elastic IPs, even at a few dollars a month, can lead to substantial hidden costs across an AWS account if not regularly monitored and released.
  • An Elastic IP is identified as unattached and incurring charges if its `describe_addresses` output from the EC2 client lacks an `AssociationId`.
  • For production environments, it is highly recommended to use an IAM Role with `ec2:DescribeAddresses` permission on an EC2 instance or Lambda function, rather than hardcoding `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in `config.env`.
  • A simple cron job can automate the script’s execution, transforming the manual task of checking for unattached EIPs into a ‘set it and forget it’ cost-saving solution.
  • The script requires a minimal IAM policy allowing the `ec2:DescribeAddresses` action on `Resource: “*”` to successfully list all Elastic IPs in the specified AWS region.

Alert on Unattached Static IPs (Elastic IPs) costing money

Alert on Unattached Static IPs (Elastic IPs) costing money

Hey team, Darian here. Let me share a quick story. A few years back, I was doing a cost-optimization pass and found something embarrassing: dozens of unattached Elastic IPs (EIPs) just sitting in one of our AWS accounts. They were from old projects, failed deployments, and long-forgotten tests. Each one costs a few dollars a month, which doesn’t sound like much until you multiply it by 30 EIPs over 12 months. It adds up. I used to spend time every other week manually checking the EC2 console for these, until I realized I was just wasting time on a task a simple script could handle.

So, I built this little Python utility. It finds those unattached EIPs and drops a neat alert into our team’s Slack channel. It’s a “set it and forget it” solution that saves us both time and money. Today, I’m going to walk you through how to set it up.

Prerequisites

Before we dive in, make sure you have the following ready:

  • An AWS account with IAM permissions to describe EC2 resources.
  • Python 3 installed on the system where you’ll run the script.
  • Access to a Slack workspace where you can create an Incoming Webhook URL.
  • The ability to install a few Python packages.

The Guide: Step-by-Step

Step 1: Setting Up Your Project

First, find a good spot on your server or machine to house this script. I won’t walk through the standard `mkdir` or virtual environment setup, as everyone has their own preferred workflow. The important part is to create a clean space for your files.

You’ll need three Python libraries: `boto3` (the AWS SDK), `python-dotenv` (to handle our config file), and `requests` (to talk to the Slack API). Once your environment is active, you can install them using pip: pip install boto3 python-dotenv requests.

Next, create two files in your project directory:

  1. check_eips.py: This will be our main script.
  2. config.env: This file will store our sensitive credentials securely, keeping them out of the code.

In your config.env file, add the following lines, filling in your own details:

AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY"
AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_KEY"
AWS_REGION="us-east-1"
SLACK_WEBHOOK_URL="YOUR_SLACK_WEBHOOK_URL"

Pro Tip: In my production setups, I never use hardcoded access keys. I recommend running this script on an EC2 instance or a Lambda function with an IAM Role attached. This is far more secure. If you use an IAM Role, you can remove the AWS key lines from your config.env file, as Boto3 will automatically pick up the credentials.

Step 2: The IAM Policy

Your script needs permission to see the EIPs in your account. The principle of least privilege is key here—we only want to grant the *exact* permissions required. Create an IAM policy with the following JSON. This policy allows read-only access to describe network addresses (which includes EIPs).

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ec2:DescribeAddresses",
            "Resource": "*"
        }
    ]
}

Attach this policy to the IAM user (or role) whose credentials the script will use.

Step 3: The Python Script

Alright, let’s get to the core logic. Open your check_eips.py file and add the following code. I’ve added comments to explain what each part does.

import os
import requests
import boto3
from dotenv import load_dotenv

def find_unattached_eips():
    """
    Scans the AWS account for unattached Elastic IPs.
    """
    # Load environment variables from config.env
    load_dotenv('config.env')
    
    # It's better to use an IAM role, but this method works for local dev.
    # Boto3 will automatically look for these env vars.
    aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID")
    aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY")
    aws_region = os.getenv("AWS_REGION")

    # Initialize the EC2 client
    # If using an IAM role, you can remove the credentials arguments.
    try:
        ec2_client = boto3.client(
            'ec2',
            region_name=aws_region,
            aws_access_key_id=aws_access_key_id,
            aws_secret_access_key=aws_secret_access_key
        )
        
        # Get all Elastic IP addresses
        addresses = ec2_client.describe_addresses()
    except Exception as e:
        print(f"Error connecting to AWS: {e}")
        return []

    unattached_ips = []
    # The logic is simple: if an EIP has no AssociationId, it's not attached.
    for address in addresses.get('Addresses', []):
        if 'AssociationId' not in address:
            # We found one! Let's grab its IP address.
            unattached_ips.append(address.get('PublicIp'))
            
    return unattached_ips

def send_slack_notification(ip_list):
    """
    Sends a formatted message to a Slack channel via a webhook.
    """
    webhook_url = os.getenv("SLACK_WEBHOOK_URL")
    if not webhook_url:
        print("Error: SLACK_WEBHOOK_URL not found in config.env.")
        return

    # Format the message
    ip_string = "\n".join([f"• `{ip}`" for ip in ip_list])
    message = {
        "text": f":warning: Found {len(ip_list)} Unattached Elastic IPs!",
        "blocks": [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": ":warning: Unattached Elastic IP Alert"
                }
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"Heads up! The following *{len(ip_list)}* Elastic IPs are unattached and incurring charges. Please review and release them if they are no longer needed."
                }
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": ip_string
                }
            },
            {
                "type": "divider"
            }
        ]
    }

    try:
        response = requests.post(webhook_url, json=message)
        response.raise_for_status() # Raises an exception for bad status codes
        print("Slack notification sent successfully!")
    except requests.exceptions.RequestException as e:
        print(f"Error sending Slack notification: {e}")


if __name__ == "__main__":
    print("Checking for unattached Elastic IPs...")
    orphan_eips = find_unattached_eips()

    if orphan_eips:
        print(f"Found {len(orphan_eips)} unattached EIPs: {orphan_eips}")
        send_slack_notification(orphan_eips)
    else:
        print("No unattached EIPs found. All good!")

Step 4: Automating the Check

Running this manually is fine, but the real power comes from automation. A simple cron job is perfect for this. You’ll want to add an entry to your system’s scheduler to run the script on a regular basis. I find that a weekly check is usually sufficient.

Here’s an example cron entry that runs the script every Monday at 2 AM:

0 2 * * 1 python3 script.py

Make sure you adjust the command to use the correct path to your Python interpreter and script file.

Common Pitfalls

Here are a couple of areas where I’ve tripped up in the past. Hopefully, you can avoid them!

  • IAM Permissions Failure: The most common issue is the script failing with an “Access Denied” error. This almost always means the IAM policy is either wrong or not attached to the correct user/role. Double-check that `ec2:DescribeAddresses` is allowed.
  • Region Mismatch: The script will only check the AWS region specified in your config.env file. If your team uses multiple regions, you won’t see EIPs in other regions. You’d need to modify the script to loop through a list of regions you want to check.
  • Invalid Slack Webhook: Slack webhooks can be deactivated or expire. If the script runs but you don’t get a notification, the webhook URL is the first thing to check. Make sure it’s correct and active in your Slack App settings.

Conclusion

And that’s it! With a simple Python script and a cron job, you’ve now automated a small but important cost-saving task. This frees up your time to focus on more complex problems, while ensuring you don’t get a surprise on your next AWS bill. You can easily adapt this script to send alerts via email, create a Jira ticket, or whatever fits your team’s workflow best.

Happy automating!

– Darian Vance

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ Why do unattached Elastic IPs cost money in AWS?

AWS charges for Elastic IPs that are not associated with a running EC2 instance, a network interface, or a load balancer. This policy encourages efficient use of public IP addresses and prevents resource hoarding.

âť“ How does this automated solution compare to manual checks for unattached EIPs?

Manual checks are time-consuming, prone to human error, and often inconsistent, leading to overlooked costs. The automated script provides a consistent, ‘set it and forget it’ solution that proactively identifies and alerts on unattached EIPs, saving engineering time and ensuring continuous cost optimization.

âť“ What are common reasons for the script failing to identify all unattached EIPs or send notifications?

Common pitfalls include insufficient IAM permissions (e.g., missing `ec2:DescribeAddresses`), a region mismatch where the script only checks one AWS region while EIPs exist in others, or an invalid/deactivated Slack Webhook URL preventing notification delivery.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading