mrsk 0.6.3 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +53 -7
- data/lib/mrsk/cli/accessory.rb +9 -12
- data/lib/mrsk/cli/app.rb +9 -9
- data/lib/mrsk/cli/base.rb +5 -0
- data/lib/mrsk/cli/build.rb +1 -1
- data/lib/mrsk/cli/healthcheck.rb +29 -0
- data/lib/mrsk/cli/main.rb +32 -7
- data/lib/mrsk/cli/prune.rb +3 -3
- data/lib/mrsk/cli/traefik.rb +4 -4
- data/lib/mrsk/commander.rb +4 -0
- data/lib/mrsk/commands/app.rb +10 -4
- data/lib/mrsk/commands/auditor.rb +13 -3
- data/lib/mrsk/commands/base.rb +4 -0
- data/lib/mrsk/commands/builder/base.rb +17 -9
- data/lib/mrsk/commands/builder/multiarch.rb +1 -3
- data/lib/mrsk/commands/builder/native/remote.rb +1 -3
- data/lib/mrsk/commands/builder/native.rb +1 -1
- data/lib/mrsk/commands/healthcheck.rb +46 -0
- data/lib/mrsk/commands/prune.rb +4 -8
- data/lib/mrsk/configuration/role.rb +1 -1
- data/lib/mrsk/configuration.rb +12 -1
- data/lib/mrsk/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de97b438ef507257a74ec7c5143028946072ac152ff631e979f71788786bc2ba
|
4
|
+
data.tar.gz: 35112851498a8a99452850077ad1845f403d198d49b39fb769a746690c48909b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f35616494c2fcf90fb00f2cfd108492147e76d5eb12b91356a520dd0c5b7974bb6336feb55fa8239df407566144b1b1e069d1981e6aad8c159068e4bb4a91d5d
|
7
|
+
data.tar.gz: 4e9e55f95b32399c465384351d209bc12709158f39ce05f4ea1b569a3e9f1bd7cb725ce8bd795aa18548230812c0309e0d277392bf942adc62e3a6f436d626d4
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# MRSK
|
2
2
|
|
3
|
-
MRSK deploys web apps in containers to servers running Docker with zero downtime. It uses the dynamic reverse-proxy Traefik to hold requests while the new application container is started and the old one is stopped. It works seamlessly across multiple hosts, using SSHKit to execute commands.
|
3
|
+
MRSK deploys web apps in containers to servers running Docker with zero downtime. It uses the dynamic reverse-proxy Traefik to hold requests while the new application container is started and the old one is stopped. It works seamlessly across multiple hosts, using SSHKit to execute commands. It was built for Rails applications, but works with any type of web app that can be bundled with Docker.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -20,7 +20,7 @@ env:
|
|
20
20
|
- RAILS_MASTER_KEY
|
21
21
|
```
|
22
22
|
|
23
|
-
Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app).
|
23
|
+
Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app).
|
24
24
|
|
25
25
|
Now you're ready to deploy to the servers:
|
26
26
|
|
@@ -37,12 +37,23 @@ This will:
|
|
37
37
|
5. Push the image to the registry.
|
38
38
|
6. Pull the image from the registry on the servers.
|
39
39
|
7. Ensure Traefik is running and accepting traffic on port 80.
|
40
|
-
8.
|
41
|
-
9.
|
42
|
-
10.
|
40
|
+
8. Ensure your app responds with `200 OK` to `GET /up`.
|
41
|
+
9. Stop any containers running a previous versions of the app.
|
42
|
+
10. Start a new container with the version of the app that matches the current git version hash.
|
43
|
+
11. Prune unused images and stopped containers to ensure servers don't fill up.
|
43
44
|
|
44
45
|
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.
|
45
46
|
|
47
|
+
## Vision
|
48
|
+
|
49
|
+
In the past decade+, there's been an explosion in commercial offerings that make deploying web apps easier. Heroku kicked it off with an incredible offering that stayed ahead of the competition seemingly forever. These days we have excellent alternatives like Fly.io and Render. And hosted Kubernetes is making things easier too on AWS, GCP, Digital Ocean, and elsewhere. But these are all offerings that have you renting computers in the cloud at a premium. If you want to run on our own hardware, or even just have a clear migration path to do so, you need to carefully consider how locked in you get to these commercial platforms. Preferably before the bills swallow your business whole!
|
50
|
+
|
51
|
+
MRSK seeks to bring the advance in ergonomics pioneered by these commercial offerings to deploying web apps anywhere. Whether that's low-cost cloud options without the managed-service markup from the likes of Digital Ocean, Hetzner, OVH, etc, or it's your own colocated metal. To MRSK, it's all the same. Feed the config file a list of IP addresses with vanilla Ubuntu servers that have seen no prep beyond an added SSH key, and you'll be running in literally minutes.
|
52
|
+
|
53
|
+
This structure also gives you enormous portability. You can have your web app deployed on several clouds at ease like this. Or you can buy the baseline with your own hardware, then deploy to a cloud before a big seasonal spike to get more capacity. When you're not locked into a single provider from a tooling perspective, there's a lot of compelling options available.
|
54
|
+
|
55
|
+
Ultimately, MRSK is meant to compress the complexity of going to production using open source tooling that isn't tied to any commercial offering. Not to zero, though. You're probably still better off with a fully managed service if basic Linux or Docker is still difficult, but from an early stage when those concepts are familiar.
|
56
|
+
|
46
57
|
## Why not just run Capistrano, Kubernetes or Docker Swarm?
|
47
58
|
|
48
59
|
MRSK basically is Capistrano for Containers, which allow us to use vanilla servers as the hosts. No need to ensure that the servers have just the right version of Ruby or other dependencies you need. That all lives in the Docker image now. You can boot a brand new Ubuntu (or whatever) server, add it to the deploy servers of MRSK, and it'll be auto-provisioned with Docker, and run right away. Docker's layer caching also allows for quicker deployments with less mucking about on the server. And the images built for MRSK can be used for CI or later introspection.
|
@@ -173,10 +184,10 @@ You can specialize the default Traefik rules by setting labels on the containers
|
|
173
184
|
|
174
185
|
```
|
175
186
|
labels:
|
176
|
-
traefik.http.routers.hey.rule:
|
187
|
+
traefik.http.routers.hey.rule: Host(\`app.hey.com\`)
|
177
188
|
```
|
178
189
|
|
179
|
-
Note: The
|
190
|
+
Note: The escaped backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
|
180
191
|
|
181
192
|
This allows you to run multiple applications on the same server sharing the same Traefik instance and port.
|
182
193
|
See https://doc.traefik.io/traefik/routing/routers/#rule for a full list of available routing rules.
|
@@ -335,6 +346,41 @@ This template can safely be checked into git. Then everyone deploying the app ca
|
|
335
346
|
|
336
347
|
If you need separate env variables for different destinations, you can set them with `.env.destination.erb` for the template, which will generate `.env.staging` when run with `mrsk envify -d staging`.
|
337
348
|
|
349
|
+
### Using audit broadcasts
|
350
|
+
|
351
|
+
If you'd like to broadcast audits of deploys, rollbacks, etc to a chatroom or elsewhere, you can configure the `audit_broadcast_cmd` setting with the path to a bin file that reads the audit line from STDIN, and then does whatever with it:
|
352
|
+
|
353
|
+
```yaml
|
354
|
+
audit_broadcast_cmd:
|
355
|
+
bin/audit_broadcast
|
356
|
+
```
|
357
|
+
|
358
|
+
The broadcast command could look something like:
|
359
|
+
|
360
|
+
```bash
|
361
|
+
#!/usr/bin/env bash
|
362
|
+
read
|
363
|
+
curl -q -d content="[My app] ${REPLY}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
|
364
|
+
```
|
365
|
+
|
366
|
+
That'll post a line like follows to a preconfigured chatbot in Basecamp:
|
367
|
+
|
368
|
+
```
|
369
|
+
[My App] [2023-02-18 11:29:52] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
370
|
+
```
|
371
|
+
|
372
|
+
### Using custom healthcheck path or port
|
373
|
+
|
374
|
+
MRSK defaults to checking the health of your application again `/up` on port 3000. You can tailor both with the `healthcheck` setting:
|
375
|
+
|
376
|
+
```yaml
|
377
|
+
healthcheck:
|
378
|
+
path: /healthz
|
379
|
+
port: 4000
|
380
|
+
```
|
381
|
+
|
382
|
+
This will ensure your application is configured with a traefik label for the healthcheck against `/healthz` and that the pre-deploy healthcheck that MRSK performs is done against the same path on port 4000.
|
383
|
+
|
338
384
|
## Commands
|
339
385
|
|
340
386
|
### Running commands on servers
|
data/lib/mrsk/cli/accessory.rb
CHANGED
@@ -9,9 +9,11 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|
9
9
|
upload(name)
|
10
10
|
|
11
11
|
on(accessory.host) do
|
12
|
-
execute *MRSK.auditor.record("
|
12
|
+
execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
13
13
|
execute *accessory.run
|
14
14
|
end
|
15
|
+
|
16
|
+
audit_broadcast "Booted accessory #{name}"
|
15
17
|
end
|
16
18
|
end
|
17
19
|
end
|
@@ -20,8 +22,6 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|
20
22
|
def upload(name)
|
21
23
|
with_accessory(name) do |accessory|
|
22
24
|
on(accessory.host) do
|
23
|
-
execute *MRSK.auditor.record("accessory #{name} upload files"), verbosity: :debug
|
24
|
-
|
25
25
|
accessory.files.each do |(local, remote)|
|
26
26
|
accessory.ensure_local_file_present(local)
|
27
27
|
|
@@ -37,8 +37,6 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|
37
37
|
def directories(name)
|
38
38
|
with_accessory(name) do |accessory|
|
39
39
|
on(accessory.host) do
|
40
|
-
execute *MRSK.auditor.record("accessory #{name} create directories"), verbosity: :debug
|
41
|
-
|
42
40
|
accessory.directories.keys.each do |host_path|
|
43
41
|
execute *accessory.make_directory(host_path)
|
44
42
|
end
|
@@ -59,7 +57,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|
59
57
|
def start(name)
|
60
58
|
with_accessory(name) do |accessory|
|
61
59
|
on(accessory.host) do
|
62
|
-
execute *MRSK.auditor.record("
|
60
|
+
execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug
|
63
61
|
execute *accessory.start
|
64
62
|
end
|
65
63
|
end
|
@@ -69,7 +67,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|
69
67
|
def stop(name)
|
70
68
|
with_accessory(name) do |accessory|
|
71
69
|
on(accessory.host) do
|
72
|
-
execute *MRSK.auditor.record("
|
70
|
+
execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
73
71
|
execute *accessory.stop, raise_on_non_zero_exit: false
|
74
72
|
end
|
75
73
|
end
|
@@ -111,14 +109,14 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|
111
109
|
when options[:reuse]
|
112
110
|
say "Launching command from existing container...", :magenta
|
113
111
|
on(accessory.host) do
|
114
|
-
execute *MRSK.auditor.record("
|
112
|
+
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
115
113
|
capture_with_info(*accessory.execute_in_existing_container(cmd))
|
116
114
|
end
|
117
115
|
|
118
116
|
else
|
119
117
|
say "Launching command from new container...", :magenta
|
120
118
|
on(accessory.host) do
|
121
|
-
execute *MRSK.auditor.record("
|
119
|
+
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
122
120
|
capture_with_info(*accessory.execute_in_new_container(cmd))
|
123
121
|
end
|
124
122
|
end
|
@@ -169,7 +167,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|
169
167
|
def remove_container(name)
|
170
168
|
with_accessory(name) do |accessory|
|
171
169
|
on(accessory.host) do
|
172
|
-
execute *MRSK.auditor.record("
|
170
|
+
execute *MRSK.auditor.record("Remove #{name} accessory container"), verbosity: :debug
|
173
171
|
execute *accessory.remove_container
|
174
172
|
end
|
175
173
|
end
|
@@ -179,7 +177,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|
179
177
|
def remove_image(name)
|
180
178
|
with_accessory(name) do |accessory|
|
181
179
|
on(accessory.host) do
|
182
|
-
execute *MRSK.auditor.record("
|
180
|
+
execute *MRSK.auditor.record("Removed #{name} accessory image"), verbosity: :debug
|
183
181
|
execute *accessory.remove_image
|
184
182
|
end
|
185
183
|
end
|
@@ -189,7 +187,6 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|
189
187
|
def remove_service_directory(name)
|
190
188
|
with_accessory(name) do |accessory|
|
191
189
|
on(accessory.host) do
|
192
|
-
execute *MRSK.auditor.record("accessory #{name} remove service directory"), verbosity: :debug
|
193
190
|
execute *accessory.remove_service_directory
|
194
191
|
end
|
195
192
|
end
|
data/lib/mrsk/cli/app.rb
CHANGED
@@ -7,7 +7,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
7
7
|
|
8
8
|
MRSK.config.roles.each do |role|
|
9
9
|
on(role.hosts) do |host|
|
10
|
-
execute *MRSK.auditor.record("app
|
10
|
+
execute *MRSK.auditor.record("Booted app version #{version}"), verbosity: :debug
|
11
11
|
|
12
12
|
begin
|
13
13
|
execute *MRSK.app.stop, raise_on_non_zero_exit: false
|
@@ -15,7 +15,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
15
15
|
rescue SSHKit::Command::Failed => e
|
16
16
|
if e.message =~ /already in use/
|
17
17
|
error "Rebooting container with same version already deployed on #{host}"
|
18
|
-
execute *MRSK.auditor.record("app
|
18
|
+
execute *MRSK.auditor.record("Rebooted app version #{version}"), verbosity: :debug
|
19
19
|
|
20
20
|
execute *MRSK.app.remove_container(version: version)
|
21
21
|
execute *MRSK.app.run(role: role.name)
|
@@ -31,7 +31,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
31
31
|
desc "start", "Start existing app on servers (use --version=<git-hash> to designate specific version)"
|
32
32
|
def start
|
33
33
|
on(MRSK.hosts) do
|
34
|
-
execute *MRSK.auditor.record("app
|
34
|
+
execute *MRSK.auditor.record("Started app version #{MRSK.version}"), verbosity: :debug
|
35
35
|
execute *MRSK.app.start, raise_on_non_zero_exit: false
|
36
36
|
end
|
37
37
|
end
|
@@ -39,7 +39,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
39
39
|
desc "stop", "Stop app on servers"
|
40
40
|
def stop
|
41
41
|
on(MRSK.hosts) do
|
42
|
-
execute *MRSK.auditor.record("app
|
42
|
+
execute *MRSK.auditor.record("Stopped app"), verbosity: :debug
|
43
43
|
execute *MRSK.app.stop, raise_on_non_zero_exit: false
|
44
44
|
end
|
45
45
|
end
|
@@ -74,7 +74,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
74
74
|
say "Launching command with version #{version} from existing container...", :magenta
|
75
75
|
|
76
76
|
on(MRSK.hosts) do |host|
|
77
|
-
execute *MRSK.auditor.record("
|
77
|
+
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
|
78
78
|
puts_by_host host, capture_with_info(*MRSK.app.execute_in_existing_container(cmd))
|
79
79
|
end
|
80
80
|
end
|
@@ -84,7 +84,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
84
84
|
using_version(options[:version] || most_recent_version_available) do |version|
|
85
85
|
say "Launching command with version #{version} from new container...", :magenta
|
86
86
|
on(MRSK.hosts) do |host|
|
87
|
-
execute *MRSK.auditor.record("
|
87
|
+
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
|
88
88
|
puts_by_host host, capture_with_info(*MRSK.app.execute_in_new_container(cmd))
|
89
89
|
end
|
90
90
|
end
|
@@ -140,7 +140,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
140
140
|
desc "remove_container [VERSION]", "Remove app container with given version from servers"
|
141
141
|
def remove_container(version)
|
142
142
|
on(MRSK.hosts) do
|
143
|
-
execute *MRSK.auditor.record("app
|
143
|
+
execute *MRSK.auditor.record("Removed app container with version #{version}"), verbosity: :debug
|
144
144
|
execute *MRSK.app.remove_container(version: version)
|
145
145
|
end
|
146
146
|
end
|
@@ -148,7 +148,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
148
148
|
desc "remove_containers", "Remove all app containers from servers"
|
149
149
|
def remove_containers
|
150
150
|
on(MRSK.hosts) do
|
151
|
-
execute *MRSK.auditor.record("app
|
151
|
+
execute *MRSK.auditor.record("Removed all app containers"), verbosity: :debug
|
152
152
|
execute *MRSK.app.remove_containers
|
153
153
|
end
|
154
154
|
end
|
@@ -156,7 +156,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
156
156
|
desc "remove_images", "Remove all app images from servers"
|
157
157
|
def remove_images
|
158
158
|
on(MRSK.hosts) do
|
159
|
-
execute *MRSK.auditor.record("app
|
159
|
+
execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug
|
160
160
|
execute *MRSK.app.remove_images
|
161
161
|
end
|
162
162
|
end
|
data/lib/mrsk/cli/base.rb
CHANGED
@@ -59,9 +59,14 @@ module Mrsk::Cli
|
|
59
59
|
def print_runtime
|
60
60
|
started_at = Time.now
|
61
61
|
yield
|
62
|
+
return Time.now - started_at
|
62
63
|
ensure
|
63
64
|
runtime = Time.now - started_at
|
64
65
|
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
|
65
66
|
end
|
67
|
+
|
68
|
+
def audit_broadcast(line)
|
69
|
+
run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug }
|
70
|
+
end
|
66
71
|
end
|
67
72
|
end
|
data/lib/mrsk/cli/build.rb
CHANGED
@@ -29,7 +29,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|
29
29
|
desc "pull", "Pull app image from the registry onto servers"
|
30
30
|
def pull
|
31
31
|
on(MRSK.hosts) do
|
32
|
-
execute *MRSK.auditor.record("
|
32
|
+
execute *MRSK.auditor.record("Pulled image with version #{MRSK.version}"), verbosity: :debug
|
33
33
|
execute *MRSK.builder.pull
|
34
34
|
end
|
35
35
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
2
|
+
desc "perform", "Health check the current version of the app"
|
3
|
+
def perform
|
4
|
+
on(MRSK.primary_host) do
|
5
|
+
begin
|
6
|
+
execute *MRSK.healthcheck.run
|
7
|
+
|
8
|
+
target = "Health check against #{MRSK.config.healthcheck["path"]}"
|
9
|
+
|
10
|
+
if capture_with_info(*MRSK.healthcheck.curl) == "200"
|
11
|
+
info "#{target} succeeded with 200 OK!"
|
12
|
+
else
|
13
|
+
# Catches 1xx, 2xx, 3xx
|
14
|
+
raise SSHKit::Command::Failed, "#{target} failed to return 200 OK!"
|
15
|
+
end
|
16
|
+
rescue SSHKit::Command::Failed => e
|
17
|
+
if e.message =~ /curl/
|
18
|
+
# Catches 4xx, 5xx
|
19
|
+
raise SSHKit::Command::Failed, "#{target} failed to return 200 OK!"
|
20
|
+
else
|
21
|
+
raise
|
22
|
+
end
|
23
|
+
ensure
|
24
|
+
execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false
|
25
|
+
execute *MRSK.healthcheck.remove, raise_on_non_zero_exit: false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/mrsk/cli/main.rb
CHANGED
@@ -10,7 +10,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|
10
10
|
|
11
11
|
desc "deploy", "Deploy the app to servers"
|
12
12
|
def deploy
|
13
|
-
print_runtime do
|
13
|
+
runtime = print_runtime do
|
14
14
|
say "Ensure Docker is installed...", :magenta
|
15
15
|
invoke "mrsk:cli:server:bootstrap"
|
16
16
|
|
@@ -23,33 +23,48 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|
23
23
|
say "Ensure Traefik is running...", :magenta
|
24
24
|
invoke "mrsk:cli:traefik:boot"
|
25
25
|
|
26
|
+
say "Ensure app can pass healthcheck...", :magenta
|
27
|
+
invoke "mrsk:cli:healthcheck:perform"
|
28
|
+
|
26
29
|
invoke "mrsk:cli:app:boot"
|
27
30
|
|
28
31
|
say "Prune old containers and images...", :magenta
|
29
32
|
invoke "mrsk:cli:prune:all"
|
30
33
|
end
|
34
|
+
|
35
|
+
audit_broadcast "Deployed app in #{runtime.to_i} seconds"
|
31
36
|
end
|
32
37
|
|
33
38
|
desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)"
|
34
39
|
def redeploy
|
35
|
-
print_runtime do
|
40
|
+
runtime = print_runtime do
|
36
41
|
say "Build and push app image...", :magenta
|
37
42
|
invoke "mrsk:cli:build:deliver"
|
38
43
|
|
44
|
+
say "Ensure app can pass healthcheck...", :magenta
|
45
|
+
invoke "mrsk:cli:healthcheck:perform"
|
46
|
+
|
39
47
|
invoke "mrsk:cli:app:boot"
|
40
48
|
end
|
49
|
+
|
50
|
+
audit_broadcast "Redeployed app in #{runtime.to_i} seconds"
|
41
51
|
end
|
42
52
|
|
43
53
|
desc "rollback [VERSION]", "Rollback the app to VERSION"
|
44
54
|
def rollback(version)
|
45
55
|
MRSK.version = version
|
46
56
|
|
47
|
-
|
57
|
+
if container_name_available?(MRSK.config.service_with_version)
|
58
|
+
say "Stop current version, then start version #{version}...", :magenta
|
48
59
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
60
|
+
on(MRSK.hosts) do |host|
|
61
|
+
execute *MRSK.app.stop, raise_on_non_zero_exit: false
|
62
|
+
execute *MRSK.app.start
|
63
|
+
end
|
64
|
+
|
65
|
+
audit_broadcast "Rolled back app to version #{version}"
|
66
|
+
else
|
67
|
+
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
|
53
68
|
end
|
54
69
|
end
|
55
70
|
|
@@ -138,6 +153,9 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|
138
153
|
desc "build", "Build the application image"
|
139
154
|
subcommand "build", Mrsk::Cli::Build
|
140
155
|
|
156
|
+
desc "healthcheck", "Healthcheck the application"
|
157
|
+
subcommand "healthcheck", Mrsk::Cli::Healthcheck
|
158
|
+
|
141
159
|
desc "prune", "Prune old application images and containers"
|
142
160
|
subcommand "prune", Mrsk::Cli::Prune
|
143
161
|
|
@@ -149,4 +167,11 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|
149
167
|
|
150
168
|
desc "traefik", "Manage the Traefik load balancer"
|
151
169
|
subcommand "traefik", Mrsk::Cli::Traefik
|
170
|
+
|
171
|
+
private
|
172
|
+
def container_name_available?(container_name, host: MRSK.primary_host)
|
173
|
+
container_names = nil
|
174
|
+
on(host) { container_names = capture_with_info(*MRSK.app.list_container_names).split("\n") }
|
175
|
+
Array(container_names).include?(container_name)
|
176
|
+
end
|
152
177
|
end
|
data/lib/mrsk/cli/prune.rb
CHANGED
@@ -5,10 +5,10 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
|
5
5
|
invoke :images
|
6
6
|
end
|
7
7
|
|
8
|
-
desc "images", "Prune unused images older than
|
8
|
+
desc "images", "Prune unused images older than 7 days"
|
9
9
|
def images
|
10
10
|
on(MRSK.hosts) do
|
11
|
-
execute *MRSK.auditor.record("
|
11
|
+
execute *MRSK.auditor.record("Pruned images"), verbosity: :debug
|
12
12
|
execute *MRSK.prune.images
|
13
13
|
end
|
14
14
|
end
|
@@ -16,7 +16,7 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
|
16
16
|
desc "containers", "Prune stopped containers for the service older than 3 days"
|
17
17
|
def containers
|
18
18
|
on(MRSK.hosts) do
|
19
|
-
execute *MRSK.auditor.record("
|
19
|
+
execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug
|
20
20
|
execute *MRSK.prune.containers
|
21
21
|
end
|
22
22
|
end
|
data/lib/mrsk/cli/traefik.rb
CHANGED
@@ -14,7 +14,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|
14
14
|
desc "start", "Start existing Traefik on servers"
|
15
15
|
def start
|
16
16
|
on(MRSK.traefik_hosts) do
|
17
|
-
execute *MRSK.auditor.record("traefik
|
17
|
+
execute *MRSK.auditor.record("Started traefik"), verbosity: :debug
|
18
18
|
execute *MRSK.traefik.start, raise_on_non_zero_exit: false
|
19
19
|
end
|
20
20
|
end
|
@@ -22,7 +22,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|
22
22
|
desc "stop", "Stop Traefik on servers"
|
23
23
|
def stop
|
24
24
|
on(MRSK.traefik_hosts) do
|
25
|
-
execute *MRSK.auditor.record("traefik
|
25
|
+
execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug
|
26
26
|
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
|
27
27
|
end
|
28
28
|
end
|
@@ -72,7 +72,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|
72
72
|
desc "remove_container", "Remove Traefik container from servers"
|
73
73
|
def remove_container
|
74
74
|
on(MRSK.traefik_hosts) do
|
75
|
-
execute *MRSK.auditor.record("traefik
|
75
|
+
execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug
|
76
76
|
execute *MRSK.traefik.remove_container
|
77
77
|
end
|
78
78
|
end
|
@@ -80,7 +80,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|
80
80
|
desc "remove_container", "Remove Traefik image from servers"
|
81
81
|
def remove_image
|
82
82
|
on(MRSK.traefik_hosts) do
|
83
|
-
execute *MRSK.auditor.record("traefik
|
83
|
+
execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug
|
84
84
|
execute *MRSK.traefik.remove_image
|
85
85
|
end
|
86
86
|
end
|
data/lib/mrsk/commander.rb
CHANGED
data/lib/mrsk/commands/app.rb
CHANGED
@@ -75,10 +75,6 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|
75
75
|
docker :ps, "-q", *service_filter
|
76
76
|
end
|
77
77
|
|
78
|
-
def container_id_for(container_name:)
|
79
|
-
docker :container, :ls, "-a", "-f", "name=#{container_name}", "-q"
|
80
|
-
end
|
81
|
-
|
82
78
|
def current_running_version
|
83
79
|
# FIXME: Find more graceful way to extract the version from "app-version" than using sed and tail!
|
84
80
|
pipe \
|
@@ -93,11 +89,21 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|
93
89
|
"head -n 1"
|
94
90
|
end
|
95
91
|
|
92
|
+
def all_versions_from_available_containers
|
93
|
+
pipe \
|
94
|
+
docker(:image, :ls, "--format", '"{{.Tag}}"', config.repository),
|
95
|
+
"head -n 1"
|
96
|
+
end
|
97
|
+
|
96
98
|
|
97
99
|
def list_containers
|
98
100
|
docker :container, :ls, "-a", *service_filter
|
99
101
|
end
|
100
102
|
|
103
|
+
def list_container_names
|
104
|
+
[ *list_containers, "--format", "'{{ .Names }}'" ]
|
105
|
+
end
|
106
|
+
|
101
107
|
def remove_container(version:)
|
102
108
|
pipe \
|
103
109
|
container_id_for(container_name: service_with_version(version)),
|
@@ -1,12 +1,22 @@
|
|
1
1
|
require "active_support/core_ext/time/conversions"
|
2
2
|
|
3
3
|
class Mrsk::Commands::Auditor < Mrsk::Commands::Base
|
4
|
+
# Runs remotely
|
4
5
|
def record(line)
|
5
6
|
append \
|
6
7
|
[ :echo, tagged_line(line) ],
|
7
8
|
audit_log_file
|
8
9
|
end
|
9
10
|
|
11
|
+
# Runs locally
|
12
|
+
def broadcast(line)
|
13
|
+
if broadcast_cmd = config.audit_broadcast_cmd
|
14
|
+
pipe \
|
15
|
+
[ :echo, tagged_line(line) ],
|
16
|
+
broadcast_cmd
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
10
20
|
def reveal
|
11
21
|
[ :tail, "-n", 50, audit_log_file ]
|
12
22
|
end
|
@@ -21,14 +31,14 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
|
|
21
31
|
end
|
22
32
|
|
23
33
|
def tags
|
24
|
-
"[#{
|
34
|
+
"[#{recorded_at}] [#{performer}]"
|
25
35
|
end
|
26
36
|
|
27
37
|
def performer
|
28
|
-
`whoami`.strip
|
38
|
+
@performer ||= `whoami`.strip
|
29
39
|
end
|
30
40
|
|
31
|
-
def
|
41
|
+
def recorded_at
|
32
42
|
Time.now.to_fs(:db)
|
33
43
|
end
|
34
44
|
end
|
data/lib/mrsk/commands/base.rb
CHANGED
@@ -5,19 +5,27 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
|
5
5
|
docker :pull, config.absolute_image
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
9
|
-
|
8
|
+
def build_options
|
9
|
+
[ *build_tags, *build_labels, *build_args, *build_secrets ]
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
private
|
13
|
+
def build_tags
|
14
|
+
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
def build_labels
|
18
|
+
argumentize "--label", { service: config.service }
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_args
|
22
|
+
argumentize "--build-arg", args, redacted: true
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_secrets
|
26
|
+
argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
|
27
|
+
end
|
19
28
|
|
20
|
-
private
|
21
29
|
def args
|
22
30
|
(config.builder && config.builder["args"]) || {}
|
23
31
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
|
2
|
+
EXPOSED_PORT = 3999
|
3
|
+
|
4
|
+
def run
|
5
|
+
web = config.role(:web)
|
6
|
+
|
7
|
+
docker :run,
|
8
|
+
"-d",
|
9
|
+
"--name", container_name_with_version,
|
10
|
+
"-p", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
|
11
|
+
"--label", "service=#{container_name}",
|
12
|
+
*web.env_args,
|
13
|
+
*config.volume_args,
|
14
|
+
config.absolute_image,
|
15
|
+
web.cmd
|
16
|
+
end
|
17
|
+
|
18
|
+
def curl
|
19
|
+
[ :curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", health_url ]
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
pipe \
|
24
|
+
container_id_for(container_name: container_name),
|
25
|
+
xargs(docker(:stop))
|
26
|
+
end
|
27
|
+
|
28
|
+
def remove
|
29
|
+
pipe \
|
30
|
+
container_id_for(container_name: container_name),
|
31
|
+
xargs(docker(:container, :rm))
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def container_name
|
36
|
+
"healthcheck-#{config.service}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def container_name_with_version
|
40
|
+
"healthcheck-#{config.service_with_version}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def health_url
|
44
|
+
"http://localhost:#{EXPOSED_PORT}#{config.healthcheck["path"]}"
|
45
|
+
end
|
46
|
+
end
|
data/lib/mrsk/commands/prune.rb
CHANGED
@@ -2,15 +2,11 @@ require "active_support/duration"
|
|
2
2
|
require "active_support/core_ext/numeric/time"
|
3
3
|
|
4
4
|
class Mrsk::Commands::Prune < Mrsk::Commands::Base
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def images
|
9
|
-
docker :image, :prune, "-f", "--filter", "until=#{PRUNE_IMAGES_AFTER}h"
|
5
|
+
def images(until_hours: 7.days.in_hours.to_i)
|
6
|
+
docker :image, :prune, "--all", "--force", "--filter", "label=service=#{config.service}", "--filter", "until=#{until_hours}h"
|
10
7
|
end
|
11
8
|
|
12
|
-
def containers
|
13
|
-
docker :
|
14
|
-
docker :container, :prune, "-f", "--filter", "label=service=#{config.service}", "--filter", "'until=#{PRUNE_CONTAINERS_AFTER}h'"
|
9
|
+
def containers(until_hours: 3.days.in_hours.to_i)
|
10
|
+
docker :container, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "until=#{until_hours}h"
|
15
11
|
end
|
16
12
|
end
|
@@ -59,7 +59,7 @@ class Mrsk::Configuration::Role
|
|
59
59
|
if running_traefik?
|
60
60
|
{
|
61
61
|
"traefik.http.routers.#{config.service}.rule" => "'PathPrefix(`/`)'",
|
62
|
-
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => "
|
62
|
+
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"],
|
63
63
|
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s",
|
64
64
|
"traefik.http.middlewares.#{config.service}.retry.attempts" => "3",
|
65
65
|
"traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms"
|
data/lib/mrsk/configuration.rb
CHANGED
@@ -107,6 +107,7 @@ class Mrsk::Configuration
|
|
107
107
|
end
|
108
108
|
end
|
109
109
|
|
110
|
+
|
110
111
|
def ssh_user
|
111
112
|
if raw_config.ssh.present?
|
112
113
|
raw_config.ssh["user"] || "root"
|
@@ -127,6 +128,15 @@ class Mrsk::Configuration
|
|
127
128
|
end
|
128
129
|
|
129
130
|
|
131
|
+
def audit_broadcast_cmd
|
132
|
+
raw_config.audit_broadcast_cmd
|
133
|
+
end
|
134
|
+
|
135
|
+
def healthcheck
|
136
|
+
{ "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {})
|
137
|
+
end
|
138
|
+
|
139
|
+
|
130
140
|
def valid?
|
131
141
|
ensure_required_keys_present && ensure_env_available
|
132
142
|
end
|
@@ -145,7 +155,8 @@ class Mrsk::Configuration
|
|
145
155
|
volume_args: volume_args,
|
146
156
|
ssh_options: ssh_options,
|
147
157
|
builder: raw_config.builder,
|
148
|
-
accessories: raw_config.accessories
|
158
|
+
accessories: raw_config.accessories,
|
159
|
+
healthcheck: healthcheck
|
149
160
|
}.compact
|
150
161
|
end
|
151
162
|
|
data/lib/mrsk/version.rb
CHANGED
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.
|
4
|
+
version: 0.7.0
|
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-02-
|
11
|
+
date: 2023-02-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -96,6 +96,7 @@ files:
|
|
96
96
|
- lib/mrsk/cli/app.rb
|
97
97
|
- lib/mrsk/cli/base.rb
|
98
98
|
- lib/mrsk/cli/build.rb
|
99
|
+
- lib/mrsk/cli/healthcheck.rb
|
99
100
|
- lib/mrsk/cli/main.rb
|
100
101
|
- lib/mrsk/cli/prune.rb
|
101
102
|
- lib/mrsk/cli/registry.rb
|
@@ -115,6 +116,7 @@ files:
|
|
115
116
|
- lib/mrsk/commands/builder/multiarch/remote.rb
|
116
117
|
- lib/mrsk/commands/builder/native.rb
|
117
118
|
- lib/mrsk/commands/builder/native/remote.rb
|
119
|
+
- lib/mrsk/commands/healthcheck.rb
|
118
120
|
- lib/mrsk/commands/prune.rb
|
119
121
|
- lib/mrsk/commands/registry.rb
|
120
122
|
- lib/mrsk/commands/traefik.rb
|