cfn-guardian 0.7.16 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 698af7ce8ae6207dbecaba853e316c45b49f4097feb4b02109a4d4cf6e4e2e50
4
- data.tar.gz: 37930ff02c6aac6e68bb84dc5d704523d67e2f0ec606bf4bc279f9dee987f59f
3
+ metadata.gz: dee3a34d28fcd37f9228a90d7c53a65d0cdfbbd27842c0ef3c6f78981f980480
4
+ data.tar.gz: b475d9c262e6620431450e74c5a0b2c5fdda3f5d76b135878f1974f114163530
5
5
  SHA512:
6
- metadata.gz: 8dd7078ef3ecbf66c1a8120d029840afa9857c1478d7ce8c2bca341731af4bd7afce412add69008ec7137477b59c3f789a30cf79cfab31f5a2a2ef6810eff9e0
7
- data.tar.gz: 23587e30e8b0fea611a60869c9b59b4acc984cb9869a5a3b16dc6d5c62a862ecf3b9adca5b5a0f45cb6868434dcfddeb1320330bbd4587647d77c21725531037
6
+ metadata.gz: 9860b56997a775b014c53d5b8de6432b1c38ca2223d65d1416482f3bfb3035c37aa0be6fd8edad99ad368e63de811f3d6e7bb30ba280b579f999b5bb1c518ca0
7
+ data.tar.gz: cf758b431fe395af861d05dbd200c5ee01e68270ed926b439bee9f83803828c0f3c63c53481996c6732a00b8b0c050f87caa7b724e8d274a5f65563faf6b3baa
@@ -0,0 +1,63 @@
1
+ # Guardian Alarm Tags
2
+
3
+ AWS tags can be applied to Cloudwatch alarms created by guardian. This is available as a separate guardian command [`cfn-guardian tag-alarms`] because Cloudformation doesn't support creating tags on Cloudwatch alarms.
4
+
5
+ ## Default Tags
6
+
7
+ Guardian will add the following default tags to each alarm
8
+
9
+ ```
10
+ guardian:resource:id
11
+ guardian:resource:group
12
+ guardian:alarm:name
13
+ guardian:alarm:metric
14
+ guardian:alarm:severity
15
+ ```
16
+
17
+ ## Adding Tags
18
+
19
+ Additional tags can added through the alarms yaml configuration file. They can be applied globally to all alarms, to all alarms in a resource group or a specific alarm.
20
+
21
+ ### Global Tags
22
+
23
+ Global tags are applied to every alarm created by guardian. Add the `GlobalTags` key at the top level of the alarms yaml config with key:value pairs.
24
+
25
+ ```yml
26
+ GlobalTags:
27
+ key: value
28
+ env: production
29
+ ```
30
+
31
+ ### Resource Group Tags
32
+
33
+ Resource group tags are applied to every alarm in a guardian resource group using the `Templates` section to add the tags.
34
+
35
+ ```yaml
36
+ Templates:
37
+ Ec2Instance:
38
+ GroupOverrides:
39
+ Tags:
40
+ key: value
41
+ env: production
42
+ ```
43
+
44
+ ### Specific Alarm Tags
45
+
46
+ To add tags to a specific guardian alarm you can apply the tags in the `Templates` section of the alarms yaml config.
47
+
48
+ ```yaml
49
+ Templates:
50
+ Ec2Instance:
51
+ CPUUtilizationHigh:
52
+ Tags:
53
+ key: value
54
+ alarm-action: restart ec2 instance
55
+ ```
56
+
57
+ ## Applying tags
58
+
59
+ To apply the tags run the `tag-alarms` command passing the alarms yaml config.
60
+
61
+ ```sh
62
+ cfn-guardian tag-alarms --config alarms.yaml
63
+ ```
data/docs/overview.md CHANGED
@@ -20,4 +20,5 @@
20
20
  7. [Maintenance Mode](maintenance_mode.md)
21
21
  8. [Composite Alarms](composite_alarms.md)
22
22
  9. [Alarms for Custom Metrics](custom_metrics.md)
23
- 10. [Dimension Variables](variables.md)
23
+ 10. [Dimension Variables](variables.md)
24
+ 11. [Alarm Tags](alarm_tags.md)
@@ -9,6 +9,10 @@ module CfnGuardian
9
9
  alarm_id = alarm.resource_name.nil? ? alarm.resource_id : alarm.resource_name
10
10
  return "guardian-#{alarm.group}-#{alarm_id}-#{alarm.name}"
11
11
  end
