Class: Gjallarhorn::Deployment::ZeroDowntime
- Defined in:
- lib/gjallarhorn/deployment/zero_downtime.rb
Overview
Zero-downtime deployment strategy
Implements zero-downtime deployments by:
- Starting new containers alongside existing ones
- Waiting for new containers to pass health checks
- Switching proxy traffic to new containers
- Gracefully stopping old containers
This ensures continuous service availability during deployments.
Instance Attribute Summary
Attributes inherited from Strategy
#adapter, #logger, #proxy_manager
Instance Method Summary collapse
-
#build_container_labels(service, environment) ⇒ Hash
private
Build container labels for identification and management.
-
#build_environment_variables(service, environment) ⇒ Hash
private
Build environment variables for container.
-
#cleanup_old_containers(service_name, exclude_container_id, keep_count = 2) ⇒ void
private
Clean up old containers, keeping a configurable number for rollback.
-
#deploy(image:, environment:, services:) ⇒ void
Deploy services with zero downtime.
-
#deploy_service_zero_downtime(service, image, environment) ⇒ Hash
private
Deploy a single service with zero downtime.
-
#get_current_containers(service_name) ⇒ Array<Hash>
private
Get currently running containers for a service.
-
#start_new_container(service, image, environment) ⇒ Hash
private
Start a new container for the service.
-
#stop_old_containers(containers, drain_timeout = 30) ⇒ void
private
Gracefully stop old containers.
-
#wait_for_container_running(container, timeout = 60) ⇒ Boolean
private
Wait for container to be in running state.
-
#zero_downtime? ⇒ Boolean
Check if strategy supports zero-downtime deployments.
Methods inherited from Strategy
Constructor Details
This class inherits a constructor from Gjallarhorn::Deployment::Strategy
Instance Method Details
#build_container_labels(service, environment) ⇒ Hash (private)
Build container labels for identification and management
191 192 193 194 195 196 197 198 199 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 191 def build_container_labels(service, environment) { "gjallarhorn.service" => service[:name], "gjallarhorn.environment" => environment, "gjallarhorn.role" => service[:role] || "web", "gjallarhorn.deployed_at" => Time.now.utc.iso8601, "gjallarhorn.strategy" => "zero_downtime" } end |
#build_environment_variables(service, environment) ⇒ Hash (private)
Build environment variables for container
171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 171 def build_environment_variables(service, environment) env_vars = { "GJALLARHORN_SERVICE" => service[:name], "GJALLARHORN_ENVIRONMENT" => environment, "GJALLARHORN_DEPLOYED_AT" => Time.now.utc.iso8601 } # Add service-specific environment variables # Handle both string and symbol keys from YAML service_env = service[:env] || service["env"] env_vars.merge!(service_env) if service_env env_vars end |
#cleanup_old_containers(service_name, exclude_container_id, keep_count = 2) ⇒ void (private)
This method returns an undefined value.
Clean up old containers, keeping a configurable number for rollback
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 254 def cleanup_old_containers(service_name, exclude_container_id, keep_count = 2) all_containers = @adapter.get_all_containers(service_name) old_containers = all_containers.reject { |c| c[:id] == exclude_container_id } # Sort by creation time (newest first) and keep only the specified count containers_to_remove = old_containers.sort_by { |c| c[:created_at] }.reverse.drop(keep_count) containers_to_remove.each do |container| @logger.info "Removing old container: #{container[:name]} (#{container[:id]})" @adapter.remove_container(container[:id]) end @logger.info "Cleaned up #{containers_to_remove.length} old containers" if containers_to_remove.any? rescue StandardError => e @logger.warn "Failed to cleanup old containers: #{e.}" # Don't fail deployment if cleanup fails end |
#deploy(image:, environment:, services:) ⇒ void
This method returns an undefined value.
Deploy services with zero downtime
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 25 def deploy(image:, environment:, services:) @logger.info "Starting zero-downtime deployment of #{image} to #{environment}" # Set the environment on the adapter so it knows which environment we're deploying to @adapter.set_environment(environment) if @adapter.respond_to?(:set_environment) deployment_results = [] services.each do |service| # Convert string keys to symbols for consistency service = service.transform_keys(&:to_sym) if service.is_a?(Hash) @logger.info "Deploying service: #{service[:name]}" result = deploy_service_zero_downtime(service, image, environment) deployment_results << result end @logger.info "Zero-downtime deployment completed successfully" deployment_results end |
#deploy_service_zero_downtime(service, image, environment) ⇒ Hash (private)
Deploy a single service with zero downtime
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 60 def deploy_service_zero_downtime(service, image, environment) # Ensure service hash uses symbol keys service = service.transform_keys(&:to_sym) if service.is_a?(Hash) service_name = service[:name] # Step 1: Get current running containers current_containers = get_current_containers(service_name) @logger.info "Found #{current_containers.length} existing containers for #{service_name}" # Step 2: Start new container new_container = start_new_container(service, image, environment) @logger.info "Started new container: #{new_container[:name]} (#{new_container[:id]})" # Step 3: Wait for new container to be healthy if service[:healthcheck] @logger.info "Waiting for health check to pass..." wait_for_container_health(new_container, service[:healthcheck]) else @logger.info "No health check configured, waiting for container to be running..." wait_for_container_running(new_container) end # Step 4: Update proxy routing to new container if @proxy_manager @logger.info "Switching proxy traffic to new container..." @proxy_manager.switch_traffic( service_name: service_name, from_containers: current_containers, to_container: new_container ) else @logger.warn "No proxy manager configured, skipping traffic switch" end # Step 5: Gracefully stop old containers if current_containers.any? @logger.info "Stopping #{current_containers.length} old containers..." stop_old_containers(current_containers, service[:drain_timeout] || 30) end # Step 6: Clean up old containers cleanup_old_containers(service_name, new_container[:id]) { service: service_name, old_containers: current_containers.map { |c| c[:id] }, new_container: new_container[:id], status: "success" } rescue StandardError => e @logger.error "Failed to deploy #{service_name}: #{e.}" # Cleanup: Remove the new container if deployment failed if defined?(new_container) && new_container @logger.info "Cleaning up failed deployment container..." @adapter.stop_container(new_container[:id], graceful: false) end raise DeploymentError, "Zero-downtime deployment failed for #{service_name}: #{e.}" end |
#get_current_containers(service_name) ⇒ Array<Hash> (private)
Get currently running containers for a service
125 126 127 128 129 130 131 132 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 125 def get_current_containers(service_name) @logger.debug "get_current_containers: Calling adapter.get_running_containers for service: #{service_name}" @adapter.get_running_containers(service_name) rescue StandardError => e @logger.warn "Failed to get current containers for #{service_name}: #{e.}" @logger.debug "get_current_containers error backtrace: #{e.backtrace.join("\n")}" [] end |
#start_new_container(service, image, environment) ⇒ Hash (private)
Start a new container for the service
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 140 def start_new_container(service, image, environment) container_name = generate_container_name(service[:name]) container_config = { name: container_name, image: image, ports: service[:ports] || [], env: build_environment_variables(service, environment), volumes: service[:volumes] || [], command: service[:cmd], labels: build_container_labels(service, environment), restart_policy: service[:restart_policy] || "unless-stopped" } @logger.debug "start_new_container: Calling adapter.start_container with config: #{container_config.inspect}" begin result = @adapter.start_container(container_config) @logger.debug "start_new_container: Result: #{result.inspect}" result rescue StandardError => e @logger.error "start_new_container failed: #{e.}" @logger.debug "start_new_container error backtrace: #{e.backtrace.join("\n")}" raise end end |
#stop_old_containers(containers, drain_timeout = 30) ⇒ void (private)
This method returns an undefined value.
Gracefully stop old containers
233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 233 def stop_old_containers(containers, drain_timeout = 30) containers.each do |container| @logger.info "Stopping container: #{container[:name]} (#{container[:id]})" begin # Give container time to finish current requests @adapter.stop_container(container[:id], graceful: true, timeout: drain_timeout) @logger.info "Successfully stopped container: #{container[:name]}" rescue StandardError => e @logger.error "Failed to stop container #{container[:name]}: #{e.}" # Continue with other containers even if one fails end end end |
#wait_for_container_running(container, timeout = 60) ⇒ Boolean (private)
Wait for container to be in running state
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 206 def wait_for_container_running(container, timeout = 60) start_time = Time.now loop do status = @adapter.get_container_status(container[:id]) if status == "running" @logger.info "Container #{container[:name]} is running" return true end elapsed = Time.now - start_time if elapsed >= timeout raise DeploymentError, "Container #{container[:name]} failed to start within #{timeout}s (status: #{status})" end @logger.debug "Container #{container[:name]} status: #{status}, waiting..." sleep 2 end end |
#zero_downtime? ⇒ Boolean
Check if strategy supports zero-downtime deployments
48 49 50 |
# File 'lib/gjallarhorn/deployment/zero_downtime.rb', line 48 def zero_downtime? true end |