Class: Hako::Schedulers::EcsAutoscaling

Inherits:
Object
  • Object
show all
Defined in:
lib/hako/schedulers/ecs_autoscaling.rb

Defined Under Namespace

Classes: Policy

Constant Summary collapse

PUT_METRIC_ALARM_OPTIONS =
%i[
  alarm_name alarm_description actions_enabled ok_actions alarm_actions
  insufficient_data_actions metric_name namespace statistic dimensions
  period unit evaluation_periods threshold comparison_operator
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(options, region, ecs_elb_client, dry_run:) ⇒ EcsAutoscaling

Returns a new instance of EcsAutoscaling.



12
13
14
15
16
17
18
19
20
# File 'lib/hako/schedulers/ecs_autoscaling.rb', line 12

def initialize(options, region, ecs_elb_client, dry_run:)
  @region = region
  @ecs_elb_client = ecs_elb_client
  @dry_run = dry_run
  @role_arn = required_option(options, 'role_arn')
  @min_capacity = required_option(options, 'min_capacity')
  @max_capacity = required_option(options, 'max_capacity')
  @policies = required_option(options, 'policies').map { |r| Policy.new(r) }
end

Instance Method Details

#apply(service) ⇒ nil

Parameters:

  • service (Aws::ECS::Types::Service)

Returns:

  • (nil)


30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/hako/schedulers/ecs_autoscaling.rb', line 30

def apply(service)
  resource_id = service_resource_id(service)
  service_namespace = 'ecs'
  scalable_dimension = 'ecs:service:DesiredCount'

  Hako.logger.info("Registering scalable target to #{resource_id}")
  unless @dry_run
    autoscaling_client.register_scalable_target(
      service_namespace: service_namespace,
      resource_id: resource_id,
      scalable_dimension: scalable_dimension,
      min_capacity: @min_capacity,
      max_capacity: @max_capacity,
      role_arn: @role_arn,
    )
  end
  @policies.each do |policy|
    Hako.logger.info("Configuring scaling policy #{policy.name}")
    if @dry_run
      if policy.policy_type == 'StepScaling'
        policy.alarms.each do |alarm_name|
          Hako.logger.info("Configuring #{alarm_name}'s alarm_action")
        end
      end
    else
      policy_params = {
        policy_name: policy.name,
        service_namespace: service_namespace,
        resource_id: resource_id,
        scalable_dimension: scalable_dimension,
        policy_type: policy.policy_type,
      }
      if policy.policy_type == 'StepScaling'
        policy_params[:step_scaling_policy_configuration] = {
          adjustment_type: policy.adjustment_type,
          step_adjustments: [
            {
              scaling_adjustment: policy.scaling_adjustment,
              metric_interval_lower_bound: policy.metric_interval_lower_bound,
              metric_interval_upper_bound: policy.metric_interval_upper_bound,
            },
          ],
          cooldown: policy.cooldown,
          metric_aggregation_type: policy.metric_aggregation_type,
        }
      else
        predefined_metric_specification = {
          predefined_metric_type: policy.predefined_metric_type,
        }
        if policy.predefined_metric_type == 'ALBRequestCountPerTarget'
          if service.load_balancers.empty? || service.load_balancers[0].target_group_arn.nil?
            raise Error.new('Target group must be attached to the ECS service for predefined metric type ALBRequestCountPerTarget')
          end

          resource_label = target_group_resource_label
          unless resource_label.start_with?('app/')
            raise Error.new("Load balancer type must be 'application' for predefined metric type ALBRequestCountPerTarget")
          end

          predefined_metric_specification[:resource_label] = resource_label
        end
        policy_params[:target_tracking_scaling_policy_configuration] = {
          target_value: policy.target_value,
          predefined_metric_specification: predefined_metric_specification,
          scale_out_cooldown: policy.scale_out_cooldown,
          scale_in_cooldown: policy.scale_in_cooldown,
          disable_scale_in: policy.disable_scale_in,
        }
      end
      policy_arn = autoscaling_client.put_scaling_policy(policy_params).policy_arn

      if policy.policy_type == 'StepScaling'
        alarms = cw_client.describe_alarms(alarm_names: policy.alarms).flat_map(&:metric_alarms).map { |a| [a.alarm_name, a] }.to_h
        policy.alarms.each do |alarm_name|
          alarm = alarms.fetch(alarm_name) { raise Error.new("Alarm #{alarm_name} does not exist") }
          Hako.logger.info("Updating #{alarm_name}'s alarm_actions from #{alarm.alarm_actions} to #{[policy_arn]}")
          params = PUT_METRIC_ALARM_OPTIONS.map { |key| [key, alarm.public_send(key)] }.to_h
          params[:alarm_actions] = [policy_arn]
          cw_client.put_metric_alarm(params)
        end
      end
    end
  end
  nil
end

#autoscaling_clientAws::ApplicationAutoScaling (private)

Returns:

  • (Aws::ApplicationAutoScaling)


141
142
143
# File 'lib/hako/schedulers/ecs_autoscaling.rb', line 141

def autoscaling_client
  @autoscaling_client ||= Aws::ApplicationAutoScaling::Client.new(region: @region)
end

#cw_clientAws::CloudWatch::Client (private)

Returns:

  • (Aws::CloudWatch::Client)


146
147
148
# File 'lib/hako/schedulers/ecs_autoscaling.rb', line 146

def cw_client
  @cw_client ||= Aws::CloudWatch::Client.new(region: @region)
end

#required_option(options, key) ⇒ Object (private)

Parameters:

  • options (Hash)
  • key (String)

Returns:

  • (Object)


136
137
138
# File 'lib/hako/schedulers/ecs_autoscaling.rb', line 136

def required_option(options, key)
  options.fetch(key) { raise Error.new("scheduler.autoscaling.#{key} must be set") }
end

#service_resource_id(service) ⇒ String (private)

Parameters:

  • service (Aws::ECS::Types::Service)

Returns:

  • (String)


152
153
154
# File 'lib/hako/schedulers/ecs_autoscaling.rb', line 152

def service_resource_id(service)
  "service/#{service.cluster_arn.slice(%r{[^/]+\z}, 0)}/#{service.service_name}"
end

#status(service) ⇒ nil

Parameters:

  • service (Aws::ECS::Types::Service)

Returns:

  • (nil)


118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/hako/schedulers/ecs_autoscaling.rb', line 118

def status(service)
  resource_id = service_resource_id(service)
  service_namespace = 'ecs'
  scalable_dimension = 'ecs:service:DesiredCount'

  autoscaling_client.describe_scaling_activities(service_namespace: service_namespace, resource_id: resource_id, scalable_dimension: scalable_dimension, max_results: 50).scaling_activities.each do |activity|
    puts "  [#{activity.start_time} - #{activity.end_time}] #{activity.status_message}"
    puts "    description: #{activity.description}"
    puts "    cause: #{activity.cause}"
    puts "    details: #{activity.details}"
  end
end

#target_group_resource_labelString (private)

Returns:

  • (String)


157
158
159
160
161
162
# File 'lib/hako/schedulers/ecs_autoscaling.rb', line 157

def target_group_resource_label
  target_group = @ecs_elb_client.describe_target_group
  load_balancer_arn = target_group.load_balancer_arns[0]
  target_group_arn = target_group.target_group_arn
  "#{load_balancer_arn.slice(%r{:loadbalancer/(.+)\z}, 1)}/#{target_group_arn.slice(/[^:]+\z/)}"
end