cfn_monitor 0.1.1 → 0.2.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: 06a68ecda9a61437e7147f46a19f1f0eae4657fed927a5707055bef7393dff89
4
- data.tar.gz: de1388e92d9c75303dafec4ce4172813b36ccf373a4847281ecaada1dec422c3
3
+ metadata.gz: 466206e0f5cedb6a5006c98a0004ce27dddf0fe0d793ed1842e1ec3d11fb8382
4
+ data.tar.gz: 72c10dbcead4deccf275f20609342474b37ae41c310643bc52d0340a0af67017
5
5
  SHA512:
6
- metadata.gz: e977e75432f12b896a96ca0d42967e3d113d67e4394f48380e624b2939bf2f1fe93ef73e1369f60ef0d6b2b009f9125dce12e8e63d038a058dc6be2cfd043847
7
- data.tar.gz: c774fecdcbfa6d8cb4f756f10bf2fa7987d9434e80c30b2980ff7911cca65d20bf12b10ba8e30d3124b4ba30326fc9ebcd38d9345974dbda501f8f2bcd1be7b2
6
+ metadata.gz: a1622b83a7cdfbf96757582b130b7c04c2ce7789f8f9de4f04c0610e2ab59795c2207361f79f157777bf7d858f42018cdaf0f430b9bb05053888b5950118058c
7
+ data.tar.gz: 9bd6dced53b3f49fa8516194a031239fe290bce680b3b59d9284a789e8509de2916c27988d830dc7b70038d561b0fa45402453e1258ebf138ce4235ee797affa
data/README.md CHANGED
@@ -9,7 +9,7 @@ monitorable resources that can be placed into a config file. This config
9
9
  can then be used to generate a cloudformation stack to create and manage
10
10
  cloudwatch alarms.
11
11
 
12
- It is packaged as a docker container `base2/cfn_monitor` and
12
+ It is packaged as a docker container `base2/cfn-monitor` and
13
13
  can be run by volume mounting in a local directory to access the config
14
14
  or by using within AWS CodePipeline.
15
15
 
@@ -152,6 +152,25 @@ timeOut | A timeout value for the endpoint monitoring | 120 seconds
152
152
  scheduleExpression | A cron expression used to schedule the endpoint monitoring | Every minute
153
153
  environments | A string or array of environment names. Monitoring will only be deployed for these environments (if specified) | All environments
154
154
 
155
+ #### SSL certificate expiry date checking
156
+ To alert on the expiry date of an SSL certificate for a particular domain, add the following config:
157
+
158
+ ```YAML
159
+ ssl:
160
+ https://www.base2services.com: Ssl
161
+ ```
162
+
163
+ The `Ssl` template is scheduled to push a metric once every day.
164
+
165
+ #### DNS domain expiry date checking
166
+ To alert on the expiry date of an DNS domain, add the following config:
167
+
168
+ ```YAML
169
+ dns:
170
+ base2services.com: Dns
171
+ ```
172
+
173
+ The `Dns` template is scheduled to push a metric once every day.
155
174
 
156
175
  #### Multiple templates
157
176
  You can specify multiple templates for the resource by providing a list/array. You may want to do this if you want to deploy some custom alarms in addition to the default alarms for a resource.
@@ -2,6 +2,7 @@ require "thor"
2
2
  require "cfn_monitor/version"
3
3
  require "cfn_monitor/query"
4
4
  require "cfn_monitor/generate"
5
+ require "cfn_monitor/validate"
5
6
  require "cfn_monitor/deploy"
6
7
 
7
8
  module CfnMonitor
@@ -13,6 +14,12 @@ module CfnMonitor
13
14
  puts CfnMonitor::VERSION
14
15
  end
15
16
 
17
+ class_option :silent,
18
+ aliases: :s,
19
+ type: :boolean,
20
+ default: false,
21
+ desc: "Don't print Cfndsl output"
22
+
16
23
  # class_option :verbose,
17
24
  # aliases: :V,
18
25
  # type: :boolean,
@@ -32,35 +39,47 @@ module CfnMonitor
32
39
  # type: :string,
33
40
  # desc: "Profile name in AWS credentials file"
34
41
 
35
- desc "generate", "Generate monitoring cloudformation templates"
42
+ desc "generate", "Generate monitoring CloudFormation templates"
36
43
  long_desc <<-LONG
37
- Generates cloudformation templates from the alarm configuration and output to the output/ directory.
44
+ Generates CloudFormation templates from the alarm configuration and output to the output/ directory.
38
45
  LONG
39
46
  method_option :application, aliases: :a, type: :string, desc: "application name"
40
- # method_option :validate, aliases: :v, type: :boolean, default: true, desc: "validate cfn templates"
47
+ method_option :validate, aliases: :v, type: :boolean, default: false, desc: "validate cfn templates"
41
48
  def generate
42
49
  CfnMonitor::Generate.run(options)
50
+ if options['validate']
51
+ CfnMonitor::Validate.run(options)
52
+ end
43
53
  end
44
54
 
45
- desc "query", "Queries a cloudformation stack for monitorable resources"
55
+ desc "query", "Queries a CloudFormation stack for monitorable resources"
46
56
  long_desc <<-LONG
47
57
  This will provide a list of resources in the correct config syntax,
48
58
  including the nested stacks and the default templates for those resources.
49
59
  LONG
50
60
  method_option :application, aliases: :a, type: :string, desc: "application name"
51
- method_option :stack, aliases: :s, type: :string, desc: "cfn stack name"
61
+ method_option :stack, aliases: :s, type: :string, desc: "CloudFormation stack name"
52
62
  def query
