inf 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|