12
+
13
+ def self.get_alarm_arn(alarm)
14
+ return "arn:aws:cloudwatch:#{Aws.config[:region]}:#{aws_account_id()}:alarm:#{self.get_alarm_name(alarm)}"
15
+ end
12
16
 
13
17
  def self.get_alarms_by_prefix(prefix:, state: nil, action_prefix: nil)
14
18
  client = Aws::CloudWatch::Client.new()
@@ -57,7 +57,7 @@ module CfnGuardian
57
57
  class Compile
58
58
  include Logging
59
59
 
60
- attr_reader :cost, :resources, :topics
60
+ attr_reader :cost, :resources, :topics, :global_tags
61
61
 
62
62
  def initialize(config_file)
63
63
  config = YAML.load_file(config_file)
@@ -68,6 +68,7 @@ module CfnGuardian
68
68
  @topics = config.fetch('Topics',{})
69
69
  @maintenance_groups = config.fetch('MaintenanceGroups', {})
70
70
  @event_subscriptions = config.fetch('EventSubscriptions', {})
71
+ @global_tags = config.fetch('GlobalTags', {})
71
72
 
72
73
  # Make sure the default topics exist if they aren't supplied in the alarms.yaml
73
74
  %w(Critical Warning Task Informational Events).each do |topic|
@@ -161,7 +162,7 @@ module CfnGuardian
161
162
  when 'Alarm'
162
163
  %w(metric_name namespace).each do |property|
163
164
  if resource.send(property).nil?
164
- errors << "Alarm #{resource.name} for resource #{resource.resource_id} has nil value for property #{property.to_camelcase}"
165
+ errors << "Alarm #{resource.name} for resource #{resource.resource_id} has nil value for property #{property.to_camelcase}. This could be due to incorrect spelling of a default alarm name or missing property #{property.to_camelcase} on a new alarm."
165
166
  end
166
167
  end
167
168
  when 'Check'
@@ -180,34 +181,18 @@ module CfnGuardian
180
181
  raise CfnGuardian::ValidationError, "#{errors.size} errors found\n[*] #{errors.join("\n[*] ")}" if errors.any?
181
182
  end
182
183
 
183
- def split_resources(bucket,path)
184
- split = @resources.each_slice(200).to_a
185
- split.each_with_index do |resources,index|
186
- @stacks.push({
187
- 'Name' => "GuardianStack#{index}",
188
- 'TemplateURL' => "https://#{bucket}.s3.amazonaws.com/#{path}/guardian-stack-#{index}.compiled.yaml",
189
- 'Reference' => index
190
- })
191
- end
192
- return split
193
- end
194
-
195
184
  def compile_templates(bucket,path)
196
185
  clean_out_directory()
197
- resources = split_resources(bucket,path)
198
186
 
199
187
  main_stack = CfnGuardian::Stacks::Main.new()
200
188
  main_stack.build_template(@stacks,@checks,@topics,@maintenance_groups,@ssm_parameters)
189
+
190
+ resource_stack = CfnGuardian::Stacks::Resources.new(main_stack.template)
191
+ resource_stack.build_template(@resources)
192
+
201
193
  valid = main_stack.template.validate
202
194
  FileUtils.mkdir_p 'out'
203
195
  File.write("out/guardian.compiled.yaml", JSON.parse(valid.to_json).to_yaml)
204
-
205
- resources.each_with_index do |resources,index|
206
- stack = CfnGuardian::Stacks::Resources.new(main_stack.parameters,index)
207
- stack.build_template(resources)
208
- valid = stack.template.validate
209
- File.write("out/guardian-stack-#{index}.compiled.yaml", JSON.parse(valid.to_json).to_yaml)
210
- end
211
196
  end
212
197
 
213
198
  def clean_out_directory
@@ -14,6 +14,14 @@ module CfnGuardian
14
14
  @template_path = "out/guardian.compiled.yaml"
15
15
  @template_url = "https://#{@bucket}.s3.amazonaws.com/#{@prefix}/guardian.compiled.yaml"
16
16
  @parameters = parameters
17
+ @changeset_role_arn = opts.fetch(:role_arn, nil)
18
+
19
+ @tags = {}
20
+ if opts.has_key?("tag_yaml")
21
+ @tags.merge!(YAML.load_file(opts[:tag_yaml]))
22
+ end
23
+ @tags.merge!(opts.fetch(:tags, {}))
24
+
17
25
  @client = Aws::CloudFormation::Client.new()
18
26
  end
19
27
 
