asger 0.2.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: abc4ad19cb57955e168d9f3c1931eec3ba3f4835
4
- data.tar.gz: d9c71640218a1819c020207e04b962be00875325
3
+ metadata.gz: 5c91487b1cd2ad1031fc3d9b1e72a789dd446ca4
4
+ data.tar.gz: fc47bf1f5b779029ef2a69d8219777c653422462
5
5
  SHA512:
6
- metadata.gz: f08683fa0fdaff1e6ab811d79d46dc855b6817063815748b1889a3dd28f404341e1089f8a0c5d7056867355a598807c649845aa19a2c39c1640ee210f1263886
7
- data.tar.gz: 58804ae9a19d5e4edab5b77fba9fa9aaf6057b57bc63fe59ca1b507b3c17db433aff00a3df7667ea14f43c20247e5235498fbfa20eee10f7cd1ed2456f212387
6
+ metadata.gz: daad5b8b810429b6989cfda98b67da0384022edb68970071ec00a339ec6cee7a7013edde5d7ce27fc60990ea63e7337112aede22ba23c5d85915f77023a591e8
7
+ data.tar.gz: 090c4f37a0f6e461e74f4933fcb5fe921f5c3a0f65e7b5e908de355dbd52ea640ee510c5e78906c8ad04ffc4d29cf5940a658201a599293bcc88f52bcfc15a2d
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.0
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `asger` #
2
2
 
3
- `asger` is a tool designed to field notifications from Amazon EC2 auto-scaling groups via a SNS topic subscribed to an SQS queue. (Which probably sounds alarmingly specific, but it's the most common way to do this!) Once a notification is fielded, the user can define Tasks that then perform actions on instance creation ("up" functions) and termination ("down" functions).
3
+ `asger` is a tool designed to field notifications from Amazon EC2 auto-scaling groups via a SNS topic subscribed to an SQS queue. (Which probably sounds alarmingly specific, but it's the most common way to do this!) Once a notification is fielded, the user can define Tasks that then perform actions on instance creation ("up" functions) and termination ("down" functions), as well as their associated failure events.
4
4
 
5
5
  ### Important Notes ###
6
6
  - When multiple tasks are running in a single `asger` instance, they will be run in order on instance creation and _in reverse order_ on instance termination.
data/asger.gemspec CHANGED
@@ -21,8 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency "rake", "~> 10.0"
22
22
  spec.add_development_dependency "pry"
23
23
 
24
- spec.add_runtime_dependency 'aws-sdk', '~> 2.0.27'
25
- spec.add_runtime_dependency 'trollop', '~> 2.1.1'
24
+ spec.add_runtime_dependency 'aws-sdk', '~> 2.2.22'
25
+ spec.add_runtime_dependency 'trollop', '~> 2.1.1'
26
26
  spec.add_runtime_dependency "hashie", "~> 3.3"
27
+ spec.add_runtime_dependency 'ice_nine', '~> 0.11.2'
27
28
  spec.add_runtime_dependency 'activesupport', '~> 4.2.0'
28
29
  end
data/lib/asger/cli.rb CHANGED
@@ -20,17 +20,19 @@ module Asger
20
20
  :type => :string, :multi => true
21
21
  opt :queue_url, "URL of the SQS queue to read from",
22
22
  :type => :string
23
- opt :pause_time, "Time (in seconds) to pause between polls.",
24
- :default => 0
25
23
  opt :verbose, "enables verbose logging",
26
24
  :default => false
27
25
  opt :die_on_error, "Terminates if an exception is thrown within the task runner.",
28
26
  :default => true
29
27
 
28
+ opt :delete_messages, 'Delete messages from the SQS queue after processing (off is useful for development).',
29
+ :default => true
30
+
30
31
  opt :aws_logging, "Provides the Asger logger to AWS (use for deep debugging).", :default => false
31
32
 
32
33
  opt :shared_credentials, "Tells Asger to use shared credentials from '~/.aws/credentials'.", :type => :string
33
34
  opt :iam, "Tells Asger to use IAM credentials.", :default => false
35
+ opt :region, 'Specifies an AWS region.', :type => :string
34
36
  end
35
37
 
36
38
  logger = Logger.new($stderr)
@@ -47,9 +49,10 @@ module Asger
47
49
  exit(1)
48
50
  end
49
51
 
50
- logger.warn "No tasks configured; Asger will run, but won't do much." unless (opts[:task_file] && !opts[:task_file].empty?)
52
+ logger.warn "No tasks configured; Asger will run, but won't do much." \
53
+ unless (opts[:task_file] && !opts[:task_file].empty?)
51
54
 
52
- param_files =
55
+ param_files =
53
56
  opts[:parameter_file].map do |pf|
54
57
  logger.debug "Parsing parameter file '#{pf}'."
55
58
  case File.extname(pf)
@@ -76,8 +79,12 @@ module Asger
76
79
  end
77
80
 
78
81
  aws_logger = opts[:aws_logging] ? logger : nil
79
- sqs_client = Aws::SQS::Client.new(logger: aws_logger, credentials: credentials)
80
- ec2_client = Aws::EC2::Client.new(logger: aws_logger, credentials: credentials)
82
+ sqs_client = Aws::SQS::Client.new(logger: aws_logger,
83
+ region: opts[:region], credentials: credentials)
84
+ ec2_client = Aws::EC2::Client.new(logger: aws_logger,
85
+ region: opts[:region], credentials: credentials)
86
+ asg_client = Aws::AutoScaling::Client.new(logger: aws_logger,
87
+ region: opts[:region], credentials: credentials)
81
88
 
82
89
 
83
90
  stock_scripts_dir = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "stock_scripts"))
