Skip to content

Handle loadbalancing for multiple web hosts #1490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/kamal/cli/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ def deploy(boot_accessories: false)

invoke "kamal:cli:app:boot", [], invoke_options

if KAMAL.config.proxy.load_balancing?
say "Updating loadbalancer configuration...", :magenta
invoke "kamal:cli:proxy:loadbalancer", [ "deploy" ], invoke_options
end

say "Prune old containers and images...", :magenta
invoke "kamal:cli:prune:all", [], invoke_options
end
Expand Down Expand Up @@ -70,6 +75,11 @@ def redeploy
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)

invoke "kamal:cli:app:boot", [], invoke_options

if KAMAL.config.proxy.load_balancing?
say "Updating loadbalancer configuration...", :magenta
invoke "kamal:cli:proxy:loadbalancer", [ "deploy" ], invoke_options
end
end
end

Expand Down
112 changes: 109 additions & 3 deletions lib/kamal/cli/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ def boot
execute *KAMAL.proxy.ensure_apps_config_directory
execute *KAMAL.proxy.start_or_run
end

if KAMAL.config.proxy.load_balancing?
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
info "Starting loadbalancer on #{host}..."
execute *KAMAL.registry.login
execute *KAMAL.loadbalancer.start_or_run
end
end
end
end

Expand Down Expand Up @@ -114,7 +122,7 @@ def reboot
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
execute *KAMAL.registry.login

"Stopping and removing kamal-proxy on #{host}, if running..."
info "Stopping and removing kamal-proxy on #{host}, if running..."
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
execute *KAMAL.proxy.remove_container
execute *KAMAL.proxy.ensure_apps_config_directory
Expand All @@ -123,6 +131,24 @@ def reboot
end
run_hook "post-proxy-reboot", hosts: host_list
end

if KAMAL.config.proxy.load_balancing?
lb_host = KAMAL.config.proxy.effective_loadbalancer
run_hook "pre-loadbalancer-reboot", hosts: lb_host

on(lb_host) do |host|
execute *KAMAL.auditor.record("Rebooted loadbalancer"), verbosity: :debug
execute *KAMAL.registry.login

info "Stopping and removing load-balancer on #{host}, if running..."
execute *KAMAL.loadbalancer.stop, raise_on_non_zero_exit: false
execute *KAMAL.loadbalancer.remove_container

execute *KAMAL.loadbalancer.run
end

run_hook "post-loadbalancer-reboot", hosts: lb_host
end
end
end
end
Expand All @@ -143,10 +169,10 @@ def upgrade
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
execute *KAMAL.registry.login

"Stopping and removing Traefik on #{host}, if running..."
info "Stopping and removing Traefik on #{host}, if running..."
execute *KAMAL.proxy.cleanup_traefik

"Stopping and removing kamal-proxy on #{host}, if running..."
info "Stopping and removing kamal-proxy on #{host}, if running..."
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
execute *KAMAL.proxy.remove_container
execute *KAMAL.proxy.remove_image
Expand Down Expand Up @@ -198,6 +224,12 @@ def restart
desc "details", "Show details about proxy container from servers"
def details
on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy.info), type: "Proxy" }

if KAMAL.config.proxy.load_balancing?
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
puts_by_host host, capture_with_info(*KAMAL.proxy.loadbalancer.info), type: "Loadbalancer"
end
end
end

desc "logs", "Show log lines from proxy on servers"
Expand Down Expand Up @@ -239,13 +271,80 @@ def remove
end
end

desc "loadbalancer STATUS", "Manage the load balancer"
def loadbalancer(status)
case status
when "info"
if KAMAL.config.proxy.load_balancing?
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
puts "Loadbalancer status on #{host}:"
puts capture_with_info(*KAMAL.loadbalancer.info)
end
else
puts "Load balancing is not configured"
end
when "start"
if KAMAL.config.proxy.load_balancing?
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
execute *KAMAL.registry.login
execute *KAMAL.loadbalancer.start_or_run
end
else
puts "Load balancing is not configured"
end
when "stop"
if KAMAL.config.proxy.load_balancing?
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
execute *KAMAL.loadbalancer.stop, raise_on_non_zero_exit: false
end
else
puts "Load balancing is not configured"
end
when "logs"
if KAMAL.config.proxy.load_balancing?
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
puts_by_host host, capture(*KAMAL.loadbalancer.logs(timestamps: true)), type: "Loadbalancer"
end
else
puts "Load balancing is not configured"
end
when "deploy"
if KAMAL.config.proxy.load_balancing?
targets = []
KAMAL.config.roles.each do |role|
next unless role.running_proxy?

role.hosts.each do |host|
targets << host
end
end

on(KAMAL.config.proxy.effective_loadbalancer) do |host|
info "Deploying to loadbalancer on #{host} with targets: #{targets.join(', ')}"
execute *KAMAL.loadbalancer.deploy(targets: targets)
end
else
puts "Load balancing is not configured"
end
else
puts "Unknown loadbalancer subcommand: #{status}. Available: info, start, stop, logs, deploy"
end
end

