class Moonshot::DeploymentMechanism::CodeDeploy

This mechanism is used to deploy software to an auto-scaling group within a stack. It currently only works with the S3Bucket ArtifactRepository.

Usage: class MyApp < Moonshot::CLI

self.artifact_repository = S3Bucket.new('foobucket')
self.deployment_mechanism = CodeDeploy.new(asg: 'AutoScalingGroup')

end

Public Class Methods

new( asg: [], role: 'CodeDeployRole', app_name: nil, group_name: nil, config_name: 'CodeDeployDefault.OneAtATime') click to toggle source

@param asg [Array, String]

The logical name of the AutoScalingGroup to create and manage a Deployment
Group for in CodeDeploy.

@param role [String]

IAM role with AWSCodeDeployRole policy. CodeDeployRole is considered as
default role if its not specified.

@param app_name [String, nil] (nil)

The name of the CodeDeploy Application. By default, this is the same as
the stack name, and probably what you want. If you have multiple
deployments in a single Stack, they must have unique names.

@param group_name [String, nil] (nil)

The name of the CodeDeploy Deployment Group. By default, this is the same
as app_name.

@param config_name [String]

Name of the Deployment Config to use for CodeDeploy,  By default we use
CodeDeployDefault.OneAtATime.
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 32
def initialize(
    asg: [],
    role: 'CodeDeployRole',
    app_name: nil,
    group_name: nil,
    config_name: 'CodeDeployDefault.OneAtATime')
  @asg_logical_ids = asg.is_a?(Array) ? asg : [asg]
  @app_name = app_name
  @group_name = group_name
  @codedeploy_role = role
  @codedeploy_config = config_name
end

Public Instance Methods

deploy_hook(artifact_repo, version_name) click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 68
def deploy_hook(artifact_repo, version_name)
  success = true
  deployment_id = nil

  ilog.start_threaded 'Creating Deployment' do |s|
    res = cd_client.create_deployment(
      application_name: app_name,
      deployment_group_name: group_name,
      revision: revision_for_artifact_repo(artifact_repo, version_name),
      deployment_config_name: @codedeploy_config,
      description: "Deploying version #{version_name}"
    )
    deployment_id = res.deployment_id
    s.continue "Created Deployment #{deployment_id.blue}."
    success = wait_for_deployment(deployment_id, s)
  end

  handle_deployment_failure(deployment_id) unless success
end
post_create_hook() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 45
def post_create_hook
  create_application_if_needed
  create_deployment_group_if_needed

  wait_for_asg_capacity
end
post_delete_hook() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 88
def post_delete_hook
  ilog.start 'Cleaning up CodeDeploy Application' do |s|
    if application_exists?
      cd_client.delete_application(application_name: app_name)
      s.success "Deleted CodeDeploy Application '#{app_name}'."
    else
      s.success "CodeDeploy Application '#{app_name}' does not exist."
    end
  end
end
post_update_hook() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 52
def post_update_hook
  post_create_hook

  unless deployment_group_ok? # rubocop:disable GuardClause
    delete_deployment_group
    create_deployment_group_if_needed
  end
end
status_hook() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 61
def status_hook
  t = Moonshot::UnicodeTable.new('')
  application = t.add_leaf("CodeDeploy Application: #{app_name}")
  application.add_line(code_deploy_status_msg)
  t.draw_children
end

Private Instance Methods

app_name() click to toggle source

By default, use the stack name as the application name, unless one has been provided.

# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 103
def app_name
  @app_name || stack.name
end
application_exists?() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 189
def application_exists?
  cd_client.get_application(application_name: app_name)
  true
rescue Aws::CodeDeploy::Errors::ApplicationDoesNotExistException
  false
end
asg_names() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 181
def asg_names
  names = []
  auto_scaling_groups.each do |auto_scaling_group|
    names.push(auto_scaling_group.auto_scaling_group_name)
  end
  names
end
auto_scaling_groups() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 158
def auto_scaling_groups
  @auto_scaling_groups ||= load_auto_scaling_groups
