All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.stratos.aws.extension.AWSLoadBalancer Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.stratos.aws.extension;

import com.amazonaws.services.elasticloadbalancing.model.CreateAppCookieStickinessPolicyResult;
import com.amazonaws.services.elasticloadbalancing.model.Instance;
import com.amazonaws.services.elasticloadbalancing.model.Listener;
import com.amazonaws.services.elasticloadbalancing.model.LoadBalancerDescription;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.stratos.aws.extension.exception.PersistenceException;
import org.apache.stratos.aws.extension.persistence.FileBasedPersistenceManager;
import org.apache.stratos.aws.extension.persistence.PersistenceManager;
import org.apache.stratos.aws.extension.persistence.dto.LBInfoDTO;
import org.apache.stratos.load.balancer.common.domain.Cluster;
import org.apache.stratos.load.balancer.common.domain.Member;
import org.apache.stratos.load.balancer.common.domain.Service;
import org.apache.stratos.load.balancer.common.domain.Topology;
import org.apache.stratos.load.balancer.extension.api.LoadBalancer;
import org.apache.stratos.load.balancer.extension.api.exception.LoadBalancerExtensionException;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class AWSLoadBalancer implements LoadBalancer {

    private static final Log log = LogFactory.getLog(AWSLoadBalancer.class);

    // A map  to store load balancer information
    // against the cluster id
    private static ConcurrentHashMap clusterIdToLoadBalancerMap = new ConcurrentHashMap();

    // Object used to invoke methods related to AWS API
    private AWSHelper awsHelper;

    // PersistenceManager: used to persist LB Information by tuples of 
    PersistenceManager persistenceManager;

    public AWSLoadBalancer() throws LoadBalancerExtensionException {
        awsHelper = new AWSHelper();
        persistenceManager = new FileBasedPersistenceManager();
        initialize();
    }

    /*
     * configure method iterates over topology and configures the AWS load
     * balancers needed. Configuration may involve creating a new load balancer
     * for a cluster, updating existing load balancers or deleting unwanted load
     * balancers.
     */
    public boolean configure(Topology topology)
            throws LoadBalancerExtensionException {

        log.info("AWS load balancer extension is being reconfigured.");

        HashSet activeClusters = new HashSet();

        for (Service service : topology.getServices()) {
            for (Cluster cluster : service.getClusters()) {
                // Check if a load balancer is created for this cluster
                if (clusterIdToLoadBalancerMap.containsKey(cluster.getClusterId())) {
                    // A load balancer is already present for this cluster
                    // Get the load balancer and update it.

                    if (log.isDebugEnabled()) {
                        log.debug("Load balancer for cluster " + cluster.getClusterId() + " is already present.");
                    }

                    LoadBalancerInfo loadBalancerInfo = clusterIdToLoadBalancerMap.get(cluster.getClusterId());

                    String loadBalancerName = loadBalancerInfo.getName();
                    String region = loadBalancerInfo.getRegion();

                    // Get all the instances attached - Attach newly added instances to load balancer

                    // attachedInstances list is useful in finding out what are the new instances which
                    // should be attached to this load balancer.
                    List attachedInstances = awsHelper.getAttachedInstances(loadBalancerName, region);

                    // clusterMembers stores all the members of a cluster.
                    Collection clusterMembers = cluster.getMembers();

                    if (clusterMembers.size() > 0) {
                        activeClusters.add(cluster.getClusterId());

                        List instancesToAddToLoadBalancer = new ArrayList();
                        List availabilityZones = new ArrayList();

                        for (Member member : clusterMembers) {
                            // if instance id of member is not in
                            // attachedInstances
                            // add this to instancesToAddToLoadBalancer
                            Instance instance = new Instance(awsHelper.getAWSInstanceName(member.getInstanceId()));

                            if (attachedInstances == null || !attachedInstances.contains(instance)) {
                                instancesToAddToLoadBalancer.add(instance);

                                if (log.isDebugEnabled()) {
                                    log.debug("Instance " + awsHelper.getAWSInstanceName(member.getInstanceId()) +
                                            " needs to be registered to load balancer " + loadBalancerName);
                                }

                                // LB Common Member has a property 'EC2_AVAILABILITY_ZONE' points to the ec2 availability zone
                                // for this member. Use the property value to update the LB about the relevant zone
                                String availabilityZone = getEC2AvaialbilityZoneOfMember(member);
                                if (availabilityZone != null) {
                                    availabilityZones.add(availabilityZone);
                                }
                            }
                        }

                        if (instancesToAddToLoadBalancer.size() > 0) {
                            awsHelper.registerInstancesToLoadBalancer(
                                    loadBalancerName,
                                    instancesToAddToLoadBalancer, region);
                        }

                        // update LB with the zones
                        if (!availabilityZones.isEmpty() && !AWSExtensionContext.getInstance().isOperatingInVPC()) {
                            awsHelper.addAvailabilityZonesForLoadBalancer(loadBalancerName, availabilityZones, region);
                        }
                    }

                } else {
                    // Create a new load balancer for this cluster
                    Collection clusterMembers = cluster.getMembers();

                    if (clusterMembers.size() > 0) {
                        Member aMember = clusterMembers.iterator().next();

                        // a unique load balancer name with user-defined prefix and a sequence number.
                        String loadBalancerName = awsHelper.generateLoadBalancerName(cluster.getServiceName());

                        String region = awsHelper.getAWSRegion(aMember.getInstanceId());

                        // list of AWS listeners obtained using port mappings of one of the members of the cluster.
                        List listenersForThisCluster = awsHelper.getRequiredListeners(aMember);

                        // Get the initial zone identifier list (a, b, c) to consider in creating
                        // the LB as defined in aws.properties file
                        Set initialZones = awsHelper.getInitialZones();

                        Set initialAvailabilityZones = new HashSet<>();
                        if (initialZones.isEmpty()) {
                            // initial availability zones not defined
                            // use the default (a)
                            initialAvailabilityZones.add(awsHelper.getAvailabilityZoneFromRegion
                                    (region));
                        } else {
                            // prepend the region and construct the availability zone list with
                            // full names ( + )
                            for (String zone : initialZones) {
                                initialAvailabilityZones.add(region + zone);
                            }
                        }

                        // Returns DNS name of load balancer which was created.
                        // This is used in the domain mapping of this cluster.
                        String loadBalancerDNSName = awsHelper.createLoadBalancer(loadBalancerName, listenersForThisCluster,
                                region, initialAvailabilityZones, AWSExtensionContext.getInstance().isOperatingInVPC());

                        // enable connection draining (default) and cross zone load balancing (if specified in aws-extension.sh)
                        awsHelper.modifyLBAttributes(loadBalancerName, region, AWSExtensionContext.getInstance().
                                isCrossZoneLoadBalancingEnabled(), true);

                        // Add the inbound rule the security group of the load balancer
                        // For each listener, add a new rule with load balancer port as allowed protocol in the security group.
                        // if security group id is defined, directly use that
                        for (Listener listener : listenersForThisCluster) {
                            int port = listener.getLoadBalancerPort();

                            if (awsHelper.getLbSecurityGroupIdDefinedInConfiguration() != null && !awsHelper.
                                    getLbSecurityGroupIdDefinedInConfiguration().isEmpty())  {
                                for (String protocol : awsHelper.getAllowedProtocolsForLBSecurityGroup()) {
                                    awsHelper.addInboundRuleToSecurityGroup(awsHelper.getLbSecurityGroupIdDefinedInConfiguration(),
                                            region, protocol, port);
                                }
                            } else if (awsHelper.getLbSecurityGroupName() != null && !awsHelper
                                    .getLbSecurityGroupName().isEmpty()) {
                                for (String protocol : awsHelper.getAllowedProtocolsForLBSecurityGroup()) {
                                    awsHelper.addInboundRuleToSecurityGroup(awsHelper.getSecurityGroupId(awsHelper
                                            .getLbSecurityGroupName(), region), region, protocol, port);
                                }
                            }
                        }

                        log.info("Load balancer '" + loadBalancerDNSName + "' created for cluster '" + cluster.getClusterId());

                        // Register instances in the cluster to load balancer
                        List instances = new ArrayList();
                        List availabilityZones = new ArrayList();

                        for (Member member : clusterMembers) {
                            String instanceId = member.getInstanceId();

                            if (log.isDebugEnabled()) {
                                log.debug("Instance " + awsHelper.getAWSInstanceName(instanceId) + " needs to be registered to load balancer "
                                        + loadBalancerName);
                            }

                            Instance instance = new Instance();
                            instance.setInstanceId(awsHelper.getAWSInstanceName(instanceId));

                            instances.add(instance);
                            // LB Common Member has a property 'EC2_AVAILABILITY_ZONE' which points to the ec2 availability
                            // zone for this member. Use the property value to update the LB about the relevant zone
                            String availabilityZone = getEC2AvaialbilityZoneOfMember(member);
                            if (availabilityZone != null) {
                                availabilityZones.add(availabilityZone);
                            }
                        }

                        awsHelper.registerInstancesToLoadBalancer(loadBalancerName, instances, region);

                        // update LB with the zones
                        if (!availabilityZones.isEmpty() && !AWSExtensionContext.getInstance().isOperatingInVPC()) {
                            awsHelper.addAvailabilityZonesForLoadBalancer(loadBalancerName, availabilityZones, region);
                        }

                        // add stickiness policy
                        if (awsHelper.getAppStickySessionCookie() != null && !awsHelper.getAppStickySessionCookie().isEmpty()) {
                            CreateAppCookieStickinessPolicyResult result = awsHelper.createStickySessionPolicy(loadBalancerName,
                                    awsHelper.getAppStickySessionCookie(), Constants.STICKINESS_POLICY, region);

                            if (result != null) {
                                // Take a single port mapping from a member, and apply the policy for
                                // the LB Listener port (Proxy port of the port mapping)
                                awsHelper.applyPolicyToLBListenerPorts(aMember.getPorts(), loadBalancerName,
                                        Constants.STICKINESS_POLICY, region);
                            }
                        }

                        // persist LB info
                        try {
                            persistenceManager.persist(new LBInfoDTO(loadBalancerName, cluster.getClusterId(), region));

                        } catch (PersistenceException e) {
                            log.error("Unable to persist LB Information for " + loadBalancerName + ", cluster id " +
                                    cluster.getClusterId());
                        }

                        LoadBalancerInfo loadBalancerInfo = new LoadBalancerInfo(
                                loadBalancerName, region);

                        clusterIdToLoadBalancerMap.put(cluster.getClusterId(),
                                loadBalancerInfo);
                        activeClusters.add(cluster.getClusterId());
                    }

                    pause(3000);
                }
            }
        }

        // if 'terminate.lb.on.cluster.removal' = true in aws-extension.sh
        if (AWSExtensionContext.getInstance().terminateLBOnClusterRemoval()) {

            // Find out clusters which were present earlier but are not now.
            List clustersToRemoveFromMap = new ArrayList();
            // TODO: improve using an iterator and removing the unwanted cluster id in this loop
            for (String clusterId : clusterIdToLoadBalancerMap.keySet()) {
                if (!activeClusters.contains(clusterId)) {
                    clustersToRemoveFromMap.add(clusterId);

                    if (log.isDebugEnabled()) {
                        log.debug("Load balancer for cluster " + clusterId
                                + " needs to be removed.");
                    }

                }
            }


            // Delete load balancers associated with these clusters.
            for (String clusterId : clustersToRemoveFromMap) {
                // Remove load balancer for this cluster.
                final String loadBalancerName = clusterIdToLoadBalancerMap.get(clusterId).getName();
                final String region = clusterIdToLoadBalancerMap.get(clusterId).getRegion();
                awsHelper.deleteLoadBalancer(loadBalancerName, region);
                //remove and persist
                try {
                    persistenceManager.remove(new LBInfoDTO(loadBalancerName, clusterId, region));

                } catch (PersistenceException e) {
                    log.error("Unable to persist LB Information for " + loadBalancerName + ", cluster id " +
                            clusterId);
                }
                clusterIdToLoadBalancerMap.remove(clusterId);
            }
        }

        activeClusters.clear();
        log.info("AWS load balancer extension was reconfigured as per the topology.");
        return true;
    }

    private String getEC2AvaialbilityZoneOfMember(Member member) {
        if (member.getProperties() != null) {
            return member.getProperties().getProperty(Constants.EC2_AVAILABILITY_ZONE_PROPERTY);
        }

        return null;
    }

    /*
     * start method is called after extension if configured first time. Does
     * nothing but logs the message.
     */
    public void start() throws LoadBalancerExtensionException {

        log.info("AWS load balancer extension started.");
    }

    private void initialize() {
        // load persisted LB information
        Set lbInfo = null;
        try {
            lbInfo = persistenceManager.retrieve();

        } catch (PersistenceException e) {
            log.error("Unable to retrieve persisted LB Information", e);
        }

        if (lbInfo != null) {
            for (LBInfoDTO lbInfoDTO : lbInfo) {
                LoadBalancerDescription lbDesc = awsHelper.getLoadBalancerDescription(lbInfoDTO.getName(),
                        lbInfoDTO.getRegion());
                if (lbDesc != null) {
                    clusterIdToLoadBalancerMap.put(lbInfoDTO.getClusterId(), new LoadBalancerInfo(lbInfoDTO.getName(),
                            lbInfoDTO.getRegion()));
                } else {
                    // make debug
                    if (log.isInfoEnabled()) {
                        log.info("Unable to locate LB " + lbInfoDTO.getName());
                    }
                    // remove the persisted entry
                    try {
                        persistenceManager.remove(new LBInfoDTO(lbInfoDTO.getName(), lbInfoDTO.getClusterId(), lbInfoDTO.getRegion()));

                    } catch (PersistenceException e) {
                        log.error("Unable to remove persisted LB Information", e);
                    }
                }

            }
        }
    }

    /*
     * reload method is called every time after extension if configured. Does
     * nothing but logs the message.
     */
    public void reload() throws LoadBalancerExtensionException {
        // Check what is appropriate to do here.
        log.info("AWS load balancer extension reloaded.");
    }

    /*
     * stop method deletes load balancers for all clusters in the topology.
     */
    public void stop() throws LoadBalancerExtensionException {
        // Remove all load balancers if 'terminate.lbs.on.extension.stop' = true in aws-extension.sh
        if (AWSExtensionContext.getInstance().terminateLBsOnExtensionStop()) {
            for (Map.Entry lbInfoEntry : clusterIdToLoadBalancerMap
                    .entrySet()) {
                // Remove load balancer
                awsHelper.deleteLoadBalancer(lbInfoEntry.getValue().getName(),
                        lbInfoEntry.getValue().getRegion());

                // remove the persisted entry
                try {
                    persistenceManager.remove(new LBInfoDTO(lbInfoEntry.getValue().getName(), lbInfoEntry.getKey(),
                            lbInfoEntry.getValue().getRegion()));

                } catch (PersistenceException e) {
                    log.error("Unable to remove persisted LB Information", e);
                }
            }
        } else {
            if (log.isInfoEnabled()) {
                log.info("Not terminating LBs since terminate.lbs.on.extension.stop=false");
            }
        }
    }

    private static void pause(long duration) {
        // sleep to stop AWS Rate Exceeding: Caused by: com.amazonaws.AmazonServiceException: Rate exceeded
        // (Service: AmazonElasticLoadBalancing; Status Code: 400; Error Code: Throttling; Request ID: xxx-xxx)
        try {
            Thread.sleep(duration);
        } catch (InterruptedException ignored) {
        }
    }

    public static ConcurrentHashMap getClusterIdToLoadBalancerMap() {
        return clusterIdToLoadBalancerMap;
    }
}

/**
 * Used to store load balancer name and the region in which it is created. This
 * helps in finding region while calling API methods to modify/delete a load
 * balancer.
 */
class LoadBalancerInfo {
    private String name;
    private String region;

    public LoadBalancerInfo(String name, String region) {
        this.name = name;
        this.region = region;
    }

    public String getName() {
        return name;
    }

    public String getRegion() {
        return region;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy