com.netflix.eureka.aws.ElasticNetworkInterfaceBinder Maven / Gradle / Ivy
package com.netflix.eureka.aws;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.auth.InstanceProfileCredentialsProvider;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.*;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Ordering;
import com.google.common.net.InetAddresses;
import com.netflix.appinfo.AmazonInfo;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.endpoint.EndpointUtils;
import com.netflix.eureka.EurekaServerConfig;
import com.netflix.eureka.registry.PeerAwareInstanceRegistry;
import java.util.ArrayList;
import java.util.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
/**
* Amazon ENI binder for instances.
*
* Candidate ENI's discovery is done using the same mechanism as Elastic ip binder, via dns records or service urls.
*
* The dns records and the service urls should use the ENI private dns or private ip
*
* Dns record examples
* txt.us-east-1.eureka="us-east-1a.eureka" "us-east-1b.eureka"
* txt.us-east-1a.eureka="ip-172-31-y-y.ec2.internal"
* txt.us-east-1b.eureka="ip-172-31-x-x.ec2.internal"
* where "ip-172-31-x-x.ec2.internal" is the ENI private dns
*
* Service url example:
* eureka.serviceUrl.us-east-1a=http://ip-172-31-x-x.ec2.internal:7001/eureka/v2/
*
* ENI Binding strategy should be configured via property like:
*
* eureka.awsBindingStrategy=ENI
*
* If there are no available ENI's for the availability zone, it will not attach any already attached ENI
*/
public class ElasticNetworkInterfaceBinder implements AwsBinder {
private static final Logger logger = LoggerFactory.getLogger(ElasticNetworkInterfaceBinder.class);
private static final int IP_BIND_SLEEP_TIME_MS = 1000;
private static final Timer timer = new Timer("Eureka-ElasticNetworkInterfaceBinder", true);
private final EurekaServerConfig serverConfig;
private final EurekaClientConfig clientConfig;
private final PeerAwareInstanceRegistry registry;
private final ApplicationInfoManager applicationInfoManager;
@Inject
public ElasticNetworkInterfaceBinder(
EurekaServerConfig serverConfig,
EurekaClientConfig clientConfig,
PeerAwareInstanceRegistry registry,
ApplicationInfoManager applicationInfoManager) {
this.serverConfig = serverConfig;
this.clientConfig = clientConfig;
this.registry = registry;
this.applicationInfoManager = applicationInfoManager;
}
@PostConstruct
public void start() {
int retries = serverConfig.getEIPBindRebindRetries();
for (int i = 0; i < retries; i++) {
try {
if (alreadyBound()) {
break;
} else {
bind();
}
} catch (Throwable e) {
logger.error("Cannot bind to IP", e);
try {
Thread.sleep(IP_BIND_SLEEP_TIME_MS);
} catch (InterruptedException e1) {
throw new RuntimeException(e1);
}
}
}
// Schedule a timer which periodically checks for IP binding.
timer.schedule(new IPBindingTask(), serverConfig.getEIPBindingRetryIntervalMsWhenUnbound());
}
@PreDestroy
public void shutdown() {
timer.cancel();
for (int i = 0; i < serverConfig.getEIPBindRebindRetries(); i++) {
try {
unbind();
break;
} catch (Exception e) {
logger.warn("Cannot unbind the IP from the instance");
try {
Thread.sleep(IP_BIND_SLEEP_TIME_MS);
} catch (InterruptedException e1) {
throw new RuntimeException(e1);
}
}
}
}
public boolean alreadyBound() throws MalformedURLException {
InstanceInfo myInfo = applicationInfoManager.getInfo();
String myInstanceId = ((AmazonInfo) myInfo.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.instanceId);
AmazonEC2 ec2Service = getEC2Service();
List instanceNetworkInterfaces = instanceData(myInstanceId, ec2Service).getNetworkInterfaces();
List candidateIPs = getCandidateIps();
for (String ip : candidateIPs) {
for(InstanceNetworkInterface ini: instanceNetworkInterfaces) {
if (ip.equals(ini.getPrivateIpAddress())) {
logger.info("My instance {} seems to be already associated with the ip {}", myInstanceId, ip);
return true;
}
}
}
return false;
}
/**
* Binds an ENI to the instance.
*
* The candidate ENI's are deduced in the same wa the EIP binder works: Via dns records or via service urls,
* depending on configuration.
*
* It will try to attach the first ENI that is:
* Available
* For this subnet
* In the list of candidate ENI's
*
* @throws MalformedURLException
*/
public void bind() throws MalformedURLException {
InstanceInfo myInfo = ApplicationInfoManager.getInstance().getInfo();
String myInstanceId = ((AmazonInfo) myInfo.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.instanceId);
String myZone = ((AmazonInfo) myInfo.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.availabilityZone);
final List ips = getCandidateIps();
Ordering ipsOrder = Ordering.natural().onResultOf(new Function() {
public Integer apply(NetworkInterface networkInterface) {
return ips.indexOf(networkInterface.getPrivateIpAddress());
}
});
AmazonEC2 ec2Service = getEC2Service();
String subnetId = instanceData(myInstanceId, ec2Service).getSubnetId();
DescribeNetworkInterfacesResult result = ec2Service
.describeNetworkInterfaces(new DescribeNetworkInterfacesRequest()
.withFilters(new Filter("private-ip-address", ips))
.withFilters(new Filter("status", Collections.singletonList("available")))
.withFilters(new Filter("subnet-id", Collections.singletonList(subnetId)))
);
if (result.getNetworkInterfaces().isEmpty()) {
logger.info("No ip is free to be associated with this instance. Candidate ips are: {} for zone: {}", ips, myZone);
} else {
NetworkInterface selected = ipsOrder.min(result.getNetworkInterfaces());
ec2Service.attachNetworkInterface(
new AttachNetworkInterfaceRequest()
.withNetworkInterfaceId(selected.getNetworkInterfaceId())
.withDeviceIndex(1)
.withInstanceId(myInstanceId)
);
}
}
/**
* Unbind the IP that this instance is associated with.
*/
public void unbind() throws Exception {
InstanceInfo myInfo = applicationInfoManager.getInfo();
String myInstanceId = ((AmazonInfo) myInfo.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.instanceId);
AmazonEC2 ec2 = getEC2Service();
List result = instanceData(myInstanceId, ec2).getNetworkInterfaces();
List ips = getCandidateIps();
for(InstanceNetworkInterface networkInterface: result){
if (ips.contains(networkInterface.getPrivateIpAddress())) {
String attachmentId = networkInterface.getAttachment().getAttachmentId();
ec2.detachNetworkInterface(new DetachNetworkInterfaceRequest().withAttachmentId(attachmentId));
break;
}
}
}
private Instance instanceData(String myInstanceId, AmazonEC2 ec2) {
return ec2.describeInstances(new DescribeInstancesRequest().withInstanceIds(myInstanceId)).getReservations().get(0).getInstances().get(0);
}
/**
* Based on shouldUseDnsForFetchingServiceUrls configuration, either retrieves candidates from dns records or from
* configuration properties.
*
*
*/
public List getCandidateIps() throws MalformedURLException {
InstanceInfo myInfo = applicationInfoManager.getInfo();
String myZone = ((AmazonInfo) myInfo.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.availabilityZone);
Collection candidates = clientConfig.shouldUseDnsForFetchingServiceUrls()
? getIPsForZoneFromDNS(myZone)
: getIPsForZoneFromConfig(myZone);
if (candidates == null || candidates.size() == 0) {
throw new RuntimeException("Could not get any ips from the pool for zone :" + myZone);
}
List ips = new ArrayList<>();
for(String candidate : candidates) {
String host = new URL(candidate).getHost();
if (InetAddresses.isInetAddress(host)) {
ips.add(host);
} else {
// ip-172-31-55-172.ec2.internal -> ip-172-31-55-172
String firstPartOfHost = Splitter.on(".").splitToList(host).get(0);
// ip-172-31-55-172 -> [172,31,55,172]
List noIpPrefix = Splitter.on("-").splitToList(firstPartOfHost).subList(1, 5);
// [172,31,55,172] -> 172.31.55.172
String ip = Joiner.on(".").join(noIpPrefix);
if (InetAddresses.isInetAddress(ip)) {
ips.add(ip);
} else {
throw new IllegalArgumentException("Illegal internal hostname " + host + " translated to '" + ip + "'");
}
}
}
return ips;
}
private Collection getIPsForZoneFromConfig(String myZone) {
return clientConfig.getEurekaServerServiceUrls(myZone);
}
private Collection getIPsForZoneFromDNS(String myZone) {
return EndpointUtils.getServiceUrlsFromDNS(
clientConfig,
myZone,
true,
new EndpointUtils.InstanceInfoBasedUrlRandomizer(applicationInfoManager.getInfo())
);
}
private AmazonEC2 getEC2Service() {
String aWSAccessId = serverConfig.getAWSAccessId();
String aWSSecretKey = serverConfig.getAWSSecretKey();
AmazonEC2 ec2Service;
if (null != aWSAccessId && !"".equals(aWSAccessId)
&& null != aWSSecretKey && !"".equals(aWSSecretKey)) {
ec2Service = new AmazonEC2Client(new BasicAWSCredentials(aWSAccessId, aWSSecretKey));
} else {
ec2Service = new AmazonEC2Client(new InstanceProfileCredentialsProvider());
}
String region = clientConfig.getRegion();
region = region.trim().toLowerCase();
ec2Service.setEndpoint("ec2." + region + ".amazonaws.com");
return ec2Service;
}
private class IPBindingTask extends TimerTask {
@Override
public void run() {
boolean alreadyBound = false;
try {
alreadyBound = alreadyBound();
// If the ip is not bound, the registry could be stale. First sync up the registry from the
// neighboring node before trying to bind the IP
if (!alreadyBound) {
registry.clearRegistry();
int count = registry.syncUp();
registry.openForTraffic(applicationInfoManager, count);
} else {
// An ip is already bound
return;
}
bind();
} catch (Throwable e) {
logger.error("Could not bind to IP", e);
} finally {
if (alreadyBound) {
timer.schedule(new IPBindingTask(), serverConfig.getEIPBindingRetryIntervalMs());
} else {
timer.schedule(new IPBindingTask(), serverConfig.getEIPBindingRetryIntervalMsWhenUnbound());
}
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy