Class: Gjallarhorn::Proxy::NginxManager

Inherits:
Manager
  • Object
show all
Defined in:
lib/gjallarhorn/proxy/nginx_manager.rb

Overview

Nginx proxy manager for zero-downtime deployments

Manages nginx configuration updates and reloads to enable zero-downtime deployments by switching upstream servers.

Since:

  • 0.1.0

Constant Summary collapse

DEFAULT_NGINX_CONF_DIR =

Default nginx configuration paths

Since:

  • 0.1.0

"/etc/nginx/conf.d"
DEFAULT_NGINX_BIN =

Since:

  • 0.1.0

"nginx"

Instance Attribute Summary

Attributes inherited from Manager

#config, #logger, #proxy_type

Instance Method Summary collapse

Methods inherited from Manager

create

Constructor Details

#initialize(config, logger = nil) ⇒ NginxManager

Initialize nginx proxy manager

Parameters:

  • config (Hash)

    Nginx configuration

  • logger (Logger) (defaults to: nil)

    Logger instance

Since:

  • 0.1.0



23
24
25
26
27
28
29
30
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 23

def initialize(config, logger = nil)
  super
  @nginx_conf_dir = config[:conf_dir] || DEFAULT_NGINX_CONF_DIR
  @nginx_bin = config[:nginx_bin] || DEFAULT_NGINX_BIN
  @domain = config[:domain] || config[:host]
  @ssl_enabled = config[:ssl] || false
  @app_port = config[:app_port] || 3000
end

Instance Method Details

#config_syntax_valid?Boolean (private)

Check if nginx configuration syntax is valid

Returns:

  • (Boolean)

    True if configuration is valid

Since:

  • 0.1.0



252
253
254
255
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 252

def config_syntax_valid?
  result = execute_nginx_command("configtest")
  result[:success]
end

#configured_upstreamsArray<String> (private)

Get configured upstream servers

Returns:

  • (Array<String>)

    List of configured upstreams

Since:

  • 0.1.0



260
261
262
263
264
265
266
267
268
269
270
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 260