@@ -85,12 +92,17 @@ module Asger
85
92
 
86
93
  logger.info "Using task files:"
87
94
  task_files.each { |tf| logger.info " - #{tf}" }
88
- runner = Runner.new(logger, sqs_client, ec2_client, opts[:queue_url], parameters, task_files)
89
-
90
- logger.info "Beginning run loop. Sleeping between steps for #{opts[:pause_time]} seconds."
95
+ runner = Runner.new(logger: logger, aws_logger: aws_logger,
96
+ region: opts[:region], credentials: credentials,
97
+ queue_url: opts[:queue_url],
98
+ parameters: parameters,
99
+ task_files: task_files,
100
+ no_delete_messages: !opts[:delete_messages])
101
+
102
+ logger.info "Beginning poll loop."
91
103
  loop do
92
104
  begin
93
- runner.step()
105
+ runner.poll
94
106
  rescue StandardError => err
95
107
  logger.error "Encountered an error."
96
108
  logger.error "#{err.class.name}: #{err.message}"
@@ -98,10 +110,11 @@ module Asger
98
110
 
99
111
  if opts[:die_on_error]
100
112
  raise err
113
+ else
114
+ logger.error "re-entering poll."
101
115
  end
102
116
  end
103
- sleep opts[:pause_time] unless opts[:pause_time] == 0
104
117
  end
105
118
  end
106
119
  end
107
- end
120
+ end
data/lib/asger/runner.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'logger'
2
2
  require 'aws-sdk'
3
- require 'hashie'
3
+ require 'ice_nine'
4
+ require 'active_support/all'
4
5
 
5
6
  require 'asger/task'
6
7
 
@@ -9,66 +10,102 @@ module Asger
9
10
  # @param logger [Logger] the logger for Asger to use
10
11
  # @param sqs_client [Aws::SQS::Client] the SQS client to use for polling
11
12
  # @param ec2_client [Aws::EC2::Client] the EC2 client to use to get instance information
13
+ # @param asg_client [Aws::AutoScaling::Client] the ASG client to use to get ASG information
12
14
  # @param queue_url [String] the queue URL to poll
13
15
  # @param parameters [Hash] a hash of parameters to pass to {Task}s
14
16
  # @param task_files [Array<String>] list of file paths to load as {Task}s
15
- def initialize(logger, sqs_client, ec2_client, queue_url, parameters, task_files)
17
+ # @param no_delete_messages [TrueClass, FalseClass] if true, don't call sqs:DeleteMessage
18
+ def initialize(logger:, aws_logger:, credentials:,
19
+ region:, queue_url:,
20
+ parameters:, task_files:, no_delete_messages:)
16
21
  @logger = logger
17
- @sqs_client = sqs_client
18
- @ec2_client = ec2_client
22
+ @region = region
23
+ @parameters = IceNine.deep_freeze(parameters.merge(
24
+ region: region, credentials: credentials
25
+ ).deep_symbolize_keys)
26
+
27
+ @sqs_client = Aws::SQS::Client.new(logger: aws_logger,
28
+ region: region, credentials: credentials)
29
+ @ec2_client = Aws::EC2::Client.new(logger: aws_logger,
30
+ region: region, credentials: credentials)
31
+ @asg_client = Aws::AutoScaling::Client.new(logger: aws_logger,
32
+ region: region, credentials: credentials)
19
33
  @ec2_resource_client = Aws::EC2::Resource.new(client: @ec2_client)
34
+ @asg_resource_client = Aws::AutoScaling::Resource.new(client: @asg_client)
20
35
  @queue_url = queue_url
