lib/capistrano/asg/rolling/autoscale_group.rb
# frozen_string_literal: true
require 'aws-sdk-autoscaling'
module Capistrano
module ASG
module Rolling
# AWS EC2 Auto Scaling Group.
class AutoscaleGroup
include AWS
LIFECYCLE_STATE_IN_SERVICE = 'InService'
LIFECYCLE_STATE_STANDBY = 'Standby'
COMPLETED_REFRESH_STATUSES = %w[Successful Failed Cancelled RollbackSuccessful RollbackFailed].freeze
FAILED_REFRESH_STATUS = 'Failed'
attr_reader :name, :properties, :refresh_id
def initialize(name, properties = {})
@name = name
@properties = properties
if properties[:healthy_percentage]
properties[:min_healthy_percentage] = properties.delete(:healthy_percentage)
Kernel.warn('WARNING: the property `healthy_percentage` is deprecated and will be removed in a future release. Please update to `min_healthy_percentage`.')
end
validate_properties!
end
def exists?
aws_autoscaling_group.exists?
end
def launch_template
@launch_template ||= begin
template = aws_autoscaling_group.launch_template
raise Capistrano::ASG::Rolling::NoLaunchTemplate if template.nil?
LaunchTemplate.new(template.launch_template_id, template.version, template.launch_template_name)
end
end
def subnet_ids
aws_autoscaling_group.vpc_zone_identifier.split(',')
end
def instance_warmup_time
aws_autoscaling_group.health_check_grace_period
end
def min_healthy_percentage
properties.fetch(:min_healthy_percentage, nil)
end
def auto_rollback
properties.fetch(:asg_instance_refresh_auto_rollback, nil)
end
def max_healthy_percentage
properties.fetch(:max_healthy_percentage, nil)
end
def start_instance_refresh(launch_template)
@refresh_id = aws_autoscaling_client.start_instance_refresh(
auto_scaling_group_name: name,
strategy: 'Rolling',
desired_configuration: {
launch_template: {
launch_template_id: launch_template.id,
version: launch_template.version
}
},
preferences: {
instance_warmup: instance_warmup_time,
skip_matching: true,
min_healthy_percentage: min_healthy_percentage,
max_healthy_percentage: max_healthy_percentage,
auto_rollback: auto_rollback
}.compact
).instance_refresh_id
rescue Aws::AutoScaling::Errors::InstanceRefreshInProgress => e
raise Capistrano::ASG::Rolling::StartInstanceRefreshError, e
end
InstanceRefreshStatus = Struct.new(:status, :percentage_complete) do
def completed?
COMPLETED_REFRESH_STATUSES.include?(status)
end
def failed?
status == FAILED_REFRESH_STATUS
end
end
def latest_instance_refresh
instance_refresh = most_recent_instance_refresh
status = instance_refresh&.dig(:status)
percentage_complete = instance_refresh&.dig(:percentage_complete)
return nil if status.nil?
InstanceRefreshStatus.new(status, percentage_complete)
end
# Returns instances with lifecycle state "InService" for this Auto Scaling Group.
def instances
instance_ids = aws_autoscaling_group.instances.select { |i| i.lifecycle_state == LIFECYCLE_STATE_IN_SERVICE }.map(&:instance_id)
return [] if instance_ids.empty?
response = aws_ec2_client.describe_instances(instance_ids: instance_ids)
response.reservations.flat_map(&:instances).map do |instance|
Instance.new(instance.instance_id, instance.private_ip_address, instance.public_ip_address, instance.image_id, self)
end
end
def enter_standby(instance)
instance = aws_autoscaling_group.instances.find { |i| i.id == instance.id }
return if instance.nil?
instance.enter_standby(should_decrement_desired_capacity: true)
loop do
instance.load
break if instance.lifecycle_state == LIFECYCLE_STATE_STANDBY
sleep 1
end
end
def exit_standby(instance)
instance = aws_autoscaling_group.instances.find { |i| i.id == instance.id }
return if instance.nil?
instance.exit_standby
end
def rolling?
properties.fetch(:rolling, true)
end
def name_tag
"Deployment for #{name}"
end
private
def most_recent_instance_refresh
parameters = {
auto_scaling_group_name: name,
max_records: 1
}
parameters[:instance_refresh_ids] = [@refresh_id] if @refresh_id
refresh = aws_autoscaling_client.describe_instance_refreshes(parameters).to_h
refresh[:instance_refreshes].first
end
def aws_autoscaling_group
@aws_autoscaling_group ||= ::Aws::AutoScaling::AutoScalingGroup.new(name: name, client: aws_autoscaling_client)
end
def validate_properties!
raise ArgumentError, 'Property `min_healthy_percentage` must be between 0-100.' if min_healthy_percentage && !(0..100).cover?(min_healthy_percentage)
if max_healthy_percentage
raise ArgumentError, 'Property `max_healthy_percentage` must be between 100-200.' unless (100..200).cover?(max_healthy_percentage)
if min_healthy_percentage
diff = max_healthy_percentage - min_healthy_percentage
raise ArgumentError, 'The difference between `min_healthy_percentage` and `max_healthy_percentage` must not be greater than 100.' if diff > 100
else
raise ArgumentError, 'Property `min_healthy_percentage` must be specified when using `max_healthy_percentage`.'
end
end
end
end
end
end
end