Skip to content

Commit a5a87d5

Browse files
committed
Build on remote server
1 parent 63fbda3 commit a5a87d5

7 files changed

Lines changed: 96 additions & 16 deletions

File tree

.kamal/hooks/post-deploy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ REPO_URL=$(git config --get remote.origin.url)
2121
API_KEY=$(vault kv get -field=content puppet/application/argo-b3/honeybadger-api-key)
2222

2323
curl --no-progress-meter --data \
24-
"deploy[environment]=${KAMAL_DESTINATION}&deploy[local_username]=${KAMAL_PERFORMER}&deploy[revision]=${KAMAL_VERSION}&deploy[repository]=${REPO_URL}&api_key=${API_KEY}" \
24+
"deploy[environment]=${KAMAL_DESTINATION}&deploy[local_username]=$(whoami)&deploy[revision]=${KAMAL_VERSION}&deploy[repository]=${REPO_URL}&api_key=${API_KEY}" \
2525
https://api.honeybadger.io/v1/deploys
2626

2727
echo "Honeybadger notification complete."

Dockerfile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,11 @@ USER 1000:1000
8888
COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
8989
COPY --chown=rails:rails --from=build /rails /rails
9090

91-
# Rails creates tmp/{cache,pids,sockets} at boot; some destinations (see
92-
# config/deploy.qa.yml) run the container as a different UID than the image's
93-
# baked-in rails user, so make tmp/ writable regardless of runtime UID.
94-
RUN chmod a+rwX tmp
91+
# Rails creates tmp/{cache,pids,sockets} at boot and writes log/production.log;
92+
# some destinations (see config/deploy.qa.yml) run the container as a different
93+
# UID than the image's baked-in rails user, so make these writable regardless
94+
# of runtime UID.
95+
RUN chmod -R a+rwX tmp log
9596

9697
# Entrypoint prepares the database.
9798
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

bin/kamal-otk

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
55
# (https://github.com/cbeer/capistrano-one_time_key). Generates a fresh, passphrase-less
66
# keypair, adds the public key to each target host's ~/.ssh/authorized_keys (relying on
77
# whatever trusted channel -- e.g. Kerberos/ksu -- already lets that connection succeed),
8-
# runs `bin/kamal deploy` using that key, then always removes it from authorized_keys.
8+
# runs `bin/kamal` using that key, then always removes it from authorized_keys.
99
#
10-
# Usage: bin/deploy DESTINATION [kamal deploy args...]
10+
# The key is both configured via config/deploy.<destination>.yml's `ssh.keys` (read from
11+
# KAMAL_SSH_KEY_PATH) and loaded into a scratch ssh-agent: Kamal's remote-builder port
12+
# forwarding (lib/kamal/cli/build.rb#remote_builder_ssh_options) builds its own SSH options
13+
# from the `builder.remote` URI and doesn't include configured keys, so it only succeeds
14+
# via agent-based auth.
15+
#
16+
# Usage: bin/kamal-otk DESTINATION [kamal args...]
1117

1218
require 'bundler/setup'
1319
require 'kamal'
@@ -29,6 +35,21 @@ def ssh_exec!(user, host, command)
2935
system('ssh', "#{user}@#{host}", command, exception: true)
3036
end
3137

38+
# Same mechanism as dlss-capistrano's controlmaster:setup/:start tasks:
39+
# https://github.com/sul-dlss/dlss-capistrano/blob/main/lib/dlss/capistrano/tasks/control_master.rake
40+
def ensure_controlmaster!
41+
return unless ENV.fetch('USE_CAPISTRANO_CONTROLMASTER', false) == 'true'
42+
43+
controlmaster_host = ENV.fetch('CONTROLMASTER_HOST', 'dlss-jump')
44+
controlmaster_socket = ENV.fetch('CONTROLMASTER_SOCKET', '~/.ssh/%r@%h:%p')
45+
46+
return if system('ssh', '-O', 'check', '-S', controlmaster_socket, controlmaster_host)
47+
48+
system('ssh', '-f', '-N', '-S', controlmaster_socket, controlmaster_host, exception: true)
49+
end
50+
51+
ensure_controlmaster!
52+
3253
Dir.mktmpdir do |dir|
3354
key_path = File.join(dir, 'deploy_otk')
3455
comment = "kamal-otk-#{SecureRandom.hex(6)}"
@@ -42,10 +63,19 @@ Dir.mktmpdir do |dir|
4263

