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