enscalator 0.4.0.pre.alpha.pre.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +59 -0
- data/.travis.yml +22 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/Rakefile +43 -0
- data/bin/console +11 -0
- data/bin/setup +7 -0
- data/enscalator.gemspec +57 -0
- data/exe/enscalator +13 -0
- data/lib/enscalator/core/cf_parameters.rb +146 -0
- data/lib/enscalator/core/cf_resources.rb +225 -0
- data/lib/enscalator/core/instance_type.rb +205 -0
- data/lib/enscalator/core/network_config.rb +21 -0
- data/lib/enscalator/core.rb +10 -0
- data/lib/enscalator/enapp.rb +248 -0
- data/lib/enscalator/helpers/dns.rb +62 -0
- data/lib/enscalator/helpers/stack.rb +107 -0
- data/lib/enscalator/helpers/sub_process.rb +72 -0
- data/lib/enscalator/helpers/wrappers.rb +55 -0
- data/lib/enscalator/helpers.rb +127 -0
- data/lib/enscalator/plugins/amazon_linux.rb +93 -0
- data/lib/enscalator/plugins/auto_scale.rb +80 -0
- data/lib/enscalator/plugins/core_os.rb +88 -0
- data/lib/enscalator/plugins/couchbase.rb +98 -0
- data/lib/enscalator/plugins/debian.rb +71 -0
- data/lib/enscalator/plugins/elastic_beanstalk.rb +74 -0
- data/lib/enscalator/plugins/elasticache.rb +168 -0
- data/lib/enscalator/plugins/elasticsearch_amazon.rb +75 -0
- data/lib/enscalator/plugins/elasticsearch_bitnami.rb +198 -0
- data/lib/enscalator/plugins/elasticsearch_opsworks.rb +225 -0
- data/lib/enscalator/plugins/elb.rb +139 -0
- data/lib/enscalator/plugins/nat_gateway.rb +71 -0
- data/lib/enscalator/plugins/rds.rb +141 -0
- data/lib/enscalator/plugins/redis.rb +38 -0
- data/lib/enscalator/plugins/rethink_db.rb +21 -0
- data/lib/enscalator/plugins/route53.rb +143 -0
- data/lib/enscalator/plugins/ubuntu.rb +85 -0
- data/lib/enscalator/plugins/user-data/elasticsearch +367 -0
- data/lib/enscalator/plugins/vpc_peering_connection.rb +48 -0
- data/lib/enscalator/plugins.rb +30 -0
- data/lib/enscalator/rich_template_dsl.rb +209 -0
- data/lib/enscalator/templates/vpc_peering.rb +112 -0
- data/lib/enscalator/templates.rb +20 -0
- data/lib/enscalator/version.rb +5 -0
- data/lib/enscalator/vpc.rb +11 -0
- data/lib/enscalator/vpc_with_nat_gateway.rb +311 -0
- data/lib/enscalator/vpc_with_nat_instance.rb +402 -0
- data/lib/enscalator.rb +103 -0
- metadata +427 -0
@@ -0,0 +1,248 @@
|
|
1
|
+
require 'cloudformation-ruby-dsl/cfntemplate'
|
2
|
+
require_relative 'rich_template_dsl'
|
3
|
+
|
4
|
+
module Enscalator
|
5
|
+
# Template DSL for common enJapan application stack
|
6
|
+
class EnAppTemplateDSL < RichTemplateDSL
|
7
|
+
include Enscalator::Helpers
|
8
|
+
|
9
|
+
attr_reader :app_name
|
10
|
+
|
11
|
+
Struct.new('Subnet', :availability_zone, :suffix, :cidr_block)
|
12
|
+
|
13
|
+
# Subnet size (256 addresses)
|
14
|
+
SUBNET_CIDR_BLOCK_SIZE = 24
|
15
|
+
|
16
|
+
# Create new EnAppTemplateDSL instance
|
17
|
+
#
|
18
|
+
# @param [Hash] options command-line arguments
|
19
|
+
def initialize(options = {})
|
20
|
+
# application name taken from template name by default
|
21
|
+
@app_name = self.class.name.demodulize
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get vpc stack
|
26
|
+
#
|
27
|
+
# @return [Aws::CloudFormation::Stack] stack instance of vpc stack
|
28
|
+
def vpc_stack
|
29
|
+
@vpc_stack ||= cfn_resource(cfn_client(region)).stack(vpc_stack_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get current stack
|
33
|
+
#
|
34
|
+
# @return [Aws::CloudFormation::Stack] current stack
|
35
|
+
def current_stack
|
36
|
+
@current_stack ||= (cfn_resource(cfn_client(region)).stack(stack_name) rescue nil) unless creating?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get vpc
|
40
|
+
#
|
41
|
+
# @return [Aws::EC2::Vpc] vpc instance
|
42
|
+
def vpc
|
43
|
+
@vpc ||= Aws::EC2::Vpc.new(id: get_resource(vpc_stack, 'VPC'), region: region)
|
44
|
+
end
|
45
|
+
|
46
|
+
# References to application subnets in all availability zones
|
47
|
+
def ref_application_subnets
|
48
|
+
availability_zones.map { |suffix, _| ref("ApplicationSubnet#{suffix.upcase}") }
|
49
|
+
end
|
50
|
+
|
51
|
+
# References to resource subnets in all availability zones
|
52
|
+
def ref_resource_subnets
|
53
|
+
availability_zones.map { |suffix, _| ref("ResourceSubnet#{suffix.upcase}") }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public subnets in all availability zones
|
57
|
+
def public_subnets
|
58
|
+
availability_zones.map { |suffix, _| get_resource(vpc_stack, "PublicSubnet#{suffix.upcase}") }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get VPC ID as reference to parameter
|
62
|
+
# @return [Hash]
|
63
|
+
def ref_vpc_id
|
64
|
+
ref('VpcId')
|
65
|
+
end
|
66
|
+
|
67
|
+
# Reference to private security group
|
68
|
+
# @return [Hash]
|
69
|
+
def ref_private_security_group
|
70
|
+
ref('PrivateSecurityGroup')
|
71
|
+
end
|
72
|
+
|
73
|
+
# Reference to resource security group
|
74
|
+
# @return [Hash]
|
75
|
+
def ref_resource_security_group
|
76
|
+
ref('ResourceSecurityGroup')
|
77
|
+
end
|
78
|
+
|
79
|
+
# Reference to application security group
|
80
|
+
# @return [Hash]
|
81
|
+
def ref_application_security_group
|
82
|
+
ref('ApplicationSecurityGroup')
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get all CIRD blocks for current VPC
|
86
|
+
# @return [Hash]
|
87
|
+
def get_all_cidr_blocks
|
88
|
+
IPAddress(
|
89
|
+
Core::NetworkConfig.mapping_vpc_net[region.to_sym][:VPC]).subnet(SUBNET_CIDR_BLOCK_SIZE).map(&:to_string)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get currently used CIDR blocks
|
93
|
+
# @return [Array]
|
94
|
+
def get_used_cidr_blocks
|
95
|
+
vpc.subnets.collect(&:cidr_block)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Get non-used CIDR blocks
|
99
|
+
# @return [Array]
|
100
|
+
def get_available_cidr_blocks
|
101
|
+
get_all_cidr_blocks - get_used_cidr_blocks
|
102
|
+
end
|
103
|
+
|
104
|
+
# Get application CIDR blocks availability zones mapping
|
105
|
+
# @return [Hash]
|
106
|
+
def get_application_to_az_mapping
|
107
|
+
cidr_blocks = get_available_cidr_blocks.dup
|
108
|
+
availability_zones.map do |suffix, az|
|
109
|
+
cidr_block = (begin
|
110
|
+
subnet_id = get_resource(current_stack, "ApplicationSubnet#{suffix.upcase}")
|
111
|
+
Aws::EC2::Subnet.new(id: subnet_id, region: region).cidr_block
|
112
|
+
end rescue nil) if current_stack
|
113
|
+
|
114
|
+
Struct::Subnet.new(az, suffix, cidr_block || cidr_blocks.shift)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# CIDR blocks allocated for application subnets
|
119
|
+
# @return [Array]
|
120
|
+
def get_application_cidr_blocks
|
121
|
+
get_application_to_az_mapping.map(&:cidr_block)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Get resource CIDR blocks availability zones mapping
|
125
|
+
# @return [Array]
|
126
|
+
def get_resource_to_az_mapping
|
127
|
+
cidr_blocks = (get_available_cidr_blocks - get_application_cidr_blocks).dup
|
128
|
+
availability_zones.map do |suffix, az|
|
129
|
+
cidr_block = (begin
|
130
|
+
subnet_id = get_resource(current_stack, "ResourceSubnet#{suffix.upcase}")
|
131
|
+
Aws::EC2::Subnet.new(id: subnet_id, region: region).cidr_block
|
132
|
+
end rescue nil) if current_stack
|
133
|
+
|
134
|
+
Struct::Subnet.new(az, suffix, cidr_block || cidr_blocks.shift)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# CIDR blocks allocated for resource subnets
|
139
|
+
# @return [Array]
|
140
|
+
def get_resource_cidr_blocks
|
141
|
+
get_resource_to_az_mapping.map(&:cidr_block)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Query and pre-configure VPC parameters required for the stack
|
145
|
+
def load_vpc_params
|
146
|
+
parameter 'VpcId',
|
147
|
+
Description: 'The Id of the VPC',
|
148
|
+
Default: vpc.id,
|
149
|
+
Type: 'String',
|
150
|
+
AllowedPattern: 'vpc-[a-zA-Z0-9]*',
|
151
|
+
ConstraintDescription: 'must begin with vpc- followed by numbers and alphanumeric characters.'
|
152
|
+
|
153
|
+
parameter 'PrivateSecurityGroup',
|
154
|
+
Description: 'Security group identifier of private instances',
|
155
|
+
Default: get_resource(vpc_stack, 'PrivateSecurityGroup'),
|
156
|
+
Type: 'String',
|
157
|
+
AllowedPattern: 'sg-[a-zA-Z0-9]*',
|
158
|
+
ConstraintDescription: 'must begin with sg- followed by numbers and alphanumeric characters.'
|
159
|
+
|
160
|
+
# allocate application/resource cidr blocks dynamically for all availability zones
|
161
|
+
availability_zones.zip(get_application_cidr_blocks,
|
162
|
+
get_resource_cidr_blocks).each do |pair, application_cidr_block, resource_cidr_block|
|
163
|
+
suffix, availability_zone = pair
|
164
|
+
|
165
|
+
private_route_table_name = "PrivateRouteTable#{suffix.upcase}"
|
166
|
+
parameter private_route_table_name,
|
167
|
+
Description: "Route table identifier for private instances of zone #{suffix}",
|
168
|
+
Default: get_resource(vpc_stack, private_route_table_name),
|
169
|
+
Type: 'String',
|
170
|
+
AllowedPattern: 'rtb-[a-zA-Z0-9]*',
|
171
|
+
ConstraintDescription: 'must begin with rtb- followed by numbers and alphanumeric characters.'
|
172
|
+
|
173
|
+
application_subnet_name = "ApplicationSubnet#{suffix.upcase}"
|
174
|
+
subnet application_subnet_name,
|
175
|
+
vpc.id,
|
176
|
+
application_cidr_block,
|
177
|
+
availability_zone: availability_zone,
|
178
|
+
tags: {
|
179
|
+
Network: 'Private',
|
180
|
+
Application: aws_stack_name,
|
181
|
+
immutable_metadata: join('', '{ "purpose": "', aws_stack_name, '-app" }')
|
182
|
+
}
|
183
|
+
|
184
|
+
resource_subnet_name = "ResourceSubnet#{suffix.upcase}"
|
185
|
+
subnet resource_subnet_name,
|
186
|
+
vpc.id,
|
187
|
+
resource_cidr_block,
|
188
|
+
availability_zone: availability_zone,
|
189
|
+
tags: {
|
190
|
+
Network: 'Private',
|
191
|
+
Application: aws_stack_name
|
192
|
+
}
|
193
|
+
|
194
|
+
resource "ApplicationRouteTableAssociation#{suffix.upcase}",
|
195
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
196
|
+
Properties: {
|
197
|
+
RouteTableId: ref(private_route_table_name),
|
198
|
+
SubnetId: ref(application_subnet_name)
|
199
|
+
}
|
200
|
+
|
201
|
+
resource "ResourceRouteTableAssociation#{suffix.upcase}",
|
202
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
203
|
+
Properties: {
|
204
|
+
RouteTableId: ref(private_route_table_name),
|
205
|
+
SubnetId: ref(resource_subnet_name)
|
206
|
+
}
|
207
|
+
end
|
208
|
+
|
209
|
+
security_group_vpc 'ResourceSecurityGroup',
|
210
|
+
'Enable internal access with ssh',
|
211
|
+
vpc.id,
|
212
|
+
security_group_ingress: [
|
213
|
+
{
|
214
|
+
IpProtocol: 'tcp',
|
215
|
+
FromPort: '22',
|
216
|
+
ToPort: '22',
|
217
|
+
CidrIp: '10.0.0.0/8'
|
218
|
+
},
|
219
|
+
{
|
220
|
+
IpProtocol: 'tcp',
|
221
|
+
FromPort: '0',
|
222
|
+
ToPort: '65535',
|
223
|
+
SourceSecurityGroupId: ref_application_security_group
|
224
|
+
}
|
225
|
+
],
|
226
|
+
tags: {
|
227
|
+
Name: join('-', aws_stack_name, 'res', 'sg'),
|
228
|
+
Application: aws_stack_name
|
229
|
+
}
|
230
|
+
|
231
|
+
security_group_vpc 'ApplicationSecurityGroup',
|
232
|
+
'Security group of the application servers',
|
233
|
+
vpc.id,
|
234
|
+
security_group_ingress: [
|
235
|
+
{
|
236
|
+
IpProtocol: 'tcp',
|
237
|
+
FromPort: '0',
|
238
|
+
ToPort: '65535',
|
239
|
+
CidrIp: '10.0.0.0/8'
|
240
|
+
}
|
241
|
+
],
|
242
|
+
tags: {
|
243
|
+
Name: join('-', aws_stack_name, 'app', 'sg'),
|
244
|
+
Application: aws_stack_name
|
245
|
+
}
|
246
|
+
end
|
247
|
+
end # class EnAppTemplateDSL
|
248
|
+
end # module Enscalator
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Enscalator
|
2
|
+
module Helpers
|
3
|
+
module Dns
|
4
|
+
# Get existing DNS records
|
5
|
+
#
|
6
|
+
# @param [String] zone_name name of the hosted zone
|
7
|
+
def get_dns_records(zone_name: nil)
|
8
|
+
client = route53_client(nil)
|
9
|
+
zone = client.list_hosted_zones[:hosted_zones].find { |x| x.name == zone_name }
|
10
|
+
records = client.list_resource_record_sets(hosted_zone_id: zone.id)
|
11
|
+
records.values.flatten.map do |x|
|
12
|
+
{
|
13
|
+
name: x.name,
|
14
|
+
type: x.type,
|
15
|
+
records: x.resource_records.map(&:value)
|
16
|
+
} if x.is_a?(Aws::Structure)
|
17
|
+
end.compact
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create DNS record in given hosted zone
|
21
|
+
#
|
22
|
+
# @param [String] region aws valid region identifier
|
23
|
+
# @param [String] zone_name name of the hosted zone
|
24
|
+
# @param [String] record_name name of the dns record
|
25
|
+
# @param [String] type record type (NS, MX, CNAME and etc.)
|
26
|
+
# @param [Array] values list of record values
|
27
|
+
# @param [Integer] ttl time to live
|
28
|
+
# @param [String] suffix additional identifier following region
|
29
|
+
def upsert_dns_record(region: nil,
|
30
|
+
zone_name: nil,
|
31
|
+
record_name: nil,
|
32
|
+
type: 'A',
|
33
|
+
values: [],
|
34
|
+
ttl: 300,
|
35
|
+
suffix: '')
|
36
|
+
client = route53_client(region: region)
|
37
|
+
zone = client.list_hosted_zones[:hosted_zones].find { |x| x.name == zone_name }
|
38
|
+
record_tokens = [record_name.gsub(zone_name, ''), region]
|
39
|
+
record_tokens << suffix if suffix && !suffix.empty?
|
40
|
+
record_name = [record_tokens.join, zone_name].join('.')
|
41
|
+
|
42
|
+
client.change_resource_record_sets(
|
43
|
+
hosted_zone_id: zone.id,
|
44
|
+
change_batch: {
|
45
|
+
comment: "dns record for #{record_name}",
|
46
|
+
changes: [
|
47
|
+
{
|
48
|
+
action: 'UPSERT',
|
49
|
+
resource_record_set: {
|
50
|
+
name: record_name,
|
51
|
+
type: type,
|
52
|
+
resource_records: values.map { |x| { value: x } },
|
53
|
+
ttl: ttl
|
54
|
+
}
|
55
|
+
}
|
56
|
+
]
|
57
|
+
}
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'ruby-progressbar'
|
2
|
+
|
3
|
+
module Enscalator
|
4
|
+
module Helpers
|
5
|
+
# Helpers for operations requiring stack instance or stack_name
|
6
|
+
module Stack
|
7
|
+
# Wait until stack gets created
|
8
|
+
#
|
9
|
+
# @param [Aws::CloudFormation::Resource] cfn accessor for cloudformation resource
|
10
|
+
# @param [String] stack_name name of the stack
|
11
|
+
# @return [Aws::CloudFormation::Stack]
|
12
|
+
def wait_stack(cfn, stack_name)
|
13
|
+
stack = cfn.stack(stack_name)
|
14
|
+
title = 'Waiting for stack to be created'
|
15
|
+
progress = ProgressBar.create(title: title, starting_at: 10, total: nil)
|
16
|
+
loop do
|
17
|
+
break unless stack.stack_status =~ /(CREATE|UPDATE)_IN_PROGRESS$/
|
18
|
+
progress.title = title + " [#{stack.stack_status}]"
|
19
|
+
progress.increment
|
20
|
+
sleep 5
|
21
|
+
stack = cfn.stack(stack_name)
|
22
|
+
end
|
23
|
+
stack
|
24
|
+
end
|
25
|
+
|
26
|
+
# Create stack using cloudformation interface
|
27
|
+
#
|
28
|
+
# @param [String] region AWS region identifier
|
29
|
+
# @param [String] dependent_stack_name name of the stack current stack depends on
|
30
|
+
# @param [String] template name
|
31
|
+
# @param [String] stack_name stack name
|
32
|
+
# @param [Array] keys keys
|
33
|
+
# @param [Array] extra_parameters additional parameters
|
34
|
+
# @return [Aws::CloudFormation::Resource]
|
35
|
+
# @deprecated this method is no longer used
|
36
|
+
def cfn_create_stack(region, dependent_stack_name, template, stack_name, keys: [], extra_parameters: [])
|
37
|
+
cfn = cfn_resource(cfn_client(region))
|
38
|
+
stack = wait_stack(cfn, dependent_stack_name)
|
39
|
+
extra_parameters_cleaned = extra_parameters.map do |x|
|
40
|
+
if x.key? 'ParameterKey'
|
41
|
+
{
|
42
|
+
parameter_key: x['ParameterKey'],
|
43
|
+
parameter_value: x['ParameterValue']
|
44
|
+
}
|
45
|
+
else
|
46
|
+
x
|
47
|
+
end
|
48
|
+
end
|
49
|
+
options = {
|
50
|
+
stack_name: stack_name,
|
51
|
+
template_body: template,
|
52
|
+
parameters: generate_parameters(stack, keys) + extra_parameters_cleaned
|
53
|
+
}
|
54
|
+
cfn.create_stack(options)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get resource for given key from given stack
|
58
|
+
#
|
59
|
+
# @param [Aws::CloudFormation::Stack] stack cloudformation stack instance
|
60
|
+
# @param [String] key resource identifier (key)
|
61
|
+
# @return [String] AWS resource identifier
|
62
|
+
# @raise [ArgumentError] when stack is nil
|
63
|
+
# @raise [ArgumentError] when key is nil or empty
|
64
|
+
def get_resource(stack, key)
|
65
|
+
fail ArgumentError, 'stack must not be nil' if stack.nil?
|
66
|
+
fail ArgumentError, 'key must not be nil nor empty' if key.nil? || key.empty?
|
67
|
+
# query with physical_resource_id
|
68
|
+
resource = begin
|
69
|
+
stack.resource(key).physical_resource_id
|
70
|
+
rescue RuntimeError
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
if resource.nil?
|
74
|
+
# fallback to values from stack.outputs
|
75
|
+
output = stack.outputs.select { |s| s.output_key == key }
|
76
|
+
resource = begin
|
77
|
+
output.first.output_value
|
78
|
+
rescue RuntimeError
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
resource
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get list of resources for given keys
|
86
|
+
#
|
87
|
+
# @param [Aws::CloudFormation::Stack] stack cloudformation stack instance
|
88
|
+
# @param [Array] keys list of resource identifiers (keys)
|
89
|
+
# @return [String] list of AWS resource identifiers
|
90
|
+
# @raise [ArgumentError] when stack is nil
|
91
|
+
# @raise [ArgumentError] when keys are nil or empty list
|
92
|
+
def get_resources(stack, keys)
|
93
|
+
fail ArgumentError, 'stack must not be nil' if stack.nil?
|
94
|
+
fail ArgumentError, 'key must not be nil nor empty' if keys.nil? || keys.empty?
|
95
|
+
keys.map { |k| get_resource(stack, k) }.compact
|
96
|
+
end
|
97
|
+
|
98
|
+
# Generate parameters list
|
99
|
+
#
|
100
|
+
# @param [Aws::CloudFormation::Stack] stack cloudformation stack instance
|
101
|
+
# @param [Array] keys list of keys
|
102
|
+
def generate_parameters(stack, keys)
|
103
|
+
keys.map { |k| { parameter_key: k, parameter_value: get_resource(stack, k) } }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Enscalator
|
4
|
+
module Helpers
|
5
|
+
# Executed command as sub-processes with stdout and stderr streams
|
6
|
+
# taken from: https://nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html
|
7
|
+
class SubProcess
|
8
|
+
# Create new subprocess and execute command there
|
9
|
+
#
|
10
|
+
# @param [String] cmd command to be executed
|
11
|
+
def initialize(cmd)
|
12
|
+
# standard input is not used
|
13
|
+
Open3.popen3(cmd) do |_stdin, stdout, stderr, thread|
|
14
|
+
{ out: stdout, err: stderr }.each do |key, stream|
|
15
|
+
Thread.new do
|
16
|
+
until (line = stream.gets).nil?
|
17
|
+
# yield the block depending on the stream
|
18
|
+
if key == :out
|
19
|
+
yield line, nil, thread if block_given?
|
20
|
+
else
|
21
|
+
yield nil, line, thread if block_given?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
thread.join # wait for external process to finish
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Call script
|
32
|
+
#
|
33
|
+
# @param [String] region AWS region identifier
|
34
|
+
# @param [String] dependent_stack_name name of the stack current stack depends on
|
35
|
+
# @param [String] script_path path to script
|
36
|
+
# @param [Array] keys list of keys
|
37
|
+
# @param [String] prepend_args prepend arguments
|
38
|
+
# @param [String] append_args append arguments
|
39
|
+
# @deprecated this method is no longer used
|
40
|
+
def cfn_call_script(region,
|
41
|
+
dependent_stack_name,
|
42
|
+
script_path,
|
43
|
+
keys,
|
44
|
+
prepend_args: '',
|
45
|
+
append_args: '')
|
46
|
+
cfn = cfn_resource(cfn_client(region))
|
47
|
+
stack = wait_stack(cfn, dependent_stack_name)
|
48
|
+
args = get_resources(stack, keys).join(' ')
|
49
|
+
cmd = [script_path, prepend_args, args, append_args]
|
50
|
+
begin
|
51
|
+
run_cmd(cmd)
|
52
|
+
rescue Errno::ENOENT
|
53
|
+
puts $ERROR_INFO.to_s
|
54
|
+
STDERR.puts cmd
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Run command and print captured output to corresponding standard streams
|
59
|
+
#
|
60
|
+
# @param [Array] cmd command array to be executed
|
61
|
+
# @return [String] produced output from executed command
|
62
|
+
def run_cmd(cmd)
|
63
|
+
# use contracts to get rid of exceptions: https://github.com/egonSchiele/contracts.ruby
|
64
|
+
fail ArgumentError, "Expected Array, but actually was given #{cmd.class}" unless cmd.is_a?(Array)
|
65
|
+
fail ArgumentError, 'Argument cannot be empty' if cmd.empty?
|
66
|
+
SubProcess.new(cmd.join(' ')) do |stdout, stderr, _thread|
|
67
|
+
STDOUT.puts stdout if stdout
|
68
|
+
STDERR.puts stderr if stderr
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Enscalator
|
2
|
+
module Helpers
|
3
|
+
# Helpers that wrap some
|
4
|
+
module Wrappers
|
5
|
+
# Cloudformation client
|
6
|
+
#
|
7
|
+
# @param [String] region Region in Amazon AWS
|
8
|
+
# @return [Aws::CloudFormation::Client]
|
9
|
+
# @raise [ArgumentError] when region is not given
|
10
|
+
def cfn_client(region)
|
11
|
+
fail ArgumentError, 'Unable to proceed without region' if region.blank? && !Aws.config.key?(:region)
|
12
|
+
opts = {}
|
13
|
+
opts[:region] = region unless Aws.config.key?(:region)
|
14
|
+
Aws::CloudFormation::Client.new(opts)
|
15
|
+
end
|
16
|
+
|
17
|
+
# EC2 client
|
18
|
+
#
|
19
|
+
# @param [String] region Region in Amazon AWS
|
20
|
+
# @return [Aws::EC2::Client]
|
21
|
+
# @raise [ArgumentError] when region is not given
|
22
|
+
def ec2_client(region)
|
23
|
+
fail ArgumentError, 'Unable to proceed without region' if region.blank? && !Aws.config.key?(:region)
|
24
|
+
opts = {}
|
25
|
+
opts[:region] = region unless Aws.config.key?(:region)
|
26
|
+
# noinspection RubyArgCount
|
27
|
+
Aws::EC2::Client.new(opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Route 53 client
|
31
|
+
#
|
32
|
+
# @param [String] region AWS region identifier
|
33
|
+
# @return [Aws::Route53::Client]
|
34
|
+
# @raise [ArgumentError] when region is not given
|
35
|
+
def route53_client(region)
|
36
|
+
fail ArgumentError, 'Unable to proceed without region' if region.blank? && !Aws.config.key?(:region)
|
37
|
+
opts = {}
|
38
|
+
opts[:region] = region unless Aws.config.key?(:region)
|
39
|
+
# noinspection RubyArgCount
|
40
|
+
Aws::Route53::Client.new(opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Cloudformation resource
|
44
|
+
#
|
45
|
+
# @param [Aws::CloudFormation::Client] client instance of AWS Cloudformation client
|
46
|
+
# @return [Aws::CloudFormation::Resource]
|
47
|
+
# @raise [ArgumentError] when client is not provided or its not expected class type
|
48
|
+
def cfn_resource(client)
|
49
|
+
fail ArgumentError,
|
50
|
+
'must be instance of Aws::CloudFormation::Client' unless client.instance_of?(Aws::CloudFormation::Client)
|
51
|
+
Aws::CloudFormation::Resource.new(client: client)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require_relative 'helpers/sub_process'
|
2
|
+
require_relative 'helpers/wrappers'
|
3
|
+
require_relative 'helpers/stack'
|
4
|
+
require_relative 'helpers/dns'
|
5
|
+
|
6
|
+
# Enscalator
|
7
|
+
module Enscalator
|
8
|
+
# Default directory to save generated assets like ssh keys, configs and etc.
|
9
|
+
ASSETS_DIR = File.join(ENV['HOME'], ".#{name.split('::').first.downcase}")
|
10
|
+
|
11
|
+
# Collection of helper classes and static methods
|
12
|
+
module Helpers
|
13
|
+
include Wrappers
|
14
|
+
include Stack
|
15
|
+
include Dns
|
16
|
+
|
17
|
+
# Initialize enscalator directory
|
18
|
+
# @return [String]
|
19
|
+
def init_assets_dir
|
20
|
+
FileUtils.mkdir_p(Enscalator::ASSETS_DIR) unless Dir.exist?(Enscalator::ASSETS_DIR)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Provision Aws.config with custom settings
|
24
|
+
# @param [String] region valid aws region
|
25
|
+
# @param [String] profile_name aws credentials profile name
|
26
|
+
def init_aws_config(region, profile_name: nil)
|
27
|
+
fail ArgumentError, 'Unable to proceed without region' if region.blank?
|
28
|
+
opts = {}
|
29
|
+
opts[:region] = region
|
30
|
+
opts[:credentials] = Aws::SharedCredentials.new(profile_name: profile_name) unless profile_name.blank?
|
31
|
+
Aws.config.update(opts)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Find ami images registered
|
35
|
+
#
|
36
|
+
# @param [Aws::EC2::Client] client instance of AWS EC2 client
|
37
|
+
# @return [Hash] images satisfying query conditions
|
38
|
+
# @raise [ArgumentError] when client is not provided or its not expected class type
|
39
|
+
def find_ami(client, owners: ['self'], filters: nil)
|
40
|
+
fail ArgumentError, 'must be instance of Aws::EC2::Client' unless client.instance_of?(Aws::EC2::Client)
|
41
|
+
query = {}
|
42
|
+
query[:dry_run] = false
|
43
|
+
query[:owners] = owners if owners.is_a?(Array) && owners.any?
|
44
|
+
query[:filters] = filters if filters.is_a?(Array) && filters.any?
|
45
|
+
client.describe_images(query)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Generate ssh keyname from app_name, region and stack name
|
49
|
+
#
|
50
|
+
# @param [String] app_name application name
|
51
|
+
# @param [String] region aws region
|
52
|
+
# @param [String] stack_name cloudformation stack name
|
53
|
+
def gen_ssh_key_name(app_name, region, stack_name)
|
54
|
+
[app_name, region, stack_name].map(&:underscore).join('_')
|
55
|
+
end
|
56
|
+
|
57
|
+
# Create ssh public/private key pair, save private key for current user
|
58
|
+
#
|
59
|
+
# @param [String] key_name key name
|
60
|
+
# @param [String] region aws region
|
61
|
+
# @param [Boolean] force_create force to create a new ssh key
|
62
|
+
def create_ssh_key(key_name, region, force_create: false)
|
63
|
+
# Ignoring attempts to generate new ssh key when not deploying
|
64
|
+
if @options && @options[:expand]
|
65
|
+
warn '[Warning] SSH key can be generated only for create or update stack actions'
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
client = ec2_client(region)
|
70
|
+
aws_profile = if Aws.config.key?(:credentials)
|
71
|
+
creds = Aws.config[:credentials]
|
72
|
+
creds.profile_name if creds.respond_to?(:profile_name)
|
73
|
+
end
|
74
|
+
target_dir = File.join(Enscalator::ASSETS_DIR, aws_profile ? aws_profile : 'default')
|
75
|
+
FileUtils.mkdir_p(target_dir) unless Dir.exist? target_dir
|
76
|
+
if !client.describe_key_pairs.key_pairs.collect(&:key_name).include?(key_name) || force_create
|
77
|
+
# delete existed ssh key
|
78
|
+
client.delete_key_pair(key_name: key_name)
|
79
|
+
|
80
|
+
# create a new ssh key
|
81
|
+
key_pair = client.create_key_pair(key_name: key_name)
|
82
|
+
STDERR.puts "Created new ssh key with fingerprint: #{key_pair.key_fingerprint}"
|
83
|
+
|
84
|
+
# save private key for current user
|
85
|
+
private_key = File.join(target_dir, key_name)
|
86
|
+
File.open(private_key, 'w') do |wfile|
|
87
|
+
wfile.write(key_pair.key_material)
|
88
|
+
end
|
89
|
+
STDERR.puts "Saved created key to: #{private_key}"
|
90
|
+
File.chmod(0600, private_key)
|
91
|
+
else
|
92
|
+
key_fingerprint =
|
93
|
+
begin
|
94
|
+
Aws::EC2::KeyPair.new(key_name, client: client).key_fingerprint
|
95
|
+
rescue NotImplementedError
|
96
|
+
# TODO: after upgrade of aws-sdk use only Aws::EC2::KeyPairInfo
|
97
|
+
Aws::EC2::KeyPairInfo.new(key_name, client: client).key_fingerprint
|
98
|
+
end
|
99
|
+
STDERR.puts "Found existing ssh key with fingerprint: #{key_fingerprint}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Read user data from file
|
104
|
+
#
|
105
|
+
# @param [String] app_name application name
|
106
|
+
def read_user_data(app_name)
|
107
|
+
user_data_path = File.join(File.expand_path('..', __FILE__), 'plugins', 'user-data', app_name)
|
108
|
+
fail("User data path #{user_data_path} not exists") unless File.exist?(user_data_path)
|
109
|
+
File.read(user_data_path)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Convert hash with nested values to flat hash
|
113
|
+
#
|
114
|
+
# @param [Hash] input that should be flatten
|
115
|
+
def flatten_hash(input)
|
116
|
+
input.each_with_object({}) do |(k, v), h|
|
117
|
+
if v.is_a?(Hash)
|
118
|
+
flatten_hash(v).map do |h_k, h_v|
|
119
|
+
h["#{k}.#{h_k}".to_sym] = h_v
|
120
|
+
end
|
121
|
+
else
|
122
|
+
h[k] = v
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end # module Helpers
|
127
|
+
end # module Enscalator
|