shiprails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +19 -0
- data/.rbenv-vars.example +3 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +42 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/.DS_Store +0 -0
- data/exe/port +6 -0
- data/exe/ship +6 -0
- data/lib/shiprails/application.rb +13 -0
- data/lib/shiprails/port.rb +53 -0
- data/lib/shiprails/ship/config.rb +121 -0
- data/lib/shiprails/ship/deploy.rb +191 -0
- data/lib/shiprails/ship/exec.rb +140 -0
- data/lib/shiprails/ship/install/.env.erb +23 -0
- data/lib/shiprails/ship/install/Dockerfile.erb +28 -0
- data/lib/shiprails/ship/install/Dockerfile.production.erb +29 -0
- data/lib/shiprails/ship/install/docker-compose.yml.erb +81 -0
- data/lib/shiprails/ship/install/shiprails.yml.erb +29 -0
- data/lib/shiprails/ship/install.rb +159 -0
- data/lib/shiprails/ship/scale.rb +81 -0
- data/lib/shiprails/ship/setup.rb +245 -0
- data/lib/shiprails/ship/task.rb +75 -0
- data/lib/shiprails/ship.rb +61 -0
- data/lib/shiprails/version.rb +3 -0
- data/lib/shiprails.rb +5 -0
- data/shiprails.gemspec +35 -0
- metadata +194 -0
@@ -0,0 +1,140 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
require "aws-sdk"
|
3
|
+
require "thor/group"
|
4
|
+
|
5
|
+
module Shiprails
|
6
|
+
class Ship < Thor
|
7
|
+
class Exec < Thor::Group
|
8
|
+
include Thor::Actions
|
9
|
+
class_option "path",
|
10
|
+
aliases: ["-p"],
|
11
|
+
default: ".",
|
12
|
+
desc: "Specify a configuration path"
|
13
|
+
class_option "environment",
|
14
|
+
default: "production",
|
15
|
+
desc: "Specify the environment"
|
16
|
+
class_option "region",
|
17
|
+
default: "us-west-2",
|
18
|
+
desc: "Specify the region"
|
19
|
+
class_option "service",
|
20
|
+
default: "app",
|
21
|
+
desc: "Specify the service name"
|
22
|
+
class_option "private-key",
|
23
|
+
default: "~/.ssh/aws.pem",
|
24
|
+
desc: "Specify the AWS SSH private key path"
|
25
|
+
|
26
|
+
def run_command
|
27
|
+
region = options['region']
|
28
|
+
service = options['service']
|
29
|
+
cluster_name = "#{project_name}_#{options['environment']}"
|
30
|
+
command_string = args.join ' '
|
31
|
+
ssh_private_key_path = options['private-key']
|
32
|
+
ecs_exec(region, cluster_name, service, command_string, ssh_private_key_path)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def configuration
|
38
|
+
YAML.load(File.read("#{options[:path]}/.shiprails.yml")).deep_symbolize_keys
|
39
|
+
end
|
40
|
+
|
41
|
+
def aws_access_key_id
|
42
|
+
@aws_access_key_id ||= ask "AWS Access Key ID", default: ENV.fetch("AWS_ACCESS_KEY_ID")
|
43
|
+
end
|
44
|
+
|
45
|
+
def aws_access_key_secret
|
46
|
+
@aws_access_key_secret ||= ask "AWS Access Key Secret", default: ENV.fetch("AWS_SECRET_ACCESS_KEY")
|
47
|
+
end
|
48
|
+
|
49
|
+
def project_name
|
50
|
+
configuration[:project_name]
|
51
|
+
end
|
52
|
+
|
53
|
+
def ecs_exec(region, cluster, service, command, ssh_private_key_path, ssh_user: 'ec2-user')
|
54
|
+
# we'll need to use both the ecs and ec2 apis
|
55
|
+
ecs = Aws::ECS::Client.new(region: region)
|
56
|
+
ec2 = Aws::EC2::Client.new(region: region)
|
57
|
+
|
58
|
+
# first we get the ARN of the task managed by the service
|
59
|
+
tasks_list = ecs.list_tasks({cluster: cluster, desired_status: 'RUNNING', service_name: service})
|
60
|
+
task_arn = tasks_list.task_arns.first
|
61
|
+
|
62
|
+
# using the ARN of the task, we can get the ARN of the container instance where its being deployed
|
63
|
+
task_descriptions = ecs.describe_tasks({cluster: cluster, tasks: [task_arn]})
|
64
|
+
task_definition_arn = task_descriptions.tasks.first.task_definition_arn
|
65
|
+
task_definition_name = task_definition_arn.split('/').last
|
66
|
+
container_instance_arn = task_descriptions.tasks.first.container_instance_arn
|
67
|
+
task_definition_description = ecs.describe_task_definition({task_definition: task_definition_name})
|
68
|
+
|
69
|
+
say "Setting up EC2 instance for SSH..."
|
70
|
+
# with the instance ARN let's grab the intance id
|
71
|
+
ec2_instance_id = ecs.describe_container_instances({cluster: cluster, container_instances: [container_instance_arn]}).container_instances.first.ec2_instance_id
|
72
|
+
ec2_instance = ec2.describe_instances({instance_ids: [ec2_instance_id]}).reservations.first.instances.first
|
73
|
+
|
74
|
+
# get its current security groups to restory later
|
75
|
+
security_group_ids = ec2_instance.security_groups.map(&:group_id)
|
76
|
+
|
77
|
+
# create new public ip
|
78
|
+
elastic_ip = ec2.allocate_address({ domain: "vpc" })
|
79
|
+
# link ip to ec2 instance
|
80
|
+
associate_address_response = ec2.associate_address({
|
81
|
+
allocation_id: elastic_ip.allocation_id,
|
82
|
+
instance_id: ec2_instance_id
|
83
|
+
})
|
84
|
+
# create security group for us
|
85
|
+
security_group_response = ec2.create_security_group({
|
86
|
+
group_name: "shiprails-exec-#{cluster}-#{Time.now.to_i}",
|
87
|
+
description: "SSH access to run interactive command (created by #{`whoami`.rstrip} via shiprails)",
|
88
|
+
vpc_id: ec2_instance.vpc_id
|
89
|
+
})
|
90
|
+
# get our public ip
|
91
|
+
my_ip_address = open('http://whatismyip.akamai.com').read
|
92
|
+
# authorize SSH access from our public ip
|
93
|
+
ec2.authorize_security_group_ingress({
|
94
|
+
group_id: security_group_response.group_id,
|
95
|
+
ip_protocol: "tcp",
|
96
|
+
from_port: 22,
|
97
|
+
to_port: 22,
|
98
|
+
cidr_ip: "#{my_ip_address}/32"
|
99
|
+
})
|
100
|
+
# add ec2 instance to our new security group
|
101
|
+
ec2.modify_instance_attribute({
|
102
|
+
instance_id: ec2_instance_id,
|
103
|
+
groups: security_group_ids + [security_group_response.group_id]
|
104
|
+
})
|
105
|
+
|
106
|
+
# build the command we'll run on the instance
|
107
|
+
command_array = ["docker run -it --rm"]
|
108
|
+
task_definition_description.task_definition.container_definitions.first.environment.each do |env|
|
109
|
+
command_array << "-e #{env.name}='#{env.value}'"
|
110
|
+
end
|
111
|
+
command_array << task_definition_description.task_definition.container_definitions.first.image
|
112
|
+
command_array << command
|
113
|
+
command_string = command_array.join ' '
|
114
|
+
|
115
|
+
say "Waiting for AWS to setup networking..."
|
116
|
+
sleep 5 # AWS just needs a little bit to setup networking
|
117
|
+
say "Connecting #{ssh_user}@#{elastic_ip.public_ip}..."
|
118
|
+
say "Executing: $ #{command_string}"
|
119
|
+
system "ssh -o ConnectTimeout=15 -o 'StrictHostKeyChecking no' -t -i #{ssh_private_key_path} #{ssh_user}@#{elastic_ip.public_ip} '#{command_string}'"
|
120
|
+
rescue => e
|
121
|
+
say "Error: #{e.message}", :red
|
122
|
+
ensure
|
123
|
+
say "Cleaning up SSH access..."
|
124
|
+
# restore original security groups
|
125
|
+
ec2.modify_instance_attribute({
|
126
|
+
instance_id: ec2_instance_id,
|
127
|
+
groups: security_group_ids
|
128
|
+
}) rescue nil
|
129
|
+
# remove our access security group
|
130
|
+
ec2.delete_security_group({ group_id: security_group_response.group_id }) rescue nil
|
131
|
+
# unlink ec2 instance from public ip
|
132
|
+
ec2.disassociate_address({ association_id: associate_address_response.association_id }) rescue nil
|
133
|
+
# release public ip address
|
134
|
+
ec2.release_address({ allocation_id: elastic_ip.allocation_id }) rescue nil
|
135
|
+
say "Done.", :green
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# https://github.com/ddollar/forego
|
2
|
+
|
3
|
+
COMPOSE_PROJECT_NAME=<%= project_name %>
|
4
|
+
APPLICATION_HOST=<%= application_host %>
|
5
|
+
ASSET_HOST=<%= application_host %>
|
6
|
+
APPLICATION_HOST=<%= application_host %>
|
7
|
+
|
8
|
+
PORT=3000
|
9
|
+
RACK_ENV=development
|
10
|
+
SECRET_KEY_BASE=development_secret
|
11
|
+
EXECJS_RUNTIME=Node
|
12
|
+
|
13
|
+
SMTP_ADDRESS=smtp.example.com
|
14
|
+
SMTP_DOMAIN=example.com
|
15
|
+
SMTP_PASSWORD=password
|
16
|
+
SMTP_USERNAME=username
|
17
|
+
|
18
|
+
RACK_MINI_PROFILER=1
|
19
|
+
WEB_CONCURRENCY=1
|
20
|
+
MAX_THREADS=4
|
21
|
+
|
22
|
+
FACEBOOK_APP_ID=FILL_ME_IN
|
23
|
+
FACEBOOK_SECRET=FILL_ME_IN
|
@@ -0,0 +1,28 @@
|
|
1
|
+
FROM ruby:<%= ruby_version %>-slim
|
2
|
+
|
3
|
+
RUN apt-get update -qq && apt-get install -y build-essential git-core
|
4
|
+
|
5
|
+
# for postgres
|
6
|
+
RUN apt-get install -y libpq-dev
|
7
|
+
|
8
|
+
# for nokogiri
|
9
|
+
RUN apt-get install -y libxml2-dev libxslt1-dev
|
10
|
+
|
11
|
+
# for capybara-webkit
|
12
|
+
RUN apt-get install -y libqt4-webkit libqt4-dev xvfb
|
13
|
+
|
14
|
+
# for a JS runtime
|
15
|
+
RUN apt-get install -y nodejs
|
16
|
+
|
17
|
+
# lighten up the image size
|
18
|
+
RUN rm -rf /var/lib/apt/lists/*
|
19
|
+
|
20
|
+
RUN gem install bundler --no-document
|
21
|
+
|
22
|
+
ENV APP_HOME /app
|
23
|
+
RUN mkdir $APP_HOME
|
24
|
+
WORKDIR $APP_HOME
|
25
|
+
|
26
|
+
ENV BUNDLE_PATH /bundle
|
27
|
+
|
28
|
+
ADD . $APP_HOME
|
@@ -0,0 +1,29 @@
|
|
1
|
+
FROM ruby:<%= ruby_version %>-slim
|
2
|
+
|
3
|
+
RUN apt-get update -qq && apt-get install -y build-essential git-core
|
4
|
+
|
5
|
+
# for postgres
|
6
|
+
RUN apt-get install -y libpq-dev
|
7
|
+
|
8
|
+
# for nokogiri
|
9
|
+
RUN apt-get install -y libxml2-dev libxslt1-dev
|
10
|
+
|
11
|
+
# for capybara-webkit
|
12
|
+
RUN apt-get install -y libqt4-webkit libqt4-dev xvfb
|
13
|
+
|
14
|
+
# for a JS runtime
|
15
|
+
RUN apt-get install -y nodejs
|
16
|
+
|
17
|
+
# lighten up the image size
|
18
|
+
RUN rm -rf /var/lib/apt/lists/*
|
19
|
+
|
20
|
+
RUN gem install bundler --no-document
|
21
|
+
|
22
|
+
ENV APP_HOME /app
|
23
|
+
RUN mkdir $APP_HOME
|
24
|
+
WORKDIR $APP_HOME
|
25
|
+
|
26
|
+
ADD Gemfile* $APP_HOME/
|
27
|
+
RUN bundle install
|
28
|
+
|
29
|
+
ADD . $APP_HOME
|
@@ -0,0 +1,81 @@
|
|
1
|
+
version: '2'
|
2
|
+
|
3
|
+
services:
|
4
|
+
memcached:
|
5
|
+
image: memcached
|
6
|
+
postgres:
|
7
|
+
environment:
|
8
|
+
LC_ALL: C.UTF-8
|
9
|
+
image: postgres
|
10
|
+
volumes:
|
11
|
+
- postgres-data:/var/lib/postgresql/data
|
12
|
+
redis:
|
13
|
+
command: redis-server --appendonly yes
|
14
|
+
image: redis
|
15
|
+
volumes:
|
16
|
+
- redis-data:/var/lib/redis
|
17
|
+
app: &app_base
|
18
|
+
build: .
|
19
|
+
command: ./bin/start
|
20
|
+
depends_on:
|
21
|
+
- memcached
|
22
|
+
- postgres
|
23
|
+
- redis
|
24
|
+
environment: &app_environment
|
25
|
+
# PostgreSQL Development Database:
|
26
|
+
DATABASE_URL: postgres://postgres:@postgres:5432/development?pool=25&encoding=unicode&schema_search_path=public
|
27
|
+
# memcached Development Cache:
|
28
|
+
MEMCACHED_URL: memcached:11211
|
29
|
+
# Redis Database:
|
30
|
+
REDIS_URL: redis://redis:6379
|
31
|
+
DOCKERIZED: 1
|
32
|
+
POOL_SIZE: 5
|
33
|
+
|
34
|
+
# Sidekiq configuration:
|
35
|
+
SIDEKIQ_CONCURRENCY: 5
|
36
|
+
SIDEKIQ_TIMEOUT: 10
|
37
|
+
|
38
|
+
# Enable the byebug debugging server - this can be overriden
|
39
|
+
# from the command line:
|
40
|
+
ENABLE_DEBUG_SERVER: 0
|
41
|
+
|
42
|
+
# Run the app in the 'development' environment:
|
43
|
+
RACK_ENV: development
|
44
|
+
env_file: .env
|
45
|
+
ports:
|
46
|
+
- "3000:3000"
|
47
|
+
stdin_open: true
|
48
|
+
tmpfs: /app/tmp
|
49
|
+
volumes:
|
50
|
+
- ".:/app"
|
51
|
+
- gems-data:/bundle
|
52
|
+
worker:
|
53
|
+
<<: *app_base
|
54
|
+
command: bundle exec sidekiq -C config/sidekiq.yml
|
55
|
+
ports: []
|
56
|
+
cable:
|
57
|
+
<<: *app_base
|
58
|
+
command: bundle exec puma --bind tcp://0.0.0.0:28080 cable/config.ru
|
59
|
+
ports:
|
60
|
+
- 28080:28080
|
61
|
+
# App Guard: Keeps running tests on a separate process:
|
62
|
+
test:
|
63
|
+
<<: *app_base # We copy from &app_base, and override:
|
64
|
+
command: bundle exec guard start --no-bundler-warning --no-interactions
|
65
|
+
environment:
|
66
|
+
<<: *app_environment
|
67
|
+
# PostgreSQL Test Database:
|
68
|
+
DATABASE_URL: postgres://postgres:@postgres:5432/test?pool=25&encoding=unicode&schema_search_path=public
|
69
|
+
|
70
|
+
# Run the app in the 'test' environment, instead of the default 'developent'
|
71
|
+
RACK_ENV: test
|
72
|
+
RAILS_ENV: test
|
73
|
+
ports: []
|
74
|
+
|
75
|
+
volumes:
|
76
|
+
gems-data:
|
77
|
+
driver: local
|
78
|
+
postgres-data:
|
79
|
+
driver: local
|
80
|
+
redis-data:
|
81
|
+
driver: local
|
@@ -0,0 +1,29 @@
|
|
1
|
+
config_s3_bucket: '<%= config_s3_bucket %>'
|
2
|
+
project_name: '<%= project_name %>'
|
3
|
+
services:
|
4
|
+
<%- services.each do |service| -%>
|
5
|
+
<%= service[:name] %>:
|
6
|
+
command: '<%= service[:command] %>'
|
7
|
+
image: '<%= service[:image] %>'
|
8
|
+
<%- if service[:ports].any? -%>
|
9
|
+
ports:
|
10
|
+
<%- service[:ports].each do |port| -%>
|
11
|
+
- <%= port %>
|
12
|
+
<%- end -%>
|
13
|
+
<%- end -%>
|
14
|
+
regions:
|
15
|
+
<%- service[:regions].each do |region, data| -%>
|
16
|
+
<%= region %>:
|
17
|
+
environments:
|
18
|
+
<%- environments.each do |environment| -%>
|
19
|
+
- <%= environment[:name] %>
|
20
|
+
<%- end -%>
|
21
|
+
<%- data.each do |k,v| -%>
|
22
|
+
<%= k %>: '<%= v %>'
|
23
|
+
<%- end -%>
|
24
|
+
<%- end -%>
|
25
|
+
resources:
|
26
|
+
<%- service[:resources].each do |k,v| -%>
|
27
|
+
<%= k %>: <%= v %>
|
28
|
+
<%- end -%>
|
29
|
+
<%- end -%>
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
require "aws-sdk"
|
3
|
+
require "thor/group"
|
4
|
+
|
5
|
+
module Shiprails
|
6
|
+
class Ship < Thor
|
7
|
+
class Install < Thor::Group
|
8
|
+
include Thor::Actions
|
9
|
+
|
10
|
+
class_option "path",
|
11
|
+
aliases: ["-p"],
|
12
|
+
default: ".",
|
13
|
+
desc: "Specify a configuration path"
|
14
|
+
|
15
|
+
def self.source_root
|
16
|
+
File.expand_path("../install", __FILE__)
|
17
|
+
end
|
18
|
+
|
19
|
+
def application_host
|
20
|
+
"#{project_name}.dev"
|
21
|
+
end
|
22
|
+
|
23
|
+
no_commands {
|
24
|
+
def aws_access_key_id
|
25
|
+
@aws_access_key_id ||= ask "AWS Access Key ID", default: ENV.fetch("AWS_ACCESS_KEY_ID")
|
26
|
+
end
|
27
|
+
|
28
|
+
def aws_access_key_secret
|
29
|
+
@aws_access_key_secret ||= ask "AWS Access Key Secret", default: ENV.fetch("AWS_SECRET_ACCESS_KEY")
|
30
|
+
end
|
31
|
+
}
|
32
|
+
|
33
|
+
def config_s3_bucket
|
34
|
+
return @bucket_name unless @bucket_name.nil?
|
35
|
+
@_s3_client ||= Aws::S3::Client.new(region: ENV.fetch("AWS_REGION", "us-west-2"), access_key_id: aws_access_key_id, secret_access_key: aws_access_key_secret)
|
36
|
+
begin
|
37
|
+
bucket_name = "#{project_name}-config"
|
38
|
+
bucket_name = ask "S3 bucket name for configuration store", default: bucket_name
|
39
|
+
resp = @_s3_client.create_bucket({
|
40
|
+
bucket: bucket_name
|
41
|
+
})
|
42
|
+
rescue Aws::S3::Errors::BucketAlreadyExists
|
43
|
+
error "'#{bucket_name}' already exists"
|
44
|
+
retry
|
45
|
+
rescue Aws::S3::Errors::BucketAlreadyOwnedByYou
|
46
|
+
end
|
47
|
+
@bucket_name = bucket_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def environments
|
51
|
+
environments = Dir.entries("#{Dir.getwd}/config/environments").grep(/\.rb$/).map { |fname| fname.chomp!(".rb") }.select{ |e| !['development', 'test'].include? e } rescue ['production']
|
52
|
+
environments ||= ['production']
|
53
|
+
@regions ||= ask("Which regions?", default: 'us-west-2').split(',')
|
54
|
+
environments.map do |environment|
|
55
|
+
{
|
56
|
+
name: environment,
|
57
|
+
regions: @regions
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
no_commands {
|
63
|
+
def services
|
64
|
+
return @services unless @services.nil?
|
65
|
+
docker_compose = YAML.load(File.read("#{options[:path]}/docker-compose.yml")).deep_symbolize_keys
|
66
|
+
image_for_build = {}
|
67
|
+
regions_for_build = {}
|
68
|
+
@services = docker_compose[:services].map do |service_name, service|
|
69
|
+
next if [:test].include? service_name
|
70
|
+
if service[:image].nil?
|
71
|
+
build_name = service[:build]
|
72
|
+
build_name = "Dockerfile" if build_name == '.'
|
73
|
+
unless image = image_for_build[build_name]
|
74
|
+
image = service_name
|
75
|
+
image_for_build[build_name] = image
|
76
|
+
end
|
77
|
+
unless regions = regions_for_build[build_name]
|
78
|
+
regions = @regions.map do |region|
|
79
|
+
[region, {
|
80
|
+
repository_url: repository_url_for_region(region, service_name)
|
81
|
+
}]
|
82
|
+
end
|
83
|
+
regions_for_build[build_name] = regions
|
84
|
+
end
|
85
|
+
{
|
86
|
+
command: service[:command],
|
87
|
+
image: image,
|
88
|
+
name: service_name.to_s,
|
89
|
+
ports: (service[:ports] || []).map{ |port| port.split(":").last },
|
90
|
+
regions: regions,
|
91
|
+
resources: {
|
92
|
+
cpu_units: 256,
|
93
|
+
memory_units: 256
|
94
|
+
}
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end.compact
|
98
|
+
end
|
99
|
+
|
100
|
+
def project_name
|
101
|
+
@project_name ||= ask "What's your project called?", default: File.basename(Dir.getwd)
|
102
|
+
end
|
103
|
+
|
104
|
+
def ruby_version
|
105
|
+
"#{RUBY_VERSION}"
|
106
|
+
end
|
107
|
+
}
|
108
|
+
|
109
|
+
def create_dockerfile
|
110
|
+
template("Dockerfile.erb", "#{options[:path]}/Dockerfile")
|
111
|
+
template("Dockerfile.production.erb", "#{options[:path]}/Dockerfile.production")
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_dot_env
|
115
|
+
template(".env.erb", "#{options[:path]}/.env")
|
116
|
+
template(".env.erb", "#{options[:path]}/.env.example")
|
117
|
+
end
|
118
|
+
|
119
|
+
def ignore_dot_env
|
120
|
+
if File.exists?(".gitignore")
|
121
|
+
append_to_file(".gitignore", <<-EOF)
|
122
|
+
|
123
|
+
# Ignore Docker ENV
|
124
|
+
/.env
|
125
|
+
EOF
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def create_docker_compose
|
130
|
+
template("docker-compose.yml.erb", "#{options[:path]}/docker-compose.yml")
|
131
|
+
end
|
132
|
+
|
133
|
+
def create_configuration
|
134
|
+
template("shiprails.yml.erb", "#{options[:path]}/.shiprails.yml")
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def repository_url_for_region(region, service_name)
|
140
|
+
@_ecr_client ||= Aws::ECR::Client.new(region: region, access_key_id: aws_access_key_id, secret_access_key: aws_access_key_secret)
|
141
|
+
resp = @_ecr_client.describe_repositories({}).to_h
|
142
|
+
say "Amazon EC2 Container Registry (ECR) for #{project_name}_#{service_name}?"
|
143
|
+
choices = ["CREATE NEW REGISTRY"] + resp[:repositories].map{|r| "#{r[:repository_name]} (#{r[:repository_uri]})" }
|
144
|
+
choices = choices.map.with_index{ |a, i| [i+1, *a]}
|
145
|
+
print_table choices
|
146
|
+
selection = ask("Pick one:").to_i
|
147
|
+
if selection == 1
|
148
|
+
resp = @_ecr_client.create_repository({
|
149
|
+
repository_name: "#{project_name}/#{service_name}",
|
150
|
+
}).to_h
|
151
|
+
resp[:repository][:repository_uri]
|
152
|
+
else
|
153
|
+
resp[:repositories][selection - 2][:repository_uri]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
require "aws-sdk"
|
3
|
+
require "git"
|
4
|
+
require "thor/group"
|
5
|
+
|
6
|
+
module Shiprails
|
7
|
+
class Ship < Thor
|
8
|
+
class Scale < Thor::Group
|
9
|
+
include Thor::Actions
|
10
|
+
|
11
|
+
argument :method_name, type: :string
|
12
|
+
argument :environment, type: :string
|
13
|
+
argument :service, type: :string
|
14
|
+
argument :scale, type: :string
|
15
|
+
|
16
|
+
class_option "region",
|
17
|
+
desc: "Specify region"
|
18
|
+
class_option "path",
|
19
|
+
aliases: ["-p"],
|
20
|
+
default: ".",
|
21
|
+
desc: "Specify a configuration path"
|
22
|
+
|
23
|
+
def update_ecs_services
|
24
|
+
say "Setting ECS service #{service} scale=#{scale} in #{environment}..."
|
25
|
+
configuration[:services].each do |service_name, service|
|
26
|
+
next unless service_name.to_s == self.service
|
27
|
+
image_name = "#{project_name}_#{service_name}"
|
28
|
+
service[:regions].each do |region_name, region|
|
29
|
+
next unless options["region"].nil? or options["region"] == region_name.to_s
|
30
|
+
ecs = Aws::ECS::Client.new(region: region_name.to_s)
|
31
|
+
region[:environments].each do |environment_name|
|
32
|
+
next unless environment_name == self.environment
|
33
|
+
cluster_name = "#{project_name}_#{environment_name}"
|
34
|
+
task_name = "#{image_name}_#{environment_name}"
|
35
|
+
begin
|
36
|
+
task_definition_description = ecs.describe_task_definition({task_definition: task_name})
|
37
|
+
task_definition = task_definition_description.task_definition.to_hash
|
38
|
+
rescue Aws::ECS::Errors::ClientException => e
|
39
|
+
say "Missing ECS task for #{task_name}!", :red
|
40
|
+
say "Run `ship setup`", :red
|
41
|
+
exit
|
42
|
+
end
|
43
|
+
begin
|
44
|
+
service_response = ecs.update_service({
|
45
|
+
cluster: cluster_name,
|
46
|
+
service: service_name,
|
47
|
+
desired_count: scale
|
48
|
+
})
|
49
|
+
say "Set ECS service #{service_name} scale=#{scale} in #{environment} (#{region_name})...", :green
|
50
|
+
rescue Aws::ECS::Errors::ServiceNotFoundException, Aws::ECS::Errors::ServiceNotActiveException => e
|
51
|
+
say "Missing ECS service for #{task_name}!", :red
|
52
|
+
say "Run `ship setup`", :red
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
say "ECS service updated.", :green
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def aws_access_key_id
|
64
|
+
@aws_access_key_id ||= ask "AWS Access Key ID", default: ENV.fetch("AWS_ACCESS_KEY_ID")
|
65
|
+
end
|
66
|
+
|
67
|
+
def aws_access_key_secret
|
68
|
+
@aws_access_key_secret ||= ask "AWS Access Key Secret", default: ENV.fetch("AWS_SECRET_ACCESS_KEY")
|
69
|
+
end
|
70
|
+
|
71
|
+
def configuration
|
72
|
+
YAML.load(File.read("#{options[:path]}/.shiprails.yml")).deep_symbolize_keys
|
73
|
+
end
|
74
|
+
|
75
|
+
def project_name
|
76
|
+
configuration[:project_name]
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|