Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 126 additions & 38 deletions lib/capify-cloud.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
34 changes: 27 additions & 7 deletions lib/capify-cloud/capistrano.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
70 changes: 64 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
====================================================

Expand All @@ -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:
Expand All @@ -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".
Expand Down