inf 0.0.9

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 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: []