cfn-guardian 0.1.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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +52 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +412 -0
  7. data/Rakefile +2 -0
  8. data/cfn-guardian.gemspec +39 -0
  9. data/exe/cfn-guardian +4 -0
  10. data/lib/cfnguardian.rb +146 -0
  11. data/lib/cfnguardian/compile.rb +116 -0
  12. data/lib/cfnguardian/deploy.rb +144 -0
  13. data/lib/cfnguardian/log.rb +40 -0
  14. data/lib/cfnguardian/models/alarm.rb +292 -0
  15. data/lib/cfnguardian/models/check.rb +114 -0
  16. data/lib/cfnguardian/models/event.rb +192 -0
  17. data/lib/cfnguardian/resources/amazonmq_broker.rb +45 -0
  18. data/lib/cfnguardian/resources/apigateway.rb +34 -0
  19. data/lib/cfnguardian/resources/application_targetgroup.rb +31 -0
  20. data/lib/cfnguardian/resources/autoscaling_group.rb +22 -0
  21. data/lib/cfnguardian/resources/base.rb +116 -0
  22. data/lib/cfnguardian/resources/cloudfront_distribution.rb +26 -0
  23. data/lib/cfnguardian/resources/domain_expiry.rb +30 -0
  24. data/lib/cfnguardian/resources/dynamodb_table.rb +48 -0
  25. data/lib/cfnguardian/resources/ec2_instance.rb +24 -0
  26. data/lib/cfnguardian/resources/ecs_cluster.rb +44 -0
  27. data/lib/cfnguardian/resources/ecs_service.rb +32 -0
  28. data/lib/cfnguardian/resources/elastic_file_system.rb +17 -0
  29. data/lib/cfnguardian/resources/elastic_loadbalancer.rb +26 -0
  30. data/lib/cfnguardian/resources/elasticache_replication_group.rb +45 -0
  31. data/lib/cfnguardian/resources/http.rb +49 -0
  32. data/lib/cfnguardian/resources/lambda.rb +38 -0
  33. data/lib/cfnguardian/resources/network_targetgroup.rb +31 -0
  34. data/lib/cfnguardian/resources/nrpe.rb +42 -0
  35. data/lib/cfnguardian/resources/rds_cluster_instance.rb +30 -0
  36. data/lib/cfnguardian/resources/rds_instance.rb +45 -0
  37. data/lib/cfnguardian/resources/redshift_cluster.rb +30 -0
  38. data/lib/cfnguardian/resources/sql.rb +37 -0
  39. data/lib/cfnguardian/resources/sqs_queue.rb +23 -0
  40. data/lib/cfnguardian/s3.rb +35 -0
  41. data/lib/cfnguardian/stacks/main.rb +149 -0
  42. data/lib/cfnguardian/stacks/resources.rb +80 -0
  43. data/lib/cfnguardian/string.rb +19 -0
  44. data/lib/cfnguardian/validate.rb +80 -0
  45. data/lib/cfnguardian/version.rb +4 -0
  46. metadata +215 -0
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,39 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "cfnguardian/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "cfn-guardian"
7
+ spec.version = CfnGuardian::VERSION
8
+ spec.authors = ["Guslington"]
9
+ spec.email = ["itsupport@base2services.com"]
10
+
11
+ spec.summary = %q{Manages AWS cloudwatch alarms with default templates using cloudformation}
12
+ spec.description = %q{Manages AWS cloudwatch alarms with default templates using cloudformation}
13
+ spec.homepage = "https://github.com/base2Services/cfn-guardian"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://github.com/base2Services/cfn-guardian"
20
+ spec.metadata["changelog_uri"] = "https://github.com/base2Services/cfn-guardian"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency "thor", "~> 0.20"
32
+ spec.add_dependency 'cfndsl', '~> 1.0', '<2'
33
+ spec.add_dependency "terminal-table", '~> 1', '<2'
34
+ spec.add_dependency 'aws-sdk-s3', '~> 1', '<2'
35
+ spec.add_dependency 'aws-sdk-cloudformation', '~> 1', '<2'
36
+
37
+ spec.add_development_dependency "bundler", "~> 2.0"
38
+ spec.add_development_dependency "rake", "~> 10.0"
39
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require "cfnguardian"
3
+
4
+ CfnGuardian::Cli.start( ARGV )
@@ -0,0 +1,146 @@
1
+ require 'thor'
2
+ require 'terminal-table'
3
+ require "cfnguardian/log"
4
+ require "cfnguardian/version"
5
+ require "cfnguardian/compile"
6
+ require "cfnguardian/validate"
7
+ require "cfnguardian/deploy"
8
+
9
+ module CfnGuardian
10
+ class Cli < Thor
11
+ include Logging
12
+
13
+ map %w[--version -v] => :__print_version
14
+ desc "--version, -v", "print the version"
15
+ def __print_version
16
+ puts CfnGuardian::VERSION
17
+ end
18
+
19
+ desc "compile", "Generate monitoring CloudFormation templates"
20
+ long_desc <<-LONG
21
+ Generates CloudFormation templates from the alarm configuration and output to the out/ directory.
22
+ LONG
23
+ method_option :config, aliases: :c, type: :string, desc: "yaml config file", required: true
24
+ method_option :validate, type: :boolean, default: true, desc: "validate cfn templates"
25
+ method_option :bucket, type: :string, desc: "provide custom bucket name, will create a default bucket if not provided"
26
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
27
+
28
+ def compile
29
+ set_region(options[:region],options[:validate])
30
+ s3 = CfnGuardian::S3.new(options[:bucket])
31
+
32
+ compiler = CfnGuardian::Compile.new(options,s3.bucket)
33
+ compiler.get_resources
34
+ compiler.compile_templates
35
+ logger.info "Clouformation templates compiled successfully in out/ directory"
36
+ if options[:validate]
37
+ s3.create_bucket_if_not_exists()
38
+ validator = CfnGuardian::Validate.new(s3.bucket)
39
+ if validator.validate
40
+ logger.error("One or more templates failed to validate")
41
+ exit(1)
42
+ else
43
+ logger.info "Clouformation templates were validated successfully"
44
+ end
45
+ end
46
+ logger.warn "AWS cloudwatch alarms defined in the templates will cost roughly $#{'%.2f' % compiler.cost} per month"
47
+ end
48
+
49
+ desc "deploy", "Generates and deploys monitoring CloudFormation templates"
50
+ long_desc <<-LONG
51
+ Generates CloudFormation templates from the alarm configuration and output to the out/ directory.
52
+ Then copies the files to the s3 bucket and deploys the cloudformation.
53
+ LONG
54
+ method_option :config, aliases: :c, type: :string, desc: "yaml config file", required: true
55
+ method_option :bucket, type: :string, desc: "provide custom bucket name, will create a default bucket if not provided"
56
+ method_option :region, aliases: :r, type: :string, desc: "set the AWS region"
57
+ method_option :stack_name, aliases: :r, type: :string, desc: "set the Cloudformation stack name. Defaults to `guardian`"
58
+ method_option :sns_critical, type: :string, desc: "sns topic arn for the critical alamrs"
59
+ method_option :sns_warning, type: :string, desc: "sns topic arn for the warning alamrs"
60
+ method_option :sns_task, type: :string, desc: "sns topic arn for the task alamrs"
61
+ method_option :sns_informational, type: :string, desc: "sns topic arn for the informational alamrs"
62
+
63
+ def deploy
64
+ set_region(options[:region],true)
65
+ s3 = CfnGuardian::S3.new(options[:bucket])
66
+
67
+ compiler = CfnGuardian::Compile.new(options,s3.bucket)
68
+ compiler.get_resources
69
+ compiler.compile_templates
70
+ logger.info "Clouformation templates compiled successfully in out/ directory"
71
+
72
+ s3.create_bucket_if_not_exists
73
+ validator = CfnGuardian::Validate.new(s3.bucket)
74
+ if validator.validate
75
+ logger.error("One or more templates failed to validate")
76
+ exit(1)
77
+ else
78
+ logger.info "Clouformation templates were validated successfully"
79
+ end
80
+
81
+ deployer = CfnGuardian::Deploy.new(options,s3.bucket)
82
+ deployer.upload_templates
83
+ change_set, change_set_type = deployer.create_change_set()
84
+ deployer.wait_for_changeset(change_set.id)
85
+ deployer.execute_change_set(change_set.id)
86
+ deployer.wait_for_execute(change_set_type)
87
+ end
88
+
89
+ desc "show-alarms", "Shows alarm settings"
90
+ long_desc <<-LONG
91
+ Displays the configured settings for each alarm. Can be filtered by resource group and alarm name.
92
+ Defaults to show all configured alarms.
93
+ LONG
94
+ method_option :config, aliases: :c, type: :string, desc: "yaml config file", required: true
95
+ method_option :group, aliases: :g, type: :string, desc: "resource group"
96
+ method_option :name, aliases: :n, type: :string, desc: "alarm name"
97
+ method_option :resource, aliases: :r, type: :string, desc: "resource id"
98
+ def show_alarms
99
+ compiler = CfnGuardian::Compile.new(options,'no-bucket')
100
+ compiler.get_resources
101
+
102
+ alarms = compiler.resources.select{|h| h[:type] == 'Alarm'}
103
+ groups = alarms.group_by{|h| h[:class]}
104
+
105
+ if options[:group]
106
+ groups = groups.fetch(options[:group],{}).group_by{|h| h[:class]}
107
+ if options[:resource]
108
+ groups = groups[options[:group]].select{|h| h[:resource] == options[:resource]}.group_by{|h| h[:class]}
109
+ end
110
+ if options[:name]
111
+ groups = groups[options[:group]].select{|h| h[:name] == options[:name]}.group_by{|h| h[:class]}
112
+ end
113
+ end
114
+
115
+ groups.each do |grp,alarms|
116
+ puts "\n\s\s#{grp}\n"
117
+ alarms.each do |alarm|
118
+ rows = alarm.reject {|k,v| [:type,:class,:name].include?(k)}
119
+ .sort_by {|k,v| k}
120
+ puts Terminal::Table.new(
121
+ :title => alarm[:name],
122
+ :headings => ['property', 'Value'],
123
+ :rows => rows.map! {|k,v| [k,v.to_s]})
124
+ end
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def set_region(region,required)
131
+ if !region.nil?
132
+ Aws.config.update({region: region})
133
+ elsif !ENV['AWS_REGION'].nil?
134
+ Aws.config.update({region: ENV['AWS_REGION']})
135
+ elsif !ENV['AWS_DEFAULT_REGION'].nil?
136
+ Aws.config.update({region: ENV['AWS_DEFAULT_REGION']})
137
+ else
138
+ if required
139
+ logger.error("No AWS region found. Please suppy the region using option `--region` or setting environment variables `AWS_REGION` `AWS_DEFAULT_REGION`")
140
+ exit(1)
141
+ end
142
+ end
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,116 @@
1
+ require 'yaml'
2
+ require 'cfnguardian/string'
3
+ require 'cfnguardian/stacks/resources'
4
+ require 'cfnguardian/stacks/main'
5
+ require 'cfnguardian/resources/base'
6
+ require 'cfnguardian/resources/apigateway'
7
+ require 'cfnguardian/resources/application_targetgroup'
8
+ require 'cfnguardian/resources/amazonmq_broker'
9
+ require 'cfnguardian/resources/autoscaling_group'
10
+ require 'cfnguardian/resources/cloudfront_distribution'
11
+ require 'cfnguardian/resources/autoscaling_group'
12
+ require 'cfnguardian/resources/domain_expiry'
13
+ require 'cfnguardian/resources/dynamodb_table'
14
+ require 'cfnguardian/resources/ec2_instance'
15
+ require 'cfnguardian/resources/ecs_cluster'
16
+ require 'cfnguardian/resources/ecs_service'
17
+ require 'cfnguardian/resources/elastic_file_system'
18
+ require 'cfnguardian/resources/elasticache_replication_group'
19
+ require 'cfnguardian/resources/elastic_loadbalancer'
20
+ require 'cfnguardian/resources/http'
21
+ require 'cfnguardian/resources/nrpe'
22
+ require 'cfnguardian/resources/lambda'
23
+ require 'cfnguardian/resources/network_targetgroup'
24
+ require 'cfnguardian/resources/rds_cluster_instance'
25
+ require 'cfnguardian/resources/rds_instance'
26
+ require 'cfnguardian/resources/redshift_cluster'
27
+ require 'cfnguardian/resources/sql'
28
+ require 'cfnguardian/resources/sqs_queue'
29
+
30
+ module CfnGuardian
31
+ class Compile
32
+ include Logging
33
+
34
+ attr_reader :cost, :resources
35
+
36
+ def initialize(opts,bucket)
37
+ @prefix = opts.fetch(:stack_name,'guardian')
38
+ @bucket = bucket
39
+
40
+ config = YAML.load_file(opts.fetch(:config))
41
+ @resource_groups = config.fetch('Resources',{})
42
+ @templates = config.fetch('Templates',{})
43
+
44
+ @resources = []
45
+ @stacks = []
46
+ @checks = []
47
+
48
+ @cost = 0
49
+ end
50
+
51
+ def get_resources
52
+ @resource_groups.each do |group,resources|
53
+ resources.each do |resource|
54
+
55
+ begin
56
+ resource_class = Kernel.const_get("CfnGuardian::Resource::#{group}").new(resource)
57
+ rescue NameError => e
58
+ if @templates.has_key?(group) && @templates[group].has_key?('Inherit')
59
+ begin
60
+ resource_class = Kernel.const_get("CfnGuardian::Resource::#{@templates[group]['Inherit']}").new(resource)
61
+ logger.debug "Inheritited resource group #{@templates[group]['Inherit']} for group #{group}"
62
+ rescue NameError => e
63
+ logger.warn "'#{@templates[group]['Inherit']}' resource group doesn't exist and is unable to be inherited from"
64
+ next
65
+ end
66
+ else
67
+ logger.error(e)
68
+ next
69
+ end
70
+ end
71
+
72
+ overides = @templates.has_key?(group) ? @templates[group] : {}
73
+ @resources.concat resource_class.get_alarms(overides)
74
+ @resources.concat resource_class.get_events()
75
+ @checks.concat resource_class.get_checks()
76
+
77
+ @cost += resource_class.get_cost
78
+ end
79
+ end
80
+ end
81
+
82
+ def split_resources
83
+ split = @resources.each_slice(200).to_a
84
+ split.each_with_index do |resources,index|
85
+ @stacks.push({
86
+ 'Name' => "GuardianStack#{index}",
87
+ 'TemplateURL' => "https://#{@bucket}.s3.amazonaws.com/#{@prefix}/guardian-stack-#{index}.compiled.yaml",
88
+ 'Reference' => index
89
+ })
90
+ end
91
+ return split
92
+ end
93
+
94
+ def compile_templates
95
+ clean_out_directory()
96
+ resources = split_resources()
97
+
98
+ main_stack = CfnGuardian::Stacks::Main.new()
99
+ template = main_stack.build_template(@stacks,@checks)
100
+ valid = template.validate
101
+ File.write("out/guardian.compiled.yaml", JSON.parse(valid.to_json).to_yaml)
102
+
103
+ resources.each_with_index do |resources,index|
104
+ stack = CfnGuardian::Stacks::Resources.new()
105
+ template = stack.build_template(resources)
106
+ valid = template.validate
107
+ File.write("out/guardian-stack-#{index}.compiled.yaml", JSON.parse(valid.to_json).to_yaml)
108
+ end
109
+ end
110
+
111
+ def clean_out_directory
112
+ Dir["out/*.yaml"].each {|file| File.delete(file)}
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,144 @@
1
+ require 'aws-sdk-cloudformation'
2
+ require 'fileutils'
3
+ require 'cfnguardian/version'
4
+ require 'cfnguardian/log'
5
+
6
+ module CfnGuardian
7
+ class Deploy
8
+ include Logging
9
+
10
+ def initialize(opts,bucket)
11
+ @stack_name = opts.fetch(:stack_name,'guardian')
12
+ @bucket = bucket
13
+ @prefix = @stack_name
14
+ @template_path = "out/guardian.compiled.yaml"
15
+ @template_url = "https://#{@bucket}.s3.amazonaws.com/#{@prefix}/guardian.compiled.yaml"
16
+ @parameters = {}
17
+
18
+ config = YAML.load_file(opts[:config])
19
+ if config.has_key?('Topics')
20
+ @parameters['Critical'] = config['Topics'].fetch('Critical','')
21
+ @parameters['Warning'] = config['Topics'].fetch('Warning','')
22
+ @parameters['Task'] = config['Topics'].fetch('Task','')
23
+ @parameters['Informational'] = config['Topics'].fetch('Informational','')
24
+ end
25
+
26
+ @parameters['Critical'] = opts.fetch(:sns_critical,@parameters['Critical'])
27
+ @parameters['Warning'] = opts.fetch(:sns_warning,@parameters['Warning'])
28
+ @parameters['Task'] = opts.fetch(:sns_task,@parameters['Task'])
29
+ @parameters['Informational'] = opts.fetch(:sns_informational,@parameters['Informational'])
30
+
31
+ @client = Aws::CloudFormation::Client.new()
32
+ end
33
+
34
+ def upload_templates
35
+ Dir["out/*.yaml"].each do |template|
36
+ prefix = "#{@prefix}/#{template.split('/').last}"
37
+ body = File.read(template)
38
+ client = Aws::S3::Client.new()
39
+ client.put_object({
40
+ body: body,
41
+ bucket: @bucket,
42
+ key: prefix
43
+ })
44
+ end
45
+ end
46
+
47
+ # TODO: check for REVIEW_IN_PROGRESS
48
+ def does_cf_stack_exist()
49
+ begin
50
+ resp = @client.describe_stacks({
51
+ stack_name: @stack_name,
52
+ })
53
+ rescue Aws::CloudFormation::Errors::ValidationError
54
+ return false
55
+ end
56
+ return resp.size > 0
57
+ end
58
+
59
+ def get_change_set_type()
60
+ return does_cf_stack_exist() ? 'UPDATE' : 'CREATE'
61
+ end
62
+
63
+ def create_change_set()
64
+ change_set_name = "#{@stack_name}-#{CfnGuardian::CHANGE_SET_VERSION}-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}"
65
+ change_set_type = get_change_set_type()
66
+
67
+ if change_set_type == 'CREATE'
68
+ params = get_parameters_from_template()
69
+ else
70
+ params = get_parameters_from_stack()
71
+ end
72
+
73
+ params.each do |param|
74
+ if !@parameters[param[:parameter_key]].nil?
75
+ param[:parameter_value] = @parameters[param[:parameter_key]]
76
+ param[:use_previous_value] = false
77
+ end
78
+ end
79
+
80
+ logger.debug "Creating changeset"
81
+ change_set = @client.create_change_set({
82
+ stack_name: @stack_name,
83
+ template_url: @template_url,
84
+ capabilities: ["CAPABILITY_IAM"],
85
+ parameters: params,
86
+ tags: [
87
+ {
88
+ key: "guardian:version",
89
+ value: CfnGuardian::VERSION,
90
+ },
91
+ {
92
+ key: 'Environment',
93
+ value: 'guardian'
94
+ }
95
+ ],
96
+ change_set_name: change_set_name,
97
+ change_set_type: change_set_type
98
+ })
99
+ return change_set, change_set_type
100
+ end
101
+
102
+ def wait_for_changeset(change_set_id)
103
+ logger.debug "Waiting for changeset to be created"
104
+ begin
105
+ @client.wait_until :change_set_create_complete, change_set_name: change_set_id
106
+ rescue Aws::Waiters::Errors::FailureStateError => e
107
+ change_set = get_change_set(change_set_id)
108
+ logger.error("change set status: #{change_set.status} reason: #{change_set.status_reason}")
109
+ exit 1
110
+ end
111
+ end
112
+
113
+ def get_change_set(change_set_id)
114
+ @client.describe_change_set({
115
+ change_set_name: change_set_id,
116
+ })
117
+ end
118
+
119
+ def execute_change_set(change_set_id)
120
+ logger.debug "Executing the changeset"
121
+ stack = @client.execute_change_set({
122
+ change_set_name: change_set_id
123
+ })
124
+ end
125
+
126
+ def wait_for_execute(change_set_type)
127
+ waiter = change_set_type == 'CREATE' ? :stack_create_complete : :stack_update_complete
128
+ logger.info "Waiting for changeset to #{change_set_type}"
129
+ resp = @client.wait_until waiter, stack_name: @stack_name
130
+ end
131
+
132
+ def get_parameters_from_stack()
133
+ resp = @client.get_template_summary({ stack_name: @stack_name })
134
+ return resp.parameters.collect { |p| { parameter_key: p.parameter_key, use_previous_value: true } }
135
+ end
136
+
137
+ def get_parameters_from_template()
138
+ template_body = File.read(@template_path)
139
+ resp = @client.get_template_summary({ template_body: template_body })
140
+ return resp.parameters.collect { |p| { parameter_key: p.parameter_key, parameter_value: p.default_value } }
141
+ end
142
+
143
+ end
144
+ end