@@ -63,25 +71,25 @@ module CfnGuardian
63
71
  end
64
72
  end
65
73
 
66
- logger.debug "Creating changeset"
67
- change_set = @client.create_change_set({
74
+ tags = get_tags()
75
+ logger.debug "tagging stack with tags #{tags}"
76
+
77
+ changeset_request = {
68
78
  stack_name: @stack_name,
69
79
  template_url: @template_url,
70
80
  capabilities: ["CAPABILITY_IAM"],
71
81
  parameters: params,
72
- tags: [
73
- {
74
- key: "guardian:version",
75
- value: CfnGuardian::VERSION,
76
- },
77
- {
78
- key: 'Environment',
79
- value: 'guardian'
80
- }
81
- ],
82
+ tags: tags,
82
83
  change_set_name: change_set_name,
83
84
  change_set_type: change_set_type
84
- })
85
+ }
86
+
87
+ unless @changeset_role_arn.nil?
88
+ changeset_request[:role_arn] = @changeset_role_arn
89
+ end
90
+
91
+ logger.debug "Creating changeset"
92
+ change_set = @client.create_change_set(changeset_request)
85
93
  return change_set, change_set_type
86
94
  end
87
95
 
@@ -126,5 +134,15 @@ module CfnGuardian
126
134
  return resp.parameters.collect { |p| { parameter_key: p.parameter_key, parameter_value: p.default_value } }
127
135
  end
128
136
 
137
+ def get_tags()
138
+ default_tags = {
139
+ 'guardian:version': CfnGuardian::VERSION,
140
+ Environment: 'guardian'
141
+ }
142
+ default_tags.merge!(@tags)
143
+ tags = default_tags.map {|k,v| {key: k, value: v}}
144
+ return tags
145
+ end
146
+
129
147
  end
130
148
  end
@@ -29,7 +29,8 @@ module CfnGuardian
29
29
  :evaluate_low_sample_count_percentile,
30
30
  :unit,
31
31
  :maintenance_groups,
32
- :additional_notifiers
32
+ :additional_notifiers,
33
+ :tags
33
34
 
34
35
  def initialize(resource)
35
36
  @type = 'Alarm'
@@ -56,6 +57,7 @@ module CfnGuardian
56
57
  @treat_missing_data = nil
57
58
  @maintenance_groups = []
58
59
  @additional_notifiers = []
60
+ @tags = {}
59
61
  end
60
62
 
61
63
  def metric_name=(metric_name)
@@ -64,7 +66,6 @@ module CfnGuardian
64
66
  end
65
67
  end
66
68
 
67
-
68
69
  class ApiGatewayAlarm < BaseAlarm
69
70
  def initialize(resource)
70
71
  super(resource)
@@ -6,7 +6,7 @@ module CfnGuardian
6
6
  include CfnDsl::CloudFormation
7
7
  include Logging
8
8
 
9
- attr_reader :parameters, :template
9
+ attr_reader :template
10
10
 
11
11
  def initialize()
12
12
  @parameters = []
@@ -32,9 +32,7 @@ module CfnGuardian
32
32
  add_iam_role(ssm_parameters)
33
33
 
34
34
  checks.each {|check| parameters["#{check.name}Function#{check.environment}"] = add_lambda(check)}
35
- stacks.each {|stack| add_stack(stack['Name'],stack['TemplateURL'],parameters,stack['Reference'])}
36
-
37
- @parameters = parameters.keys
35
+ stacks.each {|stack| add_stack(stack['Name'],stack['TemplateURL'],parameters,stack['Reference'])}
38
36
  end
39
37
 
40
38
  def add_iam_role(ssm_parameters)
@@ -6,17 +6,9 @@ module CfnGuardian
6
6
  module Stacks
7
7
  class Resources
8
8
  include CfnDsl::CloudFormation
9
-
10
- attr_reader :template
11
-
12
- def initialize(parameters,stack_id)
13
- @stack_id = stack_id
14
-
15
- @template = CloudFormation("Guardian nested - stack-id:stk#{@stack_id}")
16
- parameters.each do |name|
17
- parameter = @template.Parameter(name)
18
- parameter.Type 'String'
19
- end
9
+
10
+ def initialize(template)
11
+ @template = template
20
12
  end
21
13
 
22
14
  def build_template(resources)
@@ -41,13 +33,12 @@ module CfnGuardian
41
33
  def add_alarm(alarm)
42
34
  actions = alarm.alarm_action.kind_of?(Array) ? alarm.alarm_action.map{|action| Ref(action)} : [Ref(alarm.alarm_action)]
