diff --git a/libraries/elb.rb b/libraries/elb.rb new file mode 100644 index 0000000..aae554a --- /dev/null +++ b/libraries/elb.rb @@ -0,0 +1,31 @@ +module WebsterClay + module Aws + module Elb + def elb + @@elb ||= Fog::AWS::ELB.new( + :aws_access_key_id => new_resource.aws_access_key, + :aws_secret_access_key => new_resource.aws_secret_access_key, + :region => new_resource.region + ) + end + + def ec2 + @@ec2 ||= Fog::Compute.new(:provider => 'AWS', + :aws_access_key_id => new_resource.aws_access_key, + :aws_secret_access_key => new_resource.aws_secret_access_key, + :region => new_resource.region + ) + end + + def load_balancer_by_name(name) + elb.describe_load_balancers.body["DescribeLoadBalancersResult"]["LoadBalancerDescriptions"].detect { |lb| lb["LoadBalancerName"] == new_resource.lb_name } + end + + def availability_zone_for_instances(instances) + ec2.describe_instances('instance-id' => [*instances]).body['reservationSet'].map { |r| r['instancesSet'] }.flatten.map { |i| i['placement']['availabilityZone'] } + end + + end + end +end + \ No newline at end of file diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..a9724a8 --- /dev/null +++ b/metadata.json @@ -0,0 +1,29 @@ +{ + "dependencies": { + }, + "name": "elb", + "maintainer_email": "jesse@websterclay.com", + "attributes": { + }, + "license": "Apache 2.0", + "suggestions": { + }, + "platforms": { + }, + "maintainer": "Jesse Newland", + "long_description": "= DESCRIPTION:\n\n= REQUIREMENTS:\n\n= ATTRIBUTES: \n\n= USAGE:\n\n", + "version": "0.1.0", + "recommendations": { + }, + "recipes": { + }, + "groupings": { + }, + "conflicting": { + }, + "replacing": { + }, + "description": "Configure Elastic Load Balancers at AWS", + "providing": { + } +} \ No newline at end of file diff --git a/metadata.rb b/metadata.rb new file mode 100644 index 0000000..1ae5eb3 --- /dev/null +++ b/metadata.rb @@ -0,0 +1,6 @@ +maintainer "Jesse Newland" +maintainer_email "jesse@websterclay.com" +license "Apache 2.0" +description "Configure Elastic Load Balancers at AWS" +long_description IO.read(File.join(File.dirname(__FILE__), 'README.rdoc')) +version "0.1" diff --git a/providers/load_balancer.rb b/providers/load_balancer.rb new file mode 100644 index 0000000..aa92ec3 --- /dev/null +++ b/providers/load_balancer.rb @@ -0,0 +1,138 @@ +include WebsterClay::Aws::Elb + +# TODO: error handling +# TODO: count instances in each zone and warn if not equal +# TODO: warn if only one availability zone + +def load_current_resource + + @current_lb = load_balancer_by_name(new_resource.lb_name) + + @current_resource = Chef::Resource::ElbLoadBalancer.new(new_resource.lb_name) + @current_resource.lb_name(new_resource.lb_name) + @current_resource.aws_access_key(new_resource.aws_access_key) + @current_resource.aws_secret_access_key(new_resource.aws_secret_access_key) + @current_resource.region(new_resource.region) + @current_resource.listeners(new_resource.listeners) + @current_resource.timeout(new_resource.timeout) + + if @current_lb + @current_resource.availability_zones(@current_lb['AvailabilityZones']) + @current_resource.instances(@current_lb['Instances']) + end + @current_resource.availability_zones || @current_resource.availability_zones([]) + @current_resource.instances || @current_resource.instances([]) + + if new_resource.instances.nil? && new_resource.search_query + new_resource.instances(search(:node, new_resource.search_query).map { |n| n['ec2']['instance_id']}) + end + + all_zones = availability_zone_for_instances(new_resource.instances) + unique_zones = all_zones.compact.uniq + + if new_resource.availability_zones.nil? + new_resource.availability_zones(unique_zones) + end +end + +action :create do + ruby_block "Create ELB #{new_resource.lb_name}" do + block do + elb.create_load_balancer(new_resource.availability_zones, new_resource.lb_name, new_resource.listeners) + data = nil + begin + Timeout::timeout(new_resource.timeout) do + while true + data = load_balancer_by_name(new_resource.lb_name) + break if data + sleep 3 + end + end + rescue Timeout::Error + raise "Timed out waiting for ELB data after #{new_resource.timeout} seconds" + end + node.set[:elb][new_resource.lb_name] = data + node.save if !Chef::Config.solo + end + action :create + not_if do + if data = load_balancer_by_name(new_resource.lb_name) + node.set[:elb][new_resource.lb_name] = data + node.save if !Chef::Config.solo + true + else + false + end + end + end + new_resource.updated_by_last_action(true) + + instances_to_add = new_resource.instances - current_resource.instances + instances_to_delete = current_resource.instances - new_resource.instances + + instances_to_add.each do |instance_to_add| + ruby_block "Register instance #{instance_to_add} with ELB #{new_resource.lb_name}" do + block do + elb.register_instances_with_load_balancer([instance_to_add], new_resource.lb_name) + node.set[:elb][new_resource.lb_name] = load_balancer_by_name(new_resource.lb_name) + node.save if !Chef::Config.solo + end + action :create + end + new_resource.updated_by_last_action(true) + end + + instances_to_delete.each do |instance_to_delete| + ruby_block "Deregister instance #{instance_to_delete} from ELB #{new_resource.lb_name}" do + block do + elb.deregister_instances_from_load_balancer([instance_to_delete], new_resource.lb_name) + node.set[:elb][new_resource.lb_name] = load_balancer_by_name(new_resource.lb_name) + node.save if !Chef::Config.solo + end + action :create + end + new_resource.updated_by_last_action(true) + end + + zones_to_add = new_resource.availability_zones - current_resource.availability_zones + zones_to_delete = current_resource.availability_zones - new_resource.availability_zones + + zones_to_add.each do |zone_to_add| + ruby_block "Enabling Availability Zone #{zone_to_add} for ELB #{new_resource.lb_name}" do + block do + elb.enable_availability_zones_for_load_balancer([zone_to_add], new_resource.lb_name) + node.set[:elb][new_resource.lb_name] = load_balancer_by_name(new_resource.lb_name) + node.save if !Chef::Config.solo + end + action :create + end + new_resource.updated_by_last_action(true) + end + + zones_to_delete.each do |zone_to_delete| + ruby_block "Disable Availability Zone #{zone_to_delete} for ELB #{new_resource.lb_name}" do + block do + elb.disable_availability_zones_for_load_balancer([zone_to_delete], new_resource.lb_name) + node.set[:elb][new_resource.lb_name] = load_balancer_by_name(new_resource.lb_name) + node.save if !Chef::Config.solo + end + action :create + end + new_resource.updated_by_last_action(true) + end + +end + +action :delete do + ruby_block "Delete ELB #{new_resource.lb_name}" do + block do + elb.delete_load_balancer(new_resource.lb_name) + node.set[:elb][new_resource.lb_name] = nil + node.save if !Chef::Config.solo + end + action :create + not_if do + !!!load_balancer_by_name(new_resource.lb_name) + end + end +end \ No newline at end of file diff --git a/recipes/default.rb b/recipes/default.rb new file mode 100644 index 0000000..8aa16b9 --- /dev/null +++ b/recipes/default.rb @@ -0,0 +1,28 @@ +# +# Cookbook Name:: elb +# Recipe:: default +# +# Copyright 2011, Webster Clay, LLC +# +# 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. +# + +r = gem_package "fog" do + action :nothing +end + +r.run_action(:install) + +require 'rubygems' +Gem.clear_paths +require 'fog' diff --git a/resources/load_balancer.rb b/resources/load_balancer.rb new file mode 100644 index 0000000..3ac25be --- /dev/null +++ b/resources/load_balancer.rb @@ -0,0 +1,11 @@ +actions :create, :delete + +attribute :lb_name, :kind_of => String, :name_attribute => true +attribute :aws_access_key, :kind_of => String +attribute :aws_secret_access_key, :kind_of => String +attribute :region, :kind_of => String, :default => 'us-east-1' +attribute :availability_zones, :kind_of => Array +attribute :listeners, :kind_of => Array, :default => [{"InstancePort" => 80, "Protocol" => "HTTP", "LoadBalancerPort" => 80}] +attribute :instances, :kind_of => Array +attribute :search_query, :kind_of => String +attribute :timeout, :default => 60 \ No newline at end of file