4364
hosts.each { |host| ssh_exec!(user, host, add_key_command) }
4465

66+
agent_pid = nil
4567
begin
68+
`ssh-agent -s`.each_line do |line|
69+
ENV[Regexp.last_match(1)] = Regexp.last_match(2) if line =~ /^(SSH_AUTH_SOCK|SSH_AGENT_PID)=([^;]+);/
70+
end
71+
agent_pid = ENV['SSH_AGENT_PID']
72+
system('ssh-add', key_path, exception: true)
73+
4674
ENV['KAMAL_SSH_KEY_PATH'] = key_path
4775
system('bin/kamal', *ARGV, '-d', destination, exception: true)
4876
ensure
77+
system('ssh-agent', '-k') if agent_pid
78+
4979
# Best-effort cleanup: don't let a removal failure mask the deploy's own result.
5080
remove_key_command = "sed -i.bak -e '/#{comment}$/d' ~/.ssh/authorized_keys && rm -f ~/.ssh/authorized_keys.bak"
5181
hosts.each do |host|

config/deploy.qa.yml

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
# Deploy to the qa server. Apache (with Shibboleth SSO) already handles SSL
22
# termination and reverse proxying on this host -- it proxies to localhost:3000,
3-
# same as it does for Passenger on poc -- so Kamal's own proxy is disabled.
3+
# same as it does for Passenger on poc. kamal-proxy is still used (left enabled
4+
# for the primary "web" role), but rebound to plain HTTP on 3000 instead of its
5+
# default 80/443 (which Apache already owns) -- this lets kamal-proxy do its
6+
# normal zero-downtime container swap over the internal "kamal" network,
7+
# without the app container needing to publish (and collide on) a host port.
48
# Kamal always attaches app containers to its own "kamal" bridge network (no
59
# config to disable that), so host networking isn't possible here: instead we
6-
# publish the container's port directly and reach the host's Postgres via
7-
# host.docker.internal (see the pg_hba.conf note for the Puppet-side change
8-
# this requires).
10+
# reach the host's Postgres via host.docker.internal (see the pg_hba.conf note
11+
# for the Puppet-side change this requires).
12+
proxy:
13+
ssl: false
14+
run:
15+
http_port: 3000
16+
https_port: 3001 # unused (ssl disabled), just must not collide with Apache's 443
17+
918
servers:
1019
web:
1120
hosts:
1221
- argo-b3-qa-a.stanford.edu
13-
proxy: false
1422
options:
15-
publish: "3000:80"
1623
add-host: "host.docker.internal:host-gateway"
1724
# Overrides the image's baked-in rails (1000:1000) user so bind-mounted
1825
# writes (bulk actions, logs) land as lyberadmin, matching host ownership.
1926
user: "503:503"
27+
# Runs SolidQueue standalone (bin/jobs) rather than in-Puma, so it registers
28+
# its own graceful-shutdown signal handlers (see solid_queue/supervisor/signals.rb)
29+
# and can drain in-flight jobs on its own timeline, independent of the web
30+
# role -- analogous to Sidekiq's hotswap deploys on poc/legacy argo hosts.
31+
job:
32+
hosts:
33+
- argo-b3-qa-a.stanford.edu
34+
cmd: bin/jobs
35+
stop_timeout: <%= ENV.fetch("SOLID_QUEUE_SHUTDOWN_TIMEOUT", 86400) %>
36+
options:
37+
add-host: "host.docker.internal:host-gateway"
38+
user: "503:503"
2039