21
- @parameters = Hashie::Mash.new(parameters)
22
36
  @tasks = task_files.map { |tf| Task.from_file(@logger, tf) }
37
+ @no_delete_messages = no_delete_messages
23
38
 
24
39
  @logger.info "#{@tasks.length} task(s) set up."
40
+ @logger.warn('no_delete_messages is set; will not clear SQS messages!') \
41
+ if @no_delete_messages
25
42
 
26
- @tasks.each { |t| t.invoke_sanity_check(@parameters) }
43
+ @tasks.each { |t| t.invoke_init(@parameters) }
27
44
  end
28
45
 
29
46
 
30
- def step()
31
- messages = @sqs_client.receive_message(queue_url: @queue_url)[:messages]
32
- messages.each do |msg|
33
- notification = JSON.parse(JSON.parse(msg[:body])["Message"])
34
- if notification["Event"] != nil
35
- case notification["Event"].gsub("autoscaling:", "")
47
+ def poll()
48
+ poller = Aws::SQS::QueuePoller.new(@queue_url, client: @sqs_client,
49
+ max_number_of_messages: 10, skip_delete: true)
50
+
51
+ poller.poll do |msgs|
52
+ [ msgs ].flatten.each do |msg|
53
+ notification = JSON.parse(JSON.parse(msg.body)["Message"])
54
+ if notification["Event"] != nil
55
+ asg = @asg_resource_client.group(notification['AutoScalingGroupName'])
56
+ instance_id = notification["EC2InstanceId"]
57
+
58
+ @logger.warn("ASG '#{asg}' has fired event, but does not exist - already cleaned up?") \
59
+ unless asg.exists?
60
+
61
+ case notification["Event"].gsub("autoscaling:", "")
36
62
  when "EC2_INSTANCE_LAUNCH"
37
- instance_id = notification["EC2InstanceId"]
38
- @logger.info "Instance launched: #{instance_id}"
63
+ @logger.info "Instance launched in '#{asg.name}': #{instance_id}"
39
64
 
40
65
  instance = @ec2_resource_client.instance(instance_id)
41
66
  @tasks.each do |task|
42
- task.invoke_up(instance, @parameters)
67
+ task.invoke_up(instance, asg, @parameters)
43
68
  end
44
69
 
45
- delete_message(msg)
70
+ delete_message(msg) unless @no_delete_messages
46
71
  when "EC2_INSTANCE_LAUNCH_ERROR"
47
- @logger.warn "Instance launch error received."
48
- delete_message(msg)
72
+ @logger.warn "Instance failed to launch in '#{asg.name}'."
73
+
74
+ @tasks.each do |task|
75
+ task.invoke_up_failed(asg, @parameters)
76
+ end
77
+
78
+ delete_message(msg) unless @no_delete_messages
49
79
  when "EC2_INSTANCE_TERMINATE"
50
- instance_id = notification["EC2InstanceId"]
51
- @logger.info "Instance terminated: #{instance_id}"
80
+ @logger.info "Instance terminated in '#{asg.name}': #{instance_id}"
52
81
 
53
82
  @tasks.reverse_each do |task|
54
- task.invoke_down(instance_id, @parameters)
83
+ task.invoke_down(instance_id, asg, @parameters)
55
84
  end
56
- delete_message(msg)
85
+
86
+ delete_message(msg) unless @no_delete_messages
57
87
  when "EC2_INSTANCE_TERMINATE_ERROR"
58
- @logger.warn "Instance terminate error received."
59
- delete_message(msg)
88
+ @logger.warn "Instance failed to terminate in '#{asg.name}': #{instance_id}"
89
+
90
+ @tasks.reverse_each do |task|
91
+ task.invoke_down_failed(instance_id, asg, @parameters)
92
+ end
93
+ delete_message(msg) unless @no_delete_messages
60
94
  when "TEST_NOTIFICATION"
61
- @logger.debug "Found test notification in queue."
62
- delete_message(msg)
95
+ @logger.info "Found test notification in queue."
96
+ delete_message(msg) unless @no_delete_messages
63
97
  else
64
98
  @logger.debug "Unrecognized notification '#{notification["Event"]}', ignoring."
99
+ end
65
100
  end
66
101
  end
67
102
  end
68
103
  end
69
104
 
70
105
  private
106
+
71
107
  def delete_message(msg)
108
+ @logger.debug "Deleting message '#{msg[:receipt_handle]}'"
72
109
  @sqs_client.delete_message(queue_url: @queue_url, receipt_handle: msg[:receipt_handle])
73
110
  end
74
111
  end
