hashicorptools 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,114 @@
1
+ module Hashicorptools
2
+ class Host < Thor
3
+
4
+ desc 'hosts', 'list running instances'
5
+ option :environment, required: false
6
+ option :role, required: false
7
+ option :name, required: false
8
+ def hosts
9
+ ec2 = Aws::EC2::Client.new(region: 'us-east-1')
10
+
11
+ resp = ec2.describe_instances(filters: filters)
12
+ resp.reservations.each do |reservation|
13
+ reservation.instances.each do |instance|
14
+ name = instance.tags.find{|t| t.key == 'Name'}.value
15
+ puts "#{name} #{instance.public_dns_name}"
16
+ end
17
+ end
18
+ end
19
+
20
+ desc 'ssh', 'ssh to the first matching instance'
21
+ option :environment, required: false
22
+ option :role, required: false
23
+ option :name, required: false
24
+ def ssh(role = '')
25
+ ec2 = Aws::EC2::Client.new(region: 'us-east-1')
26
+
27
+ resp = ec2.describe_instances(filters: filters(role))
28
+ if resp.reservations.any?
29
+ instance = resp.reservations.first.instances.first
30
+ dns = if instance.public_dns_name.present?
31
+ instance.public_dns_name
32
+ else
33
+ instance.private_dns_name
34
+ end
35
+
36
+ exec "ssh #{ssh_user_fragment}#{dns}"
37
+ else
38
+ puts "no instances with #{role} role found"
39
+ end
40
+ end
41
+
42
+ desc 'console', 'run the agra rails console'
43
+ option :environment, required: false
44
+ def console
45
+ ec2 = Aws::EC2::Client.new(region: 'us-east-1')
46
+
47
+ bastion_dns = dns_from_reservations(ec2.describe_instances(filters: filters('console', 'maintenance')))
48
+ agra_console_dns = dns_from_reservations(ec2.describe_instances(filters: filters('console', 'agra')))
49
+
50
+
51
+ if bastion_dns && agra_console_dns
52
+ exec "ssh -t #{ssh_user_fragment}#{bastion_dns} 'ssh -t #{ssh_user_fragment}#{agra_console_dns}'"
53
+ else
54
+ puts "no instances found"
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def dns_from_reservations(resp)
61
+ if resp.reservations.any?
62
+ instance = resp.reservations.first.instances.first
63
+ if instance.public_dns_name.present?
64
+ instance.public_dns_name
65
+ else
66
+ instance.private_dns_name
67
+ end
68
+ end
69
+ end
70
+
71
+
72
+ def application_environment
73
+ if options[:environment].present?
74
+ options[:environment]
75
+ elsif ENV['CHANGESPROUT_APP_ENVIRONMENT'].present?
76
+ ENV['CHANGESPROUT_APP_ENVIRONMENT']
77
+ else
78
+ 'staging'
79
+ end
80
+ end
81
+
82
+ def ssh_user_fragment
83
+ ENV['AWS_SSH_USERNAME'].present? ? "#{ENV['AWS_SSH_USERNAME']}@" : ''
84
+ end
85
+
86
+ def filters(role = '', kind='')
87
+ filters = []
88
+
89
+ filters << {name: 'instance-state-name', values: ['running']}
90
+
91
+ if application_environment.present?
92
+ filters << {name: 'tag:environment', values: [ application_environment ]}
93
+ end
94
+
95
+ if options[:name].present?
96
+ filters << {name: 'tag:Name', values: [ options[:name] ]}
97
+ end
98
+
99
+ if role.blank?
100
+ role = options[:role].present?
101
+ end
102
+
103
+ if role.present?
104
+ filters << {name: 'tag:role', values: [ role ]}
105
+ end
106
+
107
+ if kind.present?
108
+ filters << {name: 'tag:kind', values: [ kind ]}
109
+ end
110
+
111
+ filters
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,129 @@
1
+ module Hashicorptools
2
+ NUMBER_OF_AMIS_TO_KEEP = 2
3
+
4
+ class Packer < Thor
5
+ include Ec2Utilities
6
+ include Variables
7
+
8
+ desc "build", "creates an AMI from the current config"
9
+ option :debug, :required => false
10
+ def build
11
+ _build
12
+ end
13
+
14
+ desc "validate", "validates the packer config"
15
+ def validate
16
+ system "packer validate #{ami_config_path}"
17
+ end
18
+
19
+ desc "console", "interactive session"
20
+ def console
21
+ require 'byebug'
22
+ byebug
23
+ end
24
+
25
+ desc "list", "list all available telize amis"
26
+ def list
27
+ amis.each do |ami|
28
+ puts ami.image_id
29
+ end
30
+ end
31
+
32
+ desc "clean", "clean old AMIs that are no longer needed"
33
+ def clean
34
+ clean_amis
35
+ end
36
+
37
+ desc "boot", "start up an instance of the latest version of AMI"
38
+ def boot
39
+ run_instances_resp = ec2.run_instances(image_id: current_ami('base-image').image_id,
40
+ min_count: 1,
41
+ max_count: 1,
42
+ instance_type: "t2.micro")
43
+
44
+ ec2.create_tags( resources: run_instances_resp.instances.collect{|i| i.instance_id },
45
+ tags: [ {key: 'Name', value: "packer test boot #{tag_name}"}, {key: 'environment', value: 'packer-development'}, {key: 'temporary', value: 'kill me'}])
46
+
47
+ require 'byebug'
48
+ byebug
49
+ end
50
+
51
+ protected
52
+
53
+ def _build(settings_overrides={})
54
+ settings_overrides.merge!({source_ami: source_ami_id, ami_tag: tag_name, cookbook_name: cookbook_name})
55
+
56
+ if options[:debug]
57
+ puts "[DEBUG] Executing 'packer build -debug #{variables(settings_overrides)} #{ami_config_path}'"
58
+ system "packer build -debug \
59
+ #{variables(settings_overrides)} \
60
+ #{ami_config_path}"
61
+ else
62
+ system "packer build \
63
+ #{variables(settings_overrides)} \
64
+ #{ami_config_path}"
65
+ end
66
+
67
+ clean_amis
68
+ end
69
+
70
+ def source_ami_id
71
+ current_ami('base-image').image_id
72
+ end
73
+
74
+ def tag_name
75
+ raise 'implement me'
76
+ end
77
+
78
+ def cookbook_name
79
+ raise 'implement me'
80
+ end
81
+
82
+ def ami_config_path
83
+ datadir_path = Gem.datadir('hashicorptools')
84
+ File.join(datadir_path, 'standard-ami.json')
85
+ end
86
+
87
+ def auto_scaling
88
+ @auto_scaling ||= Aws::AutoScaling::Client.new(region: 'us-east-1')
89
+ end
90
+
91
+ def ec2_v2
92
+ @ec2 ||= Aws::EC2::Client.new(region: 'us-east-1')
93
+ end
94
+
95
+ def amis_in_use
96
+ launch_configs = auto_scaling.describe_launch_configurations
97
+ image_ids = launch_configs.data['launch_configurations'].collect{|lc| lc.image_id}.flatten
98
+
99
+ ec2_reservations = ec2_v2.describe_instances
100
+ image_ids << ec2_reservations.reservations.collect{|res| res.instances.collect{|r| r.image_id}}.flatten
101
+ image_ids.flatten
102
+ end
103
+
104
+ def clean_amis
105
+ ami_ids = amis.collect{|a| a.image_id}
106
+ ami_ids_to_remove = ami_ids - amis_in_use
107
+ potential_amis_to_remove = amis
108
+ potential_amis_to_remove.keep_if {|a| ami_ids_to_remove.include?(a.image_id) }
109
+
110
+ if potential_amis_to_remove.size > NUMBER_OF_AMIS_TO_KEEP
111
+ amis_to_remove = potential_amis_to_remove[NUMBER_OF_AMIS_TO_KEEP..-1]
112
+ amis_to_keep = potential_amis_to_remove[0..(NUMBER_OF_AMIS_TO_KEEP-1)]
113
+
114
+ puts "Deregistering old AMIs..."
115
+ amis_to_remove.each do |ami|
116
+ puts "Deregistering #{ami.image_id}"
117
+ ami.deregister
118
+ end
119
+
120
+ puts "Currently active AMIs..."
121
+ amis_to_keep.each do |ami|
122
+ puts ami.image_id
123
+ end
124
+ else
125
+ puts "no AMIs to clean."
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,283 @@
1
+ module Hashicorptools
2
+ class Terraform < Thor
3
+ TERRAFORM_VERSION = '0.6.8'
4
+
5
+ include Ec2Utilities
6
+ include Variables
7
+
8
+ desc 'bootstrap', 'terraform a new infrastructure from scratch'
9
+ option :environment, :required => true
10
+ def bootstrap
11
+ apply
12
+ end
13
+
14
+ [:apply, :plan, :destroy, :pull, :refresh].each do |cmd|
15
+ desc cmd, "terraform #{cmd}"
16
+ option :environment, :required => true
17
+ option :debug, :required => false
18
+
19
+ define_method cmd do
20
+ send("_#{cmd}")
21
+ end
22
+
23
+ no_commands do
24
+ define_method "_#{cmd}" do |settings_overrides = {}|
25
+ enforce_version!
26
+ raise 'invalid environment' unless ['staging', 'production'].include?(options[:environment])
27
+
28
+ settings_overrides
29
+ .merge!({ app_environment: options[:environment] }
30
+ .merge(env_variable_keys)
31
+ .merge(settings)
32
+ .merge(shared_plan_variables))
33
+
34
+ decrypt_file(state_path)
35
+ decrypt_file(var_file_path)
36
+
37
+
38
+ begin
39
+ send("before_#{cmd}")
40
+
41
+ terraform_command = "terraform #{cmd} #{variables(settings_overrides)} -state #{state_path} #{var_file_param} #{config_directory}"
42
+
43
+ if (options[:debug])
44
+ puts "[DEBUG] running command: '#{terraform_command}"
45
+ end
46
+ if system terraform_command
47
+ send("after_#{cmd}")
48
+ end
49
+ rescue StandardError => e
50
+ puts e.message
51
+ puts e.backtrace
52
+ ensure
53
+ # need to always ensure the most recent tfstate is encrypted again.
54
+ encrypt_file(state_path)
55
+ delete_decrypted_var_file
56
+ end
57
+
58
+ end
59
+
60
+ define_method "before_#{cmd}" do
61
+ # no-op
62
+ end
63
+ end
64
+
65
+ no_commands do
66
+ define_method "after_#{cmd}" do
67
+ # no-op
68
+ end
69
+ end
70
+
71
+ desc cmd, "terraform #{cmd} for shared plan"
72
+ define_method "shared_#{cmd}" do
73
+ enforce_version!
74
+
75
+ decrypt_file(shared_state_path)
76
+
77
+ begin
78
+ system "terraform #{cmd} #{variables(env_variable_keys.merge(settings))} -state #{shared_state_path} #{shared_config_directory}"
79
+ rescue StandardError => e
80
+ puts e.message
81
+ puts e.backtrace
82
+ ensure
83
+ # need to always ensure the most recent tfstate is encrypted again.
84
+ encrypt_file(shared_state_path)
85
+ end
86
+ end
87
+ end
88
+
89
+ [:shared_apply, :shared_plan, :shared_destroy, :shared_pull, :shared_refresh].each do |cmd|
90
+
91
+ end
92
+
93
+ desc 'output', 'terraform output'
94
+ option :environment, :required => true
95
+ option :name, :required => true
96
+ def output
97
+ system output_cmd(state_path, options[:name])
98
+ end
99
+
100
+ desc 'taint', 'terraform taint'
101
+ option :environment, :required => true
102
+ option :name, :required => true
103
+ def taint
104
+ system "terraform taint -state #{state_path} #{options[:name]}"
105
+ end
106
+
107
+ desc 'show', 'terraform show'
108
+ option :environment, :required => true
109
+ def show
110
+ system "terraform show #{state_path}"
111
+ end
112
+
113
+ [ {commands: [:decrypt, :encrypt], file_path_method: :state_path, desc: 'upstream terraform changes'},
114
+ {commands: [:shared_decrypt, :shared_encrypt], file_path_method: :shared_state_path, desc: 'upstream shared terraform changes'},
115
+ {commands: [:var_file_decrypt, :var_file_encrypt], file_path_method: :var_file_path, desc: 'tfvars file'} ].each do |crypto_commands|
116
+
117
+ desc crypto_commands[:commands][0], "decrypt #{crypto_commands[:desc]}"
118
+ option :environment, :required => true
119
+ define_method crypto_commands[:commands][0] do
120
+ file_path = send(crypto_commands[:file_path_method])
121
+ decrypt_file(file_path)
122
+ end
123
+
124
+ desc crypto_commands[:commands][1], "encrypt #{crypto_commands[:desc]}"
125
+ option :environment, :required => true
126
+ define_method crypto_commands[:commands][1] do
127
+ file_path = send(crypto_commands[:file_path_method])
128
+ encrypt_file(file_path)
129
+ end
130
+ end
131
+
132
+ desc "console", "interactive session"
133
+ def console
134
+ require 'pry-byebug'
135
+ binding.pry
136
+ end
137
+
138
+ protected
139
+
140
+ def var_file_param
141
+ File.exist?(var_file_path) ?
142
+ "-var-file #{var_file_path}" :
143
+ ""
144
+ end
145
+
146
+ def delete_decrypted_var_file
147
+ return unless File.exist?(var_file_path)
148
+
149
+ enforce_cryptography_dependencies
150
+ if !File.exist?("#{var_file_path}.enc")
151
+ encrypt_file(var_file_path)
152
+ end
153
+
154
+ File.delete(var_file_path)
155
+ end
156
+
157
+ def encrypt_file(file_path)
158
+ enforce_cryptography_dependencies
159
+ if File.exist?(file_path)
160
+ system "openssl enc -aes-256-cbc -salt -in #{file_path} -out #{file_path}.enc -k #{ENV['TFSTATE_ENCRYPTION_PASSWORD']}"
161
+ end
162
+ end
163
+
164
+ def decrypt_file(file_path, enforce_file_existence=false)
165
+ enforce_cryptography_dependencies
166
+ if File.exist?("#{file_path}.enc")
167
+ system "openssl enc -aes-256-cbc -d -in #{file_path}.enc -out #{file_path} -k #{ENV['TFSTATE_ENCRYPTION_PASSWORD']}"
168
+ elsif enforce_file_existence
169
+ raise "Could not find #{file_path}.enc"
170
+ end
171
+ end
172
+
173
+ def state_path
174
+ "#{config_environment_path}/#{options[:environment]}.tfstate"
175
+ end
176
+
177
+ def shared_state_path
178
+ "#{shared_config_directory}/shared.tfstate"
179
+ end
180
+
181
+ def var_file_path
182
+ "#{config_environment_path}/variables.tfvars"
183
+ end
184
+
185
+ def config_directory
186
+ "config/infrastructure/#{infrastructure}"
187
+ end
188
+
189
+ def shared_config_directory
190
+ "config/infrastructure/#{infrastructure}/shared"
191
+ end
192
+
193
+ def config_environment_path
194
+ "#{config_directory}/environments/#{options[:environment]}"
195
+ end
196
+
197
+ def infrastructure
198
+ raise 'implement me'
199
+ end
200
+
201
+ def output_cmd(state_file_path, name=nil)
202
+ "terraform output -state=#{state_file_path} #{name}"
203
+ end
204
+
205
+ def output_variable(state_file_path, name)
206
+ `#{output_cmd(state_file_path, name)}`.chomp
207
+ end
208
+
209
+ def output_variables(state_file_path)
210
+ raw_plan_output = `#{output_cmd(state_file_path)}`
211
+ output_vars = {}
212
+ raw_plan_output.split("\n").each do |output_var|
213
+ key, value = output_var.split("=")
214
+ output_vars[key.strip] = value.strip
215
+ end
216
+
217
+ output_vars
218
+ end
219
+
220
+ def terraform_version
221
+ version_string = `terraform version`.chomp
222
+ version = /(\d+.\d+.\d+)/.match(version_string)
223
+ version[0]
224
+ end
225
+
226
+ def enforce_version!
227
+ if Gem::Version.new(terraform_version) < Gem::Version.new(TERRAFORM_VERSION)
228
+ raise "Terraform #{terraform_version} is out of date, please upgrade"
229
+ end
230
+ end
231
+
232
+ def enforce_cryptography_dependencies
233
+ raise "must supply TFSTATE_ENCRYPTION_PASSWORD environmental variable" if ENV['TFSTATE_ENCRYPTION_PASSWORD'].blank?
234
+ end
235
+
236
+ def settings
237
+ {} # override me to pass more variables into the terraform plan.
238
+ end
239
+
240
+ def asg_launch_config_name(asg_name)
241
+ asg_client = Aws::AutoScaling::Client.new(region: 'us-east-1')
242
+ group = asg_client.describe_auto_scaling_groups(auto_scaling_group_names: [asg_name]).auto_scaling_groups.first
243
+ group.try(:launch_configuration_name)
244
+ end
245
+
246
+ def env_variable_keys
247
+ {} # override me to pass environmental variables into the terraform plan
248
+ end
249
+
250
+ def shared_plan_variables
251
+ decrypt_file(shared_state_path, false)
252
+ if File.exist?(shared_state_path)
253
+ output_variables(shared_state_path)
254
+ else
255
+ {}
256
+ end
257
+ end
258
+
259
+ def fetch_terraform_modules
260
+ system "terraform get -update=true #{config_directory}"
261
+ end
262
+
263
+ def current_tfstate
264
+ return @current_tfstate if defined?(@current_tfstate)
265
+ raw_conf = File.read(state_path)
266
+ @current_tfstate = JSON.parse(raw_conf)
267
+ end
268
+
269
+ def read_config_file(path)
270
+ File.new('config/' + path).read
271
+ template = ERB.new File.new("config/#{path}").read, nil, "%"
272
+ template.result(OpenStruct.new(options).instance_eval { binding })
273
+ end
274
+
275
+ def dynect
276
+ @dynect ||= DynectRest.new("controlshiftlabs", ENV['DYNECT_USERNAME'], ENV['DYNECT_PASSWORD'], "controlshiftlabs.com")
277
+ end
278
+
279
+ def dns_record_exists?(parent_node_fqdn, record)
280
+ dynect.node_list(nil, parent_node_fqdn).include?(record.fqdn)
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,22 @@
1
+ module Hashicorptools
2
+ class UpdateLaunchConfiguration < Thor
3
+ desc 'deploy ASG_NAME', 'recycle instances in the ASG with no downtime'
4
+ def deploy(asg_name)
5
+ asg = AutoScalingGroup.new(name: asg_name)
6
+ if asg.group.nil?
7
+ raise "could not find asg #{asg_name}"
8
+ end
9
+ current_count = asg.group.instances.size || 1
10
+
11
+ if asg.group.max_size < (current_count * 2)
12
+ raise "max size must be more than twice current count to deploy a new AMI"
13
+ else
14
+ # first doulbe the instance count to get new launch config live.
15
+ asg.set_desired_instances(current_count * 2)
16
+
17
+ # then bring the instance count back down again.
18
+ asg.set_desired_instances(current_count)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ module Hashicorptools
2
+ module Variables
3
+ def aws_credentials_settings(settings_overrides = {})
4
+ {aws_access_key: ENV['AWS_ACCESS_KEY_ID'],
5
+ aws_secret_key: ENV['AWS_SECRET_ACCESS_KEY']}.merge(settings_overrides)
6
+ end
7
+
8
+ def variables(settings_overrides = {})
9
+ aws_credentials_settings(settings_overrides).collect{|key,value| "-var '#{key}=#{value}'" }.join(' ')
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ require 'bundler/setup'
2
+ require 'dotenv'
3
+ require 'thor'
4
+ require 'active_support/all'
5
+ require 'aws-sdk-v1'
6
+ require 'aws-sdk'
7
+ require 'dynect_rest'
8
+
9
+ module Hashicorptools
10
+ end
11
+
12
+ require_relative 'hashicorptools/variables'
13
+ require_relative 'hashicorptools/ec2_utilities'
14
+ require_relative 'hashicorptools/auto_scaling_group'
15
+ require_relative 'hashicorptools/packer'
16
+ require_relative 'hashicorptools/terraform'
17
+ require_relative 'hashicorptools/host'
18
+ require_relative 'hashicorptools/update_launch_configuration'
19
+ require_relative 'hashicorptools/code_deploy'
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Hashicorptools" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ require 'simplecov'
2
+
3
+ module SimpleCov::Configuration
4
+ def clean_filters
5
+ @filters = []
6
+ end
7
+ end
8
+
9
+ SimpleCov.configure do
10
+ clean_filters
11
+ load_adapter 'test_frameworks'
12
+ end
13
+
14
+ ENV["COVERAGE"] && SimpleCov.start do
15
+ add_filter "/.rvm/"
16
+ end
17
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
18
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
19
+
20
+ require 'rspec'
21
+ require 'hashicorptools'
22
+
23
+ # Requires supporting files with custom matchers and macros, etc,
24
+ # in ./support/ and its subdirectories.
25
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
26
+
27
+ RSpec.configure do |config|
28
+
29
+ end