Migrating from Chef
Chef and Vigo share the pull-based model and converge-on-interval pattern. The biggest practical shifts: Ruby DSL → plain YAML; Chef's 15-level attribute precedence → Vigo's three levels (match-block vars > inherited vars > configcrate defaults, with environments.vgo overrides at the top). The resource shape — declarative, idempotent, with notify / subscribes triggers — maps closely.
Architecture Differences
Both Chef and Vigo use a pull-based agent that contacts a server on a regular interval, receives desired state, and converges locally. Chef compiles Ruby code into a resource collection; Vigo interprets YAML directly — there's no compilation step.
Chef's infrastructure includes a Chef Server (or Chef Automate), client nodes running chef-client (Ruby), and often a Supermarket for cookbook distribution. Vigo has a Go server (vigosrv), a Rust agent (vigo), and a CLI (vigocli). No Ruby runtime, no package manager, no external services.
Chef stores node objects and search indexes on the server (PostgreSQL + Elasticsearch in Chef Server). Vigo uses an in-memory FleetIndex backed by SQLite — no external database.
Chef's attribute system has 15 precedence levels (default, force_default, normal, override, force_override, automatic — each at role, environment, cookbook, and recipe levels). Vigo has three levels: configcrate defaults, role vars, node vars.
Concept Mapping
| Chef Concept | Vigo Equivalent | Notes |
|---|---|---|
| Cookbook | Configcrate (.vgo file) |
Single YAML file replaces the cookbook directory tree |
| Recipe | Configcrate resources | Resources listed in the configcrate — no separate recipe files |
| Role | Role | Named list of configcrates — same concept |
| Run list | Configcrate list in envoys.vgo |
Roles and configcrates assigned to matching envoys |
| Node object | FleetIndex entry + Traits | In-memory, not stored server-side as a document |
| Ohai | Traits | System facts collected by the agent |
| Attributes (default) | Configcrate vars: |
Default values in the configcrate |
| Attributes (override) | Match-block vars: in envoys.vgo |
Match-level overrides |
| Data bag | common.vgo at any directory level |
Directory-inherited shared vars replace external YAML data files |
| Chef Vault | secret: prefix |
Resolved server-side, never in plaintext |
| Environment | environments.vgo (keyed by environment:) |
Env-specific overrides live in a sibling file, not per-match-block |
notifies |
notify |
Trigger downstream resource on change |
subscribes |
subscribes |
Same keyword, same semantics |
only_if / not_if |
when: |
Resource conditionals |
include_recipe |
Configcrate dependencies | Configcrates listed in roles or depends_on |
knife |
vigocli |
CLI for server administration |
| Chef Supermarket | No equivalent | Configcrates are plain YAML files — no registry |
| Policyfile | envoys.vgo |
Pin configcrate versions at the node level |
chef-client |
vigo agent |
Rust binary, LMDB state, offline convergence |
chef-server |
vigosrv |
Go binary, gRPC + REST + Web UI |
| ERB template | Go template in content: |
Templates only in content: attribute |
knife bootstrap |
vigocli tokens + bootstrap script |
One-time token enrollment |
Resource Type Mapping
| Chef Resource | Vigo Executor | Notes |
|---|---|---|
package |
package |
Auto-detects package manager |
template / cookbook_file |
file |
content: for templates, sourcepath: for static files |
service |
service |
state: running/stopped, enabled: true/false |
execute / bash |
exec |
command:, onlyif:, unless: |
user |
user |
username:, uid:, shell:, groups: |
group |
group |
groupname:, gid:, members: |
cron |
cron |
command:, minute:, hour:, etc. |
apt_repository / yum_repository |
repository |
Unified across distros |
remote_file (archive) |
source_package |
url:, target_path:, checksum: (tarballs/zips) |
remote_file + dpkg_package/rpm_package |
nonrepo_package |
Install .deb/.rpm/.pkg/.msi from URLs |
mount |
mount |
device:, mountpoint:, fstype: |
sysctl |
sysctl |
key:, value: |
hostname |
hostname |
hostname: |
file (line) |
file_line |
path:, line:, match: |
Walkthrough: Converting an Nginx Cookbook
Chef Version
cookbooks/nginx/recipes/default.rb:
package 'nginx' do
action :install
end
template '/etc/nginx/nginx.conf' do
source 'nginx.conf.erb'
owner 'root'
group 'root'
mode '0644'
variables(
worker_processes: node['nginx']['worker_processes'],
worker_connections: node['nginx']['worker_connections'],
server_name: node['nginx']['server_name'],
root: node['nginx']['root']
)
notifies :restart, 'service[nginx]'
end
service 'nginx' do
action [:enable, :start]
end
cookbooks/nginx/attributes/default.rb:
default['nginx']['worker_processes'] = 'auto'
default['nginx']['worker_connections'] = 1024
default['nginx']['server_name'] = 'localhost'
default['nginx']['root'] = '/var/www/html'
cookbooks/nginx/templates/default/nginx.conf.erb:
worker_processes <%= @worker_processes %>;
events {
worker_connections <%= @worker_connections %>;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name <%= @server_name %>;
root <%= @root %>;
location / {
try_files $uri $uri/ =404;
}
}
}
Vigo Version
stacks/configcrates/nginx.vgo:
name: nginx
vars:
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_server_name: localhost
nginx_root: /var/www/html
resources:
- name: nginx-package
type: package
package: nginx
state: present
- name: nginx-config
type: file
target_path: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
content: |
worker_processes {{ .Vars.nginx_worker_processes }};
events {
worker_connections {{ .Vars.nginx_worker_connections }};
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name {{ .Vars.nginx_server_name }};
root {{ .Vars.nginx_root }};
location / {
try_files $uri $uri/ =404;
}
}
}
notify:
- nginx-service
- name: nginx-service
type: service
service: nginx
state: running
enabled: true
stacks/roles/webserver.vgo:
name: webserver
configcrates:
- nginx
stacks/envoys/envoys.vgo:
envoys:
- match: "*.web.example.com"
environment: production
roles: [webserver]
vars:
nginx_worker_processes: "4"
nginx_worker_connections: "4096"
nginx_server_name: "example.com"
nginx_root: /var/www/example.com
Key Differences
- No Ruby. Chef's entire stack runs on Ruby — cookbooks, recipes, attributes, templates. Vigo is pure YAML with Go templates in
content:only. - One file per configcrate. A Chef cookbook is a directory tree (
recipes/,templates/,attributes/,metadata.rb, etc.). A Vigo configcrate is a single.vgofile. - No attribute levels. Chef's
default/normal/override/force_override/automaticat cookbook/role/environment/node levels gives 15 precedence tiers. Vigo has three: configcrate defaults → role vars → node vars. - Templates inline or in
templates/. Small templates go directly incontent:. For larger files, usesource: templates/nginx.conf.tmpl— the server reads the file from thetemplates/directory and renders it with the same Go template engine. Notemplates/default/subdirectory convention.
Variable and Data Hierarchy
Chef attributes have 15 precedence levels. In practice, most teams use default and override. Vigo maps this cleanly:
| Chef Level | Vigo Equivalent |
|---|---|
default (cookbook) |
Configcrate vars: |
default (role) |
Role vars: |
default (environment) |
common.vgo vars at ancestor directories |
override (node) |
Match-block vars: |
override (environment) |
environments.vgo keyed by environment: |
automatic (Ohai) |
Traits (accessed via .Traits in templates, or when:) |
| Data bags | common.vgo at any directory level (shared via inheritance) |
Per-environment overrides live in environments.vgo, not per-match-block:
# envoys.vgo
envoys:
- match: "prod-*.web.example.com"
environment: production
roles: [webserver]
- match: "stg-*.web.example.com"
environment: staging
roles: [webserver]
# environments.vgo
env:
production:
vars:
nginx_server_name: "example.com"
staging:
vars:
nginx_server_name: "staging.example.com"
Templates
Chef uses ERB. Vigo uses Go templates.
| Chef ERB | Vigo Go Template |
|---|---|
<%= @variable %> |
{{ .Vars.variable }} |
<%= node['attr'] %> |
{{ .Vars.attr }} |
| <%= node['fqdn'] %> | {{ .Traits.identity.fqdn }} |
| <% if condition %> | {{ if .Vars.condition }} |
| <% @items.each do \|i\| %> | {{ range .Vars.items }} |
Critical restriction: Go templates work only in content: attributes. You cannot use {{ }} in package:, target_path:, command:, or any other attribute.
Conditionals and Targeting
Chef conditionals:
package 'nginx' do
action :install
only_if { node['platform_family'] == 'debian' }
end
Vigo equivalent:
- name: nginx-package
type: package
package: nginx
state: present
when: "os_family('debian')"
Chef's only_if/not_if guards run arbitrary Ruby. Vigo's when: uses a fixed expression syntax with 15 builtins and boolean operators.
Secrets
Chef uses data bags (optionally encrypted) or Chef Vault:
db_password = data_bag_item('secrets', 'database')['password']
Vigo uses the secret: prefix:
content: "secret:vigo/database/password"
The secrets provider resolves references at bundle compile time. No Ruby code, no key management ceremony.
Common Gotchas
-
No Ruby. You cannot run arbitrary code during convergence. If Chef recipes contain procedural Ruby logic beyond resource declarations, you'll need to decompose it into declarative resources with
when:conditionals. -
No compile phase. Chef has a two-phase model (compile then converge). Vigo has no compile phase — resources are interpreted from YAML directly. There's no equivalent of
lazy {}evaluation. -
Templates only in
content:. Chef lets you use ERB in any string and template helper methods everywhere. Vigo restricts Go templates tocontent:. Usewhen:and separate resources for conditional logic. -
Flat variable namespace. Chef attributes are nested hashes (
node['nginx']['port']). Vigo vars are flat strings (nginx_port). Prefix with the configcrate name to avoid collisions. -
No search. Chef Server's search API (
search(:node, 'role:webserver')) has no direct equivalent. For cross-node coordination, use directory-levelcommon.vgoinheritance or the task/query system. -
No
run_listmanipulation. Chef can dynamically include recipes. Vigo's configcrate list is static — defined inenvoys.vgorole assignments. Usewhen:to conditionally skip entire configcrates. -
No Berkshelf/Policyfile dependency resolution. Configcrates don't declare dependencies on other configcrates. Include all required configcrates in the role definition.
-
First-match-wins node classification. Chef can merge multiple roles and environments. Vigo's
envoys.vgouses first-match-wins — only the first matching glob applies.
Related
- Config Format — Configcrates, roles, envoys.vgo, vars
- Resource DAG — depends_on, notify, subscribes
- Templates — Go template syntax and restrictions
- When Expressions — Conditional expressions and builtins
- Secrets — Secret providers and the secret: prefix
- Executors — All resource executors
- Nginx Configcrate Example — Full nginx walkthrough
- First Configcrate — Writing your first configcrate
Verified on Vigo 0.51.6 · 2026-05-13.
Confidential — Alexander4, LLC. Not for redistribution. See ../legal/license.md.