data/lib/asger/task.rb CHANGED
@@ -15,64 +15,103 @@ module Asger
15
15
  instance_eval(code, filename, 1)
16
16
  end
17
17
 
18
- def invoke_sanity_check(parameters)
19
- if @sanity_check_proc
20
- logger.debug "Sanity checking for '#{@name}'..."
21
- @sanity_check_proc.call(parameters)
18
+ def invoke_init(parameters)
19
+ if @init_proc
20
+ logger.debug "Initializing for '#{@name}'..."
21
+ @init_proc.call(parameters)
22
22
  else
23
- logger.debug "No sanity check for '#{@name}'."
23
+ logger.debug "No init for '#{@name}'."
24
24
  end
25
25
  end
26
26
 
27
- def invoke_up(instance, parameters)
27
+ def invoke_up(instance, asg, parameters)
28
28
  if @up_proc
29
29
  logger.debug "Invoking up for '#{@name}'..."
30
- @up_proc.call(instance, parameters)
30
+ @up_proc.call(instance, asg, parameters)
31
31
  logger.debug "Up invoked for '#{@name}'..."
32
32
  else
33
33
  logger.debug "No up for '#{@name}'."
34
34
  end
35
35
  end
36
36
 
37
- def invoke_down(instance_id, parameters)
37
+ def invoke_down(instance_id, asg, parameters)
38
38
  if @down_proc
39
39
  logger.debug "Invoking down for '#{@name}'..."
40
- @down_proc.call(instance_id, parameters)
40
+ @down_proc.call(instance_id, asg, parameters)
41
41
  logger.debug "Down invoked for '#{@name}'..."
42
42
  else
43
43
  logger.debug "No down for '#{@name}'."
44
44
  end
45
45
  end
46
46
 
47
+ def invoke_up_failed(asg, parameters)
48
+ if @up_failed_proc
49
+ logger.debug "Invoking up_failed for '#{@name}'..."
50
+ @up_failed_proc.call(asg, parameters)
51
+ logger.debug "up_failed invoked for '#{@name}'..."
52
+ else
53
+ logger.debug "No up_failed for '#{@name}'."
54
+ end
55
+ end
56
+
57
+ def invoke_down_failed(instance_id, asg, parameters)
58
+ if @down_failed_proc
59
+ logger.debug "Invoking down_failed for '#{@name}'..."
60
+ @down_failed_proc.call(instance_id, asg, parameters)
61
+ logger.debug "down_failed invoked for '#{@name}'..."
62
+ else
63
+ logger.debug "No down_failed for '#{@name}'."
64
+ end
65
+ end
66
+
47
67
  def self.from_file(logger, file)
48
68
  Task.new(logger, File.read(file), file)
49
69
  end
50
70
 
51
71
  private
52
- # Defines a sanity check function, which should raise and fail (which will halt
53
- # Asger before it does anything with the actual queue) if there's a problem with
54
- # the parameter set.
55
- #
72
+ # Defines an init function, which should set member vars. Raise and fail (which
73
+ # will halt Asger before it does anything with the actual queue) if there's a
74
+ # problem with the parameter set.
75
+ #
56
76
  # @yield [parameters]
57
77
  # @yieldparam parameters [Hash] the parameters passed in to Asger
58
- def sanity_check(&block)
59
- @sanity_check_proc = block
78
+ def init(&block)
79
+ @init_proc = block
60
80
  end
61
81
 
62
- # Defines an 'up' function.
82
+ # Defines an 'up' function, addressing `EC2_INSTANCE_LAUNCH`.
63
83
  # @yield [instance, parameters]
64
84
  # @yieldparam instance [Aws::EC2::Instance] the instance that has been created
85
+ # @yieldparam asg [nil, Aws::AutoScaling::AutoScalingGroup] the ASG resource of the launched instance
65
86
  # @yieldparam parameters [Hash] the parameters passed in to Asger
66
87
  def up(&block)
67
88
  @up_proc = block
68
89
  end
69
90
 
70
- # Defines a 'down' function.
91
+ # Defines a 'down' function, addressing `EC2_INSTANCE_TERMINATE`.
71
92
  # @yield [instance_id, parameters]
72
93
  # @yieldparam instance_id [String] the ID of the recently terminated instance
94
+ # @yieldparam asg [nil, Aws::AutoScaling::AutoScalingGroup] the ASG resource of the terminated instance
73
95
  # @yieldparam parameters [Hash] the parameters passed in to Asger
74
96
  def down(&block)
75
97
  @down_proc = block
76
98
  end
