KentaaNL/capistrano-asg-rolling

View on GitHub
lib/capistrano/asg/tasks/rolling.rake

Summary

Maintainability
Test Coverage
# frozen_string_literal: true

namespace :rolling do
  desc 'Setup servers to be used for (rolling) deployment'
  task :setup do
    config.autoscale_groups.each do |group|
      if group.rolling?
        logger.info "Auto Scaling Group: **#{group.name}**, rolling deployment strategy."

        # If we've already launched an instance with this image, then skip it.
        next unless config.instances.with_image(group.launch_template.image_id).empty?

        instance = Capistrano::ASG::Rolling::Instance.run(autoscaling_group: group, overrides: config.instance_overrides)
        logger.info "Launched Instance: **#{instance.id}**"
        config.instances << instance

        logger.verbose "Adding server: **#{instance.ip_address}**"

        # Add server to the Capistrano server list.
        server(instance.ip_address, group.properties)
      else
        logger.info "Auto Scaling Group: **#{group.name}**, standard deployment strategy."

        group.instances.each_with_index do |instance, index|
          if index.zero? && group.properties.key?(:primary_roles)
            server_properties = group.properties.dup
            server_properties[:roles] = server_properties.delete(:primary_roles)
          else
            server_properties = group.properties
          end

          logger.verbose "Adding server: **#{instance.ip_address}**"

          # Add server to the Capistrano server list.
          server(instance.ip_address, server_properties)
        end
      end
    end

    unless config.instances.empty?
      logger.info 'Waiting for SSH to be available...'
      config.instances.wait_for_ssh
    end
  end

  desc 'Update Auto Scaling Groups: create AMIs, update Launch Templates and start Instance Refresh'
  task :update do
    if config.rolling_update? && !config.instances.empty?
      logger.info 'Stopping instance(s)...'
      config.instances.stop

      logger.info 'Creating AMI(s)...'
      amis = config.instances.create_ami(description: revision_log_message, tags: Capistrano::ASG::Rolling::Tags.ami_tags)

      logger.info 'Updating Launch Template(s) with the new AMI(s)...'
      launch_templates = config.autoscale_groups.launch_templates
      updated_templates = launch_templates.update(amis: amis, description: revision_log_message)

      logger.info 'Triggering Instance Refresh on Auto Scaling Group(s)...'
      updated_templates.each do |launch_template|
        config.autoscale_groups.with_launch_template(launch_template).each do |group|
          group.start_instance_refresh(launch_template)

          logger.verbose "Successfully started Instance Refresh on Auto Scaling Group **#{group.name}**."
        rescue Capistrano::ASG::Rolling::StartInstanceRefreshError => e
          logger.info "Failed to start Instance Refresh on Auto Scaling Group **#{group.name}**: #{e.message}"
        end
      end

      config.launch_templates.merge(updated_templates)
    end
  end

  desc 'Clean up old Launch Template versions and AMIs and terminate instances'
  task :cleanup do
    unless config.launch_templates.empty?
      # Keep track of deleted AMIs, so we can clean up Launch Templates that use the same AMI.
      deleted_amis = []

      logger.info 'Cleaning up old Launch Template version(s) and AMI(s)...'
      config.launch_templates.each do |launch_template|
        launch_template.previous_versions.reject(&:default_version?).drop(config.keep_versions).each do |version|
          # Need to retrieve AMI before deleting the Launch Template version.
          ami = version.ami
          exists = ami.exists?
          deleted = deleted_amis.include?(ami)

          if !exists && !deleted
            logger.warning("AMI **#{ami.id}** does not exist for Launch Template **#{version.name}** version **#{version.version}**.")
            next
          end

          # Only clean up when AMI was tagged by us.
          next if exists && (!ami.tag?('capistrano-asg-rolling:version') || !ami.tag?('capistrano-asg-rolling:gem-version'))

          logger.verbose "Deleting Launch Template **#{version.name}** version **#{version.version}**..."
          version.delete

          next if deleted

          logger.verbose "Deleting AMI **#{ami.id}** and snapshots..."
          ami.delete

          deleted_amis << ami
        end
      end
    end

    instances = config.instances.auto_terminate
    if instances.any?
      logger.info 'Terminating instance(s)...'
      begin
        instances.terminate
      rescue Capistrano::ASG::Rolling::InstanceTerminateFailed => e
        logger.warning "Failed to terminate Instance **#{e.instance.id}**: #{e.message}"
      end
    end
  end

  desc 'Launch Instances by marking instances to not automatically terminate'
  task :launch_instances do
    if config.instances.any?
      config.instances.each do |instance|
        instance.auto_terminate = false
      end
    else
      raise Capistrano::ASG::Rolling::NoInstancesLaunched
    end
  end

  desc 'Do a test deployment: run the deploy task but do not trigger the update ASG task and do not automatically terminate instances'
  task :deploy_test do
    config.rolling_update = false

    if config.instances.any?
      config.instances.each do |instance|
        instance.auto_terminate = false
      end
    else
      raise Capistrano::ASG::Rolling::NoInstancesLaunched
    end

    invoke 'deploy'
  end

  desc 'Create an AMI from an Instance in the Auto Scaling Groups'
  task :create_ami do
    config.autoscale_groups.each do |group|
      logger.info 'Selecting instance to create AMI from...'

      # Pick a random instance, put it in standby and create an AMI.
      instance = group.instances.sample
      if instance
        logger.info "Instance **#{instance.id}** entering standby state..."
        group.enter_standby(instance)

        logger.info 'Stopping instance...'
        instance.stop

        logger.info 'Creating AMI...'
        ami = instance.create_ami(description: revision_log_message, tags: Capistrano::ASG::Rolling::Tags.ami_tags)

        logger.info 'Starting instance...'
        instance.start

        logger.info "Instance **#{instance.id}** exiting standby state..."
        group.exit_standby(instance)

        logger.info 'Updating Launch Template with the new AMI...'
        launch_template = group.launch_template
        launch_template.create_version(image_id: ami.id, description: revision_log_message)

        config.launch_templates << launch_template
      else
        logger.error 'Unable to create AMI. No instance with a valid state was found in the Auto Scaling Group.'
      end
    end
  end

  desc 'Get status of instance refresh'
  task :instance_refresh_status do
    if config.wait_for_instance_refresh?
      groups = config.autoscale_groups.to_h { |group| [group.name, group] }
      completed_groups = []

      while groups.any?
        groups.each do |name, group|
          refresh = group.latest_instance_refresh
          if refresh.nil? || refresh.completed?
            logger.info "Auto Scaling Group: **#{name}**, completed with status '#{refresh.status}'." if refresh.completed?
            completed_groups.push groups.delete(name)
          elsif !refresh.percentage_complete.nil?
            logger.info "Auto Scaling Group: **#{name}**, #{refresh.percentage_complete}% completed, status '#{refresh.status}'."
          else
            logger.info "Auto Scaling Group: **#{name}**, status '#{refresh.status}'."
          end
        end
        next if groups.empty?

        wait_for = config.instance_refresh_polling_interval
        logger.info "Instance refresh(es) not completed, waiting #{wait_for} seconds..."
        sleep wait_for
      end

      failed = completed_groups.any? { |group| group.latest_instance_refresh.failed? }
      raise Capistrano::ASG::Rolling::InstanceRefreshFailed if failed
    end
  end
end