end
code_deploy_status_msg() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 143
def code_deploy_status_msg
  case [application_exists?, deployment_group_exists?, deployment_group_ok?]
  when [true, true, true]
    'Application and Deployment Group are configured correctly.'.green
  when [true, true, false]
    'Deployment Group exists, but not associated with the correct '\
    "Auto-Scaling Group, try running #{'update'.yellow}."
  when [true, false, false]
    "Deployment Group does not exist, try running #{'create'.yellow}."
  when [false, false, false]
    'Application and Deployment Group do not exist, try running'\
    " #{'create'.yellow}."
  end
end
create_application_if_needed() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 121
def create_application_if_needed
  ilog.start "Creating #{pretty_app_name}." do |s|
    if application_exists?
      s.success "#{pretty_app_name} already exists."
    else
      cd_client.create_application(application_name: app_name)
      s.success "Created #{pretty_app_name}."
    end
  end
end
create_deployment_group() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 239
def create_deployment_group
  cd_client.create_deployment_group(
    application_name: app_name,
    deployment_group_name: group_name,
    service_role_arn: role.arn,
    auto_scaling_groups: asg_names)
end
create_deployment_group_if_needed() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 132
def create_deployment_group_if_needed
  ilog.start "Creating #{pretty_deploy_group}." do |s|
    if deployment_group_exists?
      s.success "CodeDeploy #{pretty_deploy_group} already exists."
    else
      create_deployment_group
      s.success "Created #{pretty_deploy_group}."
    end
  end
end
delete_deployment_group() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 230
def delete_deployment_group
  ilog.start "Deleting #{pretty_deploy_group}." do |s|
    cd_client.delete_deployment_group(
      application_name: app_name,
      deployment_group_name: group_name)
    s.success
  end
end
deployment_group() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 196
def deployment_group
  cd_client.get_deployment_group(
    application_name: app_name, deployment_group_name: group_name)
           .deployment_group_info
end
deployment_group_exists?() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 202
def deployment_group_exists?
  cd_client.get_deployment_group(
    application_name: app_name, deployment_group_name: group_name)
  true
rescue Aws::CodeDeploy::Errors::ApplicationDoesNotExistException,
       Aws::CodeDeploy::Errors::DeploymentGroupDoesNotExistException
  false
end
deployment_group_ok?() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 211
def deployment_group_ok?
  return false unless deployment_group_exists?
  asgs = deployment_group.auto_scaling_groups
  return false unless asgs
  return false unless asgs.count == auto_scaling_groups.count
  asgs.each do |asg|
    if (auto_scaling_groups.find_index { |a| a.auto_scaling_group_name == asg.name }).nil?
      return false
    end
  end
  true
end
doctor_check_auto_scaling_resource_defined() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 349
def doctor_check_auto_scaling_resource_defined
  @asg_logical_ids.each do |asg_logical_id|
    if stack.template.resource_names.include?(asg_logical_id)
      success("Resource '#{asg_logical_id}' exists in the CloudFormation template.") # rubocop:disable LineLength
    else
      critical("Resource '#{asg_logical_id}' does not exist in the CloudFormation template!") # rubocop:disable LineLength
    end
  end
end
doctor_check_code_deploy_role() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 336
  def doctor_check_code_deploy_role
    iam_client.get_role(role_name: @codedeploy_role).role
    success("#{@codedeploy_role} exists.")
  rescue => e
    help = <<-EOF
Error: #{e.message}

For information on provisioning an account for use with CodeDeploy, see:
http://docs.aws.amazon.com/codedeploy/latest/userguide/how-to-create-service-role.html
    EOF
    critical("Could not find #{@codedeploy_role}, ", help)
  end
group_name() click to toggle source

By default, use the stack name as the deployment group name, unless one has been provided.

# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 109
def group_name
  @group_name || stack.name
