Multi-Axis Configuration
Vigo manages fleets where machines vary across multiple dimensions simultaneously. A single machine might be: client Acme, production environment, webserver function, running on GCP, Debian OS, in us-east region, under PCI compliance. Each axis is handled by a different Vigo mechanism -- no combinatorial explosion, no duplication.
The Four Mechanisms
| Axis type | Mechanism | Example |
|---|---|---|
| Client/tenant | Directory tree | clients/acme/, clients/globex/ |
| Environment | per_env_overrides.vgo |
production, staging, dev |
| Function | Roles | webserver, database, cache |
| Infrastructure facts | when: expressions on traits |
Cloud provider, OS, region, architecture |
Directory tree -- client/tenant axis
The directory hierarchy under stockpile/ defines inheritance. common.vgo at any level applies modules, roles, and vars to all entries in subdirectories.
stockpile/
common.vgo # fleet-wide baseline
clients/
acme/
common.vgo # Acme-specific modules and vars
per_env_overrides.vgo # Acme's per-environment overrides
nodes.vgo # Acme's node match patterns
globex/
common.vgo # Globex-specific modules and vars
per_env_overrides.vgo # Globex's per-environment overrides
nodes.vgo
per_env_overrides.vgo -- environment axis
A per_env_overrides.vgo file at any directory level defines variable and module overrides keyed by environment name. The environment: field on each node entry determines which block applies.
# per_env_overrides.vgo
env:
production:
modules: [waf, hardening, audit-logging]
vars:
worker_count: "auto"
log_level: "warn"
backup_schedule: "0 */4 * * *"
staging:
modules: [debug-tools]
exclude_modules: [hardening]
vars:
worker_count: "2"
log_level: "debug"
dev:
modules: [debug-tools, mock-services]
exclude_modules: [hardening, audit-logging]
vars:
worker_count: "1"
log_level: "debug"
Node entries reference the environment:
# nodes.vgo
nodes:
- match: "web-*.prod.acme.com"
environment: production
roles: [webserver]
- match: "web-*.staging.acme.com"
environment: staging
roles: [webserver]
The environment override merges with the node's modules and vars. exclude_modules removes specific modules inherited from common.vgo or roles.
Precedence (last wins):
- Module default vars
common.vgovars (directory inheritance)- Node-level vars
per_env_overrides.vgovars for the matching environment
Roles -- function axis
Roles group modules into reusable profiles. A machine's function is assigned via roles on the node entry or in common.vgo.
# roles.vgo
roles:
- name: webserver
modules: [nginx, certbot, logrotate]
- name: database
modules: [postgres, pg-backup, pg-monitoring]
- name: pci-hardened
modules: [cis-ubuntu, aide, auditd, fail2ban]
Roles compose freely. A PCI web server in production gets: roles: [webserver, pci-hardened] + production's modules from per_env_overrides.vgo + fleet-wide modules from common.vgo.
when: expressions -- infrastructure facts axis
The agent discovers infrastructure facts as traits: cloud provider, OS family, region, architecture, container runtime, etc. Use when: expressions to conditionally include modules or resources based on traits -- no config axis needed because the machine already knows the answer.
# In a module or node entry:
modules:
- name: gcp-logging
when: "trait('cloud.provider') == 'gcp'"
- name: cloudwatch
when: "trait('cloud.provider') == 'aws'"
- name: bare-metal-monitoring
when: "!is_virtual"
# In a resource within a module:
resources:
- name: apt-config
type: file
when: "os_family('debian')"
# ...
- name: yum-config
type: file
when: "os_family('redhat')"
# ...
Common trait-based axes:
| Axis | Trait | Example when: expression |
|---|---|---|
| Cloud provider | cloud.provider |
trait('cloud.provider') == 'gcp' |
| OS family | os.family |
os_family('debian') |
| Architecture | os.arch |
trait('os.arch') == 'arm64' |
| Region | cloud.region |
trait('cloud.region') == 'us-east-1' |
| Container | virtual.system |
is_container |
| Virtualization | virtual.system |
is_virtual |
Putting It All Together
Example: MSP managing Acme Corp with production and staging environments across GCP and AWS.
stockpile/
common.vgo # Fleet baseline: sshd, ntp, monitoring
roles.vgo # webserver, database, pci-hardened
modules/
nginx.vgo # Module: installs nginx
gcp-logging.vgo # Module: GCP Cloud Logging agent
cloudwatch.vgo # Module: AWS CloudWatch agent
waf.vgo # Module: web application firewall
clients/
acme/
common.vgo # Acme: adds acme-specific modules/vars
per_env_overrides.vgo # prod: +waf, +hardening; staging: +debug-tools
nodes.vgo # Match patterns for Acme's machines
Acme's nodes.vgo:
nodes:
- match: "web-*.prod.acme.com"
environment: production
roles: [webserver, pci-hardened]
- match: "db-*.prod.acme.com"
environment: production
roles: [database, pci-hardened]
- match: "*.staging.acme.com"
environment: staging
roles: [webserver]
A machine web-1.prod.acme.com running on GCP gets:
- Directory inheritance: fleet baseline (sshd, ntp, monitoring) + Acme modules
- Roles: nginx, certbot, logrotate (webserver) + cis-ubuntu, aide, auditd, fail2ban (pci-hardened)
- Environment: waf, hardening, production vars (from per_env_overrides.vgo)
- Traits: gcp-logging (because
cloud.provider == 'gcp')
No duplication. Each axis is handled exactly once. Adding a new client means adding a directory. Adding a new environment means adding a block to per_env_overrides.vgo. Adding cloud-specific behavior means adding a when: expression. None of these changes affect the others.
Summary
| What you're deciding | Where it goes | Changes when... |
|---|---|---|
| Which clients you manage | Directory tree | New client onboarded |
| What environment a machine is in | environment: on node entry |
Machine promoted/demoted |
| What a machine does | roles: on node entry |
Machine repurposed |
| How environments differ | per_env_overrides.vgo |
Environment policy changes |
| What modules a function needs | roles.vgo |
Role definition evolves |
| Fleet-wide baseline | common.vgo |
Security/compliance policy changes |
| Cloud/OS/region behavior | when: on traits |
Never -- machines report their own facts |