2140
ssh:
2241
user: lyberadmin
@@ -25,6 +44,11 @@ ssh:
2544
- <%= ENV["KAMAL_SSH_KEY_PATH"] %>
2645
<% end -%>
2746

47+
# Build natively on the deploy target itself (already has Docker via Puppet)
48+
# rather than emulating amd64 via QEMU on an arm64 developer laptop.
49+
builder:
50+
remote: ssh://lyberadmin@argo-b3-qa-a.stanford.edu
51+
2852
# Bind-mount host paths for durable bulk action output (NFS-backed, shared
2953
# with poc) and Rails log output (for host-level log shipping).
3054
volumes:
@@ -44,6 +68,14 @@ env:
4468
SETTINGS__PRESERVATION_CATALOG__URL: https://preservation-catalog-qa.stanford.edu
4569
SETTINGS__STACKS__URL: https://stacks-stage.stanford.edu/image
4670
SETTINGS__PURL_FETCHER__URL: https://purl-fetcher-stage.stanford.edu
71+
72+
HONEYBADGER_ENV: qa
73+
74+
# Governs how long SolidQueue's supervisor waits for in-flight jobs to
75+
# finish on shutdown (see config/initializers/solid_queue.rb); the job
76+
# role's stop_timeout above is set to match, so Docker's SIGKILL doesn't
77+
# cut the graceful drain short.
78+
SOLID_QUEUE_SHUTDOWN_TIMEOUT: <%= ENV.fetch("SOLID_QUEUE_SHUTDOWN_TIMEOUT", 86400) %>
4779
secret:
4880
- HONEYBADGER_API_KEY
4981
- SECRET_KEY_BASE

config/deploy.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@ image: suldlss/argo-b3
1010
registry:
1111
server: localhost:5555
1212

13-
# Configure builder setup.
13+
# Configure builder setup. Destination-specific config (config/deploy.<destination>.yml)
14+
# points `remote` at the deploy target itself, for a native (non-QEMU) amd64 build.
1415
builder:
1516
arch: amd64
1617

18+
logging:
19+
driver: json-file
20+
options:
21+
max-size: "100m"
22+
max-file: "5"
23+
1724
# Destination-specific configuration (servers, env, secrets, volumes) lives in
1825
# config/deploy.<destination>.yml, e.g. config/deploy.qa.yml. Run with:
1926
# bin/kamal deploy -d qa

config/environments/production.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@
3333
# Skip http-to-https redirect for the default health check endpoint.
3434
# config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
3535

36-
# Log to STDOUT with the current request id as a default log tag.
36+
# Log to STDOUT (so Kamal/Docker can collect container logs) and to
37+
# log/production.log, with the current request id as a default log tag.
3738
config.log_tags = [ :request_id ]
38-
config.logger = ActiveSupport::TaggedLogging.logger(ENV["RAILS_LOG_TO_FILE"] || STDOUT)
39+
file_logger = ActiveSupport::Logger.new(Rails.root.join("log", "production.log"))
40+
stdout_logger = ActiveSupport::Logger.new(STDOUT)
41+
config.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::BroadcastLogger.new(file_logger, stdout_logger))
3942

4043
# Change to "debug" to log everything (including potentially personally-identifiable information!).
4144
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

config/initializers/solid_queue.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
# How long the SolidQueue supervisor waits for in-flight jobs to finish on
4+
# shutdown before force-terminating them. Kamal's job role stop_timeout
5+
# (config/deploy.qa.yml) is set to match, so Docker's SIGKILL doesn't cut
6+
# this grace period short.
7+
SolidQueue.shutdown_timeout = ENV.fetch('SOLID_QUEUE_SHUTDOWN_TIMEOUT', 86_400).to_i

0 commit comments

Comments
 (0)