Skip to content

feat(app): expose kamal-proxy rollout as kamal app rollout deploy/set/stop - #23

Merged
mhenrixon merged 1 commit into
dashfrom
issue-6-app-rollout
Jul 25, 2026
Merged

mhenrixon merged 1 commit into
dashfrom
issue-6-app-rollout

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

Summary

kamal-proxy has had cookie-keyed canary rollouts (rollout deploy, rollout set --percent/--list, rollout stop) for a while, but the gem exposed none of it — grep rollout lib/ returned nothing, so the only way to split traffic was to docker exec into the proxy container by hand.

This adds kamal app rollout deploy|set|stop through the full layer cake:

  • lib/kamal/configuration/proxy.rb — rollout_deploy_command_args / rollout_set_command_args. kamal-proxy rollout deploy accepts only --target, --read-target, --deploy-timeout, --drain-timeout, so it gets its own builder instead of reusing deploy_options: the service already exists and keeps its host/TLS/buffering/logging options.
  • lib/kamal/commands/app/proxy.rb — rollout_deploy / rollout_set / rollout_stop, emitted through the existing proxy_exec.
  • lib/kamal/cli/app.rb — kamal app rollout <deploy|set|stop> with --percent and --list.
  • lib/kamal/cli/app/rollout_boot.rb (new) — boots the canary containers alongside the live ones and registers them as the proxy's rollout target, stopping the new container again if registration fails.

Typical flow:

kamal app rollout deploy --version=<new sha>   # canary boots, still 0% traffic
kamal app rollout set --percent 10             # 10% of cookie-keyed sessions
kamal app rollout set --list alice bob         # or: only these kamal-rollout cookie values
kamal app rollout stop                         # back to 100% live
kamal deploy --version=<new sha>               # or promote for real

Closes #6

Test plan

  • kamal app rollout deploy boots a second container per proxy role without touching the live one, and the live version keeps serving
  • kamal app rollout set --percent 10 splits traffic; the kamal-rollout cookie keeps a session pinned
  • kamal app rollout set --list <value> routes only matching cookie values
  • kamal app rollout stop returns all traffic to the live target and leaves the canary containers for kamal app stale_containers --stop
  • kamal app rollout deploy against an already-deployed version fails with a readable message instead of a docker name clash
  • kamal app rollout set with neither flag raises

Deviations & judgment calls

Discoveries

  • kamal-proxy rollout deploy accepts ONLY --target, --read-target, --deploy-timeout, --drain-timeout (../kamal-proxy/internal/cmd/rollout_deploy.go). It does NOT take the full deploy option set (--host, --tls, --error-pages, buffering, …). So Proxy#deploy_command_args could not be reused; rollout needed its own, much smaller arg builder.
  • rollout set requires one of --percent / --list (cobra MarkFlagsOneRequired); rollout stop takes no flags.
  • Kamal::Cli::Base#command resolves the invocation class against Kamal::Cli::Main.subcommand_classes and would raise NoMethodError on a nested Thor subcommand class, because a nested class is not in that list. That ruled out subcommand "rollout", Kamal::Cli::App::Rollout.

Judgment calls

  • rollout takes a positional action (kamal app rollout deploy|set|stop) instead of a nested Thor subcommand — the kamal proxy boot_config <set|get|reset> precedent, and it sidesteps the Cli::Base#command limitation above. The requested surface (kamal app rollout set --percent 10) is unchanged.
  • rollout deploy boots the canary containers itself rather than requiring a pre-booted container. Without that the command is unusable: nothing else in kamal boots a container without also making it live.
  • rollout deploy prepares assets but not error pages / SSL certificates. Both are service-level proxy state established by the real deploy, and rollout deploy has no flags to pass them anyway. Assets are per-version and the canary container needs them.
  • rollout stop only stops the traffic split; it does not stop the canary containers. Removing them is kamal app stale_containers --stop (or the next kamal deploy), which already exists. Making rollout stop kill containers would surprise anyone re-enabling a split.
  • Guard against re-deploying a version that is already deployed. docker run would otherwise fail with an opaque "name already in use"; we raise a Kamal::Cli::BootError naming the version instead.
  • --list is a Thor :array so --list a b and repeated --list=a --list=b both work; optionize emits one --list= per value, which cobra's StringSlice accepts.
  • --percent 0 is honoured (checked with .nil?, not truthiness) — a legitimate way to park a rollout at zero traffic without tearing it down.
  • No --read-target support. This branch is rooted on main, which has no loadbalancer / reader-writer concept.

Known limitation to carry into dash

Like Kamal::Cli::App::Boot, this registers the rollout target per host — each host tells its own proxy about its own container. That is correct for the one-proxy-per-host model on main. With dash's loadbalancer (one proxy fronting several hosts, auto-activated for a primary role with >1 host) the per-host rollout deploy calls would overwrite each other's rollout target. Rollout and the loadbalancer are therefore not wired together yet; use rollout on single-proxy services until a follow-up teaches Kamal::Commands::Loadbalancer to emit rollout deploy --target/--read-target.

Verification

  • Unit suite: 854 runs, green apart from 7 pre-existing builder failures (host-arch dependent + local Docker), confirmed identical on a stashed clean tree.
  • bundle exec rubocop --parallel clean.
  • Integration (bin/test) not run: it needs Docker-in-Docker plus a published proxy image, and no integration coverage was added for rollout — the issue asked for "integration or command-construction test", and command construction is covered.

…/stop

## Summary

kamal-proxy has implemented cookie-keyed canary rollouts for a while, but the
gem had zero surface for it (`grep rollout lib/` returned nothing), so the only
way to split traffic was to shell into the proxy container by hand.

This adds `kamal app rollout deploy|set|stop`, following the layer cake:

- `Kamal::Configuration::Proxy` builds the rollout arg lists. `rollout deploy`
  accepts only `--target`/`--deploy-timeout`/`--drain-timeout`, so it needs its
  own builder rather than reusing `deploy_options` - the service already exists
  and keeps its host/TLS/buffering options.
- `Kamal::Commands::App::Proxy` emits the three `docker exec kamal-proxy
  kamal-proxy rollout ...` commands.
- `Kamal::Cli::App#rollout` takes the action as a positional argument, matching
  the `kamal proxy boot_config <set|get|reset>` precedent.
- `Kamal::Cli::App::RolloutBoot` boots the canary containers alongside the live
  ones and registers them as the rollout target, stopping the new container
  again if registration fails.

## Test Coverage

- commands: rollout deploy/set/stop argv, including that deploy drops the
  non-rollout proxy flags, honours custom timeouts, and that set supports
  percent, list, both, and percent zero
- cli: rollout deploy boots the canary and leaves the live version serving,
  refuses a version that is already deployed, set/stop reach every proxy role,
  and set without --percent/--list raises

## Verification

- [x] bundle exec rubocop --parallel passes
- [x] unit tests pass (7 pre-existing builder failures unchanged, verified
      against a clean tree)
@mhenrixon
mhenrixon force-pushed the issue-6-app-rollout branch from 16c1c9e to e3fa764 Compare July 25, 2026 07:12
@mhenrixon mhenrixon self-assigned this Jul 25, 2026
@mhenrixon mhenrixon added the enhancement New feature or request label Jul 25, 2026
@mhenrixon
mhenrixon merged commit 900db31 into dash Jul 25, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

R1: Expose kamal-proxy rollout as kamal app rollout deploy/set/stop

1 participant