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:
Donal McBreen
2023-08-31 10:21:57 +01:00
parent 94bf090657
commit 8a41d15b69
17 changed files with 234 additions and 65 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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,