cfn-guardian 0.7.16 → 0.8.0

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
  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