desc "remove_container", "Remove proxy container from servers", hide: true
def remove_container
with_lock do
on(KAMAL.proxy_hosts) do
execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
execute *KAMAL.proxy.remove_container
end

if KAMAL.config.proxy.load_balancing?
on(KAMAL.config.proxy.effective_loadbalancer) do
execute *KAMAL.auditor.record("Removed loadbalancer container"), verbosity: :debug
execute *KAMAL.loadbalancer.remove_container
end
end
end
end

Expand All @@ -256,6 +355,13 @@ def remove_image
execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
execute *KAMAL.proxy.remove_image
end

if KAMAL.config.proxy.load_balancing?
on(KAMAL.config.proxy.effective_loadbalancer) do
execute *KAMAL.auditor.record("Removed loadbalancer image"), verbosity: :debug
execute *KAMAL.loadbalancer.remove_image
end
end
end
end

Expand Down
8 changes: 8 additions & 0 deletions lib/kamal/commander.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ def proxy
@commands[:proxy] ||= Kamal::Commands::Proxy.new(config)
end

def loadbalancer_config
@loadbalancer_config ||= Kamal::Configuration::Loadbalancer.new(config: config, proxy_config: config.proxy.proxy_config)
end

def loadbalancer
@commands[:loadbalancer] ||= Kamal::Commands::Loadbalancer.new(config, loadbalancer_config: loadbalancer_config)
end

def prune
@commands[:prune] ||= Kamal::Commands::Prune.new(config)
end
Expand Down
100 changes: 100 additions & 0 deletions lib/kamal/commands/loadbalancer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
delegate :argumentize, :optionize, to: Kamal::Utils

attr_reader :loadbalancer_config

def initialize(config, loadbalancer_config: nil)
super(config)
@loadbalancer_config = loadbalancer_config
end

def run
pipe \
[ :echo, proxy_image ],
xargs(docker(:run,
"--name", container_name,
"--network", "kamal",
"--detach",
"--restart", "unless-stopped",
"--publish", "80:80",
"--publish", "443:443",
"--label", "org.opencontainers.image.title=kamal-loadbalancer",
"--volume", "kamal-loadbalancer-config:/home/kamal-loadbalancer/.config/kamal-loadbalancer"))
end

def start
docker :container, :start, container_name
end

def stop(name: container_name)
docker :container, :stop, name
end

def start_or_run
combine start, run, by: "||"
end

def deploy(targets: [])
target_args = targets.map { |t| "#{t}:80" }

hosts = loadbalancer_config.hosts

options = []
options << "--target=#{target_args.join(',')}"
options << "--host=#{hosts.join(',')}"
options << "--tls" if loadbalancer_config.ssl?

docker :exec, container_name, "kamal-proxy", "deploy", loadbalancer_config.config.service, *options
end

def info
docker :ps, "--filter", "name=^#{container_name}$"
end

def version
pipe \
docker(:inspect, container_name, "--format '{{.Config.Image}}'"),
[ :cut, "-d:", "-f2" ]
end

def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \
docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end

def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil)
run_over_ssh pipe(
docker(:logs, container_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
).join(" "), host: host
end

def remove_container
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-loadbalancer"
end

def remove_image
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-loadbalancer"
end

def ensure_directory
make_directory loadbalancer_config.directory
end

def remove_directory
super(loadbalancer_config.directory)
end

private
def proxy_image
[
loadbalancer_config.config.proxy_boot.image_default,
Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION
].join(":")
end

def container_name
loadbalancer_config.container_name
end
end
4 changes: 4 additions & 0 deletions lib/kamal/commands/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ def reset_run_command
remove_file config.proxy_boot.run_command_file
end

def loadbalancer
@loadbalancer ||= Kamal::Commands::Loadbalancer.new(config, loadbalancer_config: KAMAL.loadbalancer_config)
end

private
def container_name
config.proxy_boot.container_name
Expand Down
4 changes: 4 additions & 0 deletions lib/kamal/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ def proxy_roles
roles.select(&:running_proxy?)
end

def load_balancing?
proxy&.load_balancing?
end

def proxy_role_names
proxy_roles.flat_map(&:name)
end
Expand Down
7 changes: 7 additions & 0 deletions lib/kamal/configuration/docs/proxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ proxy:
hosts:
- foo.example.com
- bar.example.com

# Loadbalancer
#
# Specify a host to run the loadbalancer on. The loadbalancer will distribute requests
# to all web hosts. If not specified but multiple web hosts are configured, the first
# web host will be used as the loadbalancer host.
loadbalancer: lb.example.com

# App port
#
Expand Down
28 changes: 28 additions & 0 deletions lib/kamal/configuration/loadbalancer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class Kamal::Configuration::Loadbalancer < Kamal::Configuration::Proxy
CONTAINER_NAME = "load-balancer".freeze

def self.validation_config_key
"proxy"
end

def initialize(config:, proxy_config:)
super(config: config, proxy_config: proxy_config)
end

def deploy_options
opts = super

opts[:host] = hosts if hosts.present?
opts[:tls] = proxy_config["ssl"].presence

opts
end

def directory
File.join config.run_directory, "loadbalancer"
end

def container_name
CONTAINER_NAME
end
end
Loading