Skip to content

feat(ansible): scope check/deploy to a subset of hosts with LIMIT - #40

Merged
thiras merged 2 commits into
mainfrom
feat/ansible-limit-scoping
Sep 2, 2026
Merged

thiras merged 2 commits into
mainfrom
feat/ansible-limit-scoping

Conversation

@thiras

@thiras thiras commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Closes #38.

Problem

ansible/Makefile's check and deploy targets hardcoded the full playbook invocation, so the moment decdn_nodes holds more than one host every run is fleet-wide with no supported way to say "just this node". That is the wrong default for the two most common operations — bringing up a new node (whose first converge creates the operator account and lets ssh_hardening disable root + password login) and re-running one node after a config change, a failed play, or a binary bump. The workaround was dropping to a raw ansible-playbook … --limit fra1 command, which silently loses any flags the make targets grow later.

Change

Two optional, empty-by-default variables on both targets:

make check  LIMIT=decdn-node-1
make deploy LIMIT='decdn-node-1,decdn-node-2'
make deploy LIMIT='!decdn-node-2'
make deploy LIMIT=decdn-node-1 ANSIBLE_ARGS='--tags decdn_node -vv'

Defaults are byte-identical to before — make -n check / make -n deploy render the original commands — so existing callers are unaffected.

The --limit value is single-quoted, unlike the sketch in the issue. Unquoted, LIMIT='*' glob-expands against ansible/ before ansible ever sees it; the quoting is load-bearing.

Fail-loud guards

Per AGENTS.md rule 4, the two ways a run can look scoped but silently be fleet-wide now abort at parse time:

input before now
make deploy LIMIT=$NODE (unset) silently fleet-wide *** LIMIT was supplied but expanded empty … refusing to run fleet-wide by accident
export LIMIT=x; make deploy silently scoped *** LIMIT is set in your environment (x) and would silently scope this run
export ANSIBLE_ARGS=--check; make deploy deploy that mutates nothing, looks green *** ANSIBLE_ARGS is set in your environment and would silently alter this run
env LIMIT=x make deploy LIMIT=decdn-node-1 — runs; command line wins, as it should

?= is kept deliberately — switching to = would make $(origin LIMIT) return file and silently disarm the environment guard. Guards are scoped to check/deploy via MAKECMDGOALS, so an unrelated make lint is never blocked.

Known gap (documented, not fixed)

A well-formed argument that selects nothing exits 0: --tags decdn-node (hyphen, vs the real decdn_node) runs zero tasks and reports success, as does a --limit matching a host outside decdn_nodes. Malformed flags fail loudly; these do not. Live risk because this repo mixes separators constantly (decdn-node.service, host decdn-node-1, role decdn_node). An empty-PLAY-RECAP gate is a behaviour change beyond this issue's scope — there is a "Watch the PLAY RECAP" callout in ansible/README.md and a NOTE: in the Makefile instead. Happy to file a follow-up issue.

Also out of scope per the issue: requiring an explicit LIMIT or typed confirmation for a fleet-wide deploy.

Verification

$ make -n check                                  ansible-playbook playbooks/site.yml --check --diff
$ make -n deploy                                 ansible-playbook playbooks/site.yml
$ make -n deploy LIMIT='decdn-node-1,decdn-node-2'   … --limit 'decdn-node-1,decdn-node-2'
$ make -n deploy LIMIT='!decdn-node-2'               … --limit '!decdn-node-2'
$ make -n deploy LIMIT=x ANSIBLE_ARGS='--tags decdn_node -vv'  … --limit 'x' --tags decdn_node -vv

All four guards verified firing (exit 2) and both happy paths verified unchanged. ansible-lint passes at production profile (0 failures, 42 files); pre-commit clean. A live make check against real hosts was not run — no reachable node from here, and no YAML in either role changed.

Empirically checked while reviewing: a typo'd --limit does not fail silently — ansible exits 1 with [ERROR]: … leaves us with no hosts to target on both 2.21.3 and the repo's documented 2.15 floor.

Docs

