Automatically Assign Elastic IPs to Elastic Beanstalk Instances

I've had a need for a while to automatically assign elastic IP addresses to newly launched Elastic Beanstalk instances. This is super useful if you're using an external database host like Compose, and you only want to allow connections coming from your app instances.

But when elastic beanstalk brings up a new instance it assigns it a random IP address. The script below solves this, by triggering a lambda function with a Cloudwatch rule when a new EC2 instance is launched.

Note: This script assumes you have a pool of elastic IPs – it doesn't provision them.

1. Create a Cloudwatch Rule

Create a cloudwatch rule that runs when an EC2 instance transitions to a "running" state, and have it trigger a lambda function.

2. Create the Lambda Function

Paste the following code into a lambda function:

const AWS = require('aws-sdk');
const ec2 = new AWS.EC2();
const PROD_ENV_NAME = 'my-prod-env-name';

// Example Event
// {
//   "version": "0",
//   "id": "ee376907-2647-4179-9203-343cfb3017a4",
//   "detail-type": "EC2 Instance State-change Notification",
//   "source": "aws.ec2",
//   "account": "123456789012",
//   "time": "2015-11-11T21:30:34Z",
//   "region": "us-east-1",
//   "resources": [
//     "arn:aws:ec2:us-east-1:123456789012:instance/i-abcd1111"
//   ],
//   "detail": {
//     "instance-id": "i-abcd1111",
//     "state": "running"
//   }
// }

exports.handler = async (event) => {
  console.log("EVENT:", event);
  
  // The newly launched instance ID.
  const instanceId = event.detail['instance-id'];
  
  // Fetch info about the newly launched instance
  const result = await ec2.describeInstances({
    Filters: [ { Name: "instance-id", Values: [instanceId] } ]
  }).promise()
  
  // The instance details are buried in this object
  const instance = result.Reservations[0].Instances[0];
  const isAttached = instance.NetworkInterfaces.find(int => int.Association.IpOwnerId !== 'amazon');
  
  // Bail if the instance is already attached to another EIP
  if (isAttached) {
    console.log("This instance is already assigned to an elastic IP")
    return { statusCode: 200, body: '' }
  }
  
  // In elastic beanstalk, the instance name gets assigned to the enviroment name.
  // There is also an environment name tag, which could be used here.
  const name = instance.Tags.find(t => t.Key === 'Name').Value;
  
  // Only assign EIPs to production instances
  if (name !== PROD_ENV_NAME) {
    console.log('Not a production instance. Not assigning. Instance name:', name)
    return { statusCode: 200, body: ''}
  }
  
  // Get a list of elastic IP addresses
  const addresses = await ec2.describeAddresses().promise();
  
  // Filter out addresses already assigned to instances
  const availableAddresses = addresses.Addresses.filter(a => !a.NetworkInterfaceId);
  
  // Raise an error if we have no more available IP addresses
  if (availableAddresses.length === 0) {
    console.log("ERROR: no available ip addresses");
    return { statusCode: 400, body: JSON.stringify("ERROR: no available ip addresses") }
  }
  
  const firstAvail = availableAddresses[0]
  try {
    // Associate the instance to the address
    const result = await ec2.associateAddress({
      AllocationId: firstAvail.AllocationId,
      InstanceId: instanceId
    }).promise();
    
    console.log('allocation result', result)
    
    return { statusCode: 200, body: JSON.stringify('Associated IP address.') };
  } catch (err) {
      console.log("ERROR: ", err);
  }
};

Whenever your elastic beanstalk environment launches a new instance, the above lambda function will get hit, and if there's an available elastic IP address, it will assign it to the new instance.

#Work Journal, aws, devops