inf 0.0.9

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
+ SHA1:
3
+ metadata.gz: 575f2c3299b818ecc71262fcc870d0c37c650ff1
4
+ data.tar.gz: ee6999576321334813dfa5a7d50395b3e25ac64a
5
+ SHA512:
6
+ metadata.gz: c0d28adc8da0b26881ead0ddc7041def45d54e01ab61d51ca73ccf515ae13bb296c33a5f913866d53fa8cd7730cce22ab79a4ab9b4dfb973d91f8b47dc58a8b5
7
+ data.tar.gz: 6aeaa6eaf7ec2f03fb9198ebb22fb40603416b4c934a704aa6d56f79bd7f55ca5616a2e5d3555419a9df6ee911314c3e812269c85d516e11a6afb98b8b39cec8
data/bin/inf ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'thor'
4
+ require 'aws-sdk'
5
+ require 'time'
6
+ require 'pry'
7
+ require 'awesome_print'
8
+ require 'base64'
9
+ require 'colored'
10
+ require 'dotenv'
11
+
12
+ require_relative '../lib/inf.rb'
13
+
14
+ Dotenv.load('inf.env')
15
+
16
+ def require_env_vars(vars)
17
+ vars.each do |v|
18
+ raise ArgumentError.new("Missing env var #{v}") unless ENV[v]
19
+ end
20
+ end
21
+
22
+ require_env_vars %w(
23
+ STATE_BUCKET
24
+ APP_NAME
25
+ AWS_REGION
26
+ SPOT_PRICE
27
+ TARGET_CAPACITY
28
+ AMI
29
+ FLEET_ROLE
30
+ INSTANCE_PROFILE_NAME
31
+ SG
32
+ ELB_SG
33
+ SUBNET_IDS
34
+ KEY_NAME
35
+ CERT_ARN
36
+ JENKINS_PUBLIC_KEY
37
+ )
38
+
39
+ ENV['TERMINATE_INSTANCES'] ||= "true"
40
+ ENV['INSTANCE_TYPES'] = 'm1.small,m1.medium'
41
+ ENV['DNS_TTL'] ||= "30"
42
+
43
+ SSH_CMD='ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
44
+
45
+ class CLI < Thor
46
+ register Inf::Fleet, :fleet, 'fleet', 'operate on the fleet'
47
+ register Inf::LoadBalancer, :lb, 'lb', 'operate on load balancer'
48
+ register Inf::Env, :env, 'env', "operate on app environment"
49
+ # register Inf::Domains, :domains, 'domains', 'operate on domains'
50
+
51
+ def method_missing(m, *args, &block)
52
+ Inf.send(m, *args, &block)
53
+ end
54
+
55
+ desc 'release [TARBALL_PATH]', 'create release from slug (default: ./slug.tgz)'
56
+ def release(tarball_path='./slug.tgz')
57
+ release_id = latest_release + 1
58
+
59
+ tarball_path = File.expand_path tarball_path
60
+
61
+ puts "Uploading slug to s3 (could take a moment)...".yellow
62
+ put_state("apps/#{app_name}/releases/v#{release_id}.tgz", File.read(tarball_path))
63
+ put_state("apps/#{app_name}/latest_release_id", release_id)
64
+ end
65
+
66
+ desc 'migrate', 'migrate state'
67
+ def migrate
68
+ Inf::Migrations.maybe_migrate!
69
+ end
70
+
71
+ desc 'deploy [RELEASE_ID]', 'deploy release_id (default: currently deployed release)'
72
+ def deploy(release_id=nil)
73
+ release_id ||= current_release || latest_release
74
+
75
+ unless release_id
76
+ puts "No current or latest release; please specify a release"
77
+ return
78
+ end
79
+
80
+
81
+ # set the current release
82
+ # app servers will pull this and use
83
+ # it to deploy
84
+ put_state("apps/#{app_name}/current_release_id", release_id)
85
+
86
+ # loop over app servers and tell them
87
+ # they should deploy
88
+ Inf::Fleet.each_fleet do |name|
89
+ puts "Deploying release v#{release_id} to fleet #{name}...".blue
90
+
91
+ Inf::Fleet.ips(name).each do |ip|
92
+ puts "Deploying to #{ip}...".gray
93
+ `#{SSH_CMD} -t ubuntu@#{ip} fork-and-deploy-app`
94
+ $?.success? or exit 1
95
+ end
96
+ end
97
+ end
98
+
99
+ desc 'deploy_latest', 'deploy newest release to app'
100
+ def deploy_latest
101
+ deploy latest_release
102
+ end
103
+
104
+ desc 'latest_release', 'show version number of latest release'
105
+ def latest_release
106
+ get_state("apps/#{app_name}/latest_release_id").tap do |id|
107
+ puts "The latest release is v#{id} (it may not be deployed at the moment)".blue
108
+ end.to_i
109
+ end
110
+
111
+ desc 'current_release', 'show version number of current release'
112
+ def current_release
113
+ get_state("apps/#{app_name}/current_release_id").tap do |id|
114
+ puts "The current release is v#{id}".blue
115
+ end.to_i
116
+ end
117
+
118
+ desc 'releases', 'list releases'
119
+ def releases
120
+ list_state "apps/#{app_name}/releases" do |key|
121
+ ap key
122
+ end
123
+ end
124
+
125
+ desc 'restart', 'restart servers'
126
+ def restart
127
+ Inf::Fleet.each_fleet do |name|
128
+ puts "Restarting fleet #{name}...".blue
129
+
130
+ Inf::Fleet.ips(name).each do |ip|
131
+ puts "Restarting #{ip}...".gray
132
+ `#{SSH_CMD} -t ubuntu@#{ip} fork-and-restart-app`
133
+ $?.success? or exit 1
134
+ end
135
+ end
136
+ end
137
+
138
+ desc 'clear_state', 'resets the s3 state'
139
+ def clear_state
140
+ delete_spec = s3.list_objects_v2(
141
+ bucket: state_bucket
142
+ ).contents.map { |object| {key: object.key} }
143
+
144
+ if delete_spec.empty?
145
+ puts "Had no state".yellow
146
+ else
147
+ s3.delete_objects(
148
+ bucket: state_bucket,
149
+ delete: {
150
+ objects: delete_spec
151
+ }
152
+ )
153
+
154
+ puts "State cleared".blue
155
+ end
156
+ end
157
+
158
+ desc 'open [BROWSER]', 'opens app in browser'
159
+ def open(browser='google-chrome')
160
+ url = get_state("apps/#{app_name}/lb-dns")
161
+ puts "Opening #{url} in #{browser}".blue
162
+ `#{browser} #{url}`
163
+ end
164
+ end
165
+
166
+ CLI.start(ARGV)
@@ -0,0 +1,127 @@
1
+ #!/bin/bash
2
+
3
+ . ./bootstrap-env
4
+
5
+ INSTANCE_ID=$(curl http://169.254.169.254/latest/meta-data/instance-id)
6
+ LB_NAME=$(aws s3 cp s3://$STATE_BUCKET/apps/$APP_NAME/lb-name -)
7
+
8
+ cat <<EOF | logger -t bootstrap
9
+ beginning instance bootstrap for $INSTANCE_ID
10
+ app: $APP_NAME
11
+ state_bucket: $STATE_BUCKET
12
+ lb_name: $LB_NAME
13
+ fleet_name: $FLEET_NAME
14
+ EOF
15
+
16
+ if [ $FLEET_NAME == "web" ]; then
17
+ echo "Registering with load balancer $LB_NAME" | logger -t bootstrap
18
+
19
+ aws elb register-instances-with-load-balancer \
20
+ --load-balancer-name $LB_NAME \
21
+ --instances $INSTANCE_ID \
22
+ --region $AWS_REGION
23
+ fi
24
+
25
+ mkdir -p /var/app
26
+
27
+ cat > /var/app/env <<EOF
28
+ APP_NAME=$APP_NAME
29
+ STATE_BUCKET=$STATE_BUCKET
30
+ FLEET_NAME=$FLEET_NAME
31
+ EOF
32
+
33
+ echo "Wrote /var/app/env" | logger -t bootstrap
34
+
35
+ chown -R ubuntu:ubuntu /var/app
36
+
37
+ mkdir -p /home/ubuntu/.ssh
38
+ echo "$JENKINS_PUBLIC_KEY" >> /home/ubuntu/.ssh/authorized_keys
39
+ chown -R ubuntu:ubuntu /home/ubuntu/.ssh
40
+
41
+ echo "Wrote jenkins public key" | logger -t bootstrap
42
+
43
+ cat > /usr/local/bin/deploy-app <<EOF
44
+ #!/bin/bash
45
+
46
+ cd /var/app
47
+ . ./env
48
+
49
+ DEPLOYED_VERSION=`cat /var/app/release_id`
50
+ VERSION=\$1
51
+
52
+ if [ -z \$VERSION ]; then
53
+ VERSION=\$(aws s3 cp s3://\$STATE_BUCKET/apps/\$APP_NAME/current_release_id -)
54
+ fi
55
+
56
+ if [ "\$VERSION" == "\$DEPLOYED_VERSION" ]; then
57
+ echo "The desired version (\$VERSION) is currently deployed; exiting"
58
+ exit 0
59
+ elif [ -z \$DEPLOYED_VERSION ]; then
60
+ echo "Bootstrapping instance with version \$VERSION"
61
+ else
62
+ echo "Had version \$DEPLOYED_VERSION; deploying \$VERSION"
63
+ fi
64
+
65
+ aws s3 cp s3://\$STATE_BUCKET/apps/\$APP_NAME/releases/v\$VERSION.tgz /tmp/slug.tgz
66
+
67
+ rm -rf staging || true
68
+ mkdir staging
69
+ cd staging
70
+ tar -xzf /tmp/slug.tgz
71
+
72
+ . bin/pre-deploy
73
+
74
+ cd /var/app
75
+
76
+ rm -rf live
77
+ mv staging live
78
+
79
+ cd live
80
+
81
+ . bin/restart
82
+
83
+ echo \$VERSION > /var/app/release_id
84
+ EOF
85
+
86
+ cat > /usr/local/bin/restart-app <<EOF
87
+ #!/bin/bash
88
+
89
+ cd /var/app
90
+ . ./env
91
+
92
+ cd live
93
+
94
+ . bin/restart
95
+ EOF
96
+
97
+
98
+ cat > /usr/local/bin/fork-and-deploy-app <<EOF
99
+ #!/bin/bash -l
100
+
101
+ ( setsid deploy-app 2>&1 | setsid logger -t deploy ) & disown
102
+
103
+ sleep 1
104
+ echo "Forked... bye"
105
+ EOF
106
+
107
+ cat > /usr/local/bin/fork-and-restart-app <<EOF
108
+ #!/bin/bash -l
109
+
110
+ ( setsid restart-app 2>&1 | setsid logger -t deploy ) & disown
111
+
112
+ sleep 1
113
+ echo "Forked... bye"
114
+ EOF
115
+
116
+
117
+ chmod +x /usr/local/bin/deploy-app
118
+ chmod +x /usr/local/bin/fork-and-deploy-app
119
+ chmod +x /usr/local/bin/restart-app
120
+ chmod +x /usr/local/bin/fork-and-restart-app
121
+
122
+ echo "Wrote deploy scripts" | logger -t bootstrap
123
+
124
+ echo "Running initial deploy as user 'ubuntu'..."
125
+ nohup sudo -u ubuntu fork-and-deploy-app & disown
126
+
127
+ echo "That's it for bootstrap!"
data/lib/aws.rb ADDED
@@ -0,0 +1,131 @@
1
+ module Inf::AWS
2
+ def self.method_missing(m, *args, &block)
3
+ Inf.send(m, *args, &block)
4
+ end
5
+
6
+ def self.ec2
7
+ @ec2 ||= Aws::EC2::Client.new
8
+ end
9
+
10
+ def self.elb
11
+ @elb ||= Aws::ElasticLoadBalancing::Client.new
12
+ end
13
+
14
+ def self.s3
15
+ @s3 ||= Aws::S3::Client.new
16
+ end
17
+
18
+ def self.route53
19
+ @route53 ||= Aws::Route53::Client.new
20
+ end
21
+
22
+ def self.bootstrap_script(app_name, state_bucket, fleet_name)
23
+ <<BOOTSTRAP_SCRIPT
24
+ #!/bin/bash
25
+
26
+ cd /tmp
27
+
28
+ cat > bootstrap-env <<EOF
29
+ export APP_NAME=#{app_name}
30
+ export STATE_BUCKET=#{state_bucket}
31
+ export FLEET_NAME=#{fleet_name}
32
+ export AWS_REGION=#{ENV['AWS_REGION']}
33
+ export JENKINS_PUBLIC_KEY="#{ENV['JENKINS_PUBLIC_KEY']}"
34
+ EOF
35
+
36
+ . ./bootstrap-env
37
+
38
+ aws s3 cp s3://$STATE_BUCKET/apps/$APP_NAME/bootstrap-instance.sh ./bootstrap.sh
39
+ chmod +x bootstrap.sh
40
+
41
+ ( setsid ./bootstrap.sh 2>&1 | setsid logger -t bootstrap ) & disown
42
+
43
+ BOOTSTRAP_SCRIPT
44
+ end
45
+
46
+ def self.launch_load_balancer
47
+ lb_name = "app-lb-#{app_name}"
48
+ lb_dns = elb.create_load_balancer(
49
+ load_balancer_name: lb_name,
50
+ subnets: ENV['SUBNET_IDS'].split(','),
51
+ security_groups: [ENV['ELB_SG']],
52
+ scheme: "internet-facing",
53
+ listeners: [{
54
+ instance_port: 5000,
55
+ instance_protocol: 'HTTP',
56
+ load_balancer_port: 80,
57
+ protocol: 'HTTP'
58
+ }, {
59
+ instance_port: 5000,
60
+ instance_protocol: "HTTP",
61
+ load_balancer_port: 443,
62
+ protocol: "HTTPS",
63
+ ssl_certificate_id: ENV['CERT_ARN'],
64
+ }]
65
+ ).dns_name
66
+
67
+ elb.configure_health_check(
68
+ load_balancer_name: lb_name,
69
+ health_check: {
70
+ target: 'HTTP:80/',
71
+ interval: 10, # seconds
72
+ timeout: 5, # seconds to respond
73
+ unhealthy_threshold: 2, # two consecutive failures
74
+ healthy_threshold: 2
75
+ }
76
+ )
77
+
78
+ put_state("apps/#{app_name}/lb-name", lb_name)
79
+ put_state("apps/#{app_name}/lb-dns", lb_dns)
80
+
81
+ puts "Launched load balancer (or it was running)".blue
82
+ end
83
+
84
+ def self.request_spot_fleet(fleet_name='default')
85
+ launch_specifications = ENV['SUBNET_IDS'].split(',').map do |subnet_id|
86
+ ENV['INSTANCE_TYPES'].split(',').map do |type|
87
+ {
88
+ image_id: ENV['AMI'],
89
+ key_name: ENV['KEY_NAME'],
90
+ instance_type: type,
91
+ monitoring: {
92
+ enabled: false,
93
+ },
94
+ network_interfaces: [{
95
+ associate_public_ip_address: true,
96
+ device_index: 0,
97
+ groups: [ENV['SG']],
98
+ subnet_id: subnet_id
99
+ }],
100
+ iam_instance_profile: {
101
+ name: ENV['INSTANCE_PROFILE_NAME'],
102
+ },
103
+ # ebs_optimized: type == 'm1.small' ? false : false,
104
+ weighted_capacity: 1.0,
105
+ user_data: Base64.encode64(bootstrap_script(app_name, state_bucket, fleet_name))
106
+ }
107
+ end
108
+ end.flatten
109
+
110
+ resp = ec2.request_spot_fleet(
111
+ spot_fleet_request_config: {
112
+ spot_price: ENV['SPOT_PRICE'],
113
+ target_capacity: ENV['TARGET_CAPACITY'],
114
+ valid_from: Time.now,
115
+ valid_until: Time.parse('1/1/2050'),
116
+ terminate_instances_with_expiration: ENV['TERMINATE_INSTANCES'],
117
+ iam_fleet_role: ENV['FLEET_ROLE'],
118
+ launch_specifications: launch_specifications,
119
+ excess_capacity_termination_policy: "default", # accepts noTermination, default
120
+ allocation_strategy: "diversified", # accepts lowestPrice, diversified
121
+ type: "maintain", # accepts request, maintain
122
+ replace_unhealthy_instances: true,
123
+ },
124
+ )
125
+
126
+ resp.spot_fleet_request_id.tap do |sfr|
127
+ puts "Launched spot fleet #{fleet_name} (#{sfr})".blue
128
+ put_state("apps/#{app_name}/fleets/#{fleet_name}/sfr", sfr)
129
+ end
130
+ end
131
+ end
data/lib/domains.rb ADDED
@@ -0,0 +1,91 @@
1
+ class Inf::Domains < Thor
2
+ def method_missing(m, *args, &block)
3
+ Inf.send(m, *args, &block)
4
+ end
5
+
6
+ def self.method_missing(m, *args, &block)
7
+ Inf.send(m, *args, &block)
8
+ end
9
+
10
+ def self.list
11
+ get_state("apps/#{app_name}/domains").lines rescue []
12
+ end
13
+
14
+ def self.hosted_zones
15
+ @hosted_zones ||= route53.list_hosted_zones_by_name.hosted_zones.reduce
16
+ binding.pry
17
+ end
18
+ end
19
+
20
+ def self.sync_to_route53
21
+ lb_dns = get_state("apps/#{app_name}/lb-dns")
22
+
23
+ hosted_zones
24
+
25
+ list.each do |domain|
26
+ route53.change_resource_record_sets({
27
+ change_batch: {
28
+ changes: [
29
+ {
30
+ action: "UPSERT",
31
+ resource_record_set: {
32
+ name: domain,
33
+ resource_records: [
34
+ {
35
+ value: lb_dns,
36
+ },
37
+ ],
38
+ ttl: ENV['DNS_TTL'],
39
+ type: "CNAME",
40
+ },
41
+ },
42
+ ],
43
+ comment: "load balancer for #{app_name}",
44
+ },
45
+ hosted_zone_id: zone_id,
46
+ })
47
+ end
48
+ end
49
+
50
+ desc 'domains', 'list domains'
51
+ def list
52
+ Inf::Domains.list.each do |domain|
53
+ puts domain
54
+ end
55
+ end
56
+
57
+ desc 'sync', 'sync to route53'
58
+ def sync
59
+ Inf::Domains.sync_to_route53
60
+ end
61
+
62
+ desc 'add DOMAIN', 'cname DOMAIN to the load balancer'
63
+ def add(domain)
64
+ domains = Inf::Domains.list
65
+
66
+ if domains.include? domain
67
+ puts "Domain #{domain} is already configured".blue
68
+ return
69
+ end
70
+
71
+ domains += [domain]
72
+ put_state("apps/#{app_name}/domains", domains.join("\n"))
73
+
74
+ Inf::Domains.sync_to_route53
75
+ end
76
+
77
+ desc 'remove DOMAIN', 'un-cname DOMAIN'
78
+ def remove(domain)
79
+ domains = Inf::Domains.list
80
+
81
+ unless domains.include? domain
82
+ puts "Domain #{domain} is not configured".red
83
+ return
84
+ end
85
+
86
+ domains = domains.reject { |d| d == domain }
87
+ put_state("apps/#{app_name}/domains", domains.join("\n"))
88
+
89
+ Inf::Domains.sync_to_route53
90
+ end
91
+ end
data/lib/env.rb ADDED
@@ -0,0 +1,27 @@
1
+ class Inf::Env < Thor
2
+ def method_missing(m, *args, &block)
3
+ Inf.send(m, *args, &block)
4
+ end
5
+
6
+ def self.method_missing(m, *args, &block)
7
+ Inf.send(m, *args, &block)
8
+ end
9
+
10
+ desc 'push [FILE]', 'sets the .env file for the app (default: ./.env; pass "-" for stdin)'
11
+ def push(file='.env')
12
+ file = File.expand_path(file)
13
+
14
+ if file == '-'
15
+ env = STDIN.read
16
+ else
17
+ env = File.read(file)
18
+ end
19
+
20
+ put_state("apps/#{app_name}/env", env)
21
+ end
22
+
23
+ desc 'pull', 'gets env vars'
24
+ def pull
25
+ puts get_state("apps/#{app_name}/env")
26
+ end
27
+ end
data/lib/fleet.rb ADDED
@@ -0,0 +1,167 @@
1
+ class Inf::Fleet < Thor
2
+ def method_missing(m, *args, &block)
3
+ Inf.send(m, *args, &block)
4
+ end
5
+
6
+ def self.method_missing(m, *args, &block)
7
+ Inf.send(m, *args, &block)
8
+ end
9
+
10
+ no_commands do
11
+ def get_sfr(fleet_name='web')
12
+ @sfrs ||= {}
13
+ @sfrs[fleet_name] ||= get_state("apps/#{app_name}/fleets/#{fleet_name}/sfr")
14
+ end
15
+ end
16
+
17
+ def self.each_fleet(&block)
18
+ list_state("apps/#{app_name}/fleets") do |key|
19
+ key.match(/fleets\/([^\/]+)/)[1]
20
+ end.uniq.each { |name| block.call name }
21
+ end
22
+
23
+ def self.ips(fleet_name='web')
24
+ sfr = new.get_sfr fleet_name
25
+
26
+ unless sfr
27
+ puts "No sfr for #{app_name}/#{fleet_name}".red
28
+ puts "Is a fleet launched?"
29
+ exit 1
30
+ end
31
+
32
+ instance_ids = ec2.describe_spot_fleet_instances(
33
+ spot_fleet_request_id: sfr
34
+ ).active_instances.map(&:instance_id)
35
+
36
+ if instance_ids.empty?
37
+ []
38
+ else
39
+ ec2.describe_instances(
40
+ instance_ids: instance_ids
41
+ ).reservations.map do |it|
42
+ it.instances.first.public_ip_address
43
+ end
44
+ end
45
+ end
46
+
47
+ desc 'launch', 'launch app fleet; check env vars in code'
48
+ def launch(fleet_name='web')
49
+ sfr = get_sfr fleet_name
50
+
51
+ if sfr
52
+ puts "Kill existing sfr first".red
53
+ exit 1
54
+ end
55
+
56
+ Inf::AWS.request_spot_fleet fleet_name
57
+
58
+ script = File.expand_path "../../bootstrap-scripts/default-app.sh", __FILE__
59
+
60
+ put_state "apps/#{app_name}/bootstrap-instance.sh", File.read(script)
61
+ puts "Don't forget to deploy something".yellow
62
+ end
63
+
64
+ desc 'kill', 'kill fleet'
65
+ def kill(fleet_name='web')
66
+ sfr = get_sfr fleet_name
67
+
68
+ if sfr
69
+ ec2.cancel_spot_fleet_requests(
70
+ spot_fleet_request_ids: [sfr],
71
+ terminate_instances: true
72
+ )
73
+ puts "Killed #{sfr}".blue
74
+ else
75
+ puts "Did not find a sfr".yellow
76
+ end
77
+
78
+ delete_state "apps/#{app_name}/fleets/#{fleet_name}/sfr"
79
+ end
80
+
81
+ desc 'rename [NAME] [NEW_NAME]', 'rename fleet'
82
+ def rename(name, new_name)
83
+ sfr = get_sfr new_name
84
+
85
+ if sfr
86
+ puts "Fleet #{new_name} already exists".red
87
+ exit 1
88
+ end
89
+
90
+ list_state("apps/#{app_name}/fleets/#{name}") do |key|
91
+ leaf = key.split('/').last
92
+
93
+ s3.copy_object(
94
+ bucket: state_bucket,
95
+ key: "apps/#{app_name}/fleets/#{new_name}/#{leaf}",
96
+ copy_source: "#{state_bucket}/#{key}"
97
+ )
98
+
99
+ delete_state key
100
+ end
101
+ end
102
+
103
+ desc 'nodes', 'list public ips of nodes in fleet'
104
+ def nodes(fleet_name='web')
105
+ ips = Inf::Fleet.ips fleet_name
106
+
107
+ if ips.any?
108
+ puts ips.join("\n")
109
+ else
110
+ puts "No active instances in fleet"
111
+ end
112
+ end
113
+
114
+ # TODO make tmux optional
115
+ desc 'ssh', 'ssh to a node in the fleet'
116
+ def ssh(fleet_name='web')
117
+ ips = Inf::Fleet.ips fleet_name
118
+
119
+ if ips.empty?
120
+ puts "No instances in fleet #{fleet_name}".red
121
+ else
122
+ ip = ips.first
123
+
124
+ puts "Opening ssh to #{ip} in a tmux split".blue
125
+ `tmux split-window "#{SSH_CMD} ubuntu@#{ips.first}"`
126
+ end
127
+ end
128
+
129
+ desc 'scale FLEET_NAME COUNT', 'scales the fleet'
130
+ def scale(fleet_name, count)
131
+ sfr = get_sfr fleet_name
132
+
133
+ ec2.modify_spot_fleet_request({
134
+ spot_fleet_request_id: sfr,
135
+ target_capacity: count.to_i,
136
+ excess_capacity_termination_policy: "default"
137
+ })
138
+ end
139
+
140
+ desc 'status', 'describes the fleet'
141
+ def status(fleet_name='web', indent='')
142
+ nodes = Inf::Fleet.ips fleet_name
143
+ sfr = get_state("apps/#{app_name}/fleets/#{fleet_name}/sfr")
144
+
145
+ sfr_status = ec2.describe_spot_fleet_requests(
146
+ spot_fleet_request_ids: [sfr]
147
+ ).spot_fleet_request_configs[0].spot_fleet_request_state
148
+
149
+ if nodes.any?
150
+ puts "Fleet #{fleet_name} has #{nodes.count} nodes (#{nodes.join ', '})".blue
151
+ elsif sfr
152
+ puts "Fleet #{fleet_name} (#{sfr}: #{sfr_status}) has no nodes!".red
153
+ else
154
+ puts "No fleet #{fleet_name} is launched".yelow
155
+ end
156
+ end
157
+
158
+ desc 'list', 'lists fleets'
159
+ def list
160
+ Inf::Fleet.each_fleet { |name| puts name }
161
+ end
162
+
163
+ desc 'status_all', 'runs `status` for all fleets'
164
+ def status_all
165
+ Inf::Fleet.each_fleet { |name| status name }
166
+ end
167
+ end
data/lib/inf.rb ADDED
@@ -0,0 +1,73 @@
1
+ require 'mini_cache'
2
+
3
+ module Inf
4
+ def self.cache
5
+ @cache ||= MiniCache::Store.new
6
+ end
7
+
8
+ def self.state_bucket
9
+ ENV['STATE_BUCKET'].downcase
10
+ end
11
+
12
+ def self.app_name
13
+ ENV['APP_NAME'].downcase
14
+ end
15
+
16
+ def self.put_state(k, v)
17
+ v = v.to_s
18
+ s3.put_object(
19
+ bucket: state_bucket,
20
+ key: k,
21
+ body: v
22
+ )
23
+
24
+ summary = v.lines.count > 1 ? '[multiline]' : v
25
+ cache.unset k
26
+ puts "#{k} -> #{summary}".gray
27
+ end
28
+
29
+ def self.list_state(prefix)
30
+ s3.list_objects_v2(
31
+ bucket: state_bucket,
32
+ prefix: prefix,
33
+ ).contents.map do |it|
34
+ yield it.key
35
+ end
36
+ end
37
+
38
+ def self.get_state(k, opts={})
39
+ cache.get_or_set(k) do
40
+ begin
41
+ object = s3.get_object(bucket: state_bucket, key: k)
42
+
43
+ if opts[:raw]
44
+ object
45
+ else
46
+ object.body.read
47
+ end
48
+ rescue Aws::S3::Errors::NoSuchKey
49
+ end
50
+ end
51
+ end
52
+
53
+ def self.delete_state(k)
54
+ s3.delete_object(bucket: state_bucket, key: k) rescue nil
55
+ end
56
+
57
+ class << self
58
+ %i(s3 ec2 elb route53).each do |m|
59
+ define_method m do
60
+ AWS.send m
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ require 'thor'
67
+
68
+ require_relative 'migrations.rb'
69
+ require_relative 'aws.rb'
70
+ require_relative 'fleet.rb'
71
+ require_relative 'env.rb'
72
+ require_relative 'load_balancer.rb'
73
+ # require_relative 'domains.rb'
@@ -0,0 +1,53 @@
1
+ class Inf::LoadBalancer < Thor
2
+ def method_missing(m, *args, &block)
3
+ Inf.send(m, *args, &block)
4
+ end
5
+
6
+ def self.method_missing(m, *args, &block)
7
+ Inf.send(m, *args, &block)
8
+ end
9
+
10
+ desc 'launch', 'launch load balancer for app'
11
+ def launch
12
+ Inf::AWS.launch_load_balancer
13
+ end
14
+
15
+ desc 'kill', 'kill lb for app'
16
+ def kill
17
+ lb_name = get_state "apps/#{app_name}/lb-name"
18
+ delete_state "apps/#{app_name}/lb-name"
19
+
20
+ if lb_name
21
+ elb.delete_load_balancer(
22
+ load_balancer_name: lb_name
23
+ )
24
+
25
+ delete_state "apps/#{app_name}/lb-dns"
26
+ puts "Deleted lb #{lb_name}".blue
27
+ else
28
+ puts "Did not find an lb".yellow
29
+ end
30
+ end
31
+
32
+ desc 'status', 'lb status'
33
+ def status
34
+ lb_name = get_state "apps/#{app_name}/lb-name"
35
+
36
+ unless lb_name
37
+ puts "Load balancer is not configured".blue
38
+ return
39
+ end
40
+
41
+ lb = elb.
42
+ describe_load_balancers(load_balancer_names: [lb_name]).
43
+ load_balancer_descriptions[0]
44
+
45
+ unless lb
46
+ puts "Load balancer expected but not found!".red
47
+ return
48
+ end
49
+
50
+ puts "Load balancer is at #{lb.dns_name} with ".blue +
51
+ "#{lb.instances.count} instances".blue
52
+ end
53
+ end
data/lib/migrations.rb ADDED
@@ -0,0 +1,18 @@
1
+ module Inf::Migrations
2
+ def self.method_missing(m, *args, &block)
3
+ Inf.send(m, *args, &block)
4
+ end
5
+
6
+ def self.maybe_migrate!
7
+ if single_sfr = get_state("apps/#{app_name}/sfr")
8
+ migrate_to_multi_fleets single_sfr
9
+ end
10
+ end
11
+
12
+ # before: apps/:name/sfr is the only fleet
13
+ # after: apps/:name/fleets/web/sfr
14
+ def self.migrate_to_multi_fleets(sfr)
15
+ Inf.put_state "apps/#{app_name}/fleets/web/sfr", sfr
16
+ Inf.delete_state "apps/#{app_name}/sfr"
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.9
5
+ platform: ruby
6
+ authors:
7
+ - Kyle Brett <kyle@kylebrett.com>
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-05-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.19'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.19'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.9'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: colored
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mini_cache
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '1'
83
+ description:
84
+ email: kyle@kylebrett.com
85
+ executables:
86
+ - inf
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - bin/inf
91
+ - bootstrap-scripts/default-app.sh
92
+ - lib/aws.rb
93
+ - lib/domains.rb
94
+ - lib/env.rb
95
+ - lib/fleet.rb
96
+ - lib/inf.rb
97
+ - lib/load_balancer.rb
98
+ - lib/migrations.rb
99
+ homepage: http://www.kylebrett.com
100
+ licenses:
101
+ - Nonstandard
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.6.8
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: inf
123
+ test_files: []