Skip to content

Commit

Permalink
Merge pull request #940 from basecamp/proxy
Browse files Browse the repository at this point in the history
Replace Traefik with kamal-proxy
  • Loading branch information
djmb authored Sep 17, 2024
2 parents 66d5e25 + 267b526 commit 434490b
Show file tree
Hide file tree
Showing 91 changed files with 1,431 additions and 1,504 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kamal: Deploy web apps anywhere

From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal has the dynamic reverse-proxy Traefik hold requests while a new app container is started and the old one is stopped. Works seamlessly across multiple hosts, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker.
From bare metal to cloud VMs, deploy web apps anywhere with zero downtime. Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to seamlessly switch requests between containers. Works seamlessly across multiple servers, using SSHKit to execute commands. Originally built for Rails apps, Kamal will work with any type of web app that can be containerized with Docker.

➡️ See [kamal-deploy.org](https://kamal-deploy.org) for documentation on [installation](https://kamal-deploy.org/docs/installation), [configuration](https://kamal-deploy.org/docs/configuration), and [commands](https://kamal-deploy.org/docs/commands).

Expand Down
5 changes: 2 additions & 3 deletions bin/docs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ DOCS = {
"builder" => "Builders",
"configuration" => "Configuration overview",
"env" => "Environment variables",
"healthcheck" => "Healthchecks",
"logging" => "Logging",
"proxy" => "Proxy",
"registry" => "Docker Registry",
"role" => "Roles",
"servers" => "Servers",
"ssh" => "SSH",
"sshkit" => "SSHKit",
"traefik" => "Traefik"
"sshkit" => "SSHKit"
}

class DocWriter
Expand Down
1 change: 1 addition & 0 deletions lib/kamal/cli.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module Kamal::Cli
class BootError < StandardError; end
class HookError < StandardError; end
class LockError < StandardError; end
end
Expand Down
43 changes: 23 additions & 20 deletions lib/kamal/cli/accessory.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
class Kamal::Cli::Accessory < Kamal::Cli::Base
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
def boot(name, login: true)
def boot(name, prepare: true)
with_lock do
if name == "all"
KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
else
prepare(name) if prepare

with_accessory(name) do |accessory, hosts|
directories(name)
upload(name)

on(hosts) do
execute *KAMAL.registry.login if login
execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
execute *accessory.ensure_env_directory
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
Expand Down Expand Up @@ -57,15 +58,10 @@ def reboot(name)
if name == "all"
KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
else
with_accessory(name) do |accessory, hosts|
on(hosts) do
execute *KAMAL.registry.login
end

stop(name)
remove_container(name)
boot(name, login: false)
end
prepare(name)
stop(name)
remove_container(name)
boot(name, prepare: false)
end
end
end
Expand Down Expand Up @@ -97,10 +93,8 @@ def stop(name)
desc "restart [NAME]", "Restart existing accessory container on host"
def restart(name)
with_lock do
with_accessory(name) do
stop(name)
start(name)
end
stop(name)
start(name)
end
end

Expand Down Expand Up @@ -251,11 +245,20 @@ def accessory_hosts(accessory)
end

def remove_accessory(name)
with_accessory(name) do
stop(name)
remove_container(name)
remove_image(name)
remove_service_directory(name)
stop(name)
remove_container(name)
remove_image(name)
remove_service_directory(name)
end

def prepare(name)
with_accessory(name) do |accessory, hosts|
on(hosts) do
execute *KAMAL.registry.login
execute *KAMAL.docker.create_network
rescue SSHKit::Command::Failed => e
raise unless e.message.include?("already exists")
end
end
end
end
40 changes: 37 additions & 3 deletions lib/kamal/cli/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ def boot
with_lock do
say "Get most recent version available as an image...", :magenta unless options[:version]
using_version(version_or_latest) do |version|
say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
say "Start container with version #{version} (or reboot if already running)...", :magenta

# Assets are prepared in a separate step to ensure they are on all hosts before booting
on(KAMAL.hosts) do
Expand Down Expand Up @@ -38,8 +38,17 @@ def start
roles = KAMAL.roles_on(host)

roles.each do |role|
app = KAMAL.app(role: role, host: host)
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false
execute *app.start, raise_on_non_zero_exit: false

if role.running_proxy?
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?

execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
end
end
end
end
Expand All @@ -52,8 +61,18 @@ def stop
roles = KAMAL.roles_on(host)

roles.each do |role|
app = KAMAL.app(role: role, host: host)
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false

if role.running_proxy?
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
if endpoint.present?
execute *KAMAL.proxy.remove(role.container_prefix, target: endpoint), raise_on_non_zero_exit: false
end
end

execute *app.stop, raise_on_non_zero_exit: false
end
end
end
Expand Down Expand Up @@ -212,6 +231,7 @@ def remove
stop
remove_containers
remove_images
remove_app_directory
end
end

Expand Down Expand Up @@ -253,6 +273,20 @@ def remove_images
end
end

desc "remove_app_directory", "Remove the service directory from servers", hide: true
def remove_app_directory
with_lock do
on(KAMAL.hosts) do |host|
roles = KAMAL.roles_on(host)

roles.each do |role|
execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory} on all servers", role: role), verbosity: :debug
execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false
end
end
end
end

