mrsk 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce03312fed10073e6fd3d8fe805a1088ff759674368032ef27cdc88cf1ed5658
4
- data.tar.gz: e686e5c333552e06af7fedcf25a9d03ab55e0bfc12a92a40782fae4649917a8c
3
+ metadata.gz: cd8f29ada17aea2f0a3dbabe3a9b8917c4e951353eb7ada2022c5aa0c98a2dea
4
+ data.tar.gz: 817eb173b1ea66238f868ddbb7683e651844767cda3ac5f74ce3bc933cb74cd8
5
5
  SHA512:
6
- metadata.gz: 5cb4e0842dccb3ebccc1e156e86151d471d3b6ab4f902a0e206f9942ab920df512cbe4c467bd052fc04aec89723c8af27bfdd70b1f6fb37d94a39e44d0fbcf03
7
- data.tar.gz: 79396a77da9f1d7b4bed0658f894f3bec0e8bbf6498f4d54b15b265e599a0d8594f46963b1064d483971b2840df5a4366f3cb880d675fd41433e89bde17f4cd6
6
+ metadata.gz: 722b4f4b740c3110c620053e105a7ed32e3c185ef46b5b4ed08b8092c6b53b2baff5400340e102647a24db476c32d52dd262dd706895c1ae243ce490cd5a4a61
7
+ data.tar.gz: 3e8abd162d7255a96fec5febab21f406bfc8f964a82b71f9c7f105b80807e46c07d6027f75abec515768eb22fd989af392e43b4bf2b0d7a811afe68d89372c0e
data/README.md CHANGED
@@ -1,26 +1,32 @@
1
1
  # MRSK
2
2
 
3
- MRSK lets you do zero-downtime deploys of Rails apps packed as containers to any host running Docker. It uses the dynamic reverse-proxy Traefik to hold requests while the new application container is started and the old one is wound down. It works across multiple hosts at the same time, using SSHKit to execute commands.
3
+ MRSK ships zero-downtime deploys of Rails apps packed as containers to any host. It uses the dynamic reverse-proxy Traefik to hold requests while the new application container is started and the old one is wound down. It works across multiple hosts at the same time, using SSHKit to execute commands.
4
4
 
5
5
  ## Installation
6
6
 
7
- Create a configuration file for MRSK in `config/deploy.yml` that looks like this:
7
+ Add the gem with `bundle add mrsk`, then run `rake mrsk:init`, and then edit the new file in `config/deploy.yml` to use the proper service name, image reference, servers to deploy on, and so on. It could look something like this:
8
8
 
9
9
  ```yaml
10
- service: my-app
11
- image: name/my-app
10
+ service: hey
11
+ image: 37s/hey
12
12
  servers:
13
13
  - xxx.xxx.xxx.xxx
14
14
  - xxx.xxx.xxx.xxx
15
15
  env:
16
- DATABASE_URL: mysql2://username@localhost/database_name/
17
- REDIS_URL: redis://host:6379/1
16
+ DATABASE_URL: mysql2://db1/hey_production/
17
+ REDIS_URL: redis://redis1:6379/1
18
+ registry:
19
+ server: registry.digitalocean.com
20
+ username: <%= Rails.application.credentials.registry["username"] %>
21
+ password: <%= Rails.application.credentials.registry["password"] %>
18
22
  ```
19
23
 
20
- Then first login to the Docker Hub registry on the servers:
24
+ Then ensure your encrypted credentials have the registry username + password by editing them with `rails credentials:edit`:
21
25
 
22
26
  ```
23
- rake mrsk:registry:login DOCKER_USER=name DOCKER_PASSWORD=pw
27
+ registry:
28
+ username: real-user-name
29
+ password: real-password
24
30
  ```
25
31
 
26
32
  Now you're ready to deploy a multi-arch image (FIXME: currently you need to manually run `docker buildx create --use` once first):
@@ -31,25 +37,142 @@ rake mrsk:deploy
31
37
 
32
38
  This will:
33
39
 
34
- 1. Build the image using the standard Dockerfile in the root of the application.
35
- 2. Push the image to the registry.
36
- 3. Pull the image on all the servers.
37
- 4. Ensure Traefik is running and accepting traffic on port 80.
38
- 5. Stop any containers running a previous versions of the app.
39
- 6. Start a new container with the version of the app that matches the current git version hash.
40
+ 1. Log into the registry both locally and remotely
41
+ 2. Build the image using the standard Dockerfile in the root of the application.
42
+ 3. Push the image to the registry.
43
+ 4. Pull the image from the registry on the servers.
44
+ 5. Ensure Traefik is running and accepting traffic on port 80.
45
+ 6. Stop any containers running a previous versions of the app.
46
+ 7. Start a new container with the version of the app that matches the current git version hash.
47
+ 8. Prune unused images and stopped containers to ensure servers don't fill up.
40
48
 