53
63
  CfnMonitor::Query.run(options)
54
64
  end
55
65
 
56
- desc "deploy", "Deploys gerenated cfn templates to S3 bucket"
66
+ desc "deploy", "Deploys generated CloudFormation templates to an S3 bucket"
57
67
  long_desc <<-LONG
58
- Deploys gerenated cloudformation templates to the specified S3 source_bucket
68
+ Deploys generated CloudFormation templates to the specified S3 source bucket
59
69
  LONG
60
70
  method_option :application, aliases: :a, type: :string, desc: "application name"
61
71
  def deploy
62
72
  CfnMonitor::Deploy.run(options)
63
73
  end
64
74
 
75
+ desc "validate", "Validates CloudFormation templates in an S3 bucket"
76
+ long_desc <<-LONG
77
+ Validates generated CloudFormation templates in a specified S3 source bucket
78
+ LONG
79
+ method_option :application, aliases: :a, type: :string, desc: "application name"
80
+ def validate
81
+ CfnMonitor::Validate.run(options)
82
+ end
83
+
65
84
  end
66
85
  end
@@ -6,14 +6,16 @@ module CfnMonitor
6
6
 
7
7
  def self.run(options)
8
8
 
9
- if !options['application']
10
- raise "No application specified"
9
+ if options['application']
10
+ application = options['application']
11
+ custom_alarms_config_file = "#{application}/alarms.yml"
12
+ output_path = "output/#{application}"
13
+ else
14
+ application = File.basename(Dir.getwd)
15
+ custom_alarms_config_file = "alarms.yml"
16
+ output_path = "output"
11
17
  end
12
18
 
13
- application = options['application']
14
-
15
- custom_alarms_config_file = "#{application}/alarms.yml"
16
- output_path = "output/#{application}"
17
19
  upload_path = "cloudformation/monitoring/#{application}"
18
20
 
19
21
  # Load custom config files
@@ -10,11 +10,13 @@ module CfnMonitor
10
10
 
11
11
  def self.run(options)
12
12
 
13
- if !options['application']
14
- raise "No application specified"
13
+ if options['silent']
14
+ verbose_cfndsl = false
15
+ else
16
+ verbose_cfndsl = STDOUT
15
17
  end
16
18
 
17
- application = options['application']
19
+ application = options['application'] || '.'
18
20
 
19
21
  template_path = File.join(File.dirname(__FILE__),'../config/templates.yml')
20
22
  config_path = File.join(File.dirname(__FILE__),'../config/config.yml')
@@ -47,74 +49,81 @@ module CfnMonitor
47
49
  alarms = []
48
50
  resources = custom_alarms_config['resources']
49
51
  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 }
52
+ hosts = custom_alarms_config['hosts'] || {}
53
+ ssl = custom_alarms_config['ssl'] || {}
54
+ dns = custom_alarms_config['dns'] || {}
55
+ services = custom_alarms_config['services'] || {}
56
+ endpoints = custom_alarms_config['endpoints'] || {}
57
+ ecsClusters = custom_alarms_config['ecsClusters'] || {}
58
+
59
+ alarm_parameters = { resources: resources, metrics: metrics, endpoints: endpoints, hosts: hosts, ssl: ssl, dns: dns, services: services, ecsClusters: ecsClusters }
57
60
  source_bucket = custom_alarms_config['source_bucket']
58
61
 
59
- rme.each do | k,v |
62
+ alarm_parameters.each do | k,v |
60
63
  if !v.nil?
61
- v.each do | resource,attributes |
64
+ v.each do | resource,attributeList |
62
65
  # set environments to 'all' by default
63
66
  environments = ['all']
64
67
  # Support config hashs for additional parameters
65
68
  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
69
+ if !attributeList.kind_of?(Array)
70
+ attributeList = [attributeList]
76
71
  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 = CfnMonitor::Utils.deep_merge(template_from, template_to)
86
- templates['templates'][templateEnabled] = template_merged
72
+ attributeList.each do | attributes |
73
+ if attributes.kind_of?(Hash)
74
+ attributes.each do | a,b |
75
+ environments = b if a == 'environments'
76
+ # Convert strings to arrays for consistency
77
+ if !environments.kind_of?(Array) then environments = environments.split end
78
+ params[a] = b if !['template','environments'].member? a
87
79
  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
80
+ templatesEnabled = attributes['template']
81
+ else
82
+ templatesEnabled = attributes
83
+ end
84
+ # Convert strings to arrays for looping
85
+ if !templatesEnabled.kind_of?(Array) then templatesEnabled = templatesEnabled.split end
86
+ templatesEnabled.each do | templateEnabled |
87
+ if !templates['templates'][templateEnabled].nil?
88
+ # If a template is provided, inherit that template
89
+ if !templates['templates'][templateEnabled]['template'].nil?
90
+ template_from = Marshal.load( Marshal.dump(templates['templates'][templates['templates'][templateEnabled]['template']]) )
91
+ template_to = templates['templates'][templateEnabled].without('template')
92
+ template_merged = CfnMonitor::Utils.deep_merge(template_from, template_to)
93
+ templates['templates'][templateEnabled] = template_merged
93
94
  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
