package com.hubspot.baragon.service.elb; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.AmazonClientException; import com.amazonaws.services.elasticloadbalancing.AmazonElasticLoadBalancingClient; import com.amazonaws.services.elasticloadbalancing.model.AttachLoadBalancerToSubnetsRequest; import com.amazonaws.services.elasticloadbalancing.model.DeregisterInstancesFromLoadBalancerRequest; import com.amazonaws.services.elasticloadbalancing.model.DescribeInstanceHealthRequest; import com.amazonaws.services.elasticloadbalancing.model.DescribeInstanceHealthResult; import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersRequest; import com.amazonaws.services.elasticloadbalancing.model.DescribeLoadBalancersResult; import com.amazonaws.services.elasticloadbalancing.model.EnableAvailabilityZonesForLoadBalancerRequest; import com.amazonaws.services.elasticloadbalancing.model.Instance; import com.amazonaws.services.elasticloadbalancing.model.InstanceState; import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription; import com.amazonaws.services.elasticloadbalancing.model.RegisterInstancesWithLoadBalancerRequest; import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; import com.google.inject.Inject; import com.google.inject.name.Named; import com.hubspot.baragon.data.BaragonKnownAgentsDatastore; import com.hubspot.baragon.data.BaragonLoadBalancerDatastore; import com.hubspot.baragon.models.AgentCheckInResponse; import com.hubspot.baragon.models.BaragonAgentMetadata; import com.hubspot.baragon.models.BaragonGroup; import com.hubspot.baragon.models.TrafficSource; import com.hubspot.baragon.models.TrafficSourceState; import com.hubspot.baragon.models.TrafficSourceType; import com.hubspot.baragon.service.BaragonServiceModule; import com.hubspot.baragon.service.config.ElbConfiguration; import com.hubspot.baragon.service.exceptions.BaragonExceptionNotifier; public class ClassicLoadBalancer extends ElasticLoadBalancer { private static final Logger LOG = LoggerFactory.getLogger(ClassicLoadBalancer.class); private final AmazonElasticLoadBalancingClient elbClient; @Inject public ClassicLoadBalancer(Optional<ElbConfiguration> configuration, BaragonExceptionNotifier exceptionNotifier, BaragonLoadBalancerDatastore loadBalancerDatastore, BaragonKnownAgentsDatastore knownAgentsDatastore, @Named(BaragonServiceModule.BARAGON_AWS_ELB_CLIENT_V1) AmazonElasticLoadBalancingClient elbClient) { super(configuration, exceptionNotifier, loadBalancerDatastore, knownAgentsDatastore); this.elbClient = elbClient; } public boolean isInstanceHealthy(String instanceId, String elbName) { DescribeInstanceHealthRequest describeRequest = new DescribeInstanceHealthRequest(elbName); DescribeInstanceHealthResult result = elbClient.describeInstanceHealth(describeRequest); boolean instanceIsHealthy = false; for (InstanceState instanceState : result.getInstanceStates()) { if (instanceState.getState().equals("InService")) { if (instanceState.getInstanceId().equals(instanceId)) { instanceIsHealthy = true; } } } return instanceIsHealthy; } public AgentCheckInResponse removeInstance(Instance instance, String elbName, String agentId) { Optional<LoadBalancerDescription> elb = getElb(elbName); if (elb.isPresent()) { if (elb.get().getInstances().contains(instance)) { DeregisterInstancesFromLoadBalancerRequest request = new DeregisterInstancesFromLoadBalancerRequest(elbName, Arrays.asList(instance)); elbClient.deregisterInstancesFromLoadBalancer(request); LOG.info("Deregistered instance {} from ELB {}", request.getInstances(), request.getLoadBalancerName()); } else { LOG.debug("Agent {} already de-registered from ELB {}", agentId, elbName); } } return new AgentCheckInResponse(TrafficSourceState.DONE, Optional.absent(), 0L); } public AgentCheckInResponse registerInstance(Instance instance, String elbName, BaragonAgentMetadata agent) { Optional<String> maybeException = Optional.absent(); Optional<LoadBalancerDescription> elb = getElb(elbName); if (elb.isPresent()) { if (isVpcOk(agent, elb.get())) { if (!elb.get().getInstances().contains(instance)) { checkAZEnabled(agent, elbName, elb.get()); RegisterInstancesWithLoadBalancerRequest request = new RegisterInstancesWithLoadBalancerRequest(elbName, Arrays.asList(instance)); elbClient.registerInstancesWithLoadBalancer(request); LOG.info("Registered instances {} with ELB {}", request.getInstances(), request.getLoadBalancerName()); } else { LOG.debug("Agent {} already registered with ELB {}", agent.getAgentId(), elbName); } } else { maybeException = Optional.of(String.format("No ELB found for vpc %s", agent.getEc2().getVpcId())); } } return new AgentCheckInResponse(TrafficSourceState.DONE, maybeException, 0L); } public AgentCheckInResponse checkRegisteredInstance(Instance instance, String trafficSourceName, BaragonAgentMetadata agent) { return new AgentCheckInResponse(TrafficSourceState.DONE, Optional.absent(), 0L); } public AgentCheckInResponse checkRemovedInstance(Instance instance, String elbName, String agentId) { return new AgentCheckInResponse(TrafficSourceState.DONE, Optional.absent(), 0L); } public void syncAll(Collection<BaragonGroup> groups) { try { List<LoadBalancerDescription> elbs = elbClient.describeLoadBalancers().getLoadBalancerDescriptions(); for (BaragonGroup group : groups) { if (!group.getTrafficSources().isEmpty()) { List<LoadBalancerDescription> elbsForGroup = getElbsForGroup(elbs, group); LOG.debug("Registering new instances for group {}...", group.getName()); registerNewInstances(elbsForGroup, group); if (configuration.get().isDeregisterEnabled()) { LOG.debug("Deregistering old instances for group {}...", group.getName()); deregisterOldInstances(elbsForGroup, group); } LOG.debug("ELB sync complete for group: {}", group.getName()); } else { LOG.debug("No traffic sources present for group: {}", group.getName()); } } } catch (AmazonClientException e) { LOG.error("Could not retrieve elb information due to amazon client error %s", e); exceptionNotifier.notify(e, ImmutableMap.of("groups", groups == null ? "" : groups.toString())); } catch (Exception e) { LOG.error("Could not process elb sync due to error {}", e); exceptionNotifier.notify(e, ImmutableMap.of("groups", groups == null ? "" : groups.toString())); } } private boolean isVpcOk(BaragonAgentMetadata agent, LoadBalancerDescription elb) { if (agent.getEc2().getVpcId().isPresent()) { return agent.getEc2().getVpcId().get().equals(elb.getVPCId()) || !configuration.get().isCheckForCorrectVpc(); } else { return !configuration.get().isCheckForCorrectVpc(); } } private void checkAZEnabled(BaragonAgentMetadata agent, String elbName, List<LoadBalancerDescription> elbs) { for (LoadBalancerDescription elb : elbs) { checkAZEnabled(agent, elbName, elb); } } private void checkAZEnabled(BaragonAgentMetadata agent, String elbName,LoadBalancerDescription elb) { if (agent.getEc2().getAvailabilityZone().isPresent()) { String availabilityZone = agent.getEc2().getAvailabilityZone().get(); if (elb.getLoadBalancerName().equals(elbName) && !elb.getAvailabilityZones().contains(availabilityZone)) { try { if (agent.getEc2().getSubnetId().isPresent()) { addSubnet(agent, elb); } else { enableAZ(agent, availabilityZone, elb); } } catch (AmazonClientException e) { LOG.error("Could not enable availability zone {} for elb {} due to error", availabilityZone, elb.getLoadBalancerName(), e); exceptionNotifier.notify(e, ImmutableMap.of("elb", elbName, "subnet", agent.getEc2().getSubnetId().toString(), "availabilityZone", availabilityZone)); } } } else { LOG.warn("No availability zone specified for agent {}", agent.getAgentId()); } } private void addSubnet(BaragonAgentMetadata agent, LoadBalancerDescription elb) { LOG.info("Enabling subnet {} in preparation for agent {}", agent.getEc2().getSubnetId().get(), agent.getAgentId()); AttachLoadBalancerToSubnetsRequest request = new AttachLoadBalancerToSubnetsRequest(); request.setLoadBalancerName(elb.getLoadBalancerName()); List<String> subnets = elb.getSubnets(); subnets.add(agent.getEc2().getSubnetId().get()); request.setSubnets(subnets); elbClient.attachLoadBalancerToSubnets(request); } private void enableAZ(BaragonAgentMetadata agent, String availabilityZone, LoadBalancerDescription elb) { LOG.info("Enabling availability zone {} in preparation for agent {}", availabilityZone, agent.getAgentId()); List<String> availabilityZones = elb.getAvailabilityZones(); availabilityZones.add(availabilityZone); EnableAvailabilityZonesForLoadBalancerRequest request = new EnableAvailabilityZonesForLoadBalancerRequest(); request.setAvailabilityZones(availabilityZones); request.setLoadBalancerName(elb.getLoadBalancerName()); elbClient.enableAvailabilityZonesForLoadBalancer(request); } private List<LoadBalancerDescription> getElbsForGroup(List<LoadBalancerDescription> elbs, BaragonGroup group) { List<LoadBalancerDescription> elbsForGroup = new ArrayList<>(); for (LoadBalancerDescription elb : elbs) { List<String> trafficSourceNames = new ArrayList<>(); for (TrafficSource trafficSource : group.getTrafficSources()) { if (trafficSource.getType() == TrafficSourceType.CLASSIC) { trafficSourceNames.add(trafficSource.getName()); } } if (trafficSourceNames.contains(elb.getLoadBalancerName())) { elbsForGroup.add(elb); } } return elbsForGroup; } private void registerNewInstances(List<LoadBalancerDescription> elbs, BaragonGroup group) { Collection<BaragonAgentMetadata> agents = loadBalancerDatastore.getAgentMetadata(group.getName()); List<RegisterInstancesWithLoadBalancerRequest> requests = registerRequests(group, agents, elbs); if (!requests.isEmpty()) { for (RegisterInstancesWithLoadBalancerRequest request : requests) { try { elbClient.registerInstancesWithLoadBalancer(request); LOG.info("Registered instances {} with ELB {}", request.getInstances(), request.getLoadBalancerName()); } catch (AmazonClientException e) { LOG.error("Could not register {} with elb {} due to error", request.getInstances(), request.getLoadBalancerName(), e); exceptionNotifier.notify(e, ImmutableMap.of("elb", request.getLoadBalancerName(), "toAdd", request.getInstances().toString())); } } } else { LOG.debug("No new instances to register for group {}", group.getName()); } } private List<RegisterInstancesWithLoadBalancerRequest> registerRequests(BaragonGroup group, Collection<BaragonAgentMetadata> agents, List<LoadBalancerDescription> elbs) { List<RegisterInstancesWithLoadBalancerRequest> requests = new ArrayList<>(); for (BaragonAgentMetadata agent : agents) { try { for (TrafficSource source : group.getTrafficSources()) { if (source.getType() != TrafficSourceType.CLASSIC) { continue; } if (agent.getEc2().getInstanceId().isPresent()) { if (shouldRegister(agent, source.getName(), elbs)) { Instance instance = new Instance(agent.getEc2().getInstanceId().get()); requests.add(new RegisterInstancesWithLoadBalancerRequest(source.getName(), Arrays.asList(instance))); checkAZEnabled(agent, source.getName(), elbs); LOG.info("Will register {}-{} with ELB {}", agent.getAgentId(), agent.getEc2().getInstanceId().get(), source.getName()); } else { LOG.debug("Agent {} is already registered", agent); } } else { throw new IllegalArgumentException(String.format("Agent Instance Id must be present to register with an ELB (agent: %s)", agent.getAgentId())); } } } catch (Exception e) { LOG.error("Could not create request for BaragonAgent {} due to error: {}", agent, e); } } return requests; } private boolean shouldRegister(BaragonAgentMetadata agent, String elbName, List<LoadBalancerDescription> elbs) { Optional<LoadBalancerDescription> matchingElb = Optional.absent(); for (LoadBalancerDescription elb : elbs) { if (elbName.equals(elb.getLoadBalancerName())) { matchingElb = Optional.of(elb); } } if (!matchingElb.isPresent()) { return false; } boolean alreadyRegistered = false; for (Instance instance : matchingElb.get().getInstances()) { if (agent.getEc2().getInstanceId().get().equals(instance.getInstanceId())) { alreadyRegistered = true; } } return !alreadyRegistered && (isVpcOk(agent, matchingElb.get()) || !configuration.get().isCheckForCorrectVpc()); } private void deregisterOldInstances(List<LoadBalancerDescription> elbs, BaragonGroup group) { Collection<BaragonAgentMetadata> agents = loadBalancerDatastore.getAgentMetadata(group.getName()); try { List<DeregisterInstancesFromLoadBalancerRequest> requests = deregisterRequests(group, agents, elbs); for (DeregisterInstancesFromLoadBalancerRequest request : requests) { try { if (configuration.get().isRemoveLastHealthyEnabled() || !isLastHealthyInstance(request)) { elbClient.deregisterInstancesFromLoadBalancer(request); } else { LOG.info("Will not deregister {} because it is the last healthy instance!", request.getInstances()); } LOG.info("Deregistered instances {} from ELB {}", request.getInstances(), request.getLoadBalancerName()); } catch (AmazonClientException e) { LOG.error("Could not deregister {} from elb {} due to error {}", request.getInstances(), request.getLoadBalancerName(), e); exceptionNotifier.notify(e, ImmutableMap.of("elb", request.getLoadBalancerName(), "toRemove", request.getInstances().toString())); } } } catch (Exception e) { LOG.error("Will not try to deregister due to error: {}", e); } } private List<DeregisterInstancesFromLoadBalancerRequest> deregisterRequests(BaragonGroup group, Collection<BaragonAgentMetadata> agents, List<LoadBalancerDescription> elbs) { List<String> agentInstanceIds = agentInstanceIds(agents); List<DeregisterInstancesFromLoadBalancerRequest> requests = new ArrayList<>(); for (LoadBalancerDescription elb : elbs) { if (group.getTrafficSources().contains(new TrafficSource(elb.getLoadBalancerName(), TrafficSourceType.CLASSIC))) { for (Instance instance : elb.getInstances()) { if (!agentInstanceIds.contains(instance.getInstanceId()) && canDeregisterAgent(group, instance.getInstanceId())) { List<Instance> instanceList = new ArrayList<>(1); instanceList.add(instance); requests.add(new DeregisterInstancesFromLoadBalancerRequest(elb.getLoadBalancerName(), instanceList)); LOG.info("Will deregister instance {} from ELB {}", instance.getInstanceId(), elb.getLoadBalancerName()); } } } } return requests; } private boolean isLastHealthyInstance(DeregisterInstancesFromLoadBalancerRequest request) throws AmazonClientException { DescribeInstanceHealthRequest describeRequest = new DescribeInstanceHealthRequest(request.getLoadBalancerName()); DescribeInstanceHealthResult result = elbClient.describeInstanceHealth(describeRequest); boolean instanceIsHealthy = false; int healthyCount = 0; for (InstanceState instanceState : result.getInstanceStates()) { if (instanceState.getState().equals("InService")) { healthyCount++; if (instanceState.getInstanceId().equals(request.getInstances().get(0).getInstanceId())) { //Will only ever be one instance per request instanceIsHealthy = true; } } } return (instanceIsHealthy && healthyCount == 1); } private Optional<LoadBalancerDescription> getElb(String elbName) { DescribeLoadBalancersRequest request = new DescribeLoadBalancersRequest(Arrays.asList(elbName)); DescribeLoadBalancersResult result = elbClient.describeLoadBalancers(request); if (!result.getLoadBalancerDescriptions().isEmpty()) { return Optional.of(result.getLoadBalancerDescriptions().get(0)); } else { return Optional.absent(); } } }