43
35
  actions.concat alarm.maintenance_groups.map {|mg| Ref(mg)} if alarm.maintenance_groups.any?
44
- stack_id = @stack_id
45
36
 
46
37
  @template.declare do
47
38
  CloudWatch_Alarm("#{alarm.resource_hash}#{alarm.group}#{alarm.name.gsub(/[^0-9a-zA-Z]/i, '')}#{alarm.type}"[0..255]) do
48
39
  ActionsEnabled true
49
40
  AlarmDescription "Guardian alarm #{alarm.name} for the resource #{alarm.resource_id} in alarm group #{alarm.group}"
50
- AlarmName CfnGuardian::CloudWatch.get_alarm_name(alarm) + "-stk#{stack_id}"
41
+ AlarmName CfnGuardian::CloudWatch.get_alarm_name(alarm)
51
42
  ComparisonOperator alarm.comparison_operator
52
43
  Dimensions alarm.dimensions.map {|k,v| {Name: k, Value: v}} unless alarm.dimensions.nil?
53
44
  EvaluationPeriods alarm.evaluation_periods
@@ -75,7 +66,7 @@ module CfnGuardian
75
66
  ScheduleExpression "cron(#{event.cron})"
76
67
  Targets([
77
68
  {
78
- Arn: Ref(event.target),
69
+ Arn: FnGetAtt(event.target, :Arn),
79
70
  Id: event.hash,
80
71
  Input: FnSub(event.payload)
81
72
  }
@@ -85,13 +76,11 @@ module CfnGuardian
85
76
  end
86
77
 
87
78
  def add_composite_alarm(alarm)
88
- stack_id = @stack_id
89
-
90
79
  @template.declare do
91
80
  CloudWatch_CompositeAlarm(alarm.name.gsub(/[^0-9a-zA-Z]/i, '')) do
92
81
 
93
82
  AlarmDescription alarm.description
94
- AlarmName "guardian-#{alarm.name}-stk#{stack_id}"
83
+ AlarmName "guardian-#{alarm.name}"
95
84
  AlarmRule alarm.rule
96
85
 
97
86
  unless alarm.alarm_action.nil?
@@ -0,0 +1,69 @@
1
+ require 'aws-sdk-cloudwatch'
2
+ require 'cfnguardian/cloudwatch'
3
+ require 'cfnguardian/log'
4
+
5
+ module CfnGuardian
6
+ class Tagger
7
+ include Logging
8
+
9
+ def initialize()
10
+ @client = Aws::CloudWatch::Client.new(max_attempts: 5)
11
+ end
12
+
13
+ def tag_alarm(alarm, global_tags={})
14
+ alarm_arn = CfnGuardian::CloudWatch.get_alarm_arn(alarm)
15
+
16
+ new_tags = get_tags(alarm, global_tags)
17
+ current_tags = get_alarm_tags(alarm_arn)
18
+ tags_to_delete = get_tags_to_delete(current_tags, new_tags)
19
+
20
+ if tags_to_delete.any?
21
+ logger.debug "Removing tags #{tags_to_delete} from alarm #{alarm_arn}"
22
+ @client.untag_resource({
23
+ resource_arn: alarm_arn,
24
+ tag_keys: tags_to_delete
25
+ })
26
+ end
27
+
28
+ if tags_changed?(current_tags, new_tags)
29
+ logger.debug "Updating tags on alarm #{alarm_arn}"
30
+ @client.tag_resource({
31
+ resource_arn: alarm_arn,
32
+ tags: new_tags.map {|key,value| {key: key, value: value}}
33
+ })
34
+ end
35
+ end
36
+
37
+ def get_tags(alarm, global_tags)
38
+ defaults = {
39
+ 'guardian:resource:id' => alarm.resource_id,
40
+ 'guardian:resource:group' => alarm.group,
41
+ 'guardian:alarm:name' => alarm.name,
42
+ 'guardian:alarm:metric' => alarm.metric_name,
43
+ 'guardian:alarm:severity' => alarm.alarm_action
44
+ }
45
+ tags = global_tags.merge(defaults)
46
+ return alarm.tags.merge(tags)
47
+ end
48
+
49
+ def get_alarm_tags(alarm_arn)
50
+ resp = @client.list_tags_for_resource({
51
+ resource_arn: alarm_arn
52
+ })
53
+ return resp.tags
54
+ end
55
+
56
+ def get_tags_to_delete(current_tags, new_tags)
57
+ return current_tags.select {|tag| !new_tags.has_key?(tag.key)}.map {|tag| tag.key}
58
+ end
59
+
60
+ def tags_changed?(current_tags, new_tags)
61
+ return tags_to_hash(current_tags) != new_tags
62
+ end
63
+
64
+ def tags_to_hash(tags)
65
+ return tags.map {|tag| {tag.key => tag.value} }.reduce(Hash.new, :merge)
66
+ end
67
+
68
+ end
69
+ end
@@ -1,4 +1,4 @@
1
1
  module CfnGuardian
2
- VERSION = "0.7.16"
2
+ VERSION = "0.8.0"
3
3
  CHANGE_SET_VERSION = VERSION.gsub('.', '-').freeze
4
4
  end
data/lib/cfnguardian.rb CHANGED
@@ -11,6 +11,7 @@ require "cfnguardian/display_formatter"
11
11
  require "cfnguardian/drift"
12
12
  require "cfnguardian/codecommit"
13
13
  require "cfnguardian/codepipeline"
14
+ require "cfnguardian/tagger"
14
15
 
15
16
  module CfnGuardian
16
17
  class Cli < Thor
@@ -85,6 +86,9 @@ module CfnGuardian
85
86
  method_option :sns_task, type: :string, desc: "sns topic arn for the task alarms"
86
87
  method_option :sns_informational, type: :string, desc: "sns topic arn for the informational alarms"
87
88
  method_option :sns_events, type: :string, desc: "sns topic arn for the informational alarms"
89
+ method_option :tags, type: :hash, desc: "additional tags on the cloudformation stack"
90
+ method_option :tag_yaml, type: :string, desc: "additional tags on the cloudformation stack in a yaml file"
91
+ method_option :role_arn, type: :string, desc: "IAM role arn that CloudFormation assumes when executing the change set"
88
92
 
89
93
  def deploy
90
94
  set_log_level(options[:debug])
@@ -114,14 +118,37 @@ module CfnGuardian
114
118
  deployer.execute_change_set(change_set.id)
115
119
  deployer.wait_for_execute(change_set_type)
116
120
  end
117
-
121
+
122
+ desc "tag-alarms", "apply tags to the cloudwatch alarms deployed"
123
+ long_desc <<-LONG
124
+ Because Cloudformation desn't support tagging cloudwatch alarms this command
125
+ applies tags to each cloudwatch alarm created by guardian.
126
+ Guardian defines default tags and this can be added to through the alarms.yaml config.
127
+ LONG
128
+ method_option :config, aliases: :c, type: :string, desc: "yaml config file", required: true
129
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
130
+
131
+ def tag_alarms
132
+ set_log_level(options[:debug])
133
+ set_region(options[:region],true)
134
+
135
+ compiler = CfnGuardian::Compile.new(options[:config])
136
+ compiler.get_resources
137
+ alarms = compiler.alarms
138
+
139
+ tagger = CfnGuardian::Tagger.new()
140
+ alarms.each do |alarm|
141
+ tagger.tag_alarm(alarm, compiler.global_tags)
142
+ end
143
+ end
144
+
118
145
  desc "show-drift", "Cloudformation drift detection"
119
146
  long_desc <<-LONG
120
147
  Displays any cloudformation drift detection in the cloudwatch alarms from the deployed stacks
121
148
  LONG
122
149
  method_option :stack_name, aliases: :s, type: :string, default: 'guardian', desc: "set the Cloudformation stack name"
123
150
  method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
124
-
151
+
125
152
  def show_drift
126
153
  set_region(options[:region],true)
127
154
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfn-guardian
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.16
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guslington
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-05-17 00:00:00.000000000 Z
11
+ date: 2022-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -234,6 +234,7 @@ files:
234
234
  - README.md
235
235
  - Rakefile
236
236
  - cfn-guardian.gemspec
237
+ - docs/alarm_tags.md
237
238
  - docs/alarm_templates.md
238
239
  - docs/cli.md
239
240
  - docs/composite_alarms.md
@@ -320,6 +321,7 @@ files:
320
321
  - lib/cfnguardian/stacks/main.rb
321
322
  - lib/cfnguardian/stacks/resources.rb
322
323
  - lib/cfnguardian/string.rb
324
+ - lib/cfnguardian/tagger.rb
323
325
  - lib/cfnguardian/validate.rb
324
326
  - lib/cfnguardian/version.rb
325
327
  homepage: https://github.com/base2Services/cfn-guardian