95
+ templates['templates'][templateEnabled].each do | alarm,parameters |
96
+ resourceParams = parameters.clone
97
+ # Override template params if overrides provided
98
+ params.each do | x,y |
99
+ resourceParams[x] = y
100
+ end
101
+
102
+ if k == :hosts
103
+ resourceParams['cmds'].each do |cmd|
104
+ hostParams = resourceParams.clone
105
+ hostParams['cmd'] = cmd
106
+ # Construct alarm object per cmd
107
+ alarms << {
108
+ resource: resource,
109
+ type: k[0...-1],
110
+ template: templateEnabled,
111
+ alarm: alarm,
112
+ parameters: hostParams,
113
+ environments: environments
114
+ }
115
+ end
116
+ else
117
+ # Construct alarm object
99
118
  alarms << {
100
119
  resource: resource,
101
120
  type: k[0...-1],
102
121
  template: templateEnabled,
103
122
  alarm: alarm,
104
- parameters: hostParams,
123
+ parameters: resourceParams,
105
124
  environments: environments
106
125
  }
107
126
  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
127
  end
119
128
  end
120
129
  end
@@ -149,27 +158,29 @@ module CfnMonitor
149
158
  temp_files[i].rewind
150
159
  end
151
160
 
152
- write_cfdndsl_template(temp_file_path, temp_file_paths, custom_alarms_config_file, source_bucket, template_envs, output_path, upload_path)
161
+ write_cfdndsl_template(alarm_parameters, temp_file_path, temp_file_paths, custom_alarms_config_file, source_bucket, template_envs, output_path, upload_path, verbose_cfndsl)
153
162
 
154
163
  end
155
164
 
156
- def self.write_cfdndsl_template(alarms_config,configs,custom_alarms_config_file,source_bucket,template_envs,output_path,upload_path)
165
+ def self.write_cfdndsl_template(alarm_parameters,alarms_config_file,configs,custom_alarms_config_file,source_bucket,template_envs,output_path,upload_path,verbose_cfndsl)
157
166
  template_path = File.expand_path("../../templates", __FILE__)
158
167
  FileUtils::mkdir_p output_path
159
168
  configs.each_with_index do |config,index|
160
169
  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)))}
170
+ 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}'"]],verbose_cfndsl)))}
162
171
  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)))}
172
+ 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}"]],verbose_cfndsl)))}
173
+ end
174
+ ['endpoints', 'ssl', "dns", 'hosts', 'services','ecsClusters'].each do |template|
175
+ if !alarm_parameters[template.to_sym].nil? and alarm_parameters[template.to_sym] != {}
176
+ File.open("#{output_path}/#{template}.json", 'w') { |file|
177
+ file.write(JSON.pretty_generate( CfnDsl.eval_file_with_extras("#{template_path}/#{template}.rb",[[:yaml, alarms_config_file],[:raw, "template_envs=#{template_envs}"]],verbose_cfndsl)))
178
+ }
179
+ end
164
180
  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)))}
181
+
171
182
  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)))}
183
+ 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}'"]],verbose_cfndsl)))}
173
184
  end
174
185
 
175
186
  def self.get_alarm_envs(params)
@@ -6,19 +6,23 @@ module CfnMonitor
6
6
 
7
7
  def self.run(options)
8
8
 
9
- if !options['application'] || !options['stack']
10
- raise "No application specified"
11
- end
12
-
13
9
  if !options['stack']
14
10
  raise "No stack specified"
15
11
  end
16
12
 
13
+ if options['application']
14
+ application = options['application']
15
+ custom_alarms_config_file = "#{application}/alarms.yml"
16
+ else
17
+ application = File.basename(Dir.getwd)
18
+ custom_alarms_config_file = "alarms.yml"
19
+ end
20
+
17
21
  config_path = File.join(File.dirname(__FILE__),'../config/config.yml')
18
22
  # Load global config files
19
23
  config = YAML.load(File.read(config_path))
20
24
 
21
- custom_alarms_config_file = "#{options['application']}/alarms.yml"
25
+ custom_alarms_config_file = "#{application}/alarms.yml"
22
26
 
23
27
  # Load custom config files
24
28
  custom_alarms_config = YAML.load(File.read(custom_alarms_config_file)) if File.file?(custom_alarms_config_file)
@@ -27,7 +31,7 @@ module CfnMonitor
27
31
 
28
32
  puts "-----------------------------------------------"
29
33
  puts "stack: #{options['stack']}"
30
- puts "application: #{options['application']}"
34
+ puts "application: #{application}"
31
35
  puts "-----------------------------------------------"
32
36
  puts "Searching Stacks for Monitorable Resources"
33
37
  puts "-----------------------------------------------"
@@ -75,7 +79,7 @@ module CfnMonitor
75
79
  puts "-----------------------------------------------"
76
80
  end
77
81
  puts "Monitorable resources in #{options['stack']} stack: #{stackResourceCount}"
78
- puts "Resources in #{options['application']} alarms config: #{configResourceCount}"
82
+ puts "Resources in #{application} alarms config: #{configResourceCount}"
79
83
  if stackResourceCount > 0
80
84
  puts "Coverage: #{100-(configResources.count*100/stackResourceCount)}%"
81
85
  end
