Class: Furnish::Provisioner::EC2

Inherits:
AWS
  • Object
show all
Defined in:
lib/furnish/provisioners/ec2.rb

Overview

EC2 Instance Provisioner. See attributes and #new for argument descriptions.

If security groups are supplied to this provisioner as a part of a provisioner group (in #startup’s argument list), they are appended to any list of security group id’s already provided. This allows you to provision a security group with the instances without knowing about either beforehand.

On startup, IP addresses of the instances are returned to the next provisioner in the provisioner group.

It would be in your best interest to familiarize yourself with some of aws-sdk’s API when trying to understand some of these parameters. Here is some documentation:

rdoc.info/gems/aws-sdk

Constant Summary collapse

PASSTHROUGH_ATTRS =

Arguments that are passed through straight to AWS::EC2::InstanceCollection#create.

%w[
  key_name
  availability_zone
  image_id
  kernel_id
  ramdisk_id
  count
  block_device_mappings
  instance_type
  user_data
  security_group_ids
  monitoring_enabled
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from AWS

#access_key, #check_aws_settings, #ec2, #region, #secret_key

Constructor Details

#initialize(args) ⇒ EC2

Create a new EC2 Provisioner. Inherits the initializer from Furnish::Provisioner::AWS.new, see the documentation there for more information.

Additionally, see the attribute listing for argument documentation and their defaults.

Required arguments that do not have defaults. ArgumentError will be raised if they are not supplied at construction time:

  • #access_key (from Furnish::Provisioner::AWS)

  • #secret_key (from Furnish::Provisioner::AWS)

  • #instance_type

  • #image_id

  • #region (also see #availability_zone)

  • #key_name

Additionally see #security_group_ids, #poll_interval and #provision_timeout for some detail on special logic surrounding the operation of this provisioner.



211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/furnish/provisioners/ec2.rb', line 211

def initialize(args)
  super

  check_region
  check_ec2_args

  @count              ||= 1
  @instance_type      ||= "c1.medium"
  @security_group_ids ||= []
  @provision_wait     ||= 300
  @poll_interval      ||= 1
  @instance_ids         = []
end

Instance Attribute Details

#instance_idsObject (readonly)

After provisioning, instance identifiers managed by this provisioner will be set here.



168
169
170
# File 'lib/furnish/provisioners/ec2.rb', line 168

def instance_ids
  @instance_ids
end

Instance Method Details

#availability_zoneObject

:attr: availability_zone

EC2 availability zone. If not provided, EC2 will pick based on the region. If provided, region will be determined from this value.



69
70
71
# File 'lib/furnish/provisioners/ec2.rb', line 69

furnish_property :availability_zone,
"EC2 Availability Zone. If provided and no region supplied, region will be determined from this. If this is not supplied, AZ will be EC2's choosing based on the region.",
String

#block_device_mappingsObject

:attr: block_device_mappings

Mapping for block device information. Passed directly to AWS::EC2::InstanceCollection#create – see its documentation for more information.



139
140
# File 'lib/furnish/provisioners/ec2.rb', line 139

furnish_property :block_device_mappings,
"block device information. Passed directly to AWS::EC2::InstanceCollection#create's argument list. Not required, no default."

#check_ec2_argsObject

Validate additional required EC2-specific arguments like #key_name and #image_id.



245
246
247
248
249
250
251
252
253
# File 'lib/furnish/provisioners/ec2.rb', line 245

def check_ec2_args
  unless key_name
    raise ArgumentError, "key_name is required for instance provision"
  end

  unless image_id
    raise ArgumentError, "an AMI image_id must be provided"
  end
end

#check_regionObject

Overload of Furnish::Provisioner::AWS#check_region that also deals with availability zones.



228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/furnish/provisioners/ec2.rb', line 228

def check_region
  if availability_zone and !region
    @region = availability_zone.sub(/\D+$/, '')
  end

  if availability_zone and region and !availability_zone.start_with?(region)
    raise ArgumentError,
      "region and availability zone are both supplied and do not match: [r: #{region}, a: #{availability_zone}]"
  end

  super
end

#coerce_instances(instances) ⇒ Object

puts the return value from ec2.instances.create into a normal array no matter what it returns. See the method comments for more information.



271
272
273
274
275
276
277
278
279
280
281
# File 'lib/furnish/provisioners/ec2.rb', line 271

def coerce_instances(instances)
  # if count == 1, returns a single instance id.
  instances = [instances] unless instances.kind_of?(Array)

  # XXX there are versions of aws-sdk that respond to #kind_of?(Array)
  # but aren't actually arrays. This will always give us an array.

  tmp_instances = []
  instances.each { |i| tmp_instances.push(i) }
  return tmp_instances
end

#countObject

:attr: count

The number of instances to provision. Default is 1.



107
108
109
# File 'lib/furnish/provisioners/ec2.rb', line 107

furnish_property :count,
"Number of instances to allocate. Required, Default is 1.",
Integer

#image_idObject

:attr: image_id

The AMI identifier. Required. No default.



78
79
80
# File 'lib/furnish/provisioners/ec2.rb', line 78

furnish_property :image_id,
"AMI Identifier. Required. No Default.",
String

#instance_typeObject

:attr: instance_type

The size or “flavor” of instance(s). Required, default is ‘c1.medium’.



116
117
# File 'lib/furnish/provisioners/ec2.rb', line 116

furnish_property :instance_type,
"Size or 'flavor' of instance(s). Required, default is 'c1.medium'."

#kernel_idObject

:attr: kernel_id

The AKI identifier. No default, EC2 picks based on the AMI if not provided.



88
89
90
# File 'lib/furnish/provisioners/ec2.rb', line 88

furnish_property :kernel_id,
"AKI Identifier. If not supplied, left to EC2 to sort out.",
String

#key_nameObject

:attr: key_name

The SSH key name for EC2. Must already exists. Required. No Default.



59
60
61
# File 'lib/furnish/provisioners/ec2.rb', line 59

furnish_property :key_name,
"EC2 SSH Key Name, must already exist. Required. No Default.",
String

#launch_optionsObject

generates options to pass to AWS::EC2::InstanceCollection#create from our attributes.



259
260
261
262
263
264
265
# File 'lib/furnish/provisioners/ec2.rb', line 259

def launch_options
  options = { }

  PASSTHROUGH_ATTRS.each { |x| options[x.to_sym] = send(x) if send(x) }

  return options
end

#monitoring_enabledObject

:attr: monitoring_enabled

Mapping for configuring monitoring. Passed directly to AWS::EC2::InstanceCollection#create – see its documentation for more information.



159
160
# File 'lib/furnish/provisioners/ec2.rb', line 159

furnish_property :monitoring_enabled,
"enable montioring. Passed directly to AWS::EC2::InstanceCollection#create's argument list. Not required, no default."

#poll_intervalObject

:attr: poll_interval

Wait this long between instance status requests. Default is one second. Fractional values are OK.



50
51
52
# File 'lib/furnish/provisioners/ec2.rb', line 50

furnish_property :poll_interval,
"Wait this long between instance status requests. Default is 1 second, fractional values OK.",
Numeric

#provision_timeoutObject

:attr: provision_timeout

Give up after this long for the requested instances to be in a running state. Default is 300 seconds.



40
41
42
# File 'lib/furnish/provisioners/ec2.rb', line 40

furnish_property :provision_timeout,
"Give up after this long for the instance to come alive after requesting from the API. Default is 300 seconds.",
Integer

#ramdisk_idObject

:attr: ramdisk_id

The ARI identifier. No default, EC2 picks based on the AMI and possibly the AKI if not provided.



98
99
100
# File 'lib/furnish/provisioners/ec2.rb', line 98

furnish_property :ramdisk_id,
"ARI Identifier. If not supplied, left to EC2 to sort out.",
String

#reportObject

Furnish reporter – includes image id, number of servers, and instance id’s.



367
368
369
# File 'lib/furnish/provisioners/ec2.rb', line 367

def report
  ["ami #{image_id}; #{count} servers; instance_ids: #{instance_ids.inspect}"]
end

#security_group_idsObject

:attr: security_group_ids

Array of security group identifiers (not *group names*) that these instances will be bound to. Appends any incoming group id’s from previous provisioners. At least one must exist at provisioning time, or the provision will fail.



128
129
130
# File 'lib/furnish/provisioners/ec2.rb', line 128

furnish_property :security_group_ids,
"list of group identifiers (not names) that these instances will be bound to. Appends any incoming group id's from previous provisioners. At least one must exist at provisioning time.",
Array

#shutdown(args = {}) ⇒ Object

All instances are told to terminate, the method then waits for all of them to enter the terminated state, then returns true.



356
357
358
359
360
361
362
# File 'lib/furnish/provisioners/ec2.rb', line 356

def shutdown(args={})
  instances = instance_ids.map { |i| ec2.instances[i] }
  instances.each { |i| i.terminate rescue nil }
  wait_for_instances(instances, :terminated)

  return { }
end

#startup(args = {}) ⇒ Object

Provision instance(s).

If a security group id is supplied to this method, it will be permanently appended to #security_group_ids and used during instance creation.

Regardless of the above behavior, if no security group ids exist, will raise RuntimeError (EC2 requires instances to be in at least one security group).

Records the instance id’s, waits for them to all enter the ‘:running` state, and then returns a list of their public IP addresses to the next provisioner.



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/furnish/provisioners/ec2.rb', line 336

def startup(args={})
  if args[:security_group_ids]
    @security_group_ids += args[:security_group_ids]
  end

  if security_group_ids.empty?
    raise "no security groups supplied either at construction or provisioning time, cannot request instances."
  end

  instances = coerce_instances(ec2.instances.create(launch_options))
  @instance_ids = instances.map(&:id)
  wait_for_instances(instances, :running)

  return({ :security_group_ids => @security_group_ids, :ips => Set[*instances.map(&:ip_address)], :ec2_instance_ids => @instance_ids })
end

#user_dataObject

:attr: user_data

Mapping for user data. Passed directly to AWS::EC2::InstanceCollection#create – see its documentation for more information.



149
150
# File 'lib/furnish/provisioners/ec2.rb', line 149

furnish_property :user_data,
"user data. Passed directly to AWS::EC2::InstanceCollection#create's argument list. Not required, no default."

#wait_for_instances(instances, state) ⇒ Object

Polls the EC2 API waiting for instances to enter the state passed. Uses #poll_interval to determine how long to wait between status requests, and #provision_timeout to determine how long to wait before giving up.



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/furnish/provisioners/ec2.rb', line 289

def wait_for_instances(instances, state)
  Timeout.timeout(provision_timeout) do
    not_running = instances.dup

    until not_running.empty?
      instance = not_running.shift

      status = instance.status rescue nil

      if status
        unless status == state
          if_debug(3) do
            puts "instance #{instance.id} is not in #{state} state yet."
          end

          not_running.push(instance)
        end
      else
        if_debug(3) do
          puts "API server doesn't think #{instance.id} exists yet."
        end

        not_running.push(instance)
      end

      sleep poll_interval
    end
  end
rescue TimeoutError
  raise "instances timed out waiting for ec2 API"
end