Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edcfc77d95 | ||
|
|
a71e167a03 | ||
|
|
2daaf442fa | ||
|
|
61b7dc90f2 | ||
|
|
f6442513ae | ||
|
|
5e8df58e6b | ||
|
|
9d5a6d1321 | ||
|
|
ecfd258093 | ||
|
|
313f89a108 | ||
|
|
9ab448e186 | ||
|
|
e1433f3895 | ||
|
|
a29e188c90 | ||
|
|
95e3915991 | ||
|
|
30d342183d | ||
|
|
83f5f3f053 | ||
|
|
e6ca270537 | ||
|
|
cd88c49c42 | ||
|
|
d03195ce1c | ||
|
|
da1c049829 | ||
|
|
4095e1853d | ||
|
|
dbc9989730 | ||
|
|
e493369453 | ||
|
|
e760cfa457 | ||
|
|
f8d651af0d | ||
|
|
08172be375 | ||
|
|
a3cc2317e2 | ||
|
|
2746a48e88 |
@@ -1,7 +1,7 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
mrsk (0.14.0)
|
mrsk (0.15.1)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
bcrypt_pbkdf (~> 1.0)
|
bcrypt_pbkdf (~> 1.0)
|
||||||
dotenv (~> 2.8)
|
dotenv (~> 2.8)
|
||||||
|
|||||||
47
README.md
47
README.md
@@ -63,6 +63,24 @@ This will:
|
|||||||
|
|
||||||
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. For subsequent deploys, or if your servers already have Docker and curl installed, you can just run `mrsk deploy`.
|
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. For subsequent deploys, or if your servers already have Docker and curl installed, you can just run `mrsk deploy`.
|
||||||
|
|
||||||
|
### Rails <7 usage
|
||||||
|
|
||||||
|
MRSK is not needed to be in your application Gemfile to be used. However, if you want to guarantee specific MRSK version in your CI/CD workflows, you can create a separate Gemfile for MRSK, for example, `gemfile/mrsk.gemfile`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem 'mrsk', '~> 0.14'
|
||||||
|
```
|
||||||
|
|
||||||
|
Bundle with `BUNDLE_GEMFILE=gemfiles/mrsk.gemfile bundle`.
|
||||||
|
|
||||||
|
After this MRSK can be used for deployment:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
BUNDLE_GEMFILE=gemfiles/mrsk.gemfile bundle exec mrsk deploy
|
||||||
|
```
|
||||||
|
|
||||||
## Vision
|
## Vision
|
||||||
|
|
||||||
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 your own hardware, or even just have a clear migration path to do so in the future, you need to carefully consider how locked in you get to these commercial platforms. Preferably before the bills swallow your business whole!
|
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 your own hardware, or even just have a clear migration path to do so in the future, you need to carefully consider how locked in you get to these commercial platforms. Preferably before the bills swallow your business whole!
|
||||||
@@ -123,6 +141,8 @@ This template can safely be checked into git. Then everyone deploying the app ca
|
|||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
|
Note: If you utilize biometrics with 1Password you can remove the `session_token` related parts in the example and just call `op read op://Vault/Docker Hub/password -n`.
|
||||||
|
|
||||||
#### Bitwarden as a secret store
|
#### Bitwarden as a secret store
|
||||||
|
|
||||||
If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets.
|
If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets.
|
||||||
@@ -206,13 +226,13 @@ ssh:
|
|||||||
user: app
|
user: app
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are using non-root user, you need to bootstrap your servers manually, before using them with MRSK. On Ubuntu, you'd do:
|
If you are using non-root user (`app` as above example), you need to bootstrap your servers manually, before using them with MRSK. On Ubuntu, you'd do:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt upgrade -y
|
sudo apt upgrade -y
|
||||||
sudo apt install -y docker.io curl git
|
sudo apt install -y docker.io curl git
|
||||||
sudo usermod -a -G docker ubuntu
|
sudo usermod -a -G docker app
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using a proxy SSH host
|
### Using a proxy SSH host
|
||||||
@@ -622,6 +642,16 @@ traefik:
|
|||||||
entrypoints.otherentrypoint.address: ':9000'
|
entrypoints.otherentrypoint.address: ':9000'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Rebooting Traefik
|
||||||
|
|
||||||
|
If you make changes to Traefik args or labels, you'll need to reboot with:
|
||||||
|
|
||||||
|
`mrsk traefik reboot`
|
||||||
|
|
||||||
|
In production, reboot the Traefik containers one by one with a slower but safer approach, using a rolling reboot:
|
||||||
|
|
||||||
|
`mrsk traefik reboot --rolling`
|
||||||
|
|
||||||
### Configuring build args for new images
|
### Configuring build args for new images
|
||||||
|
|
||||||
Build arguments that aren't secret can also be configured:
|
Build arguments that aren't secret can also be configured:
|
||||||
@@ -866,7 +896,7 @@ If you wish to remove the entire application, including Traefik, containers, ima
|
|||||||
|
|
||||||
## Locking
|
## Locking
|
||||||
|
|
||||||
Commands that are unsafe to run concurrently will take a deploy lock while they run. The lock is the `mrsk_lock` directory on the primary server.
|
Commands that are unsafe to run concurrently will take a deploy lock while they run. The lock is the `mrsk_lock-<service>` directory on the primary server.
|
||||||
|
|
||||||
You can check the lock status with:
|
You can check the lock status with:
|
||||||
|
|
||||||
@@ -922,8 +952,8 @@ firing a JSON webhook. These variables include:
|
|||||||
- `MRSK_RECORDED_AT` - UTC timestamp in ISO 8601 format, e.g. `2023-04-14T17:07:31Z`
|
- `MRSK_RECORDED_AT` - UTC timestamp in ISO 8601 format, e.g. `2023-04-14T17:07:31Z`
|
||||||
- `MRSK_PERFORMER` - the local user performing the command (from `whoami`)
|
- `MRSK_PERFORMER` - the local user performing the command (from `whoami`)
|
||||||
- `MRSK_SERVICE_VERSION` - an abbreviated service and version for use in messages, e.g. app@150b24f
|
- `MRSK_SERVICE_VERSION` - an abbreviated service and version for use in messages, e.g. app@150b24f
|
||||||
- `MRSK_VERSION` - an full version being deployed
|
- `MRSK_VERSION` - the full version being deployed
|
||||||
- `MRSK_HOSTS` - a comma separated list of the hosts targeted by the command
|
- `MRSK_HOSTS` - a comma-separated list of the hosts targeted by the command
|
||||||
- `MRSK_COMMAND` - The command we are running
|
- `MRSK_COMMAND` - The command we are running
|
||||||
- `MRSK_SUBCOMMAND` - optional: The subcommand we are running
|
- `MRSK_SUBCOMMAND` - optional: The subcommand we are running
|
||||||
- `MRSK_DESTINATION` - optional: destination, e.g. "staging"
|
- `MRSK_DESTINATION` - optional: destination, e.g. "staging"
|
||||||
@@ -940,9 +970,8 @@ Used for pre-build checks - e.g. there are no uncommitted changes or that CI has
|
|||||||
3. pre-deploy
|
3. pre-deploy
|
||||||
For final checks before deploying, e.g. checking CI completed
|
For final checks before deploying, e.g. checking CI completed
|
||||||
|
|
||||||
3. post-deploy - run after a deploy, redeploy or rollback
|
3. post-deploy - run after a deploy, redeploy or rollback.
|
||||||
|
This hook is also passed a `MRSK_RUNTIME` env variable set to the total seconds the deploy took.
|
||||||
This hook is also passed a `MRSK_RUNTIME` env variable.
|
|
||||||
|
|
||||||
This could be used to broadcast a deployment message, or register the new version with an APM.
|
This could be used to broadcast a deployment message, or register the new version with an APM.
|
||||||
|
|
||||||
@@ -953,7 +982,7 @@ The command could look something like:
|
|||||||
curl -q -d content="[My App] ${MRSK_PERFORMER} Rolled back to version ${MRSK_VERSION}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
|
curl -q -d content="[My App] ${MRSK_PERFORMER} Rolled back to version ${MRSK_VERSION}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
|
||||||
```
|
```
|
||||||
|
|
||||||
That'll post a line like follows to a preconfigured chatbot in Basecamp:
|
That'll post a line like the following to a preconfigured chatbot in Basecamp:
|
||||||
|
|
||||||
```
|
```
|
||||||
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
||||||
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
||||||
def boot(name)
|
def boot(name, login: true)
|
||||||
mutating do
|
mutating do
|
||||||
if name == "all"
|
if name == "all"
|
||||||
MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
|
MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
|
||||||
@@ -10,7 +10,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
upload(name)
|
upload(name)
|
||||||
|
|
||||||
on(accessory.hosts) do
|
on(accessory.hosts) do
|
||||||
execute *MRSK.registry.login
|
execute *MRSK.registry.login if login
|
||||||
execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
||||||
execute *accessory.run
|
execute *accessory.run
|
||||||
end
|
end
|
||||||
@@ -53,9 +53,13 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
def reboot(name)
|
def reboot(name)
|
||||||
mutating do
|
mutating do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
|
on(accessory.hosts) do
|
||||||
|
execute *MRSK.registry.login
|
||||||
|
end
|
||||||
|
|
||||||
stop(name)
|
stop(name)
|
||||||
remove_container(name)
|
remove_container(name)
|
||||||
boot(name)
|
boot(name, login: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,11 +10,16 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
|
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
|
||||||
|
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
|
||||||
def reboot
|
def reboot
|
||||||
mutating do
|
mutating do
|
||||||
stop
|
on(MRSK.traefik_hosts, in: options[:rolling] ? :sequence : :parallel) do
|
||||||
remove_container
|
execute *MRSK.auditor.record("Rebooted traefik"), verbosity: :debug
|
||||||
boot
|
execute *MRSK.registry.login
|
||||||
|
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *MRSK.traefik.remove_container
|
||||||
|
execute *MRSK.traefik.run, raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Mrsk::Commands::Lock < Mrsk::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def lock_dir
|
def lock_dir
|
||||||
:mrsk_lock
|
"mrsk_lock-#{config.service}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_details_file
|
def lock_details_file
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module Mrsk
|
module Mrsk
|
||||||
VERSION = "0.14.0"
|
VERSION = "0.15.1"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ class CliAccessoryTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "reboot" do
|
test "reboot" do
|
||||||
|
Mrsk::Commands::Registry.any_instance.expects(:login)
|
||||||
Mrsk::Cli::Accessory.any_instance.expects(:stop).with("mysql")
|
Mrsk::Cli::Accessory.any_instance.expects(:stop).with("mysql")
|
||||||
Mrsk::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
|
Mrsk::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
|
||||||
Mrsk::Cli::Accessory.any_instance.expects(:boot).with("mysql")
|
Mrsk::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false)
|
||||||
|
|
||||||
run_command("reboot", "mysql")
|
run_command("reboot", "mysql")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
|
|
||||||
def stub_locking
|
def stub_locking
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |arg1, arg2| arg1 == :mkdir && arg2 == :mrsk_lock }
|
.with { |arg1, arg2| arg1 == :mkdir && arg2 == "mrsk_lock-app" }
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |arg1, arg2| arg1 == :rm && arg2 == "mrsk_lock/details" }
|
.with { |arg1, arg2| arg1 == :rm && arg2 == "mrsk_lock-app/details" }
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: nil)
|
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: nil)
|
||||||
|
|||||||
@@ -63,11 +63,11 @@ class CliMainTest < CliTestCase
|
|||||||
Thread.report_on_exception = false
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*arg| arg[0..1] == [:mkdir, :mrsk_lock] }
|
.with { |*arg| arg[0..1] == [:mkdir, 'mrsk_lock-app'] }
|
||||||
.raises(RuntimeError, "mkdir: cannot create directory ‘mrsk_lock’: File exists")
|
.raises(RuntimeError, "mkdir: cannot create directory ‘mrsk_lock-app’: File exists")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug)
|
||||||
.with(:stat, :mrsk_lock, ">", "/dev/null", "&&", :cat, "mrsk_lock/details", "|", :base64, "-d")
|
.with(:stat, 'mrsk_lock-app', ">", "/dev/null", "&&", :cat, "mrsk_lock-app/details", "|", :base64, "-d")
|
||||||
|
|
||||||
assert_raises(Mrsk::Cli::LockError) do
|
assert_raises(Mrsk::Cli::LockError) do
|
||||||
run_command("deploy")
|
run_command("deploy")
|
||||||
@@ -78,7 +78,7 @@ class CliMainTest < CliTestCase
|
|||||||
Thread.report_on_exception = false
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*arg| arg[0..1] == [:mkdir, :mrsk_lock] }
|
.with { |*arg| arg[0..1] == [:mkdir, 'mrsk_lock-app'] }
|
||||||
.raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known")
|
.raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known")
|
||||||
|
|
||||||
assert_raises(SSHKit::Runner::ExecuteError) do
|
assert_raises(SSHKit::Runner::ExecuteError) do
|
||||||
|
|||||||
@@ -9,11 +9,19 @@ class CliTraefikTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "reboot" do
|
test "reboot" do
|
||||||
Mrsk::Cli::Traefik.any_instance.expects(:stop)
|
Mrsk::Commands::Registry.any_instance.expects(:login).twice
|
||||||
Mrsk::Cli::Traefik.any_instance.expects(:remove_container)
|
|
||||||
Mrsk::Cli::Traefik.any_instance.expects(:boot)
|
|
||||||
|
|
||||||
run_command("reboot")
|
run_command("reboot").tap do |output|
|
||||||
|
assert_match "docker container stop traefik", output
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output
|
||||||
|
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{Mrsk::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reboot --rolling" do
|
||||||
|
run_command("reboot", "--rolling").tap do |output|
|
||||||
|
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output.lines[3]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "start" do
|
test "start" do
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ class CommandsLockTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "status" do
|
test "status" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"stat mrsk_lock > /dev/null && cat mrsk_lock/details | base64 -d",
|
"stat mrsk_lock-app > /dev/null && cat mrsk_lock-app/details | base64 -d",
|
||||||
new_command.status.join(" ")
|
new_command.status.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "acquire" do
|
test "acquire" do
|
||||||
assert_match \
|
assert_match \
|
||||||
/mkdir mrsk_lock && echo ".*" > mrsk_lock\/details/m,
|
/mkdir mrsk_lock-app && echo ".*" > mrsk_lock-app\/details/m,
|
||||||
new_command.acquire("Hello", "123").join(" ")
|
new_command.acquire("Hello", "123").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "release" do
|
test "release" do
|
||||||
assert_match \
|
assert_match \
|
||||||
"rm mrsk_lock/details && rm -r mrsk_lock",
|
"rm mrsk_lock-app/details && rm -r mrsk_lock-app",
|
||||||
new_command.release.join(" ")
|
new_command.release.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class IntegrationTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
teardown do
|
teardown do
|
||||||
unless passed?
|
unless passed?
|
||||||
[:deployer, :vm1, :vm2, :shared, :load_balancer].each do |container|
|
[:deployer, :vm1, :vm2, :shared, :load_balancer, :registry].each do |container|
|
||||||
puts
|
puts
|
||||||
puts "Logs for #{container}:"
|
puts "Logs for #{container}:"
|
||||||
docker_compose :logs, container
|
docker_compose :logs, container
|
||||||
|
|||||||
Reference in New Issue
Block a user