def configured_upstreams
  config_files = Dir.glob(File.join(@nginx_conf_dir, "gjallarhorn-*.conf"))
  upstreams = []

  config_files.each do |file|
    content = File.read(file)
    upstreams.concat(content.scan(/upstream\s+(\w+)\s*{/).flatten)
  end

  upstreams
end

#execute_nginx_command(action) ⇒ Hash (private)

Execute nginx command

Parameters:

  • action (String)

    Action to perform (reload, restart, configtest)

Returns:

  • (Hash)

    Execution result

Since:

  • 0.1.0



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 221

def execute_nginx_command(action)
  case action
  when "reload"
    command = "#{@nginx_bin} -s reload"
  when "restart"
    command = "systemctl restart nginx"
  when "configtest"
    command = "#{@nginx_bin} -t"
  else
    raise ArgumentError, "Unknown nginx action: #{action}"
  end

  @logger.debug "Executing: #{command}"

  result = system(command)
  {
    success: result,
    error: result ? nil : "Command failed with exit code: #{$CHILD_STATUS.exitstatus}"
  }
end

#generate_health_check_config(service_name) ⇒ String (private)

Generate health check configuration

Parameters:

  • service_name (String)

    Service name

Returns:

  • (String)

    Health check location block

Since:

  • 0.1.0



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 150

def generate_health_check_config(service_name)
  health_path = @config[:health_check_path] || "/health"

  "    # Health check endpoint with container identification\n    location \#{health_path} {\n        proxy_pass http://\#{service_name}\#{health_path};\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    \#{\"    \"}\n        # Add headers for traffic verification\n        add_header X-Proxy-Backend $upstream_addr always;\n        add_header X-Proxy-Status $upstream_status always;\n    }\n  HEALTH\nend\n"

#generate_location_config(service_name) ⇒ String (private)

Generate location configuration for main proxy

Parameters:

  • service_name (String)

    Service name

Returns:

  • (String)

    Location configuration

Since:

  • 0.1.0



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 331

def generate_location_config(service_name)
  "    location / {\n        proxy_pass http://\#{service_name};\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_set_header X-Forwarded-Host $host;\n        proxy_set_header X-Forwarded-Port $server_port;\n    \#{\"    \"}\n        # Timeouts\n        proxy_connect_timeout 5s;\n        proxy_send_timeout 60s;\n        proxy_read_timeout 60s;\n    \#{\"    \"}\n        # Buffer settings\n        proxy_buffering on;\n        proxy_buffer_size 4k;\n        proxy_buffers 8 4k;\n    \#{\"    \"}\n        # Health check support\n        proxy_next_upstream error timeout http_502 http_503 http_504;\n    }\n  LOCATION\nend\n"

#generate_security_headersString (private)

Generate security headers configuration

Returns:

  • (String)

    Security headers configuration

Since:

  • 0.1.0



313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 313

def generate_security_headers
  "    # Security headers\n    add_header X-Frame-Options DENY always;\n    add_header X-Content-Type-Options nosniff always;\n    add_header X-XSS-Protection \"1; mode=block\" always;\n    add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n\n    # Gjallarhorn identification headers\n    add_header X-Proxy-Type \"nginx\" always;\n    add_header X-Managed-By \"gjallarhorn\" always;\n  HEADERS\nend\n"

#generate_server_config(service_name) ⇒ String (private)

Generate nginx server configuration block

Parameters:

  • service_name (String)

    Service name

Returns:

  • (String)

    Server configuration block

Since:

  • 0.1.0



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 125

def generate_server_config(service_name)
  ssl_config = generate_ssl_config
  security_headers = generate_security_headers
  health_check_config = generate_health_check_config(service_name)
  location_config = generate_location_config(service_name)

  "    server {\n        listen 80;\n        \#{ssl_config}\n        server_name \#{@domain};\n\n        \#{security_headers}\n\n        \#{health_check_config}\n\n        \#{location_config}\n    }\n  SERVER\nend\n"

#generate_service_config(service_name, containers) ⇒ String (private)

Generate nginx configuration for a service

Parameters:

  • service_name (String)

    Service name

  • containers (Array<Hash>)

    Container information

Returns:

  • (String)

    Nginx configuration

Since:

  • 0.1.0



107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 107

def generate_service_config(service_name, containers)
  upstream_config = generate_upstream_config(service_name, containers)
  server_config = generate_server_config(service_name)

  "    # Generated by Gjallarhorn for \#{service_name}\n    # Generated at: \#{Time.now.utc.iso8601}\n\n    \#{upstream_config}\n\n    \#{server_config}\n  NGINX\nend\n"

#generate_ssl_configString (private)

Generate SSL configuration block

Returns:

  • (String)

    SSL configuration

Since:

  • 0.1.0



296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 296

def generate_ssl_config
  return "" unless @ssl_enabled

  "    listen 443 ssl http2;\n    ssl_certificate /etc/letsencrypt/live/\#{@domain}/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/\#{@domain}/privkey.pem;\n    ssl_session_timeout 1d;\n    ssl_session_cache shared:SSL:50m;\n    ssl_stapling on;\n    ssl_stapling_verify on;\n  SSL\nend\n"

#healthy?Boolean

Check if nginx is healthy

Returns:

  • (Boolean)

    True if nginx is responding

Since:

  • 0.1.0



96
97
98
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 96

def healthy?
  nginx_running? && config_syntax_valid?
end

#last_reload_timeTime? (private)

Get last nginx reload time

Returns:

  • (Time, nil)

    Last reload time or nil if unknown

Since:

  • 0.1.0



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 275

def last_reload_time
  # Try to get nginx master process start time as proxy for last reload
  if nginx_running?
    pid = `pgrep -f "nginx: master process"`.strip
    unless pid.empty?
      stat_file = "/proc/#{pid}/stat"
      if File.exist?(stat_file)
        boot_time = File.read("/proc/stat").match(/btime (\d+)/)[1].to_i
        start_time_ticks = File.read(stat_file).split[21].to_i
        clock_ticks = 100 # Typical value for USER_HZ
        Time.at(boot_time + start_time_ticks / clock_ticks)
      end
    end
  end
rescue StandardError
  nil
end

#nginx_running?Boolean (private)

Check if nginx is running

Returns:

  • (Boolean)

    True if nginx process is running

Since:

  • 0.1.0



245
246
247
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 245

def nginx_running?
  system("pgrep nginx > /dev/null 2>&1")
end

#reload_nginxvoid (private)

This method returns an undefined value.

Reload nginx gracefully

Raises:

Since:

  • 0.1.0



208
209
210
211
212
213
214
215
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 208

def reload_nginx
  @logger.info "Reloading nginx configuration..."

  result = execute_nginx_command("reload")
  raise ProxyError, "Failed to reload nginx: #{result[:error]}" unless result[:success]

  @logger.info "Nginx reloaded successfully"
end

#restartBoolean

Restart nginx service

Returns:

  • (Boolean)

    True if restart successful

Since:

  • 0.1.0



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 80

def restart
  @logger.info "Restarting nginx..."

  begin
    execute_nginx_command("restart")
    @logger.info "Nginx restarted successfully"
    true
  rescue StandardError => e
    @logger.error "Failed to restart nginx: #{e.message}"
    false
  end
end

#statusHash

Get nginx proxy status

Returns:

  • (Hash)

    Nginx status information

Since:

  • 0.1.0



67
68
69
70
71
72
73
74
75
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 67

def status
  {
    type: "nginx",
    status: nginx_running? ? "running" : "stopped",
    config_dir: @nginx_conf_dir,
    upstreams: configured_upstreams,
    last_reload: last_reload_time
  }
end

#switch_traffic(service_name:, to_container:, from_containers: nil) ⇒ void

This method returns an undefined value.

Switch traffic from old containers to new container

Parameters:

  • service_name (String)

    Service name

  • from_containers (Array<Hash>) (defaults to: nil)

    Containers to switch traffic from (unused in nginx implementation)

  • to_container (Hash)

    Container to switch traffic to

Since:

  • 0.1.0



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 38

def switch_traffic(service_name:, to_container:, from_containers: nil)
  @logger.info "Switching nginx traffic for #{service_name} to #{to_container[:name]}"

  # Generate new nginx configuration
  new_config = generate_service_config(service_name, [to_container])

  # Write configuration to file
  write_nginx_config(service_name, new_config)

  # Test nginx configuration
  test_nginx_config

  # Reload nginx gracefully
  reload_nginx

  # Verify traffic is flowing to new container
  if verify_traffic_switch(service_name, to_container)
    @logger.info "Successfully switched traffic to #{to_container[:name]}"
  else
    @logger.warn "Traffic switch completed but verification failed"
  end
rescue StandardError => e
  @logger.error "Failed to switch nginx traffic: #{e.message}"
  raise ProxyError, "Nginx traffic switch failed: #{e.message}"
end

#test_nginx_configvoid (private)

This method returns an undefined value.

Test nginx configuration syntax

Raises:

Since:

  • 0.1.0



195
196
197
198
199
200
201
202
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 195

def test_nginx_config
  @logger.debug "Testing nginx configuration syntax..."

  result = execute_nginx_command("configtest")
  raise ProxyError, "Invalid nginx configuration: #{result[:error]}" unless result[:success]

  @logger.debug "Nginx configuration syntax is valid"
end

#write_nginx_config(service_name, config_content) ⇒ String (private)

Write nginx configuration to file

Parameters:

  • service_name (String)

    Service name

  • config_content (String)

    Configuration content

Returns:

  • (String)

    Path to written configuration file

Since:

  • 0.1.0



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/gjallarhorn/proxy/nginx_manager.rb', line 174

def write_nginx_config(service_name, config_content)
  config_file = File.join(@nginx_conf_dir, "gjallarhorn-#{service_name}.conf")

  # Create backup of existing config if it exists
  if File.exist?(config_file)
    backup_file = "#{config_file}.backup-#{Time.now.strftime("%Y%m%d-%H%M%S")}"
    FileUtils.cp(config_file, backup_file)
    @logger.debug "Backed up existing config to #{backup_file}"
  end

  # Write new configuration
  File.write(config_file, config_content)
  @logger.debug "Wrote nginx config to #{config_file}"

  config_file
end