41
- Voila! All the servers are now serving the app on port 80, and you're ready to put them behind a load balancer to serve live traffic.
49
+ Voila! All the servers are now serving the app on port 80. If you're just running a single server, you're ready to go. If you're running multiple servers, you need to put a load balancer in front of them.
50
+
51
+ ## Operations
52
+
53
+ ### Running job hosts separately
54
+
55
+ If your application uses separate job running hosts, or other roles beyond the default web running, you can specify these hosts and their custom command like so:
56
+
57
+ ```yaml
58
+ servers:
59
+ web:
60
+ - xxx.xxx.xxx.xxx
61
+ - xxx.xxx.xxx.xxx
62
+ job:
63
+ hosts:
64
+ - xxx.xxx.xxx.xxx
65
+ - xxx.xxx.xxx.xxx
66
+ cmd: bin/jobs
67
+ ```
68
+
69
+ The application will be deployed to all hosts, but only those in the `web` role will be labeled to run under traefik. If you want to run custom commands on all hosts in a role, you can use `rake mrsk:app:exec:rails CMD=about ROLES=job`.
70
+
71
+ ### Executing commands
72
+
73
+ If you need to execute commands inside the Rails containers, you can use `rake mrsk:app:exec`, `rake mrsk:app:exec:once`, `rake mrsk:app:exec:rails`, and `rake mrsk:app:exec:once:rails`. Examples:
74
+
75
+ ```bash
76
+ # Runs command on all servers
77
+ rake mrsk:app:exec CMD='ruby -v'
78
+ App Host: xxx.xxx.xxx.xxx
79
+ ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
80
+
81
+ App Host: xxx.xxx.xxx.xxx
82
+ ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
83
+
84
+ # Runs command on first server
85
+ rake mrsk:app:exec:once CMD='cat .ruby-version'
86
+ 3.1.3
87
+
88
+ # Runs Rails command on all servers
89
+ rake mrsk:app:exec:rails CMD=about
90
+ App Host: xxx.xxx.xxx.xxx
91
+ About your application's environment
92
+ Rails version 7.1.0.alpha
93
+ Ruby version ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
94
+ RubyGems version 3.3.26
95
+ Rack version 2.2.5
96
+ Middleware ActionDispatch::HostAuthorization, Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::Callbacks, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ContentSecurityPolicy::Middleware, ActionDispatch::PermissionsPolicy::Middleware, Rack::Head, Rack::ConditionalGet, Rack::ETag, Rack::TempfileReaper
97
+ Application root /rails
98
+ Environment production
99
+ Database adapter sqlite3
100
+ Database schema version 20221231233303
101
+
102
+ App Host: xxx.xxx.xxx.xxx
103
+ About your application's environment
104
+ Rails version 7.1.0.alpha
105
+ Ruby version ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
106
+ RubyGems version 3.3.26
107
+ Rack version 2.2.5
108
+ Middleware ActionDispatch::HostAuthorization, Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::Callbacks, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ContentSecurityPolicy::Middleware, ActionDispatch::PermissionsPolicy::Middleware, Rack::Head, Rack::ConditionalGet, Rack::ETag, Rack::TempfileReaper
109
+ Application root /rails
110
+ Environment production
111
+ Database adapter sqlite3
112
+ Database schema version 20221231233303
113
+
114
+ # Runs Rails command on first server
115
+ rake mrsk:app:exec:once:rails CMD='db:version'
116
+ database: storage/production.sqlite3
117
+ Current version: 20221231233303
118
+ ```
119
+
120
+ ### Inspecting
121
+
122
+ You can see the state of your servers by running `rake mrsk:info`. It'll show something like this:
123
+
124
+ ```
125
+ Traefik Host: xxx.xxx.xxx.xxx
126
+ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
127
+ 6195b2a28c81 traefik "/entrypoint.sh --pr…" 30 minutes ago Up 19 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp traefik
128
+
129
+ Traefik Host: 164.92.105.119
130
+ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
131
+ de14a335d152 traefik "/entrypoint.sh --pr…" 30 minutes ago Up 19 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp traefik
132
+
133
+ App Host: 164.90.145.60
134
+ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
135
+ badb1aa51db3 registry.digitalocean.com/user/app:6ef8a6a84c525b123c5245345a8483f86d05a123 "/rails/bin/docker-e…" 13 minutes ago Up 13 minutes 3000/tcp chat-6ef8a6a84c525b123c5245345a8483f86d05a123
136
+
137
+ App Host: 164.92.105.119
138
+ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
139
+ 1d3c91ed1f55 registry.digitalocean.com/user/app:6ef8a6a84c525b123c5245345a8483f86d05a123 "/rails/bin/docker-e…" 13 minutes ago Up 13 minutes 3000/tcp chat-6ef8a6a84c525b123c5245345a8483f86d05a123
140
+ ```
141
+
142
+ You can also see just info for app containers with `rake mrsk:app:info` or just for Traefik with `rake mrsk:traefik:info`.
143
+
144
+ ### Rollback
145
+
146
+ If you've discovered a bad deploy, you can quickly rollback by reactivating the old, paused container image. You can see what old containers are available for rollback by running `rake mrsk:app:containers`. It'll give you a presentation similar to `rake mrsk:app:info`, but include all the old containers as well. Showing something like this:
147
+
148
+ ```
149
+ App Host: 164.92.105.119
150
+ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
151
+ 1d3c91ed1f51 registry.digitalocean.com/user/app:6ef8a6a84c525b123c5245345a8483f86d05a123 "/rails/bin/docker-e…" 19 minutes ago Up 19 minutes 3000/tcp chat-6ef8a6a84c525b123c5245345a8483f86d05a123
152
+ 539f26b28369 registry.digitalocean.com/user/app:e5d9d7c2b898289dfbc5f7f1334140d984eedae4 "/rails/bin/docker-e…" 31 minutes ago Exited (1) 27 minutes ago chat-e5d9d7c2b898289dfbc5f7f1334140d984eedae4
153
+
154
+ App Host: 164.90.145.60
155
+ CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
156
+ badb1aa51db4 registry.digitalocean.com/user/app:6ef8a6a84c525b123c5245345a8483f86d05a123 "/rails/bin/docker-e…" 19 minutes ago Up 19 minutes 3000/tcp chat-6ef8a6a84c525b123c5245345a8483f86d05a123
157
+ 6f170d1172ae registry.digitalocean.com/user/app:e5d9d7c2b898289dfbc5f7f1334140d984eedae4 "/rails/bin/docker-e…" 31 minutes ago Exited (1) 27 minutes ago chat-e5d9d7c2b898289dfbc5f7f1334140d984eedae4
158
+ ```
159
+
160
+ From the example above, we can see that `e5d9d7c2b898289dfbc5f7f1334140d984eedae4` was the last version, so it's available as a rollback target. We can perform this rollback by running `rake mrsk:rollback VERSION=e5d9d7c2b898289dfbc5f7f1334140d984eedae4`. That'll stop `6ef8a6a84c525b123c5245345a8483f86d05a123` and then start `e5d9d7c2b898289dfbc5f7f1334140d984eedae4`. Because the old container is still available, this is very quick. Nothing to download from the registry.
161
+
162
+ Note that by default old containers are pruned after 3 days when you run `rake mrsk:deploy`.
163
+
164
+ ### Removing
165
+
166
+ If you wish to remove the entire application, including Traefik, containers, images, and registry session, you can run `rake mrsk:remove`. This will leave the servers clean.
42
167
 
