cfn_monitor 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.travis.yml +18 -0
- data/Dockerfile +7 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +424 -0
- data/Rakefile +9 -0
- data/cfn_monitor.gemspec +39 -0
- data/exe/cfn_monitor +4 -0
- data/lib/cfn_monitor/deploy.rb +47 -0
- data/lib/cfn_monitor/generate.rb +186 -0
- data/lib/cfn_monitor/query.rb +135 -0
- data/lib/cfn_monitor/utils.rb +42 -0
- data/lib/cfn_monitor/version.rb +3 -0
- data/lib/cfn_monitor.rb +66 -0
- data/lib/config/config.yml +14 -0
- data/lib/config/templates.yml +428 -0
- data/lib/ext/alarms.rb +33 -0
- data/lib/lambda/getEnvironmentName.py +39 -0
- data/lib/lambda/getPhysicalId.py +67 -0
- data/lib/templates/alarms.rb +178 -0
- data/lib/templates/endpoints.rb +66 -0
- data/lib/templates/hosts.rb +126 -0
- data/lib/templates/master.rb +213 -0
- data/lib/templates/resources.rb +82 -0
- data/lib/templates/services.rb +138 -0
- metadata +250 -0
@@ -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
|
data/lib/cfn_monitor.rb
ADDED
@@ -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
|