ansible/README.md gets the LIMIT section with the new-node case; one-line mirrors in README.md and AGENTS.md. Note this also corrects a pre-existing-in-this-branch error caught in review: the converge does not edit ansible_user — no role writes it — so the text now points at Setup for the manual switch rather than crediting it to automation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WaE3ZP2xQdjd9juzRYKLwi

`make check` / `make deploy` hardcoded the playbook invocation, so the moment
`decdn_nodes` holds more than one host every run was fleet-wide with no
supported way to say "just this node". That is the wrong default for bringing
up a new node (whose first converge creates the operator account and lets
ssh_hardening disable root login) and for re-running one node after a config
change or a failed play. The workaround was dropping to a raw ansible-playbook
command, which silently loses any flags the targets grow later.

Add two optional, empty-by-default variables to both targets:

    make deploy LIMIT=decdn-node-1
    make deploy LIMIT='decdn-node-1,decdn-node-2'
    make deploy LIMIT=decdn-node-1 ANSIBLE_ARGS='--tags decdn_node -vv'

Defaults are byte-identical to before, so existing callers are unaffected.

The --limit value is single-quoted, unlike the sketch in the issue: unquoted,
`LIMIT='*'` glob-expands against ansible/ before ansible ever sees it.

Per AGENTS.md rule 4 ("fail loud"), guard the two ways a run can look scoped
but silently be fleet-wide -- an exported LIMIT/ANSIBLE_ARGS reaching make
from the operator's shell (`?=` does not override the environment), and a
LIMIT that expands empty (`make deploy LIMIT=$NODE` with NODE unset, which
drops --limit entirely and converges the whole fleet). Both now abort at parse
time, scoped to check/deploy via MAKECMDGOALS so `make lint` is never blocked.
An explicit command-line LIMIT still wins over the environment.

Not fixed, documented instead: a well-formed argument that selects nothing
exits 0. `--tags decdn-node` (hyphen, vs the real `decdn_node`) runs zero
tasks and reports success, as does a --limit matching a host outside
decdn_nodes. A malformed flag fails loudly; these do not. An empty-PLAY-RECAP
gate is a behaviour change beyond this issue's scope -- see the "Watch the
PLAY RECAP" callout in ansible/README.md.

Closes #38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaE3ZP2xQdjd9juzRYKLwi
Copilot AI lite review requested due to automatic review settings September 2, 2026 23:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A new Makefile error hint can misdirect make check users toward running make deploy, which is risky for an infrastructure repo.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds safe, optional host scoping to the Ansible check/deploy Makefile targets so operators can converge a single node (or subset) without falling back to raw ansible-playbook commands, while keeping fleet-wide behavior as the default.

Changes:

  • Introduce LIMIT (scoped --limit) and ANSIBLE_ARGS (extra flags passthrough) for make check and make deploy, including parse-time “fail loud” guards.
  • Document scoped usage patterns and shell-quoting pitfalls in Ansible and repo-level docs.
  • Add brief one-line usage reminders to top-level README.md and AGENTS.md.
File summaries
File Description
ansible/Makefile Adds LIMIT/ANSIBLE_ARGS support and guardrails to prevent accidental fleet-wide runs.
ansible/README.md Documents scoped check/deploy flows, quoting guidance, and operational gotchas.
README.md Adds a short note that LIMIT scopes and ANSIBLE_ARGS passes through.
AGENTS.md Mirrors the same short note for contributor guidance.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ansible/Makefile
The parse-time guard fires for both check and deploy, but its hint hardcoded
`make deploy LIMIT=<host>` -- so an operator who hit it during a `make check`
dry-run was pointed at the mutating target. Echo $(MAKECMDGOALS) instead.

The other two guard messages name no target and were already goal-agnostic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaE3ZP2xQdjd9juzRYKLwi
@thiras
thiras merged commit 48b68fa into main Sep 2, 2026
8 checks passed
@thiras
thiras deleted the feat/ansible-limit-scoping branch September 2, 2026 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ansible/Makefile: support scoping check/deploy to a subset of hosts (LIMIT)

2 participants