43
168
  ## Stage of development
44
169
 
45
170
  This is alpha software. Lots of stuff is missing. Here are some of the areas we seek to improve:
46
171
 
47
- - Use of other registries than Docker Hub
48
172
  - Adapterize commands to work with Podman and other container runners
49
- - Better flow for secrets and ENV
50
173
  - Possibly switching to a bin/mrsk command rather than raw rake
51
- - Integrate wirmth cloud CI pipelines
174
+ - Integrate with cloud CI pipelines
52
175
 
53
176
  ## License
54
177
 
55
- Mrsk is released under the [MIT License](https://opensource.org/licenses/MIT).
178
+ MRSK is released under the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,22 +1,66 @@
1
1
  class Mrsk::Commands::App < Mrsk::Commands::Base
2
2
  def push
3
3
  # TODO: Run 'docker buildx create --use' when needed
4
- "docker buildx build --push --platform=linux/amd64,linux/arm64 -t #{config.absolute_image} ."
4
+ # TODO: Make multiarch an option so Linux users can enjoy speedier builds
5
+ docker :buildx, :build, "--push", "--platform linux/amd64,linux/arm64", "-t", config.absolute_image, "."
5
6
  end
6
7
 
7
8
  def pull
8
- "docker pull #{config.absolute_image}"
9
+ docker :pull, config.absolute_image
10
+ end
11
+
12
+ def run(role: :web)
13
+ role = config.role(role)
14
+
15
+ docker :run,
16
+ "-d",
17
+ "--restart unless-stopped",
18
+ "--name", config.service_with_version,
19
+ "-e", redact("RAILS_MASTER_KEY=#{config.master_key}"),
20
+ *config.env_args,
21
+ *role.label_args,
22
+ config.absolute_image,
23
+ role.cmd
9
24
  end
10
25
 
11
26
  def start
12
- "docker run -d --rm --name #{config.service_with_version} #{config.envs} #{config.labels} #{config.absolute_image}"
27
+ docker :start, config.service_with_version
13
28
  end
14
29
 
15
30
  def stop
16
- "docker ps -q --filter label=service=#{config.service} | xargs docker stop"
31
+ [ "docker ps -q #{service_filter.join(" ")} | xargs docker stop" ]
17
32
  end
18
33
 
19
34
  def info
20
- "docker ps --filter label=service=#{config.service}"
35
+ docker :ps, *service_filter
36
+ end
37
+
38
+ def logs
39
+ [ "docker ps -q #{service_filter.join(" ")} | xargs docker logs -f" ]
40
+ end
41
+
42
+ def exec(*command)
43
+ docker :exec,
44
+ "-e", redact("RAILS_MASTER_KEY=#{config.master_key}"),
45
+ *config.env_args,
46
+ config.service_with_version,
47
+ *command
21
48
  end
49
+
50
+ def list_containers
51
+ docker :container, :ls, "-a", *service_filter
52
+ end
53
+
54
+ def remove_containers
55
+ docker :container, :prune, "-f", *service_filter
56
+ end
57
+
58
+ def remove_images
59
+ docker :image, :prune, "-a", "-f", *service_filter
60
+ end
61
+
62
+ private
63
+ def service_filter
64
+ [ "--filter", "label=service=#{config.service}" ]
65
+ end
22
66
  end
@@ -1,5 +1,11 @@
1
1
  class Mrsk::Commands::Registry < Mrsk::Commands::Base
2
+ delegate :registry, to: :config
3
+
2
4
  def login
3
- "docker login #{config.registry["server"]} -u #{config.registry["username"]} -p #{config.registry["password"]}"
5
+ docker :login, registry["server"], "-u", redact(registry["username"]), "-p", redact(registry["password"])
6
+ end
7
+
8
+ def logout
9
+ docker :logout, registry["server"]
4
10
  end
5
11
  end
@@ -1,17 +1,35 @@
1
1
  class Mrsk::Commands::Traefik < Mrsk::Commands::Base
2
+ def run
3
+ docker :run, "--name traefik",
4
+ "-d",
5
+ "--restart unless-stopped",
6
+ "-p 80:80",
7
+ "-v /var/run/docker.sock:/var/run/docker.sock",
8
+ "traefik",
9
+ "--providers.docker"
10
+ end
11
+
2
12
  def start
3
- "docker run --name traefik " +
4
- "--rm -d " +
5
- "-p 80:80 " +
6
- "-v /var/run/docker.sock:/var/run/docker.sock " +
7
- "traefik --providers.docker"
13
+ docker :container, :start, "traefik"
8
14
  end
9
15
 
10
16
  def stop
11
- "docker container stop traefik"
17
+ docker :container, :stop, "traefik"
12
18
  end
13
19
 
14
20
  def info
15
- "docker ps --filter name=traefik"
21
+ docker :ps, "--filter", "name=traefik"
22
+ end
23
+
24
+ def logs
25
+ docker :logs, "traefik"
26
+ end
27
+
28
+ def remove_container
29
+ docker :container, :prune, "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
30
+ end
31
+
32
+ def remove_image
33
+ docker :image, :prune, "-a", "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
16
34
  end
17
35
  end
data/lib/mrsk/commands.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require "sshkit"
2
+
1
3
  module Mrsk::Commands
2
4
  class Base
3
5
  attr_accessor :config
@@ -5,6 +7,16 @@ module Mrsk::Commands
5
7
  def initialize(config)
6
8
  @config = config
7
9
  end
10
+
11
+ private
12
+ def docker(*args)
13
+ args.compact.unshift :docker
14
+ end
15
+
16
+ # Copied from SSHKit::Backend::Abstract#redact to be available inside Commands classes
17
+ def redact(arg) # Used in execute_command to hide redact() args a user passes in
18
+ arg.to_s.extend(SSHKit::Redaction) # to_s due to our inability to extend Integer, etc
19
+ end
8
20
  end
9
21
  end
10
22
 
@@ -0,0 +1,63 @@
1
+ class Mrsk::Configuration::Role
2
+ delegate :argumentize, to: Mrsk::Configuration
3
+
4
+ attr_accessor :name
5
+
6
+ def initialize(name, config:)
7
+ @name, @config = name.inquiry, config
8
+ end
9
+
10
+ def hosts
11
+ @hosts ||= extract_hosts_from_config
12
+ end
13
+
14
+ def label_args
15
+ argumentize "--label", labels
16
+ end
17
+
18
+ def cmd
19
+ specializations["cmd"]
20
+ end
21
+
22
+ private
23
+ attr_accessor :config
24
+
25
+ def extract_hosts_from_config
26
+ if config.servers.is_a?(Array)
27
+ config.servers
28
+ else
29
+ servers = config.servers[name]
30
+ servers.is_a?(Array) ? servers : servers["hosts"]
31
+ end
32
+ end
33
+
34
+ def labels
35
+ if name.web?
36
+ default_labels.merge(traefik_labels)
37
+ else
38
+ default_labels
39
+ end
40
+ end
41
+
42
+ def default_labels
43
+ { "service" => config.service, "role" => name }
44
+ end
45
+
46
+ def traefik_labels
47
+ {
48
+ "traefik.http.routers.#{config.service}.rule" => "'PathPrefix(`/`)'",
49
+ "traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => "/up",
50
+ "traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s",
51
+ "traefik.http.middlewares.#{config.service}.retry.attempts" => "3",
52
+ "traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms"
53
+ }
54
+ end
55
+
56
+ def specializations
57
+ if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
58
+ { }
59
+ else
60
+ config.servers[name].without("hosts")
61
+ end
62
+ end
63
+ end
@@ -1,60 +1,92 @@
1
1
  require "active_support/ordered_options"
2
+ require "active_support/core_ext/string/inquiry"
3
+ require "erb"
2
4
 
3
5
  class Mrsk::Configuration
4
- delegate :service, :image, :env, :registry, :ssh_user, to: :config, allow_nil: true
6
+ delegate :service, :image, :servers, :env, :registry, :ssh_user, to: :config, allow_nil: true
7
+
8
+ class << self
9
+ def load_file(file)
10
+ if file.exist?
11
+ new YAML.load(ERB.new(IO.read(file)).result).symbolize_keys
12
+ else
13
+ raise "Configuration file not found in #{file}"
14
+ end
15
+ end
5
16
 
6
- def self.load_file(file)
7
- if file.exist?
8
- new YAML.load_file(file).symbolize_keys
9
- else
10
- raise "Configuration file not found in #{file}"
17
+ def argumentize(argument, attributes)
18
+ attributes.flat_map { |k, v| [ argument, "#{k}=#{v}" ] }
11
19
  end
12
20
  end
13
21
 
14
- def initialize(config)
22
+ def initialize(config, validate: true)
15
23
  @config = ActiveSupport::InheritableOptions.new(config)
16
- ensure_required_keys_present
24
+ ensure_required_keys_present if validate
25
+ end
26
+
27
+
28
+ def roles
29
+ @roles ||= role_names.collect { |role_name| Role.new(role_name, config: self) }
30
+ end
31
+
32
+ def role(name)
33
+ roles.detect { |r| r.name == name.to_s }
34
+ end
35
+
36
+ def hosts
37
+ hosts =
38
+ case
39
+ when ENV["HOSTS"]
40
+ ENV["HOSTS"].split(",")
41
+ when ENV["ROLES"]
42
+ role_names = ENV["ROLES"].split(",")
43
+ roles.select { |r| role_names.include?(r.name) }.flat_map(&:hosts)
44
+ else
45
+ roles.flat_map(&:hosts)
46
+ end
47
+
48
+ if hosts.any?
49
+ hosts
50
+ else
51
+ raise ArgumentError, "No hosts found"
52
+ end
17
53
  end
18
54
 
19
- def servers
20
- ENV["SERVERS"] || config.servers
55
+ def primary_host
56
+ role(:web).hosts.first
21
57
  end
22
58
 
59
+
23
60
  def version
24
61
  @version ||= ENV["VERSION"] || `git rev-parse HEAD`.strip
25
62
  end
26
63
 
27
- def absolute_image
28
- [ config.registry["server"], image_with_version ].compact.join("/")
64
+ def repository
65
+ [ config.registry["server"], image ].compact.join("/")
29
66
  end
30
67
 
31
- def image_with_version
32
- "#{image}:#{version}"
68
+ def absolute_image
69
+ "#{repository}:#{version}"
33
70
  end
34
71
 
35
72
  def service_with_version
36
73
  "#{service}-#{version}"
37
74
  end
38
75
 
39
- def envs
40
- parameterize "-e", \
41
- { "RAILS_MASTER_KEY" => master_key }.merge(env || {})
42
- end
43
76
 
44
- def labels
45
- parameterize "--label", \
46
- "service" => service,
47
- "traefik.http.routers.#{service}.rule" => "'PathPrefix(`/`)'",
48
- "traefik.http.services.#{service}.loadbalancer.healthcheck.path" => "/up",
49
- "traefik.http.services.#{service}.loadbalancer.healthcheck.interval" => "1s",
50
- "traefik.http.middlewares.#{service}.retry.attempts" => "3",
51
- "traefik.http.middlewares.#{service}.retry.initialinterval" => "500ms"
77
+ def env_args
78
+ self.class.argumentize "-e", config.env if config.env.present?
52
79
  end
53
80
 
54
81
  def ssh_options
55
82
  { user: config.ssh_user || "root", auth_methods: [ "publickey" ] }
56
83
  end
57
84
 
85
+ def master_key
86
+ ENV["RAILS_MASTER_KEY"] || File.read(Rails.root.join("config/master.key"))
87
+ end
88
+
89
+
58
90
  private
59
91
  attr_accessor :config
60
92
 
@@ -68,11 +100,9 @@ class Mrsk::Configuration
68
100
  end
69
101
  end
70
102
 
71
- def parameterize(param, hash)
72
- hash.collect { |k, v| "#{param} #{k}=#{v}" }.join(" ")
73
- end
74
-
75
- def master_key
76
- ENV["RAILS_MASTER_KEY"] || File.read(Rails.root.join("config/master.key"))
103
+ def role_names
104
+ config.servers.is_a?(Array) ? [ "web" ] : config.servers.keys.sort
77
105
  end
78
106
  end
107
+
108
+ require "mrsk/configuration/role"
data/lib/mrsk/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mrsk
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -4,28 +4,95 @@ app = Mrsk::Commands::App.new(MRSK_CONFIG)
4
4
 
5
5
  namespace :mrsk do
6
6
  namespace :app do
7
- desc "Build and push app image to servers"
7
+ desc "Deliver a newly built app image to servers"
8
+ task deliver: %i[ push pull ]
9
+
10
+ desc "Build locally and push app image to registry"
8
11
  task :push do
9
- run_locally { execute app.push }
10
- on(MRSK_CONFIG.servers) { execute app.pull }
12
+ run_locally { execute *app.push } unless ENV["VERSION"]
13
+ end
14
+
15
+ desc "Pull app image from the registry onto servers"
16
+ task :pull do
17
+ on(MRSK_CONFIG.hosts) { execute *app.pull }
18
+ end
19
+
20
+ desc "Run app on servers (or start them if they've already been run)"
21
+ task :run do
22
+ MRSK_CONFIG.roles.each do |role|
23
+ on(role.hosts) do |host|
24
+ begin
25
+ execute *app.run(role: role.name)
26
+ rescue SSHKit::Command::Failed => e
27
+ if e.message =~ /already in use/
28
+ puts "Container with same version already deployed on #{host}, starting that instead"
29
+ execute *app.start, host: host
30
+ else
31
+ raise
32
+ end
33
+ end
34
+ end
35
+ end
11
36
  end
12
37
 
13
- desc "Start app on servers"
38
+ desc "Start existing app on servers"
14
39
  task :start do
15
- on(MRSK_CONFIG.servers) { execute app.start }
40
+ on(MRSK_CONFIG.hosts) { execute *app.start, raise_on_non_zero_exit: false }
16
41
  end
17
42
 
18
43
  desc "Stop app on servers"
19
44
  task :stop do
20
- on(MRSK_CONFIG.servers) { execute app.stop, raise_on_non_zero_exit: false }
45
+ on(MRSK_CONFIG.hosts) { execute *app.stop, raise_on_non_zero_exit: false }
21
46
  end
22
47
 
23
- desc "Restart app on servers"
48
+ desc "Start app on servers (use VERSION=<git-hash> to designate which version)"
24
49
  task restart: %i[ stop start ]
25
50
 
26
51
  desc "Display information about app containers"
27
52
  task :info do
28
- on(MRSK_CONFIG.servers) { |host| puts "Host: #{host}\n" + capture(app.info) + "\n\n" }
53
+ on(MRSK_CONFIG.hosts) { |host| puts "App Host: #{host}\n" + capture(*app.info) + "\n\n" }
54
+ end
55
+
56
+ desc "Execute a custom task on servers passed in as CMD='bin/rake some:task'"
57
+ task :exec do
58
+ on(MRSK_CONFIG.hosts) { |host| puts "App Host: #{host}\n" + capture(*app.exec(ENV["CMD"])) + "\n\n" }
59
+ end
60
+
61
+ namespace :exec do
62
+ desc "Execute Rails command on servers, like CMD='runner \"puts %(Hello World)\""
63
+ task :rails do
64
+ on(MRSK_CONFIG.hosts) { |host| puts "App Host: #{host}\n" + capture(*app.exec("bin/rails", ENV["CMD"])) + "\n\n" }
65
+ end
66
+
67
+ desc "Execute a custom task on the first defined server"
68
+ task :once do
69
+ on(MRSK_CONFIG.primary_host) { |host| puts capture(*app.exec(ENV["CMD"])) }
70
+ end
71
+
72
+ namespace :once do
73
+ desc "Execute Rails command on the first defined server, like CMD='runner \"puts %(Hello World)\""
74
+ task :rails do
75
+ on(MRSK_CONFIG.primary_host) { puts capture(*app.exec("bin/rails", ENV["CMD"])) }
76
+ end
77
+ end
78
+ end
79
+
80
+ desc "List all the app containers currently on servers"
81
+ task :containers do
82
+ on(MRSK_CONFIG.hosts) { |host| puts "App Host: #{host}\n" + capture(*app.list_containers) + "\n\n" }
83
+ end
84
+
85
+ desc "Tail logs from app containers"
86
+ task :logs do
87
+ on(MRSK_CONFIG.hosts) { execute *app.logs }
88
+ end
89
+
90
+ desc "Remove app containers and images from servers"
91
+ task remove: %i[ stop ] do
92
+ on(MRSK_CONFIG.hosts) do
93
+ execute *app.remove_containers
94
+ execute *app.remove_images
95
+ end
29
96
  end
30
97
  end
31
98
  end
@@ -1,12 +1,26 @@
1
+ require_relative "setup"
2
+
1
3
  namespace :mrsk do
4
+ desc "Deploy app for the first time to a fresh server"
5
+ task fresh: %w[ server:bootstrap registry:login app:deliver traefik:run app:stop app:run ]
6
+
2
7
  desc "Push the latest version of the app, ensure Traefik is running, then restart app"
3
- task deploy: [ "app:push", "traefik:start", "app:restart" ]
8
+ task deploy: %w[ registry:login app:deliver traefik:run app:stop app:run prune ]
9
+
10
+ desc "Rollback to VERSION=x that was already run as a container on servers"
11
+ task rollback: %w[ app:restart ]
4
12
 
5
13
  desc "Display information about Traefik and app containers"
6
- task info: [ "traefik:info", "app:info" ]
14
+ task info: %w[ traefik:info app:info ]
7
15
 
8
- desc "Create config stub"
16
+ desc "Create config stub in config/deploy.yml"
9
17
  task :init do
10
- Rails.root.join("config/deploy.yml")
18
+ require "fileutils"
19
+ FileUtils.cp_r \
20
+ Pathname.new(File.expand_path("templates/deploy.yml", __dir__)),
21
+ Rails.root.join("config/deploy.yml")
11
22
  end
23
+
24
+ desc "Remove Traefik, app, and registry session from servers"
25
+ task remove: %w[ traefik:remove app:remove registry:logout ]
12
26
  end
@@ -0,0 +1,18 @@
1
+ require_relative "setup"
2
+
3
+ namespace :mrsk do
4
+ desc "Prune unused images and stopped containers"
5
+ task prune: %w[ prune:containers prune:images ]
6
+
7
+ namespace :prune do
8
+ desc "Prune unused images older than 30 days"
9
+ task :images do
10
+ on(MRSK_CONFIG.hosts) { execute "docker image prune -f --filter 'until=720h'" }
11
+ end
12
+
13
+ desc "Prune stopped containers for the service older than 3 days"
14
+ task :containers do
15
+ on(MRSK_CONFIG.hosts) { execute "docker container prune -f --filter label=service=#{MRSK_CONFIG.service} --filter 'until=72h'" }
16
+ end
17
+ end
18
+ end
@@ -6,8 +6,13 @@ namespace :mrsk do
6
6
  namespace :registry do
7
7
  desc "Login to the registry locally and remotely"
8
8
  task :login do
9
- run_locally { execute registry.login }
10
- on(MRSK_CONFIG.servers) { execute registry.login }
9
+ run_locally { execute *registry.login }
10
+ on(MRSK_CONFIG.hosts) { execute *registry.login }
11
+ end
12
+
13
+ desc "Logout of the registry remotely"
14
+ task :logout do
15
+ on(MRSK_CONFIG.hosts) { execute *registry.logout }
11
16
  end
12
17
  end
13
18
  end
@@ -0,0 +1,11 @@
1
+ require_relative "setup"
2
+
3
+ namespace :mrsk do
4
+ namespace :server do
5
+ desc "Setup Docker on the remote servers"
6
+ task :bootstrap do
7
+ # FIXME: Detect when apt-get is not available and use the appropriate alternative
8
+ on(MRSK_CONFIG.hosts) { execute "apt-get install docker.io -y" }
9
+ end
10
+ end
11
+ end
@@ -3,6 +3,14 @@ require "sshkit/dsl"
3
3
 
4
4
  include SSHKit::DSL
5
5
 
6
- MRSK_CONFIG = Mrsk::Configuration.load_file(Rails.root.join("config/deploy.yml"))
6
+ if (config_file = Rails.root.join("config/deploy.yml")).exist?
7
+ MRSK_CONFIG = Mrsk::Configuration.load_file(config_file)
7
8
 
8
- SSHKit::Backend::Netssh.configure { |ssh| ssh.ssh_options = MRSK_CONFIG.ssh_options }
9
+ SSHKit::Backend::Netssh.configure { |ssh| ssh.ssh_options = MRSK_CONFIG.ssh_options }
10
+
11
+ # No need to use /usr/bin/env, just clogs up the logs
12
+ SSHKit.config.command_map[:docker] = "docker"
13
+ else
14
+ # MRSK is missing config/deploy.yml – run 'rake mrsk:init'
15
+ MRSK_CONFIG = Mrsk::Configuration.new({}, validate: false)
16
+ end
@@ -0,0 +1,21 @@
1
+ # Name of your application will be used for uniquely configuring Traefik and app containers.
2
+ # Your Dockerfile should set LABEL service=the-same-value to ensure image pruning works.
3
+ service: my-app
4
+
5
+ # Name of the container image
6
+ image: user/chat
7
+
8
+ # All the servers targeted for deploy. You can reference a single server for a command by using SERVERS=xxx.xxx.xxx.xxx
9
+ servers:
10
+ - xxx.xxx.xxx.xxx
11
+
12
+ # The following envs are made available to the container when started
13
+ env:
14
+ # Remember never to put passwords or tokens directly into this file, use encrypted credentials
15
+ # REDIS_URL: redis://x/y
16
+
17
+ registry:
18
+ # Specify the registry server, if you're not using Docker Hub
19
+ # server: registry.digitalocean.com / ghcr.io / ...
20
+ username: <%= Rails.application.credentials.registry["username"] %>
21
+ password: <%= Rails.application.credentials.registry["password"] %>
@@ -4,22 +4,35 @@ traefik = Mrsk::Commands::Traefik.new(MRSK_CONFIG)
4
4
 
5
5
  namespace :mrsk do
6
6
  namespace :traefik do
7
- desc "Start Traefik"
7
+ desc "Run Traefik on servers"
8
+ task :run do
9
+ on(MRSK_CONFIG.role(:web).hosts) { execute *traefik.run, raise_on_non_zero_exit: false }
10
+ end
11
+
12
+ desc "Start existing Traefik on servers"
8
13
  task :start do
9
- on(MRSK_CONFIG.servers) { execute traefik.start, raise_on_non_zero_exit: false }
14
+ on(MRSK_CONFIG.role(:web).hosts) { execute *traefik.start, raise_on_non_zero_exit: false }
10
15
  end
11
16
 
12
- desc "Stop Traefik"
17
+ desc "Stop Traefik on servers"
13
18
  task :stop do
14
- on(MRSK_CONFIG.servers) { execute traefik.stop, raise_on_non_zero_exit: false }
19
+ on(MRSK_CONFIG.role(:web).hosts) { execute *traefik.stop, raise_on_non_zero_exit: false }
15
20
  end
16
21
 
17
- desc "Restart Traefik"
22
+ desc "Restart Traefik on servers"
18
23
  task restart: %i[ stop start ]
19
24
 
20
- desc "Display information about Traefik containers"
25
+ desc "Display information about Traefik containers from servers"
21
26
  task :info do
22
- on(MRSK_CONFIG.servers) { |host| puts "Host: #{host}\n" + capture(traefik.info) + "\n\n" }
27
+ on(MRSK_CONFIG.role(:web).hosts) { |host| puts "Traefik Host: #{host}\n" + capture(*traefik.info) + "\n\n" }
28
+ end
29
+
30
+ desc "Remove Traefik container and image from servers"
31
+ task remove: %i[ stop ] do
32
+ on(MRSK_CONFIG.role(:web).hosts) do
33
+ execute *traefik.remove_container
34
+ execute *traefik.remove_image
35
+ end
23
36
  end
24
37
  end
25
38
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mrsk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-07 00:00:00.000000000 Z
11
+ date: 2023-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -52,12 +52,16 @@ files:
52
52
  - lib/mrsk/commands/registry.rb
53
53
  - lib/mrsk/commands/traefik.rb
54
54
  - lib/mrsk/configuration.rb
55
+ - lib/mrsk/configuration/role.rb
55
56
  - lib/mrsk/engine.rb
56
57
  - lib/mrsk/version.rb
57
58
  - lib/tasks/mrsk/app.rake
58
59
  - lib/tasks/mrsk/mrsk.rake
60
+ - lib/tasks/mrsk/prune.rake
59
61
  - lib/tasks/mrsk/registry.rake
62
+ - lib/tasks/mrsk/server.rake
60
63
  - lib/tasks/mrsk/setup.rb
64
+ - lib/tasks/mrsk/templates/deploy.yml
61
65
  - lib/tasks/mrsk/traefik.rake
62
66
  homepage: https://github.com/rails/mrsk
63
67
  licenses:
@@ -78,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
82
  - !ruby/object:Gem::Version
79
83
  version: '0'
80
84
  requirements: []
81
- rubygems_version: 3.3.26
85
+ rubygems_version: 3.4.1
82
86
  signing_key:
83
87
  specification_version: 4
84
88
  summary: Deploy Docker containers with zero downtime to any host.