terrafying 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 677e99cbba54dcc89984681f4419031a618e7d8e569c7e3a0286ce46a18aa914
4
+ data.tar.gz: c1ac49346d7e00dbae316c945c0fe91116fb6598f58caf8a018927c54402843c
5
+ SHA512:
6
+ metadata.gz: 6137818b31a5b6ef8b753d3b91cd663c3068f27a142214c60ac7e66da4a61e0eed446ac2815cd6b3023b88cf59c402760ba0cd002a1a9d7136c1424ac4935d00
7
+ data.tar.gz: 01b7d784d0fdcf6aef53dea73634d44213e8115e30402492434f36ca8b360cf0ca5f2fefc5fbc2e2d0081672dce0c8356cfb5212a75632d4027457de86d32387
data/bin/terrafying ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'terrafying'
5
+
6
+ Terrafying::Cli.start(ARGV)
@@ -0,0 +1,6 @@
1
+ class ::Hash
2
+ def deep_merge(second)
3
+ merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
4
+ self.merge(second, &merger)
5
+ end
6
+ end
@@ -0,0 +1,542 @@
1
+ require 'aws-sdk'
2
+
3
+ Aws.use_bundled_cert!
4
+
5
+ module Terrafying
6
+ module Aws
7
+ class Ops
8
+
9
+ attr_reader :region
10
+
11
+ def initialize(region)
12
+ full_jitter = lambda { |c| Kernel.sleep(Kernel.rand(0..[2, (0.3 * 2**c.retries)].min)) }
13
+
14
+ ::Aws.config.update({
15
+ region: region,
16
+ retry_limit: 5,
17
+ retry_backoff: full_jitter
18
+ })
19
+ @autoscaling_client = ::Aws::AutoScaling::Client.new
20
+ @ec2_resource = ::Aws::EC2::Resource.new
21
+ @ec2_client = ::Aws::EC2::Client.new
22
+ @elb_client = ::Aws::ElasticLoadBalancingV2::Client.new
23
+ @route53_client = ::Aws::Route53::Client.new
24
+ @s3_client = ::Aws::S3::Client.new
25
+ @sts_client = ::Aws::STS::Client.new
26
+
27
+ @region = region
28
+ end
29
+
30
+ def account_id
31
+ @account_id_cache ||= @sts_client.get_caller_identity.account
32
+ end
33
+
34
+ def security_group(name)
35
+ @security_groups ||= {}
36
+ @security_groups[name] ||=
37
+ begin
38
+ STDERR.puts "Looking up id of security group '#{name}'"
39
+ groups = @ec2_resource.security_groups(
40
+ {
41
+ filters: [
42
+ {
43
+ name: "group-name",
44
+ values: [name],
45
+ },
46
+ ],
47
+ }).limit(2)
48
+ case
49
+ when groups.count == 1
50
+ groups.first.id
51
+ when groups.count < 1
52
+ raise "No security group with name '#{name}' was found."
53
+ when groups.count > 1
54
+ raise "More than one security group with name '#{name}' found: " + groups.join(', ')
55
+ end
56
+ end
57
+ end
58
+
59
+ def security_group_in_vpc(vpc_id, name)
60
+ @security_groups_in_vpc ||= {}
61
+ @security_groups_in_vpc[vpc_id + name] ||=
62
+ begin
63
+ STDERR.puts "Looking up id of security group '#{name}'"
64
+ groups = @ec2_resource.security_groups(
65
+ {
66
+ filters: [
67
+ {
68
+ name: "group-name",
69
+ values: [name],
70
+ },
71
+ {
72
+ name: "vpc-id",
73
+ values: [vpc_id],
74
+ }
75
+ ],
76
+ }).limit(2)
77
+ case
78
+ when groups.count == 1
79
+ groups.first.id
80
+ when groups.count < 1
81
+ raise "No security group with name '#{name}' was found."
82
+ when groups.count > 1
83
+ raise "More than one security group with name '#{name}' found: " + groups.join(', ')
84
+ end
85
+ end
86
+ end
87
+
88
+ def security_group_by_tags(tags)
89
+ @security_groups_by_tags ||= {}
90
+ @security_groups_by_tags[tags] ||=
91
+ begin
92
+ groups = @ec2_client.describe_security_groups(
93
+ {
94
+ filters: [
95
+ {
96
+ name: "tag-key",
97
+ values: tags.keys,
98
+ },
99
+ {
100
+ name: "tag-value",
101
+ values: tags.values
102
+ }
103
+ ]
104
+ },
105
+ ).security_groups
106
+ case
107
+ when groups.count == 1
108
+ groups.first.id
109
+ when groups.count < 1
110
+ raise "No security group with tags '#{tags}' was found."
111
+ when groups.count > 1
112
+ raise "More than one security group with tags '#{tags}' found: " + groups.join(', ')
113
+ end
114
+ end
115
+ end
116
+
117
+ def instance_profile(name)
118
+ @instance_profiles ||= {}
119
+ @instance_profiles[name] ||=
120
+ begin
121
+ resource = ::Aws::IAM::Resource.new
122
+ STDERR.puts "Looking up id of instance profile '#{name}'"
123
+ # unfortunately amazon don't let us filter for profiles using
124
+ # a name filter, for now we have enumerate and filter manually
125
+ coll = resource.instance_profiles
126
+ profiles = []
127
+ profiles = coll.select {|p| p.instance_profile_name =~ /#{name}/}
128
+
129
+ case
130
+ when profiles.count == 1
131
+ profiles.first.instance_profile_id
132
+ when profiles.count < 1
133
+ raise "No instance profile with name '#{name}' was found."
134
+ when profiles.count > 1
135
+ raise "More than one instance profile with name '#{name}' found: " + profiles.join(', ')
136
+ end
137
+ end
138
+ end
139
+
140
+ def route_table_for_subnet(subnet_id)
141
+ @route_table_for_subnet ||= {}
142
+ @route_table_for_subnet[subnet_id] ||=
143
+ begin
144
+ resp = @ec2_client.describe_route_tables(
145
+ {
146
+ filters: [
147
+ { name: "association.subnet-id", values: [ subnet_id ] },
148
+ ],
149
+ })
150
+
151
+ route_tables = resp.route_tables
152
+
153
+ case
154
+ when route_tables.count == 1
155
+ route_tables.first
156
+ when route_tables.count < 1
157
+ raise "No route table for subnet '#{subnet_id}' was found."
158
+ when profiles.count > 1
159
+ raise "More than route table for subnet '#{subnet_id}' found: " + route_tables.join(', ')
160
+ end
161
+ end
162
+ end
163
+
164
+ def route_table_for_vpc(vpc_id)
165
+ @route_table_for_vpc ||= {}
166
+ @route_table_for_vpc[vpc_id] ||=
167
+ begin
168
+ resp = @ec2_client.describe_route_tables(
169
+ {
170
+ filters: [
171
+ { name: "association.main", values: [ "true" ] },
172
+ { name: "vpc-id", values: [ vpc_id ] },
173
+ ],
174
+ })
175
+
176
+ route_tables = resp.route_tables
177
+
178
+ case
179
+ when route_tables.count == 1
180
+ route_tables.first
181
+ when route_tables.count < 1
182
+ raise "No route table for vpc '#{vpc_id}' was found."
183
+ when profiles.count > 1
184
+ raise "More than route table for vpc '#{vpc_id}' found: " + route_tables.join(', ')
185
+ end
186
+ end
187
+ end
188
+
189
+ def security_groups(*names)
190
+ names.map{|n| security_group(n)}
191
+ end
192
+
193
+ def security_groups_in_vpc(vpc_id, *names)
194
+ names.map{|n| security_group_in_vpc(vpc_id, n)}
195
+ end
196
+
197
+ def subnet(name)
198
+ @subnets ||= {}
199
+ @subnets[name] ||=
200
+ begin
201
+ STDERR.puts "Looking up id of subnet '#{name}'"
202
+ subnets = @ec2_resource.subnets(
203
+ {
204
+ filters: [
205
+ {
206
+ name: "tag:Name",
207
+ values: [name],
208
+ },
209
+ ],
210
+ }).limit(2)
211
+ case
212
+ when subnets.count == 1
213
+ subnets.first.id
214
+ when subnets.count < 1
215
+ raise "No subnet with name '#{name}' was found."
216
+ when subnets.count > 1
217
+ raise "More than one subnet with this name '#{name}' found : " + subnets.join(', ')
218
+ end
219
+ end
220
+ end
221
+
222
+ def subnet_by_id(id)
223
+ @subnets_by_id ||= {}
224
+ @subnets_by_id[id] ||=
225
+ begin
226
+ resp = @ec2_client.describe_subnets(
227
+ {
228
+ subnet_ids: [id],
229
+ })
230
+ subnets = resp.subnets
231
+ case
232
+ when subnets.count == 1
233
+ subnets.first
234
+ when subnets.count < 1
235
+ raise "No subnet with id '#{id}' was found."
236
+ when subnets.count > 1
237
+ raise "More than one subnet with this id '#{id}' found : " + subnets.join(', ')
238
+ end
239
+ end
240
+ end
241
+
242
+ def subnets(*names)
243
+ names.map{|n| subnet(n)}
244
+ end
245
+
246
+ def subnets_for_vpc(vpc_id)
247
+ @subnets_for_vpc ||= {}
248
+ @subnets_for_vpc[vpc_id] ||=
249
+ begin
250
+ resp = @ec2_client.describe_subnets(
251
+ {
252
+ filters: [
253
+ { name: "vpc-id", values: [ vpc_id ] },
254
+ ],
255
+ })
256
+
257
+ subnets = resp.subnets
258
+
259
+ case
260
+ when subnets.count >= 1
261
+ subnets
262
+ when subnets.count < 1
263
+ raise "No subnets found for '#{vpc_id}'."
264
+ end
265
+ end
266
+ end
267
+
268
+ def ami(name, owners=["self"])
269
+ @ami ||= {}
270
+ @ami[name] ||=
271
+ begin
272
+ STDERR.puts "looking for an image with prefix '#{name}'"
273
+ resp = @ec2_client.describe_images({owners: owners})
274
+ if resp.images.count < 1
275
+ raise "no images were found"
276
+ end
277
+ m = resp.images.select { |a| /^#{name}/.match(a.name) }
278
+ if m.count == 0
279
+ raise "no image with name '#{name}' was found"
280
+ end
281
+ m.sort { |x,y| y.creation_date <=> x.creation_date }.shift.image_id
282
+ end
283
+ end
284
+
285
+ def availability_zones
286
+ @availability_zones ||=
287
+ begin
288
+ STDERR.puts "looking for AZs in the current region"
289
+ resp = @ec2_client.describe_availability_zones({})
290
+ resp.availability_zones.map { |zone|
291
+ zone.zone_name
292
+ }
293
+ end
294
+ end
295
+
296
+ def vpc(name)
297
+ @vpcs ||= {}
298
+ @vpcs[name] ||=
299
+ begin
300
+ STDERR.puts "looking for a VPC with name '#{name}'"
301
+ resp = @ec2_client.describe_vpcs({})
302
+ matching_vpcs = resp.vpcs.select { |vpc|
303
+ name_tag = vpc.tags.select { |tag| tag.key == "Name" }.first
304
+ name_tag && name_tag.value == name
305
+ }
306
+ case
307
+ when matching_vpcs.count == 1
308
+ matching_vpcs.first
309
+ when matching_vpcs.count < 1
310
+ raise "No VPC with name '#{name}' was found."
311
+ when matching_vpcs.count > 1
312
+ raise "More than one VPC with name '#{name}' was found: " + matching_vpcs.join(', ')
313
+ end
314
+ end
315
+ end
316
+
317
+ def route_table(name)
318
+ @route_tables ||= {}
319
+ @route_tables[name] ||=
320
+ begin
321
+ STDERR.puts "looking for a route table with name '#{name}'"
322
+ route_tables = @ec2_client.describe_route_tables(
323
+ {
324
+ filters: [
325
+ {
326
+ name: "tag:Name",
327
+ values: [name],
328
+ },
329
+ ],
330
+ }
331
+ ).route_tables
332
+ case
333
+ when route_tables.count == 1
334
+ route_tables.first.route_table_id
335
+ when route_tables.count < 1
336
+ raise "No route table with name '#{name}' was found."
337
+ when route_tables.count > 1
338
+ raise "More than one route table with name '#{name}' was found: " + route_tables.join(', ')
339
+ end
340
+ end
341
+ end
342
+
343
+ def elastic_ip(alloc_id)
344
+ @ips ||= {}
345
+ @ips[alloc_id] ||=
346
+ begin
347
+ STDERR.puts "looking for an elastic ip with allocation_id '#{alloc_id}'"
348
+ ips = @ec2_client.describe_addresses(
349
+ {
350
+ filters: [
351
+ {
352
+ name: "allocation-id",
353
+ values: [alloc_id],
354
+ },
355
+ ],
356
+ }
357
+ ).addresses
358
+ case
359
+ when ips.count == 1
360
+ ips.first
361
+ when ips.count < 1
362
+ raise "No elastic ip with allocation_id '#{alloc_id}' was found."
363
+ when ips.count > 1
364
+ raise "More than one elastic ip with allocation_id '#{alloc_id}' was found: " + ips.join(', ')
365
+ end
366
+ end
367
+ end
368
+
369
+ def hosted_zone(fqdn)
370
+ @hosted_zones ||= {}
371
+ @hosted_zones[fqdn] ||=
372
+ begin
373
+ STDERR.puts "looking for a hosted zone with fqdn '#{fqdn}'"
374
+ hosted_zones = @route53_client.list_hosted_zones_by_name({ dns_name: fqdn }).hosted_zones.select { |zone|
375
+ zone.name == "#{fqdn}."
376
+ }
377
+ case
378
+ when hosted_zones.count == 1
379
+ hosted_zones.first
380
+ when hosted_zones.count < 1
381
+ raise "No hosted zone with fqdn '#{fqdn}' was found."
382
+ when hosted_zones.count > 1
383
+ raise "More than one hosted zone with name '#{fqdn}' was found: " + hosted_zones.join(', ')
384
+ end
385
+ end
386
+ end
387
+
388
+ def hosted_zone_by_tag(tag)
389
+ @hosted_zones ||= {}
390
+ @hosted_zones[tag] ||=
391
+ begin
392
+ STDERR.puts "looking for a hosted zone with tag '#{tag}'"
393
+ @aws_hosted_zones ||= @route53_client.list_hosted_zones.hosted_zones.map do |zone|
394
+ {
395
+ zone: zone,
396
+ tags: @route53_client.list_tags_for_resource({resource_type: "hostedzone", resource_id: zone.id.split('/')[2]}).resource_tag_set.tags
397
+ }
398
+ end
399
+
400
+ hosted_zones = @aws_hosted_zones.select do |z|
401
+ z[:tags].any? do |aws_tag|
402
+ tag.any? { |k, v| aws_tag.key = String(k) && aws_tag.value == v }
403
+ end
404
+ end
405
+
406
+ case
407
+ when hosted_zones.count == 1
408
+ hosted_zones.first[:zone]
409
+ when hosted_zones.count < 1
410
+ raise "No hosted zone with tag '#{tag}' was found."
411
+ when hosted_zones.count > 1
412
+ raise "More than one hosted zone with tag '#{tag}' was found: " + hosted_zones.join(', ')
413
+ end
414
+ end
415
+ end
416
+
417
+ def s3_object(bucket, key)
418
+ @s3_objects ||= {}
419
+ @s3_objects["#{bucket}-#{key}"] ||=
420
+ begin
421
+ resp = @s3_client.get_object({ bucket: bucket, key: key })
422
+ resp.body.read
423
+ end
424
+ end
425
+
426
+ def list_objects(bucket)
427
+ @list_objects ||= {}
428
+ @list_objects[bucket] ||=
429
+ begin
430
+ resp = @s3_client.list_objects_v2({ bucket: bucket })
431
+ resp.contents
432
+ end
433
+ end
434
+
435
+ def endpoint_service_by_name(service_name)
436
+ @endpoint_service ||= {}
437
+ @endpoint_service[service_name] ||=
438
+ begin
439
+ resp = @ec2_client.describe_vpc_endpoint_service_configurations(
440
+ {
441
+ filters: [
442
+ {
443
+ name: "service-name",
444
+ values: [service_name],
445
+ },
446
+ ],
447
+ }
448
+ )
449
+
450
+ endpoint_services = resp.service_configurations
451
+ case
452
+ when endpoint_services.count == 1
453
+ endpoint_services.first
454
+ when endpoint_services.count < 1
455
+ raise "No endpoint service with name '#{service_name}' was found."
456
+ when endpoint_services.count > 1
457
+ raise "More than one endpoint service with name '#{service_name}' was found: " + endpoint_services.join(', ')
458
+ end
459
+ end
460
+ end
461
+
462
+ def endpoint_service_by_lb_arn(arn)
463
+ @endpoint_services_by_lb_arn ||= {}
464
+ @endpoint_services_by_lb_arn[arn] ||=
465
+ begin
466
+ resp = @ec2_client.describe_vpc_endpoint_service_configurations
467
+
468
+ services = resp.service_configurations.select { |service|
469
+ service.network_load_balancer_arns.include?(arn)
470
+ }
471
+
472
+ case
473
+ when services.count == 1
474
+ services.first
475
+ when services.count < 1
476
+ raise "No endpoint service with lb arn '#{arn}' was found."
477
+ when services.count > 1
478
+ raise "More than one endpoint service with lb arn '#{arn}' was found: " + services.join(', ')
479
+ end
480
+ end
481
+ end
482
+
483
+ def lb_by_name(name)
484
+ @lbs ||= {}
485
+ @lbs[name] ||=
486
+ begin
487
+ load_balancers = @elb_client.describe_load_balancers({ names: [name] }).load_balancers
488
+
489
+ case
490
+ when load_balancers.count == 1
491
+ load_balancers.first
492
+ when load_balancers.count < 1
493
+ raise "No load balancer with name '#{name}' was found."
494
+ when load_balancers.count > 1
495
+ raise "More than one load balancer with name '#{name}' was found: " + load_balancers.join(', ')
496
+ end
497
+ end
498
+ end
499
+
500
+ def target_groups_by_lb(arn)
501
+ @target_groups ||= {}
502
+ @target_groups[arn] ||=
503
+ begin
504
+ resp = @elb_client.describe_target_groups(
505
+ {
506
+ load_balancer_arn: arn,
507
+ }
508
+ )
509
+
510
+ resp.target_groups
511
+ end
512
+ end
513
+
514
+ def asgs_by_tags(expectedTags = {})
515
+ asgs = []
516
+ next_token = nil
517
+
518
+ loop do
519
+ resp = @autoscaling_client.describe_auto_scaling_groups({ next_token: next_token })
520
+
521
+ asgs = asgs + resp.auto_scaling_groups.select { |asg|
522
+ matches = asg.tags.select { |tag|
523
+ expectedTags[tag.key.to_sym] == tag.value ||
524
+ expectedTags[tag.key] == tag.value
525
+ }
526
+
527
+ matches.count == expectedTags.count
528
+ }
529
+
530
+ if resp.next_token
531
+ next_token = resp.next_token
532
+ else
533
+ break
534
+ end
535
+ end
536
+
537
+ asgs
538
+ end
539
+ end
540
+
541
+ end
542
+ end
@@ -0,0 +1,73 @@
1
+ require 'thor'
2
+
3
+ module Terrafying
4
+ class Cli < Thor
5
+ class_option :lock_timeout, :type => :string, :default => nil
6
+ class_option :no_lock, :type => :boolean, :default => false
7
+ class_option :keep, :type => :boolean, :default => false
8
+ class_option :target, :type => :string, :default => nil
9
+ class_option :scope, :type => :string, :default => nil
10
+ class_option :dynamodb, :type => :boolean, :default => true
11
+
12
+ desc "list PATH", "List resources defined"
13
+ def list(path)
14
+ puts "Defined resources:\n\n"
15
+ Config.new(path, options).list.each do |name|
16
+ puts "#{name}"
17
+ end
18
+ end
19
+
20
+ desc "plan PATH", "Show execution plan"
21
+ def plan(path)
22
+ exit Config.new(path, options).plan
23
+ end
24
+
25
+ desc "graph PATH", "Show execution graph"
26
+ def graph(path)
27
+ exit Config.new(path, options).graph
28
+ end
29
+
30
+ desc "validate PATH", "Validate the generated Terraform"
31
+ def validate(path)
32
+ exit Config.new(path, options).validate
33
+ end
34
+
35
+ desc "apply PATH", "Apply changes to resources"
36
+ option :force, :aliases => ['f'], :type => :boolean, :desc => "Forcefully remove any pending locks"
37
+ def apply(path)
38
+ exit Config.new(path, options).apply
39
+ end
40
+
41
+ desc "destroy PATH", "Destroy resources"
42
+ option :force, :aliases => ['f'], :type => :boolean, :desc => "Forcefully remove any pending locks"
43
+ def destroy(path)
44
+ exit Config.new(path, options).destroy
45
+ end
46
+
47
+ desc "json PATH", "Show terraform JSON"
48
+ def json(path)
49
+ puts(Config.new(path, options).json)
50
+ end
51
+
52
+ desc "show-state PATH", "Show state"
53
+ def show_state(path)
54
+ puts(Config.new(path, options).show_state)
55
+ end
56
+
57
+ desc "use-remote-state PATH", "Migrate to using remote state storage"
58
+ def use_remote_state(path)
59
+ puts(Config.new(path, options).use_remote_state)
60
+ end
61
+
62
+ desc "use-local-state PATH", "Migrate to using local state storage"
63
+ def use_local_state(path)
64
+ puts(Config.new(path, options).use_local_state)
65
+ end
66
+
67
+ desc "import PATH ADDR ID", "Import existing infrastructure into your Terraform state"
68
+ def import(path, addr, id)
69
+ exit Config.new(path, options).import(addr, id)
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,17 @@
1
+ module Terrafying
2
+ module DynamoDb
3
+ class Config
4
+ attr_accessor :state_table, :lock_table
5
+
6
+ def initialize
7
+ @state_table = "terrafying-state"
8
+ @lock_table = "terrafying-state-lock"
9
+ end
10
+ end
11
+
12
+ def config
13
+ @config ||= Config.new
14
+ end
15
+ module_function :config
16
+ end
17
+ end