cfn_monitor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,186 @@
1
+ require "cfndsl"
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'yaml'
5
+
6
+ require "cfn_monitor/utils"
7
+
8
+ module CfnMonitor
9
+ class Generate
10
+
11
+ def self.run(options)
12
+
13
+ if !options['application']
14
+ raise "No application specified"
15
+ end
16
+
17
+ application = options['application']
18
+
19
+ template_path = File.join(File.dirname(__FILE__),'../config/templates.yml')
20
+ config_path = File.join(File.dirname(__FILE__),'../config/config.yml')
21
+ # Load global config files
22
+ global_templates_config = YAML.load(File.read(template_path))
23
+ config = YAML.load(File.read(config_path))
24
+
25
+ custom_alarms_config_file = "#{application}/alarms.yml"
26
+ custom_templates_config_file = "#{application}/templates.yml"
27
+ output_path = "output/#{application}"
28
+ upload_path = "cloudformation/monitoring/#{application}"
29
+
30
+ # Load custom config files
31
+ if File.file?(custom_alarms_config_file)
32
+ custom_alarms_config = YAML.load(File.read(custom_alarms_config_file))
33
+ else
34
+ puts "Failed to load #{custom_alarms_config_file}"
35
+ exit 1
36
+ end
37
+
38
+ # Merge custom template configs over global template configs
39
+ if File.file?(custom_templates_config_file)
40
+ custom_templates_config = YAML.load(File.read(custom_templates_config_file))
41
+ templates = CommonHelper.deep_merge(global_templates_config, custom_templates_config)
42
+ else
43
+ templates = global_templates_config
44
+ end
45
+
46
+ # Create an array of alarms based on the templates associated with each resource
47
+ alarms = []
48
+ resources = custom_alarms_config['resources']
49
+ metrics = custom_alarms_config['metrics']
50
+ hosts = custom_alarms_config['hosts']
51
+ hosts ||= {}
52
+ services = custom_alarms_config['services']
53
+ services ||= {}
54
+ endpoints = custom_alarms_config['endpoints']
55
+ endpoints ||= {}
56
+ rme = { resources: resources, metrics: metrics, endpoints: endpoints, hosts: hosts, services: services }
57
+ source_bucket = custom_alarms_config['source_bucket']
58
+
59
+ rme.each do | k,v |
60
+ if !v.nil?
61
+ v.each do | resource,attributes |
62
+ # set environments to 'all' by default
63
+ environments = ['all']
64
+ # Support config hashs for additional parameters
65
+ params = {}
66
+ if attributes.kind_of?(Hash)
67
+ attributes.each do | a,b |
68
+ environments = b if a == 'environments'
69
+ # Convert strings to arrays for consistency
70
+ if !environments.kind_of?(Array) then environments = environments.split end
71
+ params[a] = b if !['template','environments'].member? a
72
+ end
73
+ templatesEnabled = attributes['template']
74
+ else
75
+ templatesEnabled = attributes
76
+ end
77
+ # Convert strings to arrays for looping
78
+ if !templatesEnabled.kind_of?(Array) then templatesEnabled = templatesEnabled.split end
79
+ templatesEnabled.each do | templateEnabled |
80
+ if !templates['templates'][templateEnabled].nil?
81
+ # If a template is provided, inherit that template
82
+ if !templates['templates'][templateEnabled]['template'].nil?
83
+ template_from = Marshal.load( Marshal.dump(templates['templates'][templates['templates'][templateEnabled]['template']]) )
84
+ template_to = templates['templates'][templateEnabled].without('template')
85
+ template_merged = CommonHelper.deep_merge(template_from, template_to)
86
+ templates['templates'][templateEnabled] = template_merged
87
+ end
88
+ templates['templates'][templateEnabled].each do | alarm,parameters |
89
+ resourceParams = parameters.clone
90
+ # Override template params if overrides provided
91
+ params.each do | x,y |
92
+ resourceParams[x] = y
93
+ end
94
+ if k == :hosts
95
+ resourceParams['cmds'].each do |cmd|
96
+ hostParams = resourceParams.clone
97
+ hostParams['cmd'] = cmd
98
+ # Construct alarm object per cmd
99
+ alarms << {
100
+ resource: resource,
101
+ type: k[0...-1],
102
+ template: templateEnabled,
103
+ alarm: alarm,
104
+ parameters: hostParams,
105
+ environments: environments
106
+ }
107
+ end
108
+ else
109
+ # Construct alarm object
110
+ alarms << {
111
+ resource: resource,
112
+ type: k[0...-1],
113
+ template: templateEnabled,
114
+ alarm: alarm,
115
+ parameters: resourceParams,
116
+ environments: environments
117
+ }
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ # Create temp alarms file for CfnDsl
127
+ temp_file = Tempfile.new(["alarms-",'.yml'])
128
+ temp_file_path = temp_file.path
129
+ temp_file.write({'alarms' => alarms}.to_yaml)
130
+ temp_file.rewind
131
+
132
+ # Split resources for mulitple templates to avoid CloudFormation template resource limits
133
+ split = []
134
+ template_envs = ['production']
135
+ alarms.each_with_index do |alarm,index|
136
+ split[index/config['resource_limit']] ||= {}
137
+ split[index/config['resource_limit']]['alarms'] ||= []
138
+ split[index/config['resource_limit']]['alarms'] << alarm
139
+ template_envs |= get_alarm_envs(alarm[:parameters])
140
+ end
141
+
142
+ # Create temp files for split resources for CfnDsl input
143
+ temp_files=[]
144
+ temp_file_paths=[]
145
+ (alarms.count/config['resource_limit'].to_f).ceil.times do | i |
146
+ temp_files[i] = Tempfile.new(["alarms-#{i}-",'.yml'])
147
+ temp_file_paths[i] = temp_files[i].path
148
+ temp_files[i].write(split[i].to_yaml)
149
+ temp_files[i].rewind
150
+ end
151
+
152
+ write_cfdndsl_template(temp_file_path, temp_file_paths, custom_alarms_config_file, source_bucket, template_envs, output_path, upload_path)
153
+
154
+ end
155
+
156
+ def self.write_cfdndsl_template(alarms_config,configs,custom_alarms_config_file,source_bucket,template_envs,output_path,upload_path)
157
+ template_path = File.expand_path("../../templates", __FILE__)
158
+ FileUtils::mkdir_p output_path
159
+ configs.each_with_index do |config,index|
160
+ File.open("#{output_path}/resources#{index}.json", 'w') { |file|
161
+ file.write(JSON.pretty_generate( CfnDsl.eval_file_with_extras("#{template_path}/resources.rb",[[:yaml, config],[:raw, "template_number=#{index}"],[:raw, "source_bucket='#{source_bucket}'"],[:raw, "upload_path='#{upload_path}'"]],STDOUT)))}
162
+ File.open("#{output_path}/alarms#{index}.json", 'w') { |file|
163
+ file.write(JSON.pretty_generate( CfnDsl.eval_file_with_extras("#{template_path}/alarms.rb",[[:yaml, config],[:raw, "template_number=#{index}"],[:raw, "template_envs=#{template_envs}"]],STDOUT)))}
164
+ end
165
+ File.open("#{output_path}/endpoints.json", 'w') { |file|
166
+ file.write(JSON.pretty_generate( CfnDsl.eval_file_with_extras("#{template_path}/endpoints.rb",[[:yaml, alarms_config],[:raw, "template_envs=#{template_envs}"]],STDOUT)))}
167
+ File.open("#{output_path}/hosts.json", 'w') { |file|
168
+ file.write(JSON.pretty_generate( CfnDsl.eval_file_with_extras("#{template_path}/hosts.rb",[[:yaml, alarms_config],[:raw, "template_envs=#{template_envs}"]],STDOUT)))}
169
+ File.open("#{output_path}/services.json", 'w') { |file|
170
+ file.write(JSON.pretty_generate( CfnDsl.eval_file_with_extras("#{template_path}/services.rb",[[:yaml, alarms_config],[:raw, "template_envs=#{template_envs}"]],STDOUT)))}
171
+ File.open("#{output_path}/master.json", 'w') { |file|
172
+ file.write(JSON.pretty_generate( CfnDsl.eval_file_with_extras("#{template_path}/master.rb",[[:yaml, custom_alarms_config_file],[:raw, "templateCount=#{configs.count}"],[:raw, "template_envs=#{template_envs}"],[:raw, "upload_path='#{upload_path}'"]],STDOUT)))}
173
+ end
174
+
175
+ def self.get_alarm_envs(params)
176
+ envs = []
177
+ params.each do | key,value |
178
+ if key.include? '.'
179
+ envs << key.split('.').last
180
+ end
181
+ end
182
+ return envs
183
+ end
184
+
185
+ end
186
+ end
@@ -0,0 +1,135 @@
1
+ require 'aws-sdk-cloudformation'
2
+ require 'aws-sdk-elasticloadbalancingv2'
3
+
4
+ module CfnMonitor
5
+ class Query
6
+
7
+ def self.run(options)
8
+
9
+ if !options['application'] || !options['stack']
10
+ raise "No application specified"
11
+ end
12
+
13
+ if !options['stack']
14
+ raise "No stack specified"
15
+ end
16
+
17
+ config_path = File.join(File.dirname(__FILE__),'../config/config.yml')
18
+ # Load global config files
19
+ config = YAML.load(File.read(config_path))
20
+
21
+ custom_alarms_config_file = "#{options['application']}/alarms.yml"
22
+
23
+ # Load custom config files
24
+ custom_alarms_config = YAML.load(File.read(custom_alarms_config_file)) if File.file?(custom_alarms_config_file)
25
+ custom_alarms_config ||= {}
26
+ custom_alarms_config['resources'] ||= {}
27
+
28
+ puts "-----------------------------------------------"
29
+ puts "stack: #{options['stack']}"
30
+ puts "application: #{options['application']}"
31
+ puts "-----------------------------------------------"
32
+ puts "Searching Stacks for Monitorable Resources"
33
+ puts "-----------------------------------------------"
34
+
35
+ cfClient = Aws::CloudFormation::Client.new()
36
+ elbClient = Aws::ElasticLoadBalancingV2::Client.new()
37
+
38
+ stackResourceQuery = query_stacks(config,cfClient,elbClient,options['stack'])
39
+ stackResourceCount = stackResourceQuery[:stackResourceCount]
40
+ stackResources = stackResourceQuery[:stackResources]
41
+
42
+ configResourceCount = custom_alarms_config['resources'].keys.count
43
+ configResources = []
44
+ keyUpdates = []
45
+
46
+ stackResources[:template].each do | k,v |
47
+ if stackResources[:physical_resource_id].key? k.partition('/').last
48
+ keyUpdates << k
49
+ end
50
+ end
51
+
52
+ keyUpdates.each do | k |
53
+ stackResources[:template]["#{k.partition('/').first}/#{stackResources[:physical_resource_id][k.partition('/').last]}"] = stackResources[:template].delete(k)
54
+ end
55
+
56
+ stackResources[:template].each do | k,v |
57
+ if !custom_alarms_config['resources'].any? {|x, y| x == k}
58
+ configResources.push("#{k}: #{v}")
59
+ end
60
+ end
61
+
62
+ puts "-----------------------------------------------"
63
+ puts "Monitorable Resources (with default templates)"
64
+ puts "-----------------------------------------------"
65
+ stackResources[:template].each do | k,v |
66
+ puts "#{k}: #{v}"
67
+ end
68
+ puts "-----------------------------------------------"
69
+ if configResourceCount < stackResourceCount
70
+ puts "Missing resources (with default templates)"
71
+ puts "-----------------------------------------------"
72
+ configResources.each do | r |
73
+ puts r
74
+ end
75
+ puts "-----------------------------------------------"
76
+ end
77
+ puts "Monitorable resources in #{options['stack']} stack: #{stackResourceCount}"
78
+ puts "Resources in #{options['application']} alarms config: #{configResourceCount}"
79
+ if stackResourceCount > 0
80
+ puts "Coverage: #{100-(configResources.count*100/stackResourceCount)}%"
81
+ end
82
+ puts "-----------------------------------------------"
83
+
84
+ end
85
+
86
+ def self.query_stacks (config,cfClient,elbClient,stack,stackResources={template:{},physical_resource_id:{}},location='')
87
+ stackResourceCount = 0
88
+ stackResourceCountLocal = 0
89
+ begin
90
+ resp = cfClient.list_stack_resources({
91
+ stack_name: stack
92
+ })
93
+ rescue Aws::CloudFormation::Errors::ServiceError => e
94
+ puts "Error: #{e}"
95
+ exit 1
96
+ end
97
+
98
+ resp.stack_resource_summaries.each do | resource |
99
+ if resource['resource_type'] == 'AWS::CloudFormation::Stack'
100
+ query = query_stacks(config,cfClient,elbClient,resource['physical_resource_id'],stackResources,"#{location}.#{resource['logical_resource_id']}")
101
+ stackResourceCount += query[:stackResourceCount]
102
+ end
103
+ if config['resource_defaults'].key? resource['resource_type']
104
+ if resource['resource_type'] == 'AWS::ElasticLoadBalancingV2::TargetGroup'
105
+ begin
106
+ tg = elbClient.describe_target_groups({
107
+ target_group_arns: [ resource['physical_resource_id'] ]
108
+ })
109
+ rescue Aws::ElasticLoadBalancingV2::Errors::ServiceError => e
110
+ puts "Error: #{e}"
111
+ exit 1
112
+ end
113
+ stackResources[:template]["#{location[1..-1]}.#{resource['logical_resource_id']}/#{tg['target_groups'][0]['load_balancer_arns'][0]}"] = config['resource_defaults'][resource['resource_type']]
114
+ else
115
+ stackResources[:template]["#{location[1..-1]}.#{resource['logical_resource_id']}"] = config['resource_defaults'][resource['resource_type']]
116
+ end
117
+ stackResourceCount += 1
118
+ stackResourceCountLocal += 1
119
+ print "#{location[1..-1]}: Found #{stackResourceCount} resource#{"s" if stackResourceCount != 1}\r"
120
+ sleep 0.2
121
+ elsif resource['resource_type'] == 'AWS::ElasticLoadBalancingV2::LoadBalancer'
122
+ stackResources[:physical_resource_id][resource['physical_resource_id']] = "#{location[1..-1]}.#{resource['logical_resource_id']}"
123
+ end
124
+ end
125
+ stackResourceQuery = {
126
+ stackResourceCount: stackResourceCount,
127
+ stackResources: stackResources
128
+ }
129
+ sleep 0.2
130
+ puts "#{stack if location == ''}#{location[1..-1]}: Found #{stackResourceCountLocal} resource#{"s" if stackResourceCountLocal != 1}"
131
+ stackResourceQuery
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,42 @@
1
+ module CfnMonitor
2
+ class Utils
3
+
4
+ # Merge Hash B into hash A. If any values or hashes as well, merge will be performed recursively
5
+ # Returns Hash A with updated values
6
+ def self.deep_merge(a, b)
7
+
8
+ # Loop over key/value pairs
9
+ b.each { |key, value|
10
+
11
+ # If key from B present in map A
12
+ if a.key? key
13
+
14
+ # If both are hashes call recursively
15
+ if (a[key].class == Hash and b[key].class == Hash)
16
+ a[key] = deep_merge(a[key], value)
17
+ else
18
+ # Overwrite value with value from B
19
+ a[key] = value
20
+ end
21
+ else
22
+ # Add key from B
23
+ a[key] = value
24
+ end
25
+ }
26
+
27
+ # Return hash a
28
+ return a
29
+ end
30
+
31
+ end
32
+ end
33
+
34
+ class Hash
35
+ def without(*keys)
36
+ dup.without!(*keys)
37
+ end
38
+
39
+ def without!(*keys)
40
+ reject! { |key| keys.include?(key) }
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module CfnMonitor
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,66 @@
1
+ require "thor"
2
+ require "cfn_monitor/version"
3
+ require "cfn_monitor/query"
4
+ require "cfn_monitor/generate"
5
+ require "cfn_monitor/deploy"
6
+
7
+ module CfnMonitor
8
+ class Commands < Thor
9
+
10
+ map %w[--version -v] => :__print_version
11
+ desc "--version, -v", "print the version"
12
+ def __print_version
13
+ puts CfnMonitor::VERSION
14
+ end
15
+
16
+ # class_option :verbose,
17
+ # aliases: :V,
18
+ # type: :boolean,
19
+ # default: false,
20
+ # lazy_default: true,
21
+ # desc: "Extra logging"
22
+ #
23
+ # class_option :region,
24
+ # group: :aws,
25
+ # aliases: :r,
26
+ # type: :string,
27
+ # desc: "AWS Region"
28
+ #
29
+ # class_option :profile,
30
+ # group: :aws,
31
+ # aliases: :p,
32
+ # type: :string,
33
+ # desc: "Profile name in AWS credentials file"
34
+
35
+ desc "generate", "Generate monitoring cloudformation templates"
36
+ long_desc <<-LONG
37
+ Generates cloudformation templates from the alarm configuration and output to the output/ directory.
38
+ LONG
39
+ method_option :application, aliases: :a, type: :string, desc: "application name"
40
+ # method_option :validate, aliases: :v, type: :boolean, default: true, desc: "validate cfn templates"
41
+ def generate
42
+ CfnMonitor::Generate.run(options)
43
+ end
44
+
45
+ desc "query", "Queries a cloudformation stack for monitorable resources"
46
+ long_desc <<-LONG
47
+ This will provide a list of resources in the correct config syntax,
48
+ including the nested stacks and the default templates for those resources.
49
+ LONG
50
+ method_option :application, aliases: :a, type: :string, desc: "application name"
51
+ method_option :stack, aliases: :s, type: :string, desc: "cfn stack name"
52
+ def query
53
+ CfnMonitor::Query.run(options)
54
+ end
55
+
56
+ desc "deploy", "Deploys gerenated cfn templates to S3 bucket"
57
+ long_desc <<-LONG
58
+ Deploys gerenated cloudformation templates to the specified S3 source_bucket
59
+ LONG
60
+ method_option :application, aliases: :a, type: :string, desc: "application name"
61
+ def deploy
62
+ CfnMonitor::Deploy.run(options)
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,14 @@
1
+ resource_limit: 50
2
+ resource_defaults:
3
+ AWS::ElasticLoadBalancing::LoadBalancer: ElasticLoadBalancer
4
+ AWS::ElasticLoadBalancingV2::TargetGroup: ApplicationELBTargetGroup
5
+ AWS::AutoScaling::AutoScalingGroup: AutoScalingGroup
6
+ AWS::RDS::DBInstance: RDSInstance
7
+ AWS::RDS::DBCluster: DBCluster
8
+ AWS::ECS::Cluster: ECSCluster
9
+ AWS::ElastiCache::ReplicationGroup: ElastiCacheReplicationGroup
10
+ AWS::EFS::FileSystem: ElasticFileSystem
11
+ AWS::EC2::Instance: Ec2Instance
12
+ AWS::Redshift::Cluster: RedshiftCluster
13
+ AWS::DynamoDB::Table: DynamoDBTable
14
+ AWS::ApiGateway::RestApi: ApiGateway