Skip to content

Commit

Permalink
New provider: AWS ELB Classic
Browse files Browse the repository at this point in the history
  • Loading branch information
janeczku committed Sep 23, 2016
1 parent 261a0ad commit 20f555d
Show file tree
Hide file tree
Showing 9 changed files with 1,127 additions and 0 deletions.
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/rancher/external-lb/metadata"
"github.com/rancher/external-lb/model"
"github.com/rancher/external-lb/providers"
_ "github.com/rancher/external-lb/providers/elbv1"
_ "github.com/rancher/external-lb/providers/f5"
"os"
"reflect"
Expand Down
50 changes: 50 additions & 0 deletions providers/elbv1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
AWS ELBv1 (ELB Classic Load Balancer) Provider
==========

#### About ELB Classic Load Balancers
The [Classic Load Balancer](https://aws.amazon.com/elasticloadbalancing/classicloadbalancer/) option in AWS routes traffic based on application or network level information and is ideal for simple load balancing of traffic across multiple EC2 instances.

#### About this provider
This provider keeps pre-existing Classic Load Balancers updated with the EC2 instances Rancher services are running on, allowing one to use Elastic Load Balancing to load balancer Rancher services.

### Usage

1. Deploy the stack for this provider from the Rancher Catalog
2. Using the AWS Console create a Classic ELB load balancer with one or more listeners and configure it according to your applications requirements. Configure the listener(s) with an "instance protocol" matching that of your application as well as the "instance port" that your Rancher service will expose to the hosts.
3. Create or update your service to expose one or multiple host ports that match the configuration of your ELB listener(s). Then add the service label `io.rancher.service.external_lb.endpoint` using as value the name of the previously created ELB load balancer.

Environment Variables
==========

The following environment variables are used to configure global options for this provider.

| Variable | Description | Default value |
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
| ELBV2_AWS_ACCESS_KEY | Your AWS Access Key. Make sure this key has sufficient permissions for the operations required to manage an ELB load balancer. | `-` |
| ELBV2_AWS_SECRET_KEY | Your AWS Secret Key. | `-` |
| ELBV2_AWS_REGION | By default the service will use the region of the instance it is running on to look up the IDs of EC instances. You can override the region by setting this variable. | `<Self-Region>` |
| ELBV2_AWS_VPCID | By default the service will use the VPC of the instance this service is running on to look up the IDs of EC instances. You can override the VPC by setting this variable. | `<Self-VPC>` |
| ELBV2_USE_PRIVATE_IP | If your EC2 instances are registered in Rancher with their private IP addresses, then set this variable to "true". | `false` |

Note: Instead of specifying AWS credentials when deploying the stack you can create an IAM policy and role and associate it with your EC2 instances.

Example IAM policy with the minimum required permissions
==========

TODO

License
=======
Copyright (c) 2016 [Rancher Labs, Inc.](http://rancher.com)

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](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.
338 changes: 338 additions & 0 deletions providers/elbv1/aws_elbv1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
package awselbv1

import (
"fmt"
"os"
"strconv"

"github.com/Sirupsen/logrus"
"github.com/rancher/external-lb/model"
"github.com/rancher/external-lb/providers"
"github.com/rancher/external-lb/providers/elbv1/elbv1svc"
)

const (
ProviderName = "AWS ELB Classic"
ProviderSlug = "elbv1"
)

const (
TagNameTargetPool = "external-lb/targetPoolName"
TagNameServicePort = "external-lb/servicePort"
)

const (
EnvVarAWSAccessKey = "ELBV1_AWS_ACCESS_KEY"
EnvVarAWSSecretKey = "ELBV1_AWS_SECRET_KEY"
EnvVarAWSRegion = "ELBV1_AWS_REGION"
EnvVarAWSVpcID = "ELBV1_AWS_VPCID"
EnvVarUsePrivateIP = "ELBV1_USE_PRIVATE_IP"
)

// AWSELBv1Provider implements the providers.Provider interface.
type AWSELBv1Provider struct {
svc *elbv1svc.ELBClassicService
region string
vpcID string
usePrivateIP bool
}

func init() {
providers.RegisterProvider(ProviderSlug, new(AWSELBv1Provider))
}

func (p *AWSELBv1Provider) Init() error {
var err error
accessKey := os.Getenv(EnvVarAWSAccessKey)
secretKey := os.Getenv(EnvVarAWSSecretKey)

p.region = os.Getenv(EnvVarAWSRegion)
p.vpcID = os.Getenv(EnvVarAWSVpcID)

if env := os.Getenv(EnvVarUsePrivateIP); len(env) > 0 {
p.usePrivateIP, err = strconv.ParseBool(env)
if err != nil {
return fmt.Errorf("'%s' must be set to a string "+
"representing a boolean value", EnvVarUsePrivateIP)
}
}

if p.vpcID == "" || p.region == "" {
p.vpcID, p.region, err = elbv1svc.GetInstanceInfo()
if err != nil {
return err
}
}

logrus.Debugf("Initialized provider: region: %s, vpc: %s, usePrivateIP %t",
p.region, p.vpcID, p.usePrivateIP)

p.svc, err = elbv1svc.NewService(accessKey, secretKey, p.region, p.vpcID)
if err != nil {
return err
}

if err := p.svc.CheckAPIConnection(); err != nil {
return fmt.Errorf("AWS API connection check failed: %v", err)
}

logrus.Infof("Configured %s provider in region %s and VPC %s",
p.GetName(), p.region, p.vpcID)

return nil
}

/*
* Methods implementing the providers.Provider interface
*/

func (*AWSELBv1Provider) GetName() string {
return ProviderName
}

func (p *AWSELBv1Provider) HealthCheck() error {
return p.svc.CheckAPIConnection()
}

func (p *AWSELBv1Provider) GetLBConfigs() ([]model.LBConfig, error) {
logrus.Debugf("GetLBConfigs =>")

var lbConfigs []model.LBConfig
allLb, err := p.svc.GetLoadBalancers()
if err != nil {
return lbConfigs, fmt.Errorf("Failed to lookup load balancers: %v", err)
}

logrus.Debugf("GetLBConfigs => found %d load balancers", len(allLb))
if len(allLb) == 0 {
return lbConfigs, nil
}

allNames := make([]string, len(allLb))
for i, lb := range allLb {
allNames[i] = *lb.LoadBalancerName
}

lbTags, err := p.svc.DescribeLBTags(allNames)
if err != nil {
return lbConfigs, fmt.Errorf("Failed to lookup load balancer tags: %v", err)
}

for _, lb := range allLb {
if _, ok := lbTags[*lb.LoadBalancerName]; !ok {
continue
}

tags := lbTags[*lb.LoadBalancerName]

var targetPoolName, servicePort string
var ok bool
if targetPoolName, ok = tags[TagNameTargetPool]; !ok {
logrus.Debugf("Skipping LB without targetPool tag: %s", *lb.LoadBalancerName)
continue
}
if servicePort, ok = tags[TagNameServicePort]; !ok {
logrus.Debugf("Skipping LB without servicePort tag: %s", *lb.LoadBalancerName)
continue
}

lbConfig := model.LBConfig{}
lbConfig.LBEndpoint = *lb.LoadBalancerName
lbConfig.LBTargetPoolName = targetPoolName
var targets []model.LBTarget

// get currently registered backend instances
instanceIds, err := p.svc.GetRegisteredInstances(*lb.LoadBalancerName)
if err != nil {
return lbConfigs, fmt.Errorf("Failed to get registered instance IDs: %v", err)
}

if len(instanceIds) > 0 {
ec2Instances, err := p.svc.GetInstancesByID(instanceIds)
if err != nil {
return lbConfigs, fmt.Errorf("Failed to lookup EC2 instances: %v", err)
}
for _, in := range ec2Instances {
var ip string
if p.usePrivateIP {
ip = in.PrivateIPAddress
} else {
ip = in.PublicIPAddress
}

target := model.LBTarget{
HostIP: ip,
Port: servicePort,
}
targets = append(targets, target)
}
}

lbConfig.LBTargets = targets
lbConfigs = append(lbConfigs, lbConfig)
}

logrus.Debugf("GetLBConfigs => Returning %d LB configs", len(lbConfigs))
return lbConfigs, nil
}

func (p *AWSELBv1Provider) AddLBConfig(config model.LBConfig) (string, error) {
logrus.Debugf("AddLBConfig => config: %v", config)

lb, err := p.svc.GetLoadBalancerByName(config.LBEndpoint)
if err != nil {
return "", err
}
if lb == nil {
return "", fmt.Errorf("Could not find ELB named '%s'", config.LBEndpoint)
}

ec2InstanceIds, err := p.getEC2Instances(config.LBTargets)
if err != nil {
return "", fmt.Errorf("Failed to get EC2 instances: %v", err)
}

// update the ELB backend instances
if err := p.ensureBackendInstances(config.LBEndpoint, ec2InstanceIds); err != nil {
return "", fmt.Errorf("Failed to ensure registered instances: %v", err)
}

// tag the ELB
tags := map[string]string{
TagNameTargetPool: config.LBTargetPoolName,
TagNameServicePort: config.LBTargetPort,
}

if err := p.svc.AddLBTags(config.LBEndpoint, tags); err != nil {
return "", fmt.Errorf("Failed to tag ELB: %v", err)
}

logrus.Debug("GetLBConfigs => Done")
return *lb.DNSName, nil
}

func (p *AWSELBv1Provider) UpdateLBConfig(config model.LBConfig) (string, error) {
logrus.Debugf("UpdateLBConfig => config: %v", config)

lb, err := p.svc.GetLoadBalancerByName(config.LBEndpoint)
if err != nil {
return "", err
}
if lb == nil {
return "", fmt.Errorf("Could not find ELB named '%s'", config.LBEndpoint)
}

ec2InstanceIds, err := p.getEC2Instances(config.LBTargets)
if err != nil {
return "", fmt.Errorf("Failed to get EC2 instances: %v", err)
}

// update the ELB instances
if err := p.ensureBackendInstances(config.LBEndpoint, ec2InstanceIds); err != nil {
return "", fmt.Errorf("Failed to ensure registered instances on ELB %s: %v",
config.LBEndpoint, err)
}

// update the tags
tags := map[string]string{
TagNameTargetPool: config.LBTargetPoolName,
TagNameServicePort: config.LBTargetPort,
}

if err := p.svc.AddLBTags(config.LBEndpoint, tags); err != nil {
return "", fmt.Errorf("Failed to update servicePort tag: %v", err)
}

logrus.Debug("UpdateLBConfig => Done!")
return *lb.DNSName, nil
}

func (p *AWSELBv1Provider) RemoveLBConfig(config model.LBConfig) error {
logrus.Debugf("RemoveLBConfig => config: %v", config)

lb, err := p.svc.GetLoadBalancerByName(config.LBEndpoint)
if err != nil {
return err
}
if lb == nil {
return fmt.Errorf("Could not find ELB load balancer named '%s'", config.LBEndpoint)
}

// Remove all instances
if err := p.ensureBackendInstances(*lb.LoadBalancerName, nil); err != nil {
return fmt.Errorf("Failed to clean up registered instances: %v", err)
}

// Remove tag
if err := p.svc.RemoveLBTag(*lb.LoadBalancerName, TagNameTargetPool); err != nil {
return fmt.Errorf("Failed to remove targetPool tag: %v", err)
}

if err := p.svc.RemoveLBTag(*lb.LoadBalancerName, TagNameServicePort); err != nil {
return fmt.Errorf("Failed to remove servicePort tag: %v", err)
}

logrus.Debug("RemoveLBConfigs => Done")
return nil
}

/*
* Private methods
*/

// makes sure the specified instances are registered with specified the load balancer
func (p *AWSELBv1Provider) ensureBackendInstances(loadBalancerName string, instanceIds []string) error {
logrus.Debugf("ensureBackendInstances => lb: %s, instanceIds: %v", loadBalancerName, instanceIds)
registeredInstanceIds, err := p.svc.GetRegisteredInstances(loadBalancerName)
if err != nil {
return err
}

toRegister := differenceStringSlice(instanceIds, registeredInstanceIds)
toDeregister := differenceStringSlice(registeredInstanceIds, instanceIds)
logrus.Debugf("Registering instances to ELB %s: %v", loadBalancerName, toRegister)
logrus.Debugf("Deregistering instances from ELB %s: %v", loadBalancerName, toDeregister)

if len(toRegister) > 0 {
if err := p.svc.RegisterInstances(loadBalancerName, toRegister); err != nil {
return err
}
}

if len(toDeregister) > 0 {
if err := p.svc.DeregisterInstances(loadBalancerName, toDeregister); err != nil {
return err
}
}

return nil
}

// looks up the EC2 instances for each of the HostIP in the specified model.LBTarget slice
// and returns their IDs.
func (p *AWSELBv1Provider) getEC2Instances(targets []model.LBTarget) ([]string, error) {
var instanceIds []string
var targetIps []string
for _, t := range targets {
targetIps = append(targetIps, t.HostIP)
}

targetIps = removeDuplicates(targetIps)
if len(targetIps) == 0 {
return instanceIds, nil
}

ec2Instances, err := p.svc.LookupInstancesByIPAddress(targetIps, p.usePrivateIP)
if err != nil {
return instanceIds, err
}

logrus.Debugf("getEC2Instances => Looked up %d IP addresses, got %d instances",
len(targetIps), len(ec2Instances))

for _, instance := range ec2Instances {
instanceIds = append(instanceIds, instance.ID)
}

return instanceIds, nil
}
Loading

0 comments on commit 20f555d

Please sign in to comment.