shiprails 0.1.0

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.
@@ -0,0 +1,245 @@
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 Setup < Thor::Group
9
+ include Thor::Actions
10
+
11
+ class_option "path",
12
+ aliases: ["-p"],
13
+ default: ".",
14
+ desc: "Specify a configuration path"
15
+
16
+ def create_cloudwatch_logs_group
17
+ say "Creating CloudWatch Log groups..."
18
+ created_groups = []
19
+ configuration[:services].each do |service_name, service|
20
+ service[:regions].each do |region_name, region|
21
+ client = Aws::CloudWatchLogs::Client.new(region: region_name.to_s)
22
+ region[:environments].each do |environment_name|
23
+ cluster_name = "#{project_name}_#{environment_name}"
24
+ unless created_groups.include? cluster_name
25
+ client.create_log_group({ log_group_name: cluster_name })
26
+ say "Created #{cluster_name} log group."
27
+ created_groups << cluster_name
28
+ end
29
+ end
30
+ end
31
+ end
32
+ say "Created CloudWatch Log groups.", :green
33
+ end
34
+
35
+ def create_ecs_tasks
36
+ say "Creating ECS tasks..."
37
+ configuration[:services].each do |service_name, service|
38
+ image_name = "#{project_name}_#{service_name}"
39
+ service[:regions].each do |region_name, region|
40
+ ecs = Aws::ECS::Client.new(region: region_name.to_s)
41
+ region[:environments].each do |environment_name|
42
+ cluster_name = "#{project_name}_#{environment_name}"
43
+ task_name = "#{image_name}_#{environment_name}"
44
+ begin
45
+ task_definition_description = ecs.describe_task_definition({task_definition: task_name})
46
+ task_definition = task_definition_description.task_definition.to_hash
47
+ task_definition.delete :task_definition_arn
48
+ task_definition.delete :revision
49
+ task_definition.delete :status
50
+ task_definition.delete :requires_attributes
51
+ say "Updating ECS task (#{task_name})."
52
+ rescue Aws::ECS::Errors::ClientException => e
53
+ task_definition = {
54
+ container_definitions: [
55
+ {
56
+ command: service[:command],
57
+ cpu: service[:resources][:cpu_units],
58
+ essential: true,
59
+ environment: [
60
+ { name: "AWS_REGION", value: region_name.to_s },
61
+ { name: "RACK_ENV", value: environment_name },
62
+ { name: "S3_CONFIG_BUCKET", value: config_s3_bucket },
63
+ { name: "S3_CONFIG_REVISION", value: "0" }
64
+ ],
65
+ image: "#{region[:repository_url]}:latest",
66
+ log_configuration: {
67
+ log_driver: "awslogs",
68
+ options: {
69
+ "awslog-group" => cluster_name,
70
+ "awslogs-region" => region_name.to_s,
71
+ "awslogs-stream-prefix" => ""
72
+ }
73
+ },
74
+ memory: service[:resources][:memory_units],
75
+ name: service_name,
76
+ port_mappings: (service[:ports] || []).map { |port|
77
+ {
78
+ container_port: port,
79
+ host_port: 0,
80
+ protocol: "tcp"
81
+ }
82
+ }
83
+ }
84
+ ],
85
+ family: task_name
86
+ }
87
+ say "Creating new ECS task (#{task_name})!"
88
+ end
89
+ task_definition_response = ecs.register_task_definition(task_definition)
90
+ end
91
+ end
92
+ end
93
+ say "Created ECS tasks!", :green
94
+ end
95
+
96
+ def create_ecs_clusters
97
+ say "Creating ECS clusters..."
98
+ cluster_names = []
99
+ configuration[:services].each do |service_name, service|
100
+ image_name = "#{project_name}_#{service_name}"
101
+ service[:regions].each do |region_name, region|
102
+ ecs = Aws::ECS::Client.new(region: region_name.to_s)
103
+ region[:environments].each do |environment_name|
104
+ cluster_name = "#{project_name}_#{environment_name}"
105
+ next if cluster_names.include? cluster_name
106
+ cluster_names << cluster_name
107
+ ecs.create_cluster({
108
+ cluster_name: cluster_name
109
+ })
110
+ say "Created ECS cluster (#{cluster_name})!"
111
+ end
112
+ end
113
+ end
114
+ say "Created ECS clusters!", :green
115
+ end
116
+
117
+ def create_ec2_launch_configurations
118
+ say "TODO: create cluster launch config", :blue
119
+ end
120
+
121
+ def create_ec2_autoscaling_groups
122
+ say "TODO: create cluster group", :blue
123
+ end
124
+
125
+ def create_cloudwatch_ecs_alarms
126
+ say "TODO: create cloudwatch alarms for cluster memory", :blue
127
+ end
128
+
129
+ def create_ecs_services
130
+ say "Creating ECS services..."
131
+ configuration[:services].each do |service_name, service|
132
+ image_name = "#{project_name}_#{service_name}"
133
+ service[:regions].each do |region_name, region|
134
+ ecs = Aws::ECS::Client.new(region: region_name.to_s)
135
+ elb = Aws::ElasticLoadBalancingV2::Client.new(region: region_name.to_s)
136
+ region[:environments].each do |environment_name|
137
+ cluster_name = "#{project_name}_#{environment_name}"
138
+ task_name = "#{image_name}_#{environment_name}"
139
+ task_definition_response = ecs.describe_task_definition({task_definition: task_name})
140
+ task_definition = task_definition_response.task_definition.to_hash
141
+ ecs_service = {
142
+ cluster: cluster_name,
143
+ deployment_configuration: {
144
+ maximum_percent: 200,
145
+ minimum_healthy_percent: 50,
146
+ },
147
+ desired_count: 0,
148
+ service_name: service_name,
149
+ task_definition: task_definition_response.task_definition.task_definition_arn
150
+ }
151
+ (service[:ports] || []).each do |port|
152
+ if yes? "Should port #{port} for #{image_name} be load balanced?"
153
+ ecs_service[:role] = "ecsServiceRole"
154
+ load_balancers = elb.describe_load_balancers.to_h
155
+ say "EC2 Load Balancers"
156
+ choices = ["CREATE NEW ELB"] + load_balancers[:load_balancers].map{|lb| "#{lb[:load_balancer_name]} (#{lb[:load_balancer_arn]})" }
157
+ choices = choices.map.with_index{ |a, i| [i+1, *a]}
158
+ print_table choices
159
+ selection = ask("Pick one:").to_i
160
+ if selection == 1
161
+ say "Creating new ELB not yet supported.", :red
162
+ say "Create a new ELB in your console.", :red
163
+ say "Then, run `ship setup` again.", :red
164
+ exit
165
+ else
166
+ load_balancer = load_balancers[:load_balancers][selection - 2]
167
+ end
168
+ say "Selected: #{load_balancer[:load_balancer_name]}"
169
+ target_group_name = "#{project_name}-#{service_name}-#{environment_name}"
170
+ target_group_resp = elb.create_target_group({
171
+ name: target_group_name,
172
+ port: port,
173
+ protocol: "HTTP",
174
+ vpc_id: load_balancer[:vpc_id]
175
+ }).to_h
176
+ target_group_arn = target_group_resp[:target_groups][0][:target_group_arn]
177
+ say "Created target group: #{target_group_name}."
178
+ ecs_service[:load_balancers] = [
179
+ {
180
+ container_name: service_name,
181
+ container_port: port,
182
+ target_group_arn: target_group_arn
183
+ }
184
+ ]
185
+ end
186
+ end
187
+ begin
188
+ service_response = ecs.create_service(ecs_service)
189
+ say "Created ECS service (#{service_name})!"
190
+ rescue Aws::ECS::Errors::InvalidParameterException => e
191
+ case e.message
192
+ when "Creation of service was not idempotent."
193
+ say "Service #{service_name} already exists.", :yellow
194
+ say "If you've changed load balancers setup, you must delete the existing service.", :yellow
195
+ when /The target group with targetGroupArn ([^\s\\]+) does not have an associated load balancer./
196
+ say "Link Target Group to Load Balancer", :red
197
+ say "Visit `https://#{region_name}.console.aws.amazon.com/ec2/v2/home?region=#{region_name}#LoadBalancers:`", :red
198
+ say "Add listener for Target Group (#{$1})", :red
199
+ else
200
+ raise e
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ say "Created ECS services!", :green
207
+ end
208
+
209
+ def create_cloudwatch_elb_alarms
210
+ say "TODO: create cloudwatch alarms for elb latency / service units", :blue
211
+ end
212
+
213
+ def create_iam_groups
214
+ say "TODO: create cloudwatch logs read group", :blue
215
+ say "TODO: create scale group", :blue
216
+ say "TODO: create deploy IAM group", :blue
217
+ say "TODO: create run task IAM group", :blue
218
+ say "TODO: create exec interactive IAM group", :blue
219
+ end
220
+
221
+ private
222
+
223
+ def aws_access_key_id
224
+ @aws_access_key_id ||= ask "AWS Access Key ID", default: ENV.fetch("AWS_ACCESS_KEY_ID")
225
+ end
226
+
227
+ def aws_access_key_secret
228
+ @aws_access_key_secret ||= ask "AWS Access Key Secret", default: ENV.fetch("AWS_SECRET_ACCESS_KEY")
229
+ end
230
+
231
+ def configuration
232
+ YAML.load(File.read("#{options[:path]}/.shiprails.yml")).deep_symbolize_keys
233
+ end
234
+
235
+ def project_name
236
+ configuration[:project_name]
237
+ end
238
+
239
+ def config_s3_bucket
240
+ configuration[:config_s3_bucket]
241
+ end
242
+
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,75 @@
1
+ require "active_support/all"
2
+ require "aws-sdk"
3
+ require "thor/group"
4
+
5
+ module Shiprails
6
+ class Ship < Thor
7
+ class Task < 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
+
23
+ def run_command
24
+ command_string = args.join ' '
25
+ cluster_name = "#{project_name}_#{options['environment']}"
26
+ task_name = "#{project_name}_#{options['service']}_#{options['environment']}"
27
+ ecs = Aws::ECS::Client.new(region: options['region'])
28
+ task_definition_response = ecs.describe_task_definition({task_definition: task_name})
29
+ task_definition_arn = task_definition_response.task_definition.task_definition_arn
30
+ say "Running `#{command_string}` in #{options['environment']} #{options['service']} (#{options['region']})..."
31
+ task_response = ecs.run_task({
32
+ cluster: cluster_name,
33
+ task_definition: task_definition_arn,
34
+ overrides: {
35
+ container_overrides: [{
36
+ name: options['service'],
37
+ command: command_string.split(' ')
38
+ }]
39
+ }
40
+ })
41
+ task_arn = task_response.tasks.first.task_arn
42
+ resp = ecs.describe_tasks({ cluster: cluster_name, tasks: [task_arn] })
43
+ while resp.tasks.first.containers.first.exit_code.nil?
44
+ sleep 1
45
+ resp = ecs.describe_tasks({ cluster: cluster_name, tasks: [task_arn] })
46
+ say "."
47
+ end
48
+ if resp.tasks.first.containers.first.exit_code > 0
49
+ say "Task exited other than 0: #{resp.tasks.first.containers.first.exit_code} (#{task_arn})", :red
50
+ else
51
+ say "Ran `#{command_string}` in #{options['environment']} #{options['service']} (#{options['region']}).", :green
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def configuration
58
+ YAML.load(File.read("#{options[:path]}/.shiprails.yml")).deep_symbolize_keys
59
+ end
60
+
61
+ def aws_access_key_id
62
+ @aws_access_key_id ||= ask "AWS Access Key ID", default: ENV.fetch("AWS_ACCESS_KEY_ID")
63
+ end
64
+
65
+ def aws_access_key_secret
66
+ @aws_access_key_secret ||= ask "AWS Access Key Secret", default: ENV.fetch("AWS_SECRET_ACCESS_KEY")
67
+ end
68
+
69
+ def project_name
70
+ configuration[:project_name]
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,61 @@
1
+ require "active_support/all"
2
+ require "thor"
3
+
4
+ module Shiprails
5
+ class Ship < Thor
6
+
7
+ desc "install", "Install Shiprails"
8
+ def install
9
+ require "shiprails/ship/install"
10
+ Install.start
11
+ end
12
+
13
+ desc "setup", "Setup a Shiprails environment"
14
+ def setup
15
+ require "shiprails/ship/setup"
16
+ Setup.start
17
+ end
18
+
19
+ desc "config", "Configure services"
20
+ def config(*command_args)
21
+ require "shiprails/ship/config"
22
+ Config.start command_args
23
+ end
24
+
25
+ desc "deploy", "Deploy services"
26
+ def deploy
27
+ require "shiprails/ship/deploy"
28
+ Deploy.start
29
+ end
30
+
31
+ desc "logs", "Fetch logs"
32
+ def logs
33
+ say "TODO: fetch logs", :blue
34
+ end
35
+
36
+ desc "task", "Run one off commands"
37
+ def task(*command_args)
38
+ require "shiprails/ship/task"
39
+ Task.start command_args
40
+ end
41
+
42
+ desc "exec", "Run interactive commands"
43
+ def exec(*command_args)
44
+ require "shiprails/ship/exec"
45
+ Exec.start command_args
46
+ end
47
+
48
+ desc "scale ENVIRONMENT SERVICE PROCESS_COUNT", "Change service instances"
49
+ def scale(*args)
50
+ require "shiprails/ship/scale"
51
+ Scale.start
52
+ end
53
+
54
+ private
55
+
56
+ def configuration
57
+ YAML.load(File.read(".shiprails.yml")).deep_symbolize_keys
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,3 @@
1
+ module Shiprails
2
+ VERSION = "0.1.0"
3
+ end
data/lib/shiprails.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "shiprails/version"
2
+ require "shiprails/application"
3
+
4
+ module Shiprails
5
+ end
data/shiprails.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'shiprails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "shiprails"
8
+ spec.version = Shiprails::VERSION
9
+ spec.authors = ["Zane Shannon"]
10
+ spec.email = ["zcs@smileslaughs.com"]
11
+
12
+ spec.summary = %q{Shiprails helps you deploy Rails to AWS ECS.}
13
+ spec.description = %q{Shiprails aims to provide Heroku's Ship APIs for AWS ECS.}
14
+ spec.homepage = "https://github.com/rails2017/shiprails"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "activesupport", "~> 5"
23
+ spec.add_dependency "aws-sdk", "~> 2"
24
+ spec.add_dependency "git", "~> 1.3"
25
+ spec.add_dependency "thor", "~> 0.14"
26
+ spec.add_dependency "s3_config", "~> 0.1.0"
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.10"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec"
31
+
32
+ spec.executables << "port"
33
+ spec.executables << "ship"
34
+
35
+ end
metadata ADDED
@@ -0,0 +1,194 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shiprails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Zane Shannon
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-09-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
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'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: git
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: thor
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.14'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.14'
69
+ - !ruby/object:Gem::Dependency
70
+ name: s3_config
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.1.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.1.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.10'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.10'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Shiprails aims to provide Heroku's Ship APIs for AWS ECS.
126
+ email:
127
+ - zcs@smileslaughs.com
128
+ executables:
129
+ - ".DS_Store"
130
+ - port
131
+ - ship
132
+ extensions: []
133
+ extra_rdoc_files: []
134
+ files:
135
+ - ".DS_Store"
136
+ - ".gitignore"
137
+ - ".rbenv-vars.example"
138
+ - ".rspec"
139
+ - ".ruby-gemset"
140
+ - ".ruby-version"
141
+ - ".travis.yml"
142
+ - CODE_OF_CONDUCT.md
143
+ - Gemfile
144
+ - LICENSE
145
+ - README.md
146
+ - Rakefile
147
+ - bin/console
148
+ - bin/setup
149
+ - exe/.DS_Store
150
+ - exe/port
151
+ - exe/ship
152
+ - lib/shiprails.rb
153
+ - lib/shiprails/application.rb
154
+ - lib/shiprails/port.rb
155
+ - lib/shiprails/ship.rb
156
+ - lib/shiprails/ship/config.rb
157
+ - lib/shiprails/ship/deploy.rb
158
+ - lib/shiprails/ship/exec.rb
159
+ - lib/shiprails/ship/install.rb
160
+ - lib/shiprails/ship/install/.env.erb
161
+ - lib/shiprails/ship/install/Dockerfile.erb
162
+ - lib/shiprails/ship/install/Dockerfile.production.erb
163
+ - lib/shiprails/ship/install/docker-compose.yml.erb
164
+ - lib/shiprails/ship/install/shiprails.yml.erb
165
+ - lib/shiprails/ship/scale.rb
166
+ - lib/shiprails/ship/setup.rb
167
+ - lib/shiprails/ship/task.rb
168
+ - lib/shiprails/version.rb
169
+ - shiprails.gemspec
170
+ homepage: https://github.com/rails2017/shiprails
171
+ licenses:
172
+ - MIT
173
+ metadata: {}
174
+ post_install_message:
175
+ rdoc_options: []
176
+ require_paths:
177
+ - lib
178
+ required_ruby_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ requirements: []
189
+ rubyforge_project:
190
+ rubygems_version: 2.5.1
191
+ signing_key:
192
+ specification_version: 4
193
+ summary: Shiprails helps you deploy Rails to AWS ECS.
194
+ test_files: []