end
handle_deployment_failure(deployment_id) click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 291
def handle_deployment_failure(deployment_id)
  instances = cd_client.list_deployment_instances(deployment_id: deployment_id)
                       .instances_list.map do |instance_id|
    cd_client.get_deployment_instance(deployment_id: deployment_id,
                                      instance_id: instance_id)
  end

  instances.map(&:instance_summary).each do |inst_summary|
    next unless inst_summary.status == 'Failed'

    inst_summary.lifecycle_events.each do |event|
      next unless event.status == 'Failed'

      ilog.error(event.diagnostics.message)
      event.diagnostics.log_tail.each_line do |line|
        ilog.error(line)
      end
    end
  end

  raise 'Deployment was unsuccessful!'
end
load_auto_scaling_groups() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 162
def load_auto_scaling_groups
  autoscaling_groups = []
  @asg_logical_ids.each do |asg_logical_id|
    asg_name = stack.physical_id_for(asg_logical_id)
    unless asg_name
      raise "Could not find #{asg_logical_id} resource in Stack."
    end

    groups = as_client.describe_auto_scaling_groups(
      auto_scaling_group_names: [asg_name])
    if groups.auto_scaling_groups.empty?
      raise "Could not find ASG #{asg_name}."
    end

    autoscaling_groups.push(groups.auto_scaling_groups.first)
  end
  autoscaling_groups
end
pretty_app_name() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 113
def pretty_app_name
  "CodeDeploy Application #{app_name.blue}"
end
pretty_deploy_group() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 117
def pretty_deploy_group
  "CodeDeploy Deployment Group #{app_name.blue}"
end
revision_for_artifact_repo(artifact_repo, version_name) click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 314
def revision_for_artifact_repo(artifact_repo, version_name)
  case artifact_repo
  when Moonshot::ArtifactRepository::S3Bucket
    s3_revision_for(artifact_repo, version_name)
  when NilClass
    raise 'Must specify an ArtifactRepository with CodeDeploy. Take a look at the S3Bucket example.' # rubocop:disable LineLength
  else
    raise "Cannot use #{artifact_repo.class} to deploy with CodeDeploy."
  end
end
role() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 224
def role
  iam_client.get_role(role_name: @codedeploy_role).role
rescue Aws::IAM::Errors::NoSuchEntity
  raise "Did not find an IAM Role: #{@codedeploy_role}"
end
s3_revision_for(artifact_repo, version_name) click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 325
def s3_revision_for(artifact_repo, version_name)
  {
    revision_type: 'S3',
    s3_location: {
      bucket: artifact_repo.bucket_name,
      key: artifact_repo.filename_for_version(version_name),
      bundle_type: 'tgz'
    }
  }
end
wait_for_asg_capacity() click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 247
def wait_for_asg_capacity
  ilog.start_threaded 'Waiting for AutoScaling Group(s) to reach capacity...' do |s|
    loop do
      asgs_at_capacity = 0
      asgs = load_auto_scaling_groups
      asgs.each do |asg|
        count = asg.instances.count { |i| i.lifecycle_state == 'InService' }
        if asg.desired_capacity == count
          asgs_at_capacity += 1
          s.continue "#{asg.auto_scaling_group_name} DesiredCapacity is #{asg.desired_capacity}, currently #{count} instance(s) are InService." # rubocop:disable LineLength
        end
      end
      break if asgs.count == asgs_at_capacity
      sleep 5
    end

    s.success 'AutoScaling Group(s) up to capacity!'
  end
end
wait_for_deployment(id, step) click to toggle source
# File lib/moonshot/deployment_mechanism/code_deploy.rb, line 267
def wait_for_deployment(id, step)
  success = true

  loop do
    sleep 5
    info = cd_client.get_deployment(deployment_id: id).deployment_info
    status = info.status

    case status
    when 'Created', 'Queued', 'InProgress'
      step.continue "Waiting for Deployment #{id.blue} to complete, current status is '#{status}'." # rubocop:disable LineLength
    when 'Succeeded'
      step.success "Deployment #{id.blue} completed successfully!"
      break
    when 'Failed', 'Stopped'
      step.failure "Deployment #{id.blue} failed with status '#{status}'"
      success = false
      break
    end
  end

  success
end