/* * Copyright 2012 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.eureka.aws; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Timer; import java.util.TimerTask; 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.Address; import com.amazonaws.services.ec2.model.AssociateAddressRequest; import com.amazonaws.services.ec2.model.DescribeAddressesRequest; import com.amazonaws.services.ec2.model.DescribeAddressesResult; import com.amazonaws.services.ec2.model.DisassociateAddressRequest; import com.netflix.appinfo.AmazonInfo; import com.netflix.appinfo.AmazonInfo.MetaDataKey; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.DataCenterInfo.Name; 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 com.netflix.servo.monitor.Monitors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Singleton; /** * An AWS specific <em>elastic ip</em> binding utility for binding eureka * servers for a well known <code>IP address</code>. * * <p> * <em>Eureka</em> clients talk to <em>Eureka</em> servers bound with well known * <code>IP addresses</code> since that is the most reliable mechanism to * discover the <em>Eureka</em> servers. When Eureka servers come up they bind * themselves to a well known <em>elastic ip</em> * </p> * * <p> * This binding mechanism gravitates towards one eureka server per zone for * resilience.Atleast one elastic ip should be slotted for each eureka server in * a zone. If more than eureka server is launched per zone and there are not * enough elastic ips slotted, the server tries to pick a free EIP slotted for other * zones and if it still cannot find a free EIP, waits and keeps trying. * </p> * * @author Karthik Ranganathan, Greg Kim * */ @Singleton public class EIPManager implements AwsBinder { private static final Logger logger = LoggerFactory.getLogger(EIPManager.class); private static final String US_EAST_1 = "us-east-1"; private static final int EIP_BIND_SLEEP_TIME_MS = 1000; private static final Timer timer = new Timer("Eureka-EIPBinder", true); private final EurekaServerConfig serverConfig; private final EurekaClientConfig clientConfig; private final PeerAwareInstanceRegistry registry; private final ApplicationInfoManager applicationInfoManager; @Inject public EIPManager(EurekaServerConfig serverConfig, EurekaClientConfig clientConfig, PeerAwareInstanceRegistry registry, ApplicationInfoManager applicationInfoManager) { this.serverConfig = serverConfig; this.clientConfig = clientConfig; this.registry = registry; this.applicationInfoManager = applicationInfoManager; try { Monitors.registerObject(this); } catch (Throwable e) { logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e); } } @PostConstruct public void start() throws Exception { handleEIPBinding(); } @PreDestroy public void shutdown() throws Exception { timer.cancel(); for (int i = 0; i < serverConfig.getEIPBindRebindRetries(); i++) { try { unbindEIP(); break; } catch (Exception e) { logger.warn("Cannot unbind the EIP from the instance"); Thread.sleep(1000); } } } /** * Handles EIP binding process in AWS Cloud. * * @throws InterruptedException */ private void handleEIPBinding() throws InterruptedException { int retries = serverConfig.getEIPBindRebindRetries(); // Bind to EIP if needed for (int i = 0; i < retries; i++) { try { if (isEIPBound()) { break; } else { bindEIP(); } } catch (Throwable e) { logger.error("Cannot bind to EIP", e); Thread.sleep(EIP_BIND_SLEEP_TIME_MS); } } // Schedule a timer which periodically checks for EIP binding. timer.schedule(new EIPBindingTask(), serverConfig.getEIPBindingRetryIntervalMsWhenUnbound()); } /** * Checks if an EIP is already bound to the instance. * @return true if an EIP is bound, false otherwise */ public boolean isEIPBound() { InstanceInfo myInfo = applicationInfoManager.getInfo(); String myInstanceId = ((AmazonInfo) myInfo.getDataCenterInfo()).get(MetaDataKey.instanceId); String myZone = ((AmazonInfo) myInfo.getDataCenterInfo()).get(MetaDataKey.availabilityZone); String myPublicIP = ((AmazonInfo) myInfo.getDataCenterInfo()).get(MetaDataKey.publicIpv4); Collection<String> candidateEIPs = getCandidateEIPs(myInstanceId, myZone); for (String eipEntry : candidateEIPs) { if (eipEntry.equals(myPublicIP)) { logger.info("My instance {} seems to be already associated with the public ip {}", myInstanceId, myPublicIP); return true; } } return false; } /** * Checks if an EIP is bound and optionally binds the EIP. * * The list of EIPs are arranged with the EIPs allocated in the zone first * followed by other EIPs. * * If an EIP is already bound to this instance this method simply returns. Otherwise, this method tries to find * an unused EIP based on information from AWS. If it cannot find any unused EIP this method, it will be retried * for a specified interval. * * One of the following scenarios can happen here : * * 1) If the instance is already bound to an EIP as deemed by AWS, no action is taken. * 2) If an EIP is already bound to another instance as deemed by AWS, that EIP is skipped. * 3) If an EIP is not already bound to an instance and if this instance is not bound to an EIP, then * the EIP is bound to this instance. */ public void bindEIP() { InstanceInfo myInfo = applicationInfoManager.getInfo(); String myInstanceId = ((AmazonInfo) myInfo.getDataCenterInfo()).get(MetaDataKey.instanceId); String myZone = ((AmazonInfo) myInfo.getDataCenterInfo()).get(MetaDataKey.availabilityZone); Collection<String> candidateEIPs = getCandidateEIPs(myInstanceId, myZone); AmazonEC2 ec2Service = getEC2Service(); boolean isMyinstanceAssociatedWithEIP = false; Address selectedEIP = null; for (String eipEntry : candidateEIPs) { try { String associatedInstanceId; // Check with AWS, if this EIP is already been used by another instance DescribeAddressesRequest describeAddressRequest = new DescribeAddressesRequest().withPublicIps(eipEntry); DescribeAddressesResult result = ec2Service.describeAddresses(describeAddressRequest); if ((result.getAddresses() != null) && (!result.getAddresses().isEmpty())) { Address eipAddress = result.getAddresses().get(0); associatedInstanceId = eipAddress.getInstanceId(); // This EIP is not used by any other instance, hence mark it for selection if it is not // already marked. if (((associatedInstanceId == null) || (associatedInstanceId.isEmpty()))) { if (selectedEIP == null) { selectedEIP = eipAddress; } } else if (isMyinstanceAssociatedWithEIP = (associatedInstanceId.equals(myInstanceId))) { // This EIP is associated with an instance, check if this is the same as the current instance. // If it is the same, stop searching for an EIP as this instance is already associated with an // EIP selectedEIP = eipAddress; break; } else { // The EIP is used by some other instance, hence skip it logger.warn("The selected EIP {} is associated with another instance {} according to AWS," + " hence skipping this", eipEntry, associatedInstanceId); } } } catch (Throwable t) { logger.error("Failed to bind elastic IP: {} to {}", eipEntry, myInstanceId, t); } } if (null != selectedEIP) { String publicIp = selectedEIP.getPublicIp(); // Only bind if the EIP is not already associated if (!isMyinstanceAssociatedWithEIP) { AssociateAddressRequest associateAddressRequest = new AssociateAddressRequest() .withInstanceId(myInstanceId); String domain = selectedEIP.getDomain(); if ("vpc".equals(domain)) { associateAddressRequest.setAllocationId(selectedEIP.getAllocationId()); } else { associateAddressRequest.setPublicIp(publicIp); } ec2Service.associateAddress(associateAddressRequest); logger.info("\n\n\nAssociated {} running in zone: {} to elastic IP: {}", myInstanceId, myZone, publicIp); } logger.info("My instance {} seems to be already associated with the EIP {}", myInstanceId, publicIp); } else { logger.info("No EIP is free to be associated with this instance. Candidate EIPs are: {}", candidateEIPs); } } /** * Unbind the EIP that this instance is associated with. */ public void unbindEIP() throws Exception { InstanceInfo myInfo = applicationInfoManager.getInfo(); String myPublicIP = null; if (myInfo != null && myInfo.getDataCenterInfo().getName() == Name.Amazon) { myPublicIP = ((AmazonInfo) myInfo.getDataCenterInfo()) .get(MetaDataKey.publicIpv4); if (myPublicIP == null) { logger.info("Instance is not associated with an EIP. Will not try to unbind"); return; } try { AmazonEC2 ec2Service = getEC2Service(); DescribeAddressesRequest describeAddressRequest = new DescribeAddressesRequest() .withPublicIps(myPublicIP); DescribeAddressesResult result = ec2Service.describeAddresses(describeAddressRequest); if ((result.getAddresses() != null) && (!result.getAddresses().isEmpty())) { Address eipAddress = result.getAddresses().get(0); DisassociateAddressRequest dissociateRequest = new DisassociateAddressRequest(); String domain = eipAddress.getDomain(); if ("vpc".equals(domain)) { dissociateRequest.setAssociationId(eipAddress.getAssociationId()); } else { dissociateRequest.setPublicIp(eipAddress.getPublicIp()); } ec2Service.disassociateAddress(dissociateRequest); logger.info("Dissociated the EIP {} from this instance", myPublicIP); } } catch (Throwable e) { throw new RuntimeException("Cannot dissociate address from this instance", e); } } } /** * Get the list of EIPs in the order of preference depending on instance zone. * * @param myInstanceId * the instance id for this instance * @param myZone * the zone where this instance is in * @return Collection containing the list of available EIPs */ public Collection<String> getCandidateEIPs(String myInstanceId, String myZone) { if (myZone == null) { myZone = "us-east-1d"; } Collection<String> eipCandidates = clientConfig.shouldUseDnsForFetchingServiceUrls() ? getEIPsForZoneFromDNS(myZone) : getEIPsForZoneFromConfig(myZone); if (eipCandidates == null || eipCandidates.size() == 0) { throw new RuntimeException("Could not get any elastic ips from the EIP pool for zone :" + myZone); } return eipCandidates; } /** * Get the list of EIPs from the configuration. * * @param myZone * - the zone in which the instance resides. * @return collection of EIPs to choose from for binding. */ private Collection<String> getEIPsForZoneFromConfig(String myZone) { List<String> ec2Urls = clientConfig.getEurekaServerServiceUrls(myZone); return getEIPsFromServiceUrls(ec2Urls); } /** * Get the list of EIPs from the ec2 urls. * * @param ec2Urls * the ec2urls for which the EIP needs to be obtained. * @return collection of EIPs. */ private Collection<String> getEIPsFromServiceUrls(List<String> ec2Urls) { List<String> returnedUrls = new ArrayList<String>(); String region = clientConfig.getRegion(); String regionPhrase = ""; if (!US_EAST_1.equals(region)) { regionPhrase = "." + region; } for (String cname : ec2Urls) { int beginIndex = cname.indexOf("ec2-") + 4; // Handle case where there are no cnames containing "ec2-" // Reasons include: // Systems without public addresses - purely attached to corp lan via AWS Direct Connect if (-1 < beginIndex) { int endIndex = cname.indexOf(regionPhrase + ".compute"); String eipStr = cname.substring(beginIndex, endIndex); String eip = eipStr.replaceAll("\\-", "."); returnedUrls.add(eip); } } return returnedUrls; } /** * Get the list of EIPS from the DNS. * * <p> * This mechanism looks for the EIP pool in the zone the instance is in by * looking up the DNS name <code>{zone}.{region}.{domainName}</code>. The * zone is fetched from the {@link InstanceInfo} object;the region is picked * up from the specified configuration * {@link com.netflix.discovery.EurekaClientConfig#getRegion()};the domain name is picked up from * the specified configuration {@link com.netflix.discovery.EurekaClientConfig#getEurekaServerDNSName()} * with a "txt." prefix (see {@link com.netflix.discovery.endpoint.EndpointUtils * #getZoneBasedDiscoveryUrlsFromRegion(com.netflix.discovery.EurekaClientConfig, String)}. * </p> * * @param myZone * the zone where this instance exist in. * @return the collection of EIPs that exist in the zone this instance is * in. */ private Collection<String> getEIPsForZoneFromDNS(String myZone) { List<String> ec2Urls = EndpointUtils.getServiceUrlsFromDNS( clientConfig, myZone, true, new EndpointUtils.InstanceInfoBasedUrlRandomizer(applicationInfoManager.getInfo()) ); return getEIPsFromServiceUrls(ec2Urls); } /** * Gets the EC2 service object to call AWS APIs. * * @return the EC2 service object to call AWS APIs. */ 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; } /** * An EIP binding timer task which constantly polls for EIP in the * same zone and binds it to itself.If the EIP is taken away for some * reason, this task tries to get the EIP back. Hence it is advised to take * one EIP assignment per instance in a zone. */ private class EIPBindingTask extends TimerTask { @Override public void run() { boolean isEIPBound = false; try { isEIPBound = isEIPBound(); // If the EIP is not bound, the registry could be stale. First sync up the registry from the // neighboring node before trying to bind the EIP if (!isEIPBound) { registry.clearRegistry(); int count = registry.syncUp(); registry.openForTraffic(applicationInfoManager, count); } else { // An EIP is already bound return; } bindEIP(); } catch (Throwable e) { logger.error("Could not bind to EIP", e); } finally { if (isEIPBound) { timer.schedule(new EIPBindingTask(), serverConfig.getEIPBindingRetryIntervalMs()); } else { timer.schedule(new EIPBindingTask(), serverConfig.getEIPBindingRetryIntervalMsWhenUnbound()); } } } }; }