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..c4255f2 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) @@ -69,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) @@ -133,6 +149,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..8a23cff 100644 --- a/readme.md +++ b/readme.md @@ -153,6 +153,70 @@ cloud_roles :name=>:app, :options=>{ :default => true } would be the equivalent of 'cap app web deploy' +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"}} +cloud_roles :name=>:app, :exclude => { :instance_type => "t1.micro", :tags => {'new' => "no"} } +``` + +See `Load balancing (AWS)` below for more details. + +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. + +```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. + +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 register AWS instances which are in the 'running' state. To do that: + +```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: + +```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: + +```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: + +```ruby +loadbalancer :lb_appserver, :app, :exclude => { :instance_type => "t1.micro" } +``` + +You can exclude instances that have certain tags: + +```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 + +Some code ported from [cap-elb](https://github.com/danmiley/cap-elb) + Cloud config ==================================================== @@ -167,7 +231,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 +241,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".