desc "version", "Show app version currently running on servers"
def version
on(KAMAL.hosts) do |host|
Expand Down
26 changes: 12 additions & 14 deletions lib/kamal/cli/app/boot.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class Kamal::Cli::App::Boot
attr_reader :host, :role, :version, :barrier, :sshkit
delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, :upload!, to: :sshkit
delegate :uses_cord?, :assets?, :running_traefik?, to: :role
delegate :assets?, :running_proxy?, to: :role

def initialize(host, role, sshkit, version, barrier)
@host = host
Expand Down Expand Up @@ -45,32 +45,30 @@ def old_version_renamed_if_clashing

def start_new_version
audit "Booted app version #{version}"

execute *app.tie_cord(role.cord_host_file) if uses_cord?
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"

execute *app.ensure_env_directory
upload! role.secrets_io(host), role.secrets_path, mode: "0600"
execute *app.run(hostname: hostname)

Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
execute *app.run(hostname: hostname)
if running_proxy?
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
else
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
end
rescue => e
error "Failed to boot #{role} on #{host}"
raise e
end

def stop_new_version
execute *app.stop(version: version), raise_on_non_zero_exit: false
end

def stop_old_version(version)
if uses_cord?
cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
if cord.present?
execute *app.cut_cord(cord)
Kamal::Cli::Healthcheck::Poller.wait_for_unhealthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
end
end

execute *app.stop(version: version), raise_on_non_zero_exit: false

execute *app.clean_up_assets if assets?
end

Expand Down
8 changes: 2 additions & 6 deletions lib/kamal/cli/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def confirming(question)
end

def acquire_lock
ensure_run_and_locks_directory
ensure_run_directory

raise_if_locked do
say "Acquiring the deploy lock...", :magenta
Expand Down Expand Up @@ -174,14 +174,10 @@ def first_invocation
instance_variable_get("@_invocations").first
end

def ensure_run_and_locks_directory
def ensure_run_directory
on(KAMAL.hosts) do
execute(*KAMAL.server.ensure_run_directory)
end

on(KAMAL.primary_host) do
execute(*KAMAL.lock.ensure_locks_directory)
end
end
end
end
57 changes: 18 additions & 39 deletions lib/kamal/cli/healthcheck/poller.rb
Original file line number Diff line number Diff line change
@@ -1,59 +1,38 @@
module Kamal::Cli::Healthcheck::Poller
extend self

TRAEFIK_UPDATE_DELAY = 5


def wait_for_healthy(pause_after_ready: false, &block)
def wait_for_healthy(role, &block)
attempt = 1
max_attempts = KAMAL.config.healthcheck.max_attempts
timeout_at = Time.now + KAMAL.config.readiness_timeout
readiness_delay = KAMAL.config.readiness_delay

begin
case status = block.call
when "healthy"
sleep TRAEFIK_UPDATE_DELAY if pause_after_ready
when "running" # No health check configured
sleep KAMAL.config.readiness_delay if pause_after_ready
else
raise Kamal::Cli::Healthcheck::Error, "container not ready (#{status})"
end
rescue Kamal::Cli::Healthcheck::Error => e
if attempt <= max_attempts
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
sleep attempt
attempt += 1
retry
else
raise
status = block.call

if status == "running"
# Wait for the readiness delay and confirm it is still running
if readiness_delay > 0
info "Container is running, waiting for readiness delay of #{readiness_delay} seconds"
sleep readiness_delay
status = block.call
end
end
end

info "Container is healthy!"
end

def wait_for_unhealthy(pause_after_ready: false, &block)
attempt = 1
max_attempts = KAMAL.config.healthcheck.max_attempts

begin
case status = block.call
when "unhealthy"
sleep TRAEFIK_UPDATE_DELAY if pause_after_ready
else
raise Kamal::Cli::Healthcheck::Error, "container not unhealthy (#{status})"
unless %w[ running healthy ].include?(status)
raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.readiness_timeout} seconds (#{status})"
end
rescue Kamal::Cli::Healthcheck::Error => e
if attempt <= max_attempts
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
sleep attempt
time_left = timeout_at - Time.now
if time_left > 0
sleep [ attempt, time_left ].min
attempt += 1
retry
else
raise
end
end

info "Container is unhealthy!"
info "Container is healthy!"
end

private
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/cli/lock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def status
option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
def acquire
message = options[:message]
ensure_run_and_locks_directory
ensure_run_directory

raise_if_locked do
on(KAMAL.primary_host) do
Expand Down
Loading

0 comments on commit 434490b

Please sign in to comment.