shiprails 0.1.0

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