class CloudFormationWrapper::StackManager

Stack Manager Class Class containing static convenience methods for deploying and managing CloudFormation Stacks. @since 1.0

Public Class Methods

construct_template_parameters(parameters) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 134
def self.construct_template_parameters(parameters)
  template_parameters = []
  parameters.each do |k, v|
    template_parameters.push(
      parameter_key: k.to_s,
      parameter_value: v.to_s
    )
  end
  template_parameters
end
delete_change_set(change_set_id, cf_client) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 173
def self.delete_change_set(change_set_id, cf_client)
  puts 'Deleting Change Set'
  cf_client.delete_change_set(change_set_name: change_set_id)
end
deploy(options) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 6
def self.deploy(options)
  unless options[:client]
    access_key_id = options[:access_key_id] || ENV['AWS_ACCESS_KEY_ID'] || ENV['ACCESS_KEY'] ||
                    raise(ArgumentError, 'Cannot find AWS Access Key ID.')

    secret_access_key = options[:secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY'] || ENV['SECRET_KEY'] ||
                        raise(ArgumentError, 'Cannot find AWS Secret Key.')

    credentials = Aws::Credentials.new(access_key_id, secret_access_key)
  end

  region = options[:region] || ENV['AWS_REGION'] || ENV['AMAZON_REGION'] || ENV['AWS_DEFAULT_REGION'] ||
           raise(ArgumentError, 'Cannot find AWS Region.')

  verified_options = verify_options(options)

  cf_client = if verified_options.key?('client')
                verified_options[:client]
              elsif verified_options.key?(:client)
                verified_options[:client]
              else
                Aws::CloudFormation::Client.new(credentials: credentials, region: region)
              end

  ensure_template_file_exists(verified_options[:template_path], cf_client)

  deploy_stack(
    verified_options[:parameters],
    verified_options[:name],
    verified_options[:template_path],
    verified_options[:capabilities],
    cf_client
  )
end
deploy_stack(parameters, stack_name, template_path, capabilities, cf_client) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 96
def self.deploy_stack(parameters, stack_name, template_path, capabilities, cf_client)
  template_parameters = construct_template_parameters(parameters)
  client_token = ENV.fetch('BUILD_NUMBER', SecureRandom.uuid.delete('-'))
  old_stack = describe_stack(stack_name, cf_client)
  change_set_type = old_stack ? 'UPDATE' : 'CREATE'

  create_change_set_params = {
    stack_name: stack_name,
    template_body: File.read(template_path),
    parameters: template_parameters,
    change_set_name: "ChangeSet-#{client_token}",
    client_token: client_token,
    description: ENV.fetch('BUILD_TAG', 'Stack Updates.'),
    change_set_type: change_set_type,
    capabilities: capabilities
  }

  change_set_id = cf_client.create_change_set(create_change_set_params).id

  unless wait_for_stack_change_set_creation(change_set_id, cf_client)
    puts "No changes required for #{stack_name}"
    delete_change_set(change_set_id, cf_client)
    return return_outputs(old_stack)
  end

  list_changes(change_set_id, cf_client)
  time_change_set_executed = Time.now
  execute_change_set(change_set_id, cf_client)
  updated_stack = wait_for_stack_to_complete(stack_name, time_change_set_executed, cf_client)
  if updated_stack.stack_status == 'CREATE_COMPLETE' || updated_stack.stack_status == 'UPDATE_COMPLETE'
    puts "Stack finished updating: #{updated_stack.stack_status}"
  else
    puts "Stack failed to update: #{updated_stack.stack_status} (#{updated_stack.stack_status_reason})"
    return false
  end
  return_outputs(updated_stack)
end
describe_stack(stack_name, cf_client) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 145
def self.describe_stack(stack_name, cf_client)
  response = cf_client.describe_stacks(stack_name: stack_name)
  return false if response.stacks.length != 1
  return response.stacks[0]
rescue Aws::CloudFormation::Errors::ServiceError
  return false
end
ensure_template_file_exists(template_path, cf_client) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 90
def self.ensure_template_file_exists(template_path, cf_client)
  raise ArgumentError, 'CF Template File does not exist.' unless File.file?(template_path)
  cf_client.validate_template(template_body: File.read(template_path))
  puts 'Valid Template File.'
end
execute_change_set(change_set_id, cf_client) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 202
def self.execute_change_set(change_set_id, cf_client)
  puts 'Executing Change Set...'

  client_token = ENV.fetch('BUILD_NUMBER', SecureRandom.uuid.delete('-'))

  cf_client.execute_change_set(change_set_name: change_set_id, client_request_token: client_token)
end
get_latest_events(stack_name, minimum_timestamp_for_events, most_recent_event_id, cf_client) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 246
def self.get_latest_events(stack_name, minimum_timestamp_for_events, most_recent_event_id, cf_client)
  no_new_events = false
  response = nil
  events = []
  loop do
    params = {
      stack_name: stack_name
    }

    params[:next_token] = response.next_token unless response.nil?

    response = cf_client.describe_stack_events(params)

    response.stack_events.each do |event|
      if (event.event_id == most_recent_event_id) || (event.timestamp < minimum_timestamp_for_events)
        no_new_events = true
        break
      end
      events << event
    end

    break if no_new_events || !response.next_token
  end
  events
end
list_changes(change_set_id, cf_client) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 178
def self.list_changes(change_set_id, cf_client)
  response = cf_client.describe_change_set(change_set_name: change_set_id)
  puts
  puts 'Stack Set Changes:'
  response.changes.each do |change|
    resource_change = change.resource_change
    puts "\t#{resource_change.action} - " \
      "#{resource_change.logical_resource_id} " \
      "aka #{resource_change.physical_resource_id} " \
      "(#{resource_change.resource_type})"
    puts "\t\tScope: #{resource_change.scope}"
    puts "\t\tReplacment: #{resource_change.replacement}"
    puts "\t\tDetails:"
    resource_change.details.each do |detail|
      puts "\t\t\tTarget: #{detail.target.attribute} - " \
        "#{detail.target.name} - " \
        "recreate:#{detail.target.requires_recreation}"
      puts "\t\t\tCaused By: #{detail.causing_entity}"
      puts "\t\t\tChange Source: #{detail.change_source}"
    end
  end
  puts
end
return_outputs(stack) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 272
def self.return_outputs(stack)
  return if stack.outputs.empty?

  output_name_width = 30
  output_value_width = 50

  outputs = {}

  puts '   '
  puts "#{'Output Name'.ljust(output_name_width)} " \
    "#{'Value'.ljust(output_value_width)} "
  puts "#{'-'.center(output_name_width, '-')} #{'-'.center(output_value_width, '-')}"

  stack.outputs.each do |output|
    outputs[output.output_key.to_sym] = output.output_value
    puts "#{output.output_key.ljust(output_name_width)} #{output.output_value.ljust(output_value_width)}"
  end

  puts '   '
  outputs
end
validate_template(options) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 41
def self.validate_template(options)
  unless options[:client]
    access_key_id = options[:access_key_id] || ENV['AWS_ACCESS_KEY_ID'] || ENV['ACCESS_KEY'] ||
                    raise(ArgumentError, 'Cannot find AWS Access Key ID.')

    secret_access_key = options[:secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY'] || ENV['SECRET_KEY'] ||
                        raise(ArgumentError, 'Cannot find AWS Secret Key.')

    credentials = Aws::Credentials.new(access_key_id, secret_access_key)
  end

  region = options[:region] || ENV['AWS_REGION'] || ENV['AMAZON_REGION'] || ENV['AWS_DEFAULT_REGION'] ||
           raise(ArgumentError, 'Cannot find AWS Region.')

  unless options[:template_path] && (options[:template_path].is_a? String)
    raise ArgumentError, 'template_path must be provided (String)'
  end

  cf_client = if options.key?('client')
                options[:client]
              elsif options.key?(:client)
                options[:client]
              else
                Aws::CloudFormation::Client.new(credentials: credentials, region: region)
              end

  cf_client.validate_template(template_body: File.read(options[:template_path]))
end
verify_options(options) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 70
def self.verify_options(options)
  defaults = {
    description: 'Deployed with CloudFormation Wrapper.', parameters: {}, wait_for_stack: true,
    capabilities: []
  }

  options_with_defaults = options.reverse_merge(defaults)

  unless options_with_defaults[:template_path] && (options_with_defaults[:template_path].is_a? String)
    raise ArgumentError, 'template_path must be provided (String)'
  end

  unless options_with_defaults[:parameters] && (options_with_defaults[:parameters].is_a? Hash)
    raise ArgumentError, 'parameters must be provided (Hash)'
  end

  return options_with_defaults if options_with_defaults[:name] && (options_with_defaults[:name].is_a? String)
  raise ArgumentError, 'name must be provided (String)'
end
wait_for_stack_change_set_creation(change_set_id, cf_client) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 153
def self.wait_for_stack_change_set_creation(change_set_id, cf_client)
  polling_period = 1 # second

  puts "Waiting for the Change Set (#{change_set_id}) to be reviewed..."

  loop do
    sleep(polling_period)
    response = cf_client.describe_change_set(change_set_name: change_set_id)
    if response.status == 'CREATE_COMPLETE'
      puts "Change Set (#{change_set_id}) created."
      return true
    end
    if response.status == 'FAILED'
      puts "Change Set (#{change_set_id}) creation failed: #{response.status_reason}"
      return false
    end
    puts '...'
  end
end
wait_for_stack_to_complete(stack_name, minimum_timestamp_for_events, cf_client) click to toggle source
# File lib/cloudformation_wrapper/stack_manager.rb, line 210
def self.wait_for_stack_to_complete(stack_name, minimum_timestamp_for_events, cf_client)
  timestamp_width = 30
  logical_resource_width = 40
  resource_status_width = 40
  polling_period = 3 # seconds
  most_recent_event_id = ''

  puts
  puts "#{'Timestamp'.ljust(timestamp_width)} " \
  "#{'Logical Resource Id'.ljust(logical_resource_width)} " \
  "#{'Status'.ljust(resource_status_width)} "

  puts "#{'-'.center(timestamp_width, '-')} " \
    "#{'-'.center(logical_resource_width, '-')} " \
    "#{'-'.center(resource_status_width, '-')}"

  stack = {}
  loop do
    sleep(polling_period)
    stack = describe_stack(stack_name, cf_client)
    events = get_latest_events(stack_name, minimum_timestamp_for_events, most_recent_event_id, cf_client)
    most_recent_event_id = events[0].event_id unless events.empty?
    events.reverse_each do |event|
      line = "#{event.timestamp.to_s.ljust(timestamp_width)} " \
      "#{event.logical_resource_id.ljust(logical_resource_width)} " \
      "#{event.resource_status.ljust(resource_status_width)} "
      if !event.resource_status.end_with?('IN_PROGRESS') && !event.resource_status_reason.nil?
        line << event.resource_status_reason
      end
      puts line
    end
    break unless stack.stack_status.end_with?('IN_PROGRESS')
  end
  stack
end