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 +7 -0
- data/bin/inf +166 -0
- data/bootstrap-scripts/default-app.sh +127 -0
- data/lib/aws.rb +131 -0
- data/lib/domains.rb +91 -0
- data/lib/env.rb +27 -0
- data/lib/fleet.rb +167 -0
- data/lib/inf.rb +73 -0
- data/lib/load_balancer.rb +53 -0
- data/lib/migrations.rb +18 -0
- metadata +123 -0
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: []
|