99
+
100
+ # Defines an 'up_failed' function, addressing `EC2_INSTANCE_LAUNCH_ERROR`.
101
+ # @yield [asg, parameters]
102
+ # @yieldparam asg [nil, Aws::AutoScaling::AutoScalingGroup] the ASG resource of the failed instance
103
+ # @yieldparam parameters [Hash] the parameters passed in to Asger
104
+ def up_failed(&block)
105
+ @up_failed_proc = block
106
+ end
107
+
108
+ # Defines an 'up_failed' function, addressing `EC2_INSTANCE_TERMINATE_ERROR`.
109
+ # @yield [asg, parameters]
110
+ # @yieldparam instance_id [String] the ID of the instance that failed to terminate
111
+ # @yieldparam asg [nil, Aws::AutoScaling::AutoScalingGroup] the ASG resource of the failed instance
112
+ # @yieldparam parameters [Hash] the parameters passed in to Asger
113
+ def down_failed(&block)
114
+ @down_failed_proc = block
115
+ end
77
116
  end
78
117
  end
data/lib/asger/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Asger
2
2
  # The current version of Asger.
3
- VERSION = "0.2.1"
3
+ VERSION = "1.0.2"
4
4
  end
@@ -2,31 +2,31 @@
2
2
  # It will perform a Chef search for an 'instance_id' tag that matches the instance ID
3
3
  # provided by the AWS notification. To tag a node with the instance_id from bash, do
4
4
  # something like this on-node (probably in cloud-init):
5
- #
5
+ #
6
6
  # ```bash
7
7
  # knife tag create -c /path/to/client.rb $NODE_NAME $INSTANCE_ID
8
8
  # ```
9
- #
9
+ #
10
10
  # Which, in our naming scheme at Leaf, becomes:
11
- #
11
+ #
12
12
  # ```bash
13
13
  # knife tag create -c /etc/chef/client.rb vpn.test-cloud.infra.06f1d39c i-06f1d39c
14
14
  # ```
15
- #
15
+ #
16
16
  # PARAMETERS:
17
- #
17
+ #
18
18
  # chef_deregister.knife_config: the path to the knife config to use for search/node delete
19
19
 
20
20
  require 'json'
21
21
 
22
- sanity_check do |parameters|
22
+ init do |parameters|
23
23
  knife_config = parameters[:chef_deregister][:knife_config]
24
24
  raise "parameters[:chef_deregister][:knife_config] is not set" unless knife_config
25
25
 
26
26
  raise "file '#{knife_config}' does not exist" unless File.exist?(knife_config)
27
27
  end
28
28
 
29
- down do |instance_id, parameters|
29
+ down do |instance_id, asg, parameters|
30
30
  knife_config = parameters[:chef_deregister][:knife_config]
31
31
 
32
32
  search_result = Asger::Util::run_command("knife search: #{instance_id}",
@@ -1,9 +1,20 @@
1
1
  # Just logs the instances being created and destroyed to the logger for testing.
2
+ init do |parameters|
3
+ logger.info 'echo - init'
4
+ end
5
+
6
+ up do |instance, asg, parameters|
7
+ logger.info "echo - upping instance in '#{asg.name}': #{instance}"
8
+ end
2
9
 
3
- up do |instance, parameters|
4
- logger.info "upping instance: #{instance}"
10
+ up_failed do |asg, parameters|
11
+ logger.warn "echo - failed to up instance in '#{asg.name}'"
5
12
  end
6
13
 
7
- down do |instance_id, parameters|
8
- logger.info "downing instance: #{instance_id}"
9
- end
14
+ down do |instance_id, asg, parameters|
15
+ logger.info "echo - downing instance in '#{asg.name}': #{instance_id}"
16
+ end
17
+
18
+ down_failed do |instance_id, asg, parameters|
19
+ logger.warn "echo - failed to down instance in '#{asg.name}': #{instance_id}"
20
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ed Ropple
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-26 00:00:00.000000000 Z
11
+ date: 2016-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 2.0.27
61
+ version: 2.2.22
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: 2.0.27
68
+ version: 2.2.22
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: trollop
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: ice_nine
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.11.2
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.11.2
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: activesupport
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -117,6 +131,7 @@ extensions: []
117
131
  extra_rdoc_files: []
118
132
  files:
119
133
  - ".gitignore"
134
+ - ".ruby-version"
120
135
  - ".yardopts"
121
136
  - Gemfile
122
137
  - LICENSE.txt
@@ -152,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
167
  version: '0'
153
168
  requirements: []
154
169
  rubyforge_project:
155
- rubygems_version: 2.4.5
170
+ rubygems_version: 2.5.1
156
171
  signing_key:
157
172
  specification_version: 4
158
173
  summary: A persistent daemon that watches an AWS autoscaling group for changes and