@@ -0,0 +1,76 @@
1
+ require 'aws-sdk-s3'
2
+ require 'yaml'
3
+
4
+ module CfnMonitor
5
+ class Validate
6
+
7
+ def self.run(options)
8
+
9
+ if options['application']
10
+ application = options['application']
11
+ custom_alarms_config_file = "#{application}/alarms.yml"
12
+ output_path = "output/#{application}"
13
+ else
14
+ application = File.basename(Dir.getwd)
15
+ custom_alarms_config_file = "alarms.yml"
16
+ output_path = "output"
17
+ end
18
+
19
+ validate_path = "cloudformation/monitoring/#{application}/validate"
20
+
21
+ # Load custom config files
22
+ if File.file?(custom_alarms_config_file)
23
+ custom_alarms_config = YAML.load(File.read(custom_alarms_config_file)) if File.file?(custom_alarms_config_file)
24
+ else
25
+ puts "Failed to load #{custom_alarms_config_file}"
26
+ exit 1
27
+ end
28
+
29
+ source_region = custom_alarms_config['source_region']
30
+ source_bucket = custom_alarms_config['source_bucket']
31
+
32
+ cfn = Aws::CloudFormation::Client.new(region: source_region)
33
+ s3 = Aws::S3::Client.new(region: source_region)
34
+ validated = 0
35
+ unvalidated = 0
36
+
37
+ puts "-----------------------------------------------"
38
+
39
+ ["#{output_path}/*.json"].each { |path|
40
+ Dir.glob(path) do |file|
41
+ template = File.open(file, 'rb')
42
+ filename = file.gsub("#{output_path}/", "")
43
+ begin
44
+ puts "INFO - Copying #{file} to s3://#{source_bucket}/#{validate_path}/#{filename}"
45
+ s3.put_object({
46
+ body: template,
47
+ bucket: "#{source_bucket}",
48
+ key: "#{validate_path}/#{filename}",
49
+ })
50
+ template_url = "https://#{source_bucket}.s3.amazonaws.com/#{validate_path}/#{filename}"
51
+ puts "INFO - Validating #{template_url}"
52
+ begin
53
+ resp = cfn.validate_template({ template_url: template_url })
54
+ puts "INFO - Template #{filename} validated successfully"
55
+ validated += 1
56
+ rescue => e
57
+ puts "ERROR - Template #{filename} failed to validate: #{e}"
58
+ unvalidated += 1
59
+ end
60
+ rescue => e
61
+ puts "ERROR - #{e.class}, #{e}"
62
+ exit 1
63
+ end
64
+ end
65
+ }
66
+
67
+ if unvalidated > 0
68
+ puts "ERROR - #{validated}/#{Dir["output/**/*.json"].count} templates validated successfully"
69
+ exit 1
70
+ else
71
+ puts "INFO - #{validated}/#{Dir["output/**/*.json"].count} templates validated successfully"
72
+ end
73
+ end
74
+
75
+ end
76
+ end
@@ -1,3 +1,3 @@
1
1
  module CfnMonitor
2
- VERSION = "0.1.1".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end
@@ -1,4 +1,16 @@
1
1
  templates:
2
+ EcsCICheck:
3
+ ECSContianerInstancesDisconnected:
4
+ AlarmActions: crit
5
+ Namespace: EcsCICheck
6
+ ComparisonOperator: GreaterThanThreshold
7
+ DimensionsName: Cluster
8
+ Statistic: Maximum
9
+ Threshold: 0
10
+ Period: 60
11
+ EvaluationPeriods: 2
12
+ MetricName: ECSContianerInstancesDisconnected
13
+ TreatMissingData: notBreaching
2
14
  HttpCheck:
3
15
  EndpointAvailable:
4
16
  AlarmActions: crit
@@ -66,6 +78,52 @@ templates:
66
78
  # MetricName: Failed_SSM_Agent
67
79
  # AlarmDescription: { 'Fn::Join': [ ' ', [ Ref: 'MonitoredStack', { 'Fn::Sub': ['${resource}', {'env': { Ref: 'EnvironmentName' } } ] }, '${alarmName}' ] ] }
68
80
  # TreatMissingData: notBreaching
81
+ Ssl:
82
+ Crit:
83
+ AlarmActions: crit
84
+ Namespace: SSL
85
+ ComparisonOperator: LessThanThreshold
86
+ Dimensions: [ { Name: 'URL', Value: '${resource}' } ]
87
+ Statistic: Maximum
88
+ Threshold: 30 # 30 days
89
+ Period: 60
90
+ EvaluationPeriods: 1
91
+ MetricName: ExpiresInDays
92
+ AlarmDescription: { 'Fn::Join': [ ' ', [ '${resource}', '${templateName}' ] ] }
93
+ Warn:
94
+ AlarmActions: warn
95
+ Namespace: SSL
96
+ ComparisonOperator: LessThanThreshold
97
+ Dimensions: [ { Name: 'URL', Value: '${resource}' } ]
98
+ Statistic: Maximum
99
+ Threshold: 60 # 60 days
100
+ Period: 60
101
+ EvaluationPeriods: 1
102
+ MetricName: ExpiresInDays
103
+ AlarmDescription: { 'Fn::Join': [ ' ', [ '${resource}', '${templateName}' ] ] }
104
+ Dns:
105
+ Crit:
106
+ AlarmActions: crit
107
+ Namespace: DNS
108
+ ComparisonOperator: LessThanThreshold
109
+ Dimensions: [ { Name: 'Domain', Value: '${resource}' } ]
110
+ Statistic: Maximum
111
+ Threshold: 30 # 30 days
112
+ Period: 60
113
+ EvaluationPeriods: 1
114
+ MetricName: ExpiresInDays
115
+ AlarmDescription: { 'Fn::Join': [ ' ', [ '${resource}', '${templateName}' ] ] }
116
+ Warn:
117
+ AlarmActions: warn
118
+ Namespace: DNS
119
+ ComparisonOperator: LessThanThreshold
120
+ Dimensions: [ { Name: 'Domain', Value: '${resource}' } ]
121
+ Statistic: Maximum
122
+ Threshold: 60 # 60 days
123
+ Period: 60
124
+ EvaluationPeriods: 1
125
+ MetricName: ExpiresInDays
126
+ AlarmDescription: { 'Fn::Join': [ ' ', [ '${resource}', '${templateName}' ] ] }
69
127
  Nrpe:
70
128
  Warn:
71
129
  AlarmActions: warn
@@ -399,14 +457,14 @@ templates:
399
457
  EvaluationPeriods: 5
400
458
  ApiGateway: #AWS:ApiGateway
401
459
  ApiEndpoint5xx:
402
- AlarmActions: crit
460
+ AlarmActions: warn
403
461
  Namespace: AWS/ApiGateway
404
462
  MetricName: 5XXError
405
463
  ComparisonOperator: GreaterThanThreshold
406
464
  DimensionsName: ApiName
407
465
  Statistic: Sum
408
466
  Threshold: 5
409
- EvaluationPdyneriods: 2
467
+ EvaluationPeriods: 2
410
468
  DynamoDBTable: #AWS::DynamoDB::Table
411
469
  DynamoDBReadUsage:
412
470
  AlarmActions: warn
@@ -425,4 +483,4 @@ templates:
425
483
  DimensionsName: TableName
426
484
  Statistic: Sum
427
485
  Threshold: 80
428
- EvaluationPeriods: 2
486
+ EvaluationPeriods: 2
@@ -53,7 +53,7 @@ CloudFormation do
53
53
  template = alarm[:template]
54
54
  name = alarm[:alarm]
55
55
  params = alarm[:parameters]
56
- cmd = params['cmd']
56
+ cmd = params['cmd'] || ''
57
57
 
58
58
  alarmHash = Digest::MD5.hexdigest "#{resourceGroup}#{template}#{name}#{cmd}"
59
59
 
@@ -104,7 +104,7 @@ CloudFormation do
104
104
  conditions = []
105
105
 
106
106
  # Configure resource parameters
107
- if type == 'resource'
107
+ if ['resource','ecsCluster'].include? type
108
108
  # Configure physical resource inputs
109
109
  dimensionsNames = params['DimensionsName'].split('/')
110
110
  dimensions = []
@@ -0,0 +1,61 @@
1
+ require 'cfndsl'
2
+
3
+ CloudFormation do
4
+ Description("CloudWatch Endpoints")
5
+
6
+ Parameter("MonitoredStack"){
7
+ Type 'String'
8
+ }
9
+ Parameter("EnvironmentName"){
10
+ Type 'String'
11
+ }
12
+ Parameter("DnsCheckFunctionArn"){
13
+ Type 'String'
14
+ }
15
+
16
+ alarms.each do |alarm|
17
+ if alarm[:type] == 'dn'
18
+ endpointHash = Digest::MD5.hexdigest "dns-" + alarm[:resource]
19
+
20
+ # Conditionally create shedule based on environments attribute
21
+ if alarm[:environments] != ['all']
22
+ conditions = []
23
+ alarm[:environments].each do | env |
24
+ conditions << FnEquals(Ref("EnvironmentName"),env)
25
+ end
26
+ if conditions.length > 1
27
+ Condition("Condition#{endpointHash}", FnOr(conditions))
28
+ else
29
+ Condition("Condition#{endpointHash}", conditions[0])
30
+ end
31
+ end
32
+
33
+ params = alarm[:parameters]
34
+
35
+ # Set defaults
36
+ params['scheduleExpression'] ||= "0 12 * * ? *" # 12PM every day
37
+
38
+ # Create payload
39
+ payload = {}
40
+ payload['Domain'] = alarm[:resource]
41
+ payload['Region'] = "${region}"
42
+
43
+ endpointHash = Digest::MD5.hexdigest alarm[:resource]
44
+ Resource("DnsCheckSchedule#{endpointHash}") do
45
+ Condition "Condition#{endpointHash}" if alarm[:environments] != ['all']
46
+ Type 'AWS::Events::Rule'
47
+ Property('Description', "#{payload['Domain']}")
48
+ Property('ScheduleExpression', "cron(#{params['scheduleExpression']})")
49
+ Property('State', 'ENABLED')
50
+ Property('Targets', [
51
+ {
52
+ Arn: Ref('DnsCheckFunctionArn'),
53
+ Id: endpointHash,
54
+ Input: FnSub(payload.to_json, region: Ref("AWS::Region"))
55
+ }
56
+ ])
57
+ end
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,76 @@
1
+ require 'cfndsl'
2
+
3
+ CloudFormation do
4
+ Description("CloudWatch ECS Container Instances")
5
+
6
+ Parameter("EcsCICheckFunctionArn"){
7
+ Type 'String'
8
+ }
9
+
10
+ Parameter("MonitoredStack"){
11
+ Type 'String'
12
+ }
13
+ Parameter("GetPhysicalIdFunctionArn"){
14
+ Type 'String'
15
+ }
16
+ Parameter("EnvironmentName"){
17
+ Type 'String'
18
+ }
19
+ Parameter("ConfigToggle"){
20
+ Type 'String'
21
+ }
22
+
23
+ alarms.each do |alarm|
24
+ if alarm[:type] == 'ecsCluster'
25
+
26
+ resourceHash = Digest::MD5.hexdigest alarm[:resource]
27
+
28
+ Resource("GetPhysicalId#{resourceHash}") do
29
+ Type 'Custom::GetResourcePhysicalId'
30
+ Property('ServiceToken', Ref('GetPhysicalIdFunctionArn'))
31
+ Property('StackName', Ref('MonitoredStack'))
32
+ if alarm[:resource].include? "::"
33
+ Property('LogicalResourceId', alarm[:resource].gsub('::','.') )
34
+ else
35
+ Property('LogicalResourceId', FnJoin( '.', [ Ref('MonitoredStack'), alarm[:resource] ] ))
36
+ end
37
+ Property('Region', Ref('AWS::Region'))
38
+ Property('ConfigToggle', Ref('ConfigToggle'))
39
+ end
40
+
41
+ # Conditionally create shedule based on environments attribute
42
+ if alarm[:environments] != ['all']
43
+ conditions = []
44
+ alarm[:environments].each do | env |
45
+ conditions << FnEquals(Ref("EnvironmentName"),env)
46
+ end
47
+ if conditions.length > 1
48
+ Condition("Condition#{resourceHash}", FnOr(conditions))
49
+ else
50
+ Condition("Condition#{resourceHash}", conditions[0])
51
+ end
52
+ end
53
+
54
+ # Set defaults
55
+ params = alarm[:parameters]
56
+ params['scheduleExpression'] ||= "0/5 * * * ? *"
57
+
58
+ Events_Rule("EcsCICheckSchedule#{resourceHash}") {
59
+ Condition "Condition#{resourceHash}" if alarm[:environments] != ['all']
60
+ Description FnJoin("",["ECS container instance check for the ", FnGetAtt("GetPhysicalId#{resourceHash}",'PhysicalResourceId') ," ECS cluster"])
61
+ ScheduleExpression "cron(#{params['scheduleExpression']})"
62
+ State params['enabled'] ? 'ENABLED' : 'DISABLED'
63
+ Targets([
64
+ {
65
+ Arn: Ref('EcsCICheckFunctionArn'),
66
+ Id: resourceHash,
67
+ Input: FnSub({CLUSTER: '${cluster}'}.to_json, cluster: FnGetAtt("GetPhysicalId#{resourceHash}",'PhysicalResourceId'))
68
+ }
69
+ ])
70
+ }
71
+
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -73,8 +73,8 @@ CloudFormation do
73
73
  Resource("NrpeCheckFunction#{hostHash}") do
