Zero downtime deployment with cord file
When replacing a container currently we:
1. Boot the new container
2. Wait for it to become healthy
3. Stop the old container
Traefik will send requests to the old container until it notices that it
is unhealthy. But it may have stopped serving requests before that point
which can result in errors.
To get round that the new boot process is:
1. Create a directory with a single file on the host
2. Boot the new container, mounting the cord file into /tmp and
including a check for the file in the docker healthcheck
3. Wait for it to become healthy
4. Delete the healthcheck file ("cut the cord") for the old container
5. Wait for it to become unhealthy and give Traefik a couple of seconds
to notice
6. Stop the old container
The extra steps ensure that Traefik stops sending requests before the
old container is shutdown.
This commit is contained in:
@@ -1,34 +1,29 @@
|
||||
class Kamal::Commands::App < Kamal::Commands::Base
|
||||
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
||||
|
||||
attr_reader :role
|
||||
attr_reader :role, :role_config
|
||||
|
||||
def initialize(config, role: nil)
|
||||
super(config)
|
||||
@role = role
|
||||
end
|
||||
|
||||
def start_or_run(hostname: nil)
|
||||
combine start, run(hostname: hostname), by: "||"
|
||||
@role_config = config.role(self.role)
|
||||
end
|
||||
|
||||
def run(hostname: nil)
|
||||
role = config.role(self.role)
|
||||
|
||||
docker :run,
|
||||
"--detach",
|
||||
"--restart unless-stopped",
|
||||
"--name", container_name,
|
||||
*(["--hostname", hostname] if hostname),
|
||||
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||
*role.env_args,
|
||||
*role.health_check_args,
|
||||
*role_config.env_args,
|
||||
*role_config.health_check_args,
|
||||
*config.logging_args,
|
||||
*config.volume_args,
|
||||
*role.label_args,
|
||||
*role.option_args,
|
||||
*role_config.label_args,
|
||||
*role_config.option_args,
|
||||
config.absolute_image,
|
||||
role.cmd
|
||||
role_config.cmd
|
||||
end
|
||||
|
||||
def start
|
||||
@@ -76,14 +71,12 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def execute_in_new_container(*command, interactive: false)
|
||||
role = config.role(self.role)
|
||||
|
||||
docker :run,
|
||||
("-it" if interactive),
|
||||
"--rm",
|
||||
*role&.env_args,
|
||||
*role_config&.env_args,
|
||||
*config.volume_args,
|
||||
*role&.option_args,
|
||||
*role_config&.option_args,
|
||||
config.absolute_image,
|
||||
*command
|
||||
end
|
||||
@@ -112,7 +105,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
def list_versions(*docker_args, statuses: nil)
|
||||
pipe \
|
||||
docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
|
||||
%(while read line; do echo ${line##{service_role_dest}-}; done) # Extract SHA from "service-role-dest-SHA"
|
||||
%(while read line; do echo ${line##{role_config.full_name}-}; done) # Extract SHA from "service-role-dest-SHA"
|
||||
end
|
||||
|
||||
def list_containers
|
||||
@@ -150,16 +143,30 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def make_env_directory
|
||||
make_directory config.role(role).host_env_directory
|
||||
make_directory role_config.host_env_directory
|
||||
end
|
||||
|
||||
def remove_env_file
|
||||
[:rm, "-f", config.role(role).host_env_file_path]
|
||||
[:rm, "-f", role_config.host_env_file_path]
|
||||
end
|
||||
|
||||
def cord(version:)
|
||||
pipe \
|
||||
docker(:inspect, "-f '{{ range .Mounts }}{{ .Source }} {{ .Destination }} {{ end }}'", container_name(version)),
|
||||
[:awk, "'$2 == \"#{role_config.cord_container_directory}\" {print $1}'"]
|
||||
end
|
||||
|
||||
def tie_cord(cord)
|
||||
create_empty_file(cord)
|
||||
end
|
||||
|
||||
def cut_cord(cord)
|
||||
remove_directory(cord)
|
||||
end
|
||||
|
||||
private
|
||||
def container_name(version = nil)
|
||||
[ config.service, role, config.destination, version || config.version ].compact.join("-")
|
||||
[ role_config.full_name, version || config.version ].compact.join("-")
|
||||
end
|
||||
|
||||
def filter_args(statuses: nil)
|
||||
|
||||
@@ -34,6 +34,10 @@ module Kamal::Commands
|
||||
[ :mkdir, "-p", path ]
|
||||
end
|
||||
|
||||
def remove_directory(path)
|
||||
[ :rm, "-r", path ]
|
||||
end
|
||||
|
||||
private
|
||||
def combine(*commands, by: "&&")
|
||||
commands
|
||||
@@ -69,5 +73,11 @@ module Kamal::Commands
|
||||
def tags(**details)
|
||||
Kamal::Tags.from_config(config, **details)
|
||||
end
|
||||
|
||||
def create_empty_file(file)
|
||||
chain \
|
||||
make_directory_for(file),
|
||||
[:touch, file]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class Kamal::Commands::Healthcheck < Kamal::Commands::Base
|
||||
"--label", "service=#{container_name}",
|
||||
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||
*web.env_args,
|
||||
*web.health_check_args,
|
||||
*web.health_check_args(cord: false),
|
||||
*config.volume_args,
|
||||
*web.option_args,
|
||||
config.absolute_image,
|
||||
|
||||
Reference in New Issue
Block a user