Replace Traefik with parachute

[mproxy](https://github.com/basecamp/parachute) is a custom minimal
proxy designed specifically for Kamal.

It has two big advantages over Traefik:
1. Imperative deployments - we tell it to switch from container A to
   container B, and it waits for container B to start then switches. No
   need to poll for health checks ourselves or mess around with forcing
   health checks to fail.
2. Support for multiple apps - as much as possible, configuration is
   supplied at runtime by the deploy command, allowing us to have
   multiple apps share an instance of mproxy without conflicting config.
This commit is contained in:
Donal McBreen
2024-03-08 08:19:48 +00:00
parent 10b8c826d8
commit 822590dcf6
66 changed files with 754 additions and 1161 deletions

View File

@@ -1,6 +1,6 @@
require_relative "integration_test"
class AccessoryTest < IntegrationTest
class IntegrationAccessoryTest < IntegrationTest
test "boot, stop, start, restart, logs, remove" do
kamal :envify

View File

@@ -1,16 +1,18 @@
require_relative "integration_test"
class AppTest < IntegrationTest
class IntegrationAppTest < IntegrationTest
test "stop, start, boot, logs, images, containers, exec, remove" do
kamal :envify
kamal :setup
kamal :deploy
assert_app_is_up
kamal :app, :stop
assert_app_is_down
assert_app_is_down response_code: "504"
kamal :app, :start
@@ -24,7 +26,7 @@ class AppTest < IntegrationTest
logs = kamal :app, :logs, capture: true
assert_match /App Host: vm1/, logs
assert_match /App Host: vm2/, logs
assert_match /GET \/ HTTP\/1.1/, logs
assert_match /GET \/up HTTP\/1.1/, logs
images = kamal :app, :images, capture: true
assert_match /App Host: vm1/, images
@@ -50,6 +52,6 @@ class AppTest < IntegrationTest
kamal :app, :remove
assert_app_is_down
assert_app_is_down response_code: "504"
end
end

View File

@@ -29,6 +29,5 @@ class BrokenDeployTest < IntegrationTest
assert_match /First web container is unhealthy on vm[12], not booting other roles/, output
assert_match "First web container is unhealthy, not booting workers on vm3", output
assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output
assert_match 'ERROR {"Status":"unhealthy","FailingStreak":0,"Log":[]}', output
end
end

View File

@@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooted proxy on ${KAMAL_HOSTS}"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-proxy-reboot

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooted Traefik on ${KAMAL_HOSTS}"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot

View File

@@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooting proxy on ${KAMAL_HOSTS}..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooting Traefik on ${KAMAL_HOSTS}..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot

View File

@@ -6,4 +6,5 @@ ARG COMMIT_SHA
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
RUN echo "Up!" > /usr/share/nginx/html/up

View File

@@ -26,14 +26,11 @@ builder:
multiarch: false
args:
COMMIT_SHA: <%= `git rev-parse HEAD` %>
healthcheck:
cmd: wget -qO- http://localhost > /dev/null || exit 1
max_attempts: 3
traefik:
args:
accesslog: true
accesslog.format: json
image: registry:4443/traefik:v2.10
proxy:
image: registry:4443/basecamp/parachute:latest
http_port: 80
https_port: 443
debug: true
accessories:
busybox:
service: custom-busybox

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooted Traefik on ${KAMAL_HOSTS}"
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-traefik-reboot

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooting Traefik on ${KAMAL_HOSTS}..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-traefik-reboot

View File

@@ -6,4 +6,4 @@ ARG COMMIT_SHA
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
RUN echo "Up!" > /usr/share/nginx/html/up

View File

@@ -20,14 +20,8 @@ builder:
multiarch: false
args:
COMMIT_SHA: <%= `git rev-parse HEAD` %>
healthcheck:
cmd: wget -qO- http://localhost > /dev/null || exit 1
max_attempts: 3
traefik:
args:
accesslog: true
accesslog.format: json
image: registry:4443/traefik:v2.10
proxy:
image: registry:4443/basecamp/parachute:latest
accessories:
busybox:
service: custom-busybox

View File

@@ -19,7 +19,7 @@ push_image_to_registry_4443() {
install_kamal
push_image_to_registry_4443 nginx 1-alpine-slim
push_image_to_registry_4443 traefik v2.10
push_image_to_registry_4443 basecamp/parachute latest
push_image_to_registry_4443 busybox 1.36.0
# .ssh is on a shared volume that persists between runs. Clean it up as the

View File

@@ -44,10 +44,10 @@ class IntegrationTest < ActiveSupport::TestCase
deployer_exec(:kamal, *commands, **options)
end
def assert_app_is_down
def assert_app_is_down(response_code: "503")
response = app_response
debug_response_code(response, "502")
assert_equal "502", response.code
debug_response_code(response, response_code)
assert_equal response_code, response.code
end
def assert_app_is_up(version: nil)
@@ -101,8 +101,8 @@ class IntegrationTest < ActiveSupport::TestCase
def assert_200(response)
code = response.code
if code != "200"
puts "Got response code #{code}, here are the traefik logs:"
kamal :traefik, :logs
puts "Got response code #{code}, here are the proxy logs:"
kamal :proxy, :logs
puts "And here are the load balancer logs"
docker_compose :logs, :load_balancer
puts "Tried to get the response code again and got #{app_response.code}"
@@ -129,10 +129,10 @@ class IntegrationTest < ActiveSupport::TestCase
def debug_response_code(app_response, expected_code)
code = app_response.code
if code != expected_code
puts "Got response code #{code}, here are the traefik logs:"
kamal :traefik, :logs
puts "Got response code #{code}, here are the proxy logs:"
kamal :proxy, :logs, raise_on_error: false
puts "And here are the load balancer logs"
docker_compose :logs, :load_balancer
docker_compose :logs, :load_balancer, raise_on_error: false
puts "Tried to get the response code again and got #{app_response.code}"
end
end

View File

@@ -1,6 +1,6 @@
require_relative "integration_test"
class LockTest < IntegrationTest
class IntegrationLockTest < IntegrationTest
test "acquire, release, status" do
kamal :envify

View File

@@ -1,14 +1,15 @@
require_relative "integration_test"
class MainTest < IntegrationTest
class IntegrationMainTest < IntegrationTest
test "envify, deploy, redeploy, rollback, details and audit" do
kamal :server, :bootstrap
kamal :envify
assert_env_files
remove_local_env_file
first_version = latest_app_version
assert_app_is_down
assert_app_is_down response_code: "502"
kamal :deploy
assert_app_is_up version: first_version
@@ -28,11 +29,11 @@ class MainTest < IntegrationTest
assert_app_is_up version: first_version
details = kamal :details, capture: true
assert_match /Traefik Host: vm1/, details
assert_match /Traefik Host: vm2/, details
assert_match /Proxy Host: vm1/, details
assert_match /Proxy Host: vm2/, details
assert_match /App Host: vm1/, details
assert_match /App Host: vm2/, details
assert_match /traefik:v2.10/, details
assert_match /basecamp\/parachute:latest/, details
assert_match /registry:4443\/app:#{first_version}/, details
audit = kamal :audit, capture: true
@@ -45,11 +46,12 @@ class MainTest < IntegrationTest
test "app with roles" do
@app = "app_with_roles"
kamal :server, :bootstrap
kamal :envify
version = latest_app_version
assert_app_is_down
assert_app_is_down response_code: "502"
kamal :deploy
@@ -79,7 +81,6 @@ class MainTest < IntegrationTest
assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options])
assert_equal({ "multiarch" => false, "args" => { "COMMIT_SHA" => version } }, config[:builder])
assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging]
assert_equal({ "path" => "/up", "port" => 3000, "max_attempts" => 3, "cord"=>"/tmp/kamal-cord", "log_lines" => 50, "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1" }, config[:healthcheck])
end
test "setup and remove" do