74
74
  Condition "Condition#{hostHash}" if alarm[:environments] != ['all']
75
75
  Type 'AWS::Lambda::Function'
76
- Property('Code', { S3Bucket: FnJoin('.',['base2.lambda',Ref('AWS::Region')]), S3Key: 'nrpe.zip' })
77
- Property('Handler', 'nrpe')
76
+ Property('Code', { S3Bucket: FnJoin('.', ['base2.lambda', Ref('AWS::Region')]), S3Key: 'aws-lambda-nrpe-check/0.1/handler.zip' })
77
+ Property('Handler', 'main')
78
78
  Property('MemorySize', 128)
79
79
  Property('Runtime', 'go1.x')
80
80
  Property('Timeout', 300)
@@ -64,7 +64,41 @@ CloudFormation do
64
64
  ])
65
65
  end
66
66
 
67
- Resource("HttpLambdaExecutionRole") do
67
+ Resource("EcsCICheckLambdaExecutionRole") do
68
+ Type 'AWS::IAM::Role'
69
+ Property('AssumeRolePolicyDocument', {
70
+ Version: '2012-10-17',
71
+ Statement: [{
72
+ Effect: 'Allow',
73
+ Principal: { Service: [ 'lambda.amazonaws.com' ] },
74
+ Action: [ 'sts:AssumeRole' ]
75
+ }]
76
+ })
77
+ Property('Path','/')
78
+ Property('Policies', [
79
+ PolicyName: 'CloudFormationReadOnly',
80
+ PolicyDocument: {
81
+ Version: '2012-10-17',
82
+ Statement: [{
83
+ Effect: 'Allow',
84
+ Action: [ 'logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents' ],
85
+ Resource: 'arn:aws:logs:*:*:*'
86
+ },
87
+ {
88
+ Effect: 'Allow',
89
+ Action: [ 'ecs:ListContainerInstances', 'ecs:DescribeContainerInstances' ],
90
+ Resource: '*'
91
+ },
92
+ {
93
+ Effect: 'Allow',
94
+ Action: [ 'cloudwatch:PutMetricData' ],
95
+ Resource: '*'
96
+ }]
97
+ }
98
+ ])
99
+ end
100
+
101
+ Resource("LambdaExecutionRole") do
68
102
  Type 'AWS::IAM::Role'
