From d9671c83dd728260a40c83e301e99cac7b1df427 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 8 Sep 2012 16:19:39 -0400 Subject: [PATCH 1/5] Added flexible load balancer configuration --- lib/capify-cloud.rb | 164 +++++++++++++++++++++++++-------- lib/capify-cloud/capistrano.rb | 24 ++++- readme.md | 52 +++++++++-- 3 files changed, 191 insertions(+), 49 deletions(-) diff --git a/lib/capify-cloud.rb b/lib/capify-cloud.rb index 51561d0..9cfe59b 100644 --- a/lib/capify-cloud.rb +++ b/lib/capify-cloud.rb @@ -6,10 +6,11 @@ class CapifyCloud - attr_accessor :load_balancer, :instances + attr_accessor :load_balancers, :instances, :lb_instances, :deregister_lb_instances SLEEP_COUNT = 5 def initialize(cloud_config = "config/cloud.yml") + case cloud_config when Hash @cloud_config = cloud_config @@ -21,7 +22,11 @@ def initialize(cloud_config = "config/cloud.yml") @cloud_providers = @cloud_config[:cloud_providers] + @load_balancers = elb.load_balancers @instances = [] + @lb_instances = {} + @deregister_lb_instances = {} + @cloud_providers.each do |cloud_provider| config = @cloud_config[cloud_provider.to_sym] case cloud_provider @@ -87,60 +92,143 @@ def instance_health(load_balancer, instance) end def elb - Fog::AWS::ELB.new(:aws_access_key_id => @cloud_config[:aws_access_key_id], :aws_secret_access_key => @cloud_config[:aws_secret_access_key], :region => @cloud_config[:aws_params][:region]) - end + @elb ||= Fog::AWS::ELB.new(:aws_access_key_id => @cloud_config[:AWS][:aws_access_key_id], :aws_secret_access_key => @cloud_config[:AWS][:aws_secret_access_key], :region => @cloud_config[:AWS][:params][:region]) + end + + def loadbalancer (roles, named_load_balancer, *args) + role = args[0] + deregister_arg = args[1][:deregister] rescue false + require_arglist = args[1][:require] rescue {} + exclude_arglist = args[1][:exclude] rescue {} + + # get the named load balancer + named_elb = get_load_balancer_by_name(named_load_balancer.to_s) + + # must exit if no load balancer on record for this account by given name + raise Exception, "No load balancer found named: #{named_load_balancer.to_s}" if named_elb.nil? + + @lb_instances[named_elb] = @instances.clone + + # keep only instances belonging to this role + ips = roles[role].servers.map{|s| Socket.getaddrinfo(s.to_s, nil)[0][2]} + @lb_instances[named_elb].delete_if { |i| !ips.include?(i.contact_point) } + + # reduce against 'require' args, if an instance doesnt have the args in require_arglist, remove + @lb_instances[named_elb].delete_if { |i| ! all_args_within_instance(i, require_arglist) } unless require_arglist.nil? or require_arglist.empty? + + # reduce against 'exclude_arglist', if an instance has any of the args in exclude_arglist, remove + @lb_instances[named_elb].delete_if { |i| any_args_within_instance(i, exclude_arglist) } unless exclude_arglist.nil? or exclude_arglist.empty? + + @lb_instances[named_elb].compact! if @lb_instances[named_elb] + + # Save instances for deregistration hook + @deregister_lb_instances[named_elb] ||= [] + @deregister_lb_instances[named_elb] += @lb_instances[named_elb] if deregister_arg + end + + def any_args_within_instance(instance, exclude_arglist) + exargs = exclude_arglist.clone # must copy since delete transcends scope; if we don't copy, subsequent 'map'ped enum arglists would be side-effected + tag_exclude_state = nil # default assumption + # pop off a :tags arg to treat separately, its a separate namespace + tag_exclude_arglist = exargs.delete(:tags) + + tag_exclude_state = tag_exclude_arglist.map { |k, v| (instance.tags[k] == v rescue nil) }.inject(nil) { |inj, el| el || inj } if !tag_exclude_arglist.nil? + # we want all nils for the result here, so we logical-or the result map, and invert it + tag_exclude_state || exargs.map { |k, v| instance.send(k) == v }.inject(nil) { |inj, el| inj || el } + end + + # the instance has attributes + def all_args_within_instance(instance, require_arglist) + reqargs = require_arglist.clone # must copy since delete transcends scope; if we don't copy, subsequent 'map'ped enum arglists would be side-effected + tag_require_state = true # default assumption + # pop off a :tags arg to treat separately, effectively a separate namespace to be checked agains + tag_require_arglist = reqargs.delete(:tags) + tag_require_state = tag_require_arglist.map { |k, v| (instance.tags[k] == v rescue nil) }.inject(nil) { |inj, el| el || inj } if !tag_require_arglist.nil? + + # require arglist is a hash with k/v's, each of those need to be in the instance + tag_require_state && reqargs.map { |k, v| instance.send(k) == v }.inject(true) { |inj, el| inj && el } + end - def get_load_balancer_by_instance(instance_id) - hash = elb.load_balancers.inject({}) do |collect, load_balancer| - load_balancer.instances.each {|load_balancer_instance_id| collect[load_balancer_instance_id] = load_balancer} + def get_load_balancers_by_instance(instance_id) + hash = @load_balancers.inject({}) do |collect, load_balancer| + collect ||= {} + load_balancer.instances.each {|load_balancer_instance_id| collect[load_balancer_instance_id] ||= []; collect[load_balancer_instance_id] << load_balancer} collect end - hash[instance_id] + + hash[instance_id] || [] end def get_load_balancer_by_name(load_balancer_name) lbs = {} - elb.load_balancers.each do |load_balancer| + @load_balancers.each do |load_balancer| lbs[load_balancer.id] = load_balancer end - lbs[load_balancer_name] + lbs[load_balancer_name.to_s] + end + def register_instance_in_elb(instance, load_balancer) + puts "\tREGISTER: #{instance.name}@#{load_balancer.id}" + elb.register_instances_with_load_balancer(instance.id, load_balancer.id) + + fail_after = @cloud_config[:fail_after] || 30 + state = instance_health(load_balancer, instance) + time_elapsed = 0 + + while time_elapsed < fail_after + break if state == "InService" + sleep SLEEP_COUNT + time_elapsed += SLEEP_COUNT + puts "\tVerifying Instance Health: #{instance.name}@#{load_balancer.id}" + state = instance_health(load_balancer, instance) + end + if state == 'InService' + puts "\t#{instance.name}@#{load_balancer.id}: Healthy" + else + puts "\t#{instance.name}@#{load_balancer.id}: tests timed out after #{time_elapsed} seconds." + end + end + + def deregister_instance_from_elb(instance, load_balancer) + puts "\tDEREGISTER: #{instance.name}@#{load_balancer.id}" + elb.deregister_instances_from_load_balancer(instance.id, load_balancer.id) + end + + def deregister_instance_from_elbs(instance) + load_balancers = get_load_balancers_by_instance(instance.id) + return if load_balancers.empty? + load_balancers.each do |load_balancer| + deregister_instance_from_elb(instance, load_balancer) + end + end + + def register_instances_in_elb_hook + @lb_instances.each do |load_balancer, instances| + instances.each do |instance| + register_instance_in_elb(instance, load_balancer) + end + end + end + + def deregister_instances_from_elb_hook + @deregister_lb_instances.each do |load_balancer, instances| + instances.each do |instance| + deregister_instance_from_elb(instance, load_balancer) + end + end end - - def deregister_instance_from_elb(instance_name) - return unless @cloud_config[:load_balanced] + + def deregister_instance_from_elbs_hook(instance_name) instance = get_instance_by_name(instance_name) return if instance.nil? - @@load_balancer = get_load_balancer_by_instance(instance.id) - return if @@load_balancer.nil? - - elb.deregister_instances_from_load_balancer(instance.id, @@load_balancer.id) + deregister_instance_from_elbs(instance) end - def register_instance_in_elb(instance_name, load_balancer_name = '') - return if !@cloud_config[:load_balanced] + def register_instance_in_elb_hook(instance_name, load_balancer_name) instance = get_instance_by_name(instance_name) return if instance.nil? - load_balancer = get_load_balancer_by_name(load_balancer_name) || @@load_balancer + load_balancer = get_load_balancer_by_name(load_balancer_name) return if load_balancer.nil? - - elb.register_instances_with_load_balancer(instance.id, load_balancer.id) - - fail_after = @cloud_config[:fail_after] || 30 - state = instance_health(load_balancer, instance) - time_elapsed = 0 - - while time_elapsed < fail_after - break if state == "InService" - sleep SLEEP_COUNT - time_elapsed += SLEEP_COUNT - STDERR.puts 'Verifying Instance Health' - state = instance_health(load_balancer, instance) - end - if state == 'InService' - STDERR.puts "#{instance.name}: Healthy" - else - STDERR.puts "#{instance.name}: tests timed out after #{time_elapsed} seconds." - end + register_instance_in_elb(instance, load_balancer) end end diff --git a/lib/capify-cloud/capistrano.rb b/lib/capify-cloud/capistrano.rb index dd9edba..3f84bef 100644 --- a/lib/capify-cloud/capistrano.rb +++ b/lib/capify-cloud/capistrano.rb @@ -16,14 +16,24 @@ def capify_cloud desc "Deregisters instance from its ELB" task :deregister_instance do instance_name = variables[:logger].instance_variable_get("@options")[:actions].first - capify_cloud.deregister_instance_from_elb(instance_name) + capify_cloud.deregister_instance_from_elbs_hook(instance_name) end desc "Registers an instance with an ELB." task :register_instance do instance_name = variables[:logger].instance_variable_get("@options")[:actions].first load_balancer_name = variables[:logger].instance_variable_get("@options")[:vars][:loadbalancer] - capify_cloud.register_instance_in_elb(instance_name, load_balancer_name) + capify_cloud.register_instance_in_elb_hook(instance_name, load_balancer_name) + end + + desc "Registers an instances with an ELB." + task :register_instances do + capify_cloud.register_instances_in_elb_hook + end + + desc "Deregisters an instances with an ELB." + task :deregister_instances do + capify_cloud.deregister_instances_from_elb_hook end task :date do @@ -47,9 +57,9 @@ def capify_cloud end namespace :deploy do - before "deploy", "cloud:deregister_instance" - after "deploy", "cloud:register_instance" - after "deploy:rollback", "cloud:register_instance" + before "deploy", "cloud:deregister_instances" + after "deploy", "cloud:register_instances" + after "deploy:rollback", "cloud:register_instances" end def cloud_roles(*roles) @@ -133,6 +143,10 @@ def define_role(role, instance) role role[:name].to_sym, instance.contact_point end end + + def loadbalancer (named_load_balancer, *args) + capify_cloud.loadbalancer(@roles, named_load_balancer, *args) + end def numeric?(object) true if Float(object) rescue false diff --git a/readme.md b/readme.md index ca073ec..9a251a7 100644 --- a/readme.md +++ b/readme.md @@ -153,6 +153,52 @@ cloud_roles :name=>:app, :options=>{ :default => true } would be the equivalent of 'cap app web deploy' +Load balancing (AWS) +==================================================== + +`loadbalancer` configuration allow you to automatically register instances to specified load balancer. There's post-deploy hook `cloud:register_instances` which will register your instances after deploy. +In order to define your instance sets associated with your load balancer, you must specify the load balancer name, the associated roles for that load balancer and any optional params: +For example, in deploy.rb, you would enter the load balancer name (e.g. 'lb_webserver'), the capistrano role associated with that load balancer (.e.g. 'web'), +and any optional params. + + loadbalancer :lb_webserver, :web + loadbalancer :lb_appserver, :app + loadbalancer :lb_dbserver, :db, :port => 22000 + +There are three special optional parameters you can add, `:require`, `:exclude` and `:deregister` . These allow you to register instances associated with your named load balancer, if they meet or fail to meet your `:require`/`:exclude` specifications. If `:deregister` is set to true, the gem uses pre-deploy hook "cloud:deregister_instances" to deregister the instances before deploy. + +The :require and :exclude parameters work on Amazon EC2 instance metadata. + +AWS instances have top level metadata and user defined tag data, and this data can be used by your loadbalancer rule + to include or exclude certain instances from the instance set. + +Take the :require keyword; Lets say we only want to deploy to AWS instances which are in the 'running' state. To do that: + + loadbalancer :lb_appserver, :app, :require => { :state => "running" } + +The server set defined here for role :app are all instances in the loadbalancer 'lb_appserver' with state set to 'running'. + +Perhaps you have added tags to your instances, if so, you might want to deploy to only the instances meeting a specific tag value: + + loadbalancer :lb_appserver, :app, :require => { :state => "running", :tags => {'fleet_color' => "green", 'tier' => 'free'} } + +Or if you want deregister instances for role :app and specific tag during deployment: + + loadbalancer :lb_appserver, :app, :deregister => true, :require => { :tags => {'master' => 'true'} } + + +Now consider the :exclude keyword; Lets say we want to exclude from load balancer AWS instances which are 'micro' sized. To do that: + + loadbalancer :lb_appserver, :app, :exclude => { :instance_type => "t1.micro" } + +You can exclude instances that have certain tags: + + loadbalancer :lb_appserver, :app, :exclude => { :instance_type => "t1.micro", :tags => {'state' => 'dontdeploy' } } + +NOTE: `:exclude` won't deregester instances manually registered or registered during previous deployments + +Some code ported from [cap-elb](https://github.com/danmiley/cap-elb) + Cloud config ==================================================== @@ -167,7 +213,6 @@ The yml file needs to look something like this: :aws_secret_access_key: "YOUR SECRET" :params: :region: 'eu-west-1' - :load_balanced: true :project_tag: "YOUR APP NAME" :Brightbox: @@ -178,11 +223,6 @@ aws_access_key_id, aws_secret_access_key, and region are required for AWS. Other brightbox_client_id and brightbox_secret: are required for Brightbox. If you do not specify a cloud_provider, AWS is assumed. -If :load_balanced is set to true, the gem uses pre and post-deploy -hooks to deregister the instance, reregister it, and validate its -health. -:load_balanced only works for individual instances, not for roles. - The :project_tag parameter is optional. It will limit any commands to running against those instances with a "Project" tag set to the value "YOUR APP NAME". From 10e13409fd94c50dad348c76c3dff9e1e003944d Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 8 Sep 2012 17:25:55 -0300 Subject: [PATCH 2/5] Minor fixes --- readme.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 9a251a7..01a4cf3 100644 --- a/readme.md +++ b/readme.md @@ -172,13 +172,11 @@ The :require and :exclude parameters work on Amazon EC2 instance metadata. AWS instances have top level metadata and user defined tag data, and this data can be used by your loadbalancer rule to include or exclude certain instances from the instance set. -Take the :require keyword; Lets say we only want to deploy to AWS instances which are in the 'running' state. To do that: +Take the :require keyword; Lets say we only want to register AWS instances which are in the 'running' state. To do that: loadbalancer :lb_appserver, :app, :require => { :state => "running" } -The server set defined here for role :app are all instances in the loadbalancer 'lb_appserver' with state set to 'running'. - -Perhaps you have added tags to your instances, if so, you might want to deploy to only the instances meeting a specific tag value: +Perhaps you have added tags to your instances, if so, you might want to register only the instances meeting a specific tag value: loadbalancer :lb_appserver, :app, :require => { :state => "running", :tags => {'fleet_color' => "green", 'tier' => 'free'} } From dc2f67902392a9a5e5667eedb8a63d8e4980a8e6 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 9 Sep 2012 10:03:26 -0300 Subject: [PATCH 3/5] Fixed markup code --- readme.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/readme.md b/readme.md index 01a4cf3..612bfed 100644 --- a/readme.md +++ b/readme.md @@ -161,9 +161,11 @@ In order to define your instance sets associated with your load balancer, you mu For example, in deploy.rb, you would enter the load balancer name (e.g. 'lb_webserver'), the capistrano role associated with that load balancer (.e.g. 'web'), and any optional params. - loadbalancer :lb_webserver, :web - loadbalancer :lb_appserver, :app - loadbalancer :lb_dbserver, :db, :port => 22000 +```ruby +loadbalancer :lb_webserver, :web +loadbalancer :lb_appserver, :app +loadbalancer :lb_dbserver, :db, :port => 22000 +``` There are three special optional parameters you can add, `:require`, `:exclude` and `:deregister` . These allow you to register instances associated with your named load balancer, if they meet or fail to meet your `:require`/`:exclude` specifications. If `:deregister` is set to true, the gem uses pre-deploy hook "cloud:deregister_instances" to deregister the instances before deploy. @@ -174,24 +176,33 @@ AWS instances have top level metadata and user defined tag data, and this data c Take the :require keyword; Lets say we only want to register AWS instances which are in the 'running' state. To do that: - loadbalancer :lb_appserver, :app, :require => { :state => "running" } +```ruby +loadbalancer :lb_appserver, :app, :require => { :state => "running" } +``` Perhaps you have added tags to your instances, if so, you might want to register only the instances meeting a specific tag value: - loadbalancer :lb_appserver, :app, :require => { :state => "running", :tags => {'fleet_color' => "green", 'tier' => 'free'} } +```ruby +loadbalancer :lb_appserver, :app, :require => { :state => "running", :tags => {'fleet_color' => "green", 'tier' => 'free'} } +``` Or if you want deregister instances for role :app and specific tag during deployment: - loadbalancer :lb_appserver, :app, :deregister => true, :require => { :tags => {'master' => 'true'} } - +```ruby +loadbalancer :lb_appserver, :app, :deregister => true, :require => { :tags => {'master' => 'true'} } +``` Now consider the :exclude keyword; Lets say we want to exclude from load balancer AWS instances which are 'micro' sized. To do that: - loadbalancer :lb_appserver, :app, :exclude => { :instance_type => "t1.micro" } +```ruby +loadbalancer :lb_appserver, :app, :exclude => { :instance_type => "t1.micro" } +``` You can exclude instances that have certain tags: - loadbalancer :lb_appserver, :app, :exclude => { :instance_type => "t1.micro", :tags => {'state' => 'dontdeploy' } } +```ruby +loadbalancer :lb_appserver, :app, :exclude => { :instance_type => "t1.micro", :tags => {'state' => 'dontdeploy' } } +``` NOTE: `:exclude` won't deregester instances manually registered or registered during previous deployments From 690e50d520e51f6fcd4fb6e4a5fb1358af531ce4 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Sep 2012 13:00:23 -0400 Subject: [PATCH 4/5] Added filter for cloud_roles --- lib/capify-cloud/capistrano.rb | 10 ++++++++-- readme.md | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/capify-cloud/capistrano.rb b/lib/capify-cloud/capistrano.rb index 3f84bef..c4255f2 100644 --- a/lib/capify-cloud/capistrano.rb +++ b/lib/capify-cloud/capistrano.rb @@ -79,11 +79,17 @@ def cloud_roles(*roles) roles.each {|role| cloud_role(role)} end - def cloud_role(role_name_or_hash) + def cloud_role(role_name_or_hash, *args) role = role_name_or_hash.is_a?(Hash) ? role_name_or_hash : {:name => role_name_or_hash,:options => {}} + role[:options] ||= {} @roles[role[:name]] - + instances = capify_cloud.get_instances_by_role(role[:name]) + + # Filter instances if :require or :exclude parameters are given + instances.delete_if { |i| ! @capify_cloud.all_args_within_instance(i, role[:require]) } unless role[:require].nil? or role[:require].empty? + instances.delete_if { |i| @capify_cloud.any_args_within_instance(i, role[:exclude]) } unless role[:exclude].nil? or role[:exclude].empty? + if role[:options].delete(:default) instances.each do |instance| define_role(role, instance) diff --git a/readme.md b/readme.md index 612bfed..bdd92e1 100644 --- a/readme.md +++ b/readme.md @@ -153,6 +153,15 @@ cloud_roles :name=>:app, :options=>{ :default => true } would be the equivalent of 'cap app web deploy' +You also can use `:require` and `:exclude` parameters: + + ```ruby +cloud_roles :name=>:web, :options=>{ :default => true }, :require => { :state => "running", :tags => {'new' => "yes"}} +cloud_roles :name=>:app, :exclude => { :instance_type => "t1.micro", :tags => {'new' => "no"} } +``` + +See `Load balancing (AWS)` below for more details. + Load balancing (AWS) ==================================================== From 03e8f889535f6fef961f2fc82442e2390d346449 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Sep 2012 13:01:27 -0400 Subject: [PATCH 5/5] README fix; --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index bdd92e1..8a23cff 100644 --- a/readme.md +++ b/readme.md @@ -153,7 +153,7 @@ cloud_roles :name=>:app, :options=>{ :default => true } would be the equivalent of 'cap app web deploy' -You also can use `:require` and `:exclude` parameters: +You also can use `:require` and `:exclude` parameters to include or exclude instances from roles: ```ruby cloud_roles :name=>:web, :options=>{ :default => true }, :require => { :state => "running", :tags => {'new' => "yes"}}