View File

@@ -0,0 +1,66 @@
require_relative "integration_test"
class IntegrationProxyTest < IntegrationTest
test "boot, reboot, stop, start, restart, logs, remove" do
kamal :server, :bootstrap
kamal :envify
kamal :proxy, :boot
assert_proxy_running
output = kamal :proxy, :reboot, "-y", "--verbose", capture: true
assert_proxy_running
assert_hooks_ran "pre-proxy-reboot", "post-proxy-reboot"
assert_match /Rebooting proxy on vm1,vm2.../, output
assert_match /Rebooted proxy on vm1,vm2/, output
output = kamal :proxy, :reboot, "--rolling", "-y", "--verbose", capture: true
assert_proxy_running
assert_hooks_ran "pre-proxy-reboot", "post-proxy-reboot"
assert_match /Rebooting proxy on vm1.../, output
assert_match /Rebooted proxy on vm1/, output
assert_match /Rebooting proxy on vm2.../, output
assert_match /Rebooted proxy on vm2/, output
kamal :proxy, :boot
assert_proxy_running
# Check booting when booted doesn't raise an error
kamal :proxy, :stop
assert_proxy_not_running
# Check booting when stopped works
kamal :proxy, :boot
assert_proxy_running
kamal :proxy, :stop
assert_proxy_not_running
kamal :proxy, :start
assert_proxy_running
kamal :proxy, :restart
assert_proxy_running
logs = kamal :proxy, :logs, capture: true
assert_match %r{"level":"INFO","msg":"Server started","http":80,"https":443}, logs
kamal :proxy, :remove
assert_proxy_not_running
kamal :env, :delete
end
private
def assert_proxy_running
assert_match %r{registry:4443/basecamp/parachute:latest "parachute run"}, proxy_details
end
def assert_proxy_not_running
assert_no_match %r{registry:4443/basecamp/parachute:latest "parachute run"}, proxy_details
end
def proxy_details
kamal :proxy, :details, capture: true
end
end

View File

@@ -1,65 +0,0 @@
require_relative "integration_test"
class TraefikTest < IntegrationTest
test "boot, reboot, stop, start, restart, logs, remove" do
kamal :envify
kamal :traefik, :boot
assert_traefik_running
output = kamal :traefik, :reboot, "-y", "--verbose", capture: true
assert_traefik_running
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
assert_match /Rebooting Traefik on vm1,vm2.../, output
assert_match /Rebooted Traefik on vm1,vm2/, output
output = kamal :traefik, :reboot, "--rolling", "-y", "--verbose", capture: true
assert_traefik_running
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
assert_match /Rebooting Traefik on vm1.../, output
assert_match /Rebooted Traefik on vm1/, output
assert_match /Rebooting Traefik on vm2.../, output
assert_match /Rebooted Traefik on vm2/, output
kamal :traefik, :boot
assert_traefik_running
# Check booting when booted doesn't raise an error
kamal :traefik, :stop
assert_traefik_not_running
# Check booting when stopped works
kamal :traefik, :boot
assert_traefik_running
kamal :traefik, :stop
assert_traefik_not_running
kamal :traefik, :start
assert_traefik_running
kamal :traefik, :restart
assert_traefik_running
logs = kamal :traefik, :logs, capture: true
assert_match /Traefik version [\d.]+ built on/, logs
kamal :traefik, :remove
assert_traefik_not_running
kamal :env, :delete
end
private
def assert_traefik_running
assert_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
end
def assert_traefik_not_running
assert_no_match /traefik:v2.10 "\/entrypoint.sh/, traefik_details
end
def traefik_details
kamal :traefik, :details, capture: true
end
end