69
103
  Property('AssumeRolePolicyDocument', {
70
104
  Version: '2012-10-17',
@@ -123,12 +157,12 @@ CloudFormation do
123
157
 
124
158
  Resource("HttpCheckFunction") do
125
159
  Type 'AWS::Lambda::Function'
126
- Property('Code', { S3Bucket: FnJoin('.',[Ref('AWS::Region'),'aws-lambda-http-check']), S3Key: 'httpCheck-v2.zip' })
127
- Property('Handler', 'handler.http_check')
160
+ Property('Code', { S3Bucket: FnJoin('.', ['base2.lambda', Ref('AWS::Region')]), S3Key: 'aws-lambda-http-check/0.1/handler.zip' })
161
+ Property('Handler', 'handler.main')
128
162
  Property('MemorySize', 128)
129
163
  Property('Runtime', 'python3.6')
130
164
  Property('Timeout', 300)
131
- Property('Role', FnGetAtt('HttpLambdaExecutionRole','Arn'))
165
+ Property('Role', FnGetAtt('LambdaExecutionRole','Arn'))
132
166
  end
133
167
 
134
168
  Resource("HttpCheckPermissions") do
@@ -138,6 +172,57 @@ CloudFormation do
138
172
  Property('Principal', 'events.amazonaws.com')
139
173
  end
140
174
 
175
+ Resource("SslCheckFunction") do
176
+ Type 'AWS::Lambda::Function'
177
+ Property('Code', { S3Bucket: FnJoin('.', ['base2.lambda', Ref('AWS::Region')]), S3Key: 'aws-lambda-ssl-check/0.1/handler.zip' })
178
+ Property('Handler', 'main')
179
+ Property('MemorySize', 128)
180
+ Property('Runtime', 'go1.x')
181
+ Property('Timeout', 300)
182
+ Property('Role', FnGetAtt('LambdaExecutionRole','Arn'))
183
+ end
184
+
185
+ Resource("SslCheckPermissions") do
186
+ Type 'AWS::Lambda::Permission'
187
+ Property('FunctionName', Ref('SslCheckFunction'))
188
+ Property('Action', 'lambda:InvokeFunction')
189
+ Property('Principal', 'events.amazonaws.com')
190
+ end
191
+
192
+ Resource("DnsCheckFunction") do
193
+ Type 'AWS::Lambda::Function'
194
+ Property('Code', { S3Bucket: FnJoin('.', ['base2.lambda', Ref('AWS::Region')]), S3Key: 'aws-lambda-dns-check/0.1/handler.zip' })
195
+ Property('Handler', 'main')
196
+ Property('MemorySize', 128)
197
+ Property('Runtime', 'go1.x')
198
+ Property('Timeout', 300)
199
+ Property('Role', FnGetAtt('LambdaExecutionRole','Arn'))
200
+ end
201
+
202
+ Resource("DnsCheckPermissions") do
203
+ Type 'AWS::Lambda::Permission'
204
+ Property('FunctionName', Ref('DnsCheckFunction'))
205
+ Property('Action', 'lambda:InvokeFunction')
206
+ Property('Principal', 'events.amazonaws.com')
207
+ end
208
+
209
+ Resource("EcsCICheckFunction") do
210
+ Type 'AWS::Lambda::Function'
211
+ Property('Code', { S3Bucket: FnJoin('.', ['base2.lambda', Ref('AWS::Region')]), S3Key: 'aws-lambda-ecs-container-instance-check/0.1/handler.zip' })
212
+ Property('Handler', 'handler.run_check')
213
+ Property('MemorySize', 128)
214
+ Property('Runtime', 'python3.6')
215
+ Property('Timeout', 300)
216
+ Property('Role', FnGetAtt('EcsCICheckLambdaExecutionRole','Arn'))
217
+ end
218
+
219
+ Resource("EcsCICheckPermissions") do
220
+ Type 'AWS::Lambda::Permission'
221
+ Property('FunctionName', Ref('EcsCICheckFunction'))
222
+ Property('Action', 'lambda:InvokeFunction')
223
+ Property('Principal', 'events.amazonaws.com')
224
+ end
225
+
141
226
  params = {
142
227
  MonitoredStack: Ref('MonitoredStack'),
143
228
  SnsTopicCrit: Ref('SnsTopicCrit'),
@@ -175,6 +260,38 @@ CloudFormation do
175
260
  end
176
261
  end
177
262
 
263
+ sslParams = {
264
+ MonitoredStack: Ref('MonitoredStack'),
265
+ SslCheckFunctionArn: FnGetAtt('SslCheckFunction','Arn'),
266
+ EnvironmentName: FnGetAtt('GetEnvironmentName', 'EnvironmentName' )
267
+ }
268
+
269
+ ssl ||= {}
270
+ if !ssl.empty?
271
+ Resource("SslStack") do
272
+ Type 'AWS::CloudFormation::Stack'
273
+ Property('TemplateURL', "https://#{source_bucket}.s3.amazonaws.com/#{upload_path}/ssl.json")
274
+ Property('TimeoutInMinutes', 5)
275
+ Property('Parameters', sslParams)
276
+ end
277
+ end
278
+
279
+ dnsParams = {
280
+ MonitoredStack: Ref('MonitoredStack'),
281
+ DnsCheckFunctionArn: FnGetAtt('DnsCheckFunction','Arn'),
282
+ EnvironmentName: FnGetAtt('GetEnvironmentName', 'EnvironmentName' )
283
+ }
284
+
285
+ dns ||= {}
286
+ if !dns.empty?
287
+ Resource("DnsStack") do
288
+ Type 'AWS::CloudFormation::Stack'
289
+ Property('TemplateURL', "https://#{source_bucket}.s3.amazonaws.com/#{upload_path}/dns.json")
290
+ Property('TimeoutInMinutes', 5)
291
+ Property('Parameters', dnsParams)
292
+ end
293
+ end
294
+
178
295
  hostParams = {
179
296
  EnvironmentName: FnGetAtt('GetEnvironmentName', 'EnvironmentName' )
180
297
  }
@@ -203,6 +320,24 @@ CloudFormation do
203
320
  end
204
321
  end
205
322
 
323
+ ecsClusterParams = {
324
+ MonitoredStack: Ref('MonitoredStack'),
325
+ GetPhysicalIdFunctionArn: FnGetAtt('GetPhysicalIdFunction','Arn'),
326
+ EnvironmentName: FnGetAtt('GetEnvironmentName', 'EnvironmentName' ),
327
+ ConfigToggle: Ref('ConfigToggle'),
328
+ EcsCICheckFunctionArn: FnGetAtt('EcsCICheckFunction','Arn')
329
+ }
330
+
331
+ ecsClusters ||= {}
332
+ if !ecsClusters.empty?
333
+ Resource("ServicesStack") do
334
+ Type 'AWS::CloudFormation::Stack'
335
+ Property('TemplateURL', "https://#{source_bucket}.s3.amazonaws.com/#{upload_path}/ecsClusters.json")
336
+ Property('TimeoutInMinutes', 5)
337
+ Property('Parameters', ecsClusterParams)
338
+ end
339
+ end
340
+
206
341
  Output("CfnMonitorVersion") { Value(CfnMonitor::VERSION) }
207
342
  Output("RenderDate") { Value(Time.now.strftime("%Y-%m-%d")) }
208
343
  Output("MonitoredStack") { Value(Ref("MonitoredStack")) }
@@ -46,13 +46,17 @@ CloudFormation do
46
46
  Type 'Custom::GetResourcePhysicalId'
47
47
  Property('ServiceToken', Ref('GetPhysicalIdFunctionArn'))
48
48
  Property('StackName', Ref('MonitoredStack'))
49
- Property('LogicalResourceId', FnJoin( '.', [ Ref('MonitoredStack'), resource ] ))
49
+ if resource.include? "::"
50
+ Property('LogicalResourceId', resource.gsub('::','.') )
51
+ else
52
+ Property('LogicalResourceId', FnJoin( '.', [ Ref('MonitoredStack'), resource ] ))
53
+ end
50
54
  Property('Region', Ref('AWS::Region'))
51
55
  Property('ConfigToggle', Ref('ConfigToggle'))
52
56
  end
53
57
  customResources << "GetPhysicalId#{resourceHash}"
54
58
  # Create outputs for user reference
55
- Output("#{resource.delete('.')}") { Value(FnGetAtt("GetPhysicalId#{resourceHash}",'PhysicalResourceId')) }
59
+ Output("#{resource.delete('.').delete('-').delete('::')}") { Value(FnGetAtt("GetPhysicalId#{resourceHash}",'PhysicalResourceId')) }
56
60
  end
57
61
  end
58
62
  end
@@ -0,0 +1,61 @@
1
+ require 'cfndsl'
2
+
3
+ CloudFormation do
4
+ Description("CloudWatch Endpoints")
5
+
6
+ Parameter("MonitoredStack"){
7
+ Type 'String'
8
+ }
9
+ Parameter("EnvironmentName"){
10
+ Type 'String'
11
+ }
12
+ Parameter("SslCheckFunctionArn"){
13
+ Type 'String'
14
+ }
15
+
16
+ alarms.each do |alarm|
17
+ if alarm[:type] == 'ss'
18
+ endpointHash = Digest::MD5.hexdigest "ssl-" + alarm[:resource]
19
+
20
+ # Conditionally create shedule based on environments attribute
21
+ if alarm[:environments] != ['all']
22
+ conditions = []
23
+ alarm[:environments].each do | env |
24
+ conditions << FnEquals(Ref("EnvironmentName"),env)
25
+ end
26
+ if conditions.length > 1
27
+ Condition("Condition#{endpointHash}", FnOr(conditions))
28
+ else
29
+ Condition("Condition#{endpointHash}", conditions[0])
30
+ end
31
+ end
32
+
33
+ params = alarm[:parameters]
34
+
35
+ # Set defaults
36
+ params['scheduleExpression'] ||= "0 12 * * ? *" # 12PM every day
37
+
38
+ # Create payload
39
+ payload = {}
40
+ payload['Url'] = alarm[:resource]
41
+ payload['Region'] = "${region}"
42
+
43
+ endpointHash = Digest::MD5.hexdigest alarm[:resource]
44
+ Resource("SslCheckSchedule#{endpointHash}") do
45
+ Condition "Condition#{endpointHash}" if alarm[:environments] != ['all']
46
+ Type 'AWS::Events::Rule'
47
+ Property('Description', "#{payload['Url']}")
48
+ Property('ScheduleExpression', "cron(#{params['scheduleExpression']})")
49
+ Property('State', 'ENABLED')
50
+ Property('Targets', [
51
+ {
52
+ Arn: Ref('SslCheckFunctionArn'),
53
+ Id: endpointHash,
54
+ Input: FnSub(payload.to_json, region: Ref("AWS::Region"))
55
+ }
56
+ ])
57
+ end
58
+ end
59
+ end
60
+
61
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfn_monitor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Base2Services
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2018-10-25 00:00:00.000000000 Z
13
+ date: 2019-01-21 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: thor
@@ -211,6 +211,7 @@ files:
211
211
  - lib/cfn_monitor/generate.rb
212
212
  - lib/cfn_monitor/query.rb
213
213
  - lib/cfn_monitor/utils.rb
214
+ - lib/cfn_monitor/validate.rb
214
215
  - lib/cfn_monitor/version.rb
215
216
  - lib/config/config.yml
216
217
  - lib/config/templates.yml
@@ -218,11 +219,14 @@ files:
218
219
  - lib/lambda/getEnvironmentName.py
219
220
  - lib/lambda/getPhysicalId.py
220
221
  - lib/templates/alarms.rb
222
+ - lib/templates/dns.rb
223
+ - lib/templates/ecsClusters.rb
221
224
  - lib/templates/endpoints.rb
222
225
  - lib/templates/hosts.rb
223
226
  - lib/templates/master.rb
224
227
  - lib/templates/resources.rb
225
228
  - lib/templates/services.rb
229
+ - lib/templates/ssl.rb
226
230
  homepage: https://github.com/base2Services/cfn-monitor/blob/master/README.md
227
231
  licenses:
228
232
  - MIT
@@ -242,8 +246,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
246
  - !ruby/object:Gem::Version
243
247
  version: '0'
244
248
  requirements: []
245
- rubyforge_project:
246
- rubygems_version: 2.7.7
249
+ rubygems_version: 3.0.2
247
250
  signing_key:
248
251
  specification_version: 4
249
252
  summary: Configure and generate a cloudwatch monitoring cloudformation stack