Skip to content

Inventory

An inventory lists the machines a recipe runs on, groups them, and gives groups and hosts their own vars and connection options. It is a file named hosts.rb next to the recipe (or any file passed with --inventory), and like a recipe it is plain Ruby: a static list is a few host lines, and a dynamic inventory is code that calls host in a loop over data from a file or a cloud provider's CLI. There is no plugin system to learn.

Minimal example

examples/08-multi-role-inventory/hosts.rb
# Three servers in two roles. The names are placeholders: replace them with
# yours, or point them at real machines in ~/.ssh/config.
group :web do
  host "web1.example.com", user: "deploy", sudo: true
  host "web2.example.com", user: "deploy", sudo: true, vars: { workers: 8 }
  vars workers: 4, server_name: "www.example.com"
end

group :db do
  # sshd listens on a non-standard port here.
  host "db1.example.com", user: "deploy", port: 2222, sudo: true
  vars backup_hour: 3
end

group :all, include: [:web, :db] do
  vars env: "production"
end

sadame hosts shows what sadame makes of it: every host, how it will be reached, and each var with the layer it came from:

$ sadame hosts
sadame 0.5.1 hosts: 3 hosts from hosts.rb

web1.example.com
  address   web1.example.com
  user      deploy
  port      (ssh default)
  sudo      yes
  ssh_opts  (none)
  groups    web, all
  vars
    env          "production"       group all
    workers      4                  group web
    server_name  "www.example.com"  group web
    backup_hour  1                  vars.rb

web2.example.com
  address   web2.example.com
  user      deploy
  port      (ssh default)
  sudo      yes
  ssh_opts  (none)
  groups    web, all
  vars
    env          "production"       group all
    workers      8                  host
    server_name  "www.example.com"  group web
    backup_hour  1                  vars.rb

db1.example.com
  address   db1.example.com
  user      deploy
  port      2222
  sudo      yes
  ssh_opts  (none)
  groups    db, all
  vars
    env          "production"  group all
    workers      2             vars.rb
    server_name  "localhost"   vars.rb
    backup_hour  3             group db

The hosts.rb language

Method Where Meaning
group :name do ... end top level Declares a group, or adds to one declared earlier. Groups cannot be nested.
group :name, include: [:a, :b] top level The group also contains every host of groups a and b. The block is optional.
host "name", options in a group, or top level for an ungrouped host Declares a host. A host may appear in several groups.
vars key: value, ... in a group Vars for every host of the group.
command_output("command") anywhere Runs a command on the machine running sadame and returns its output; fails when the command fails.

JSON.parse and File.read are available, as in recipes.

Options of host:

Option Type Default Meaning
address String the host's name Address to connect to, when the name is only a label.
user String ssh's default (~/.ssh/config or your user name) User to log in as.
port Integer ssh's default (~/.ssh/config or 22) SSH port.
sudo true or false false Run sadame with sudo on this host.
ssh_opts Array of Strings [] Extra options for ssh and scp, such as ["-i", "~/.ssh/deploy"] or ["-F", "ssh_config"].
vars Hash {} Vars for this host only.

Connection options may be given in any one declaration of a host. Giving the same option two different values is an error; vars from several declarations are merged.

Vars precedence

Every target gets one vars Hash, merged from these layers. A higher layer wins for the same key:

Precedence Layer Shown by sadame hosts as
1 (highest) --var key=value on the command line --var
2 vars: of the host host
3 vars of the host's groups; a group overrides the groups it includes, and otherwise a group declared later overrides one declared earlier group NAME
4 (lowest) vars.rb next to the recipe vars.rb

In the example, workers is 2 in vars.rb, 4 for the web group and 8 for web2; env is "staging" in vars.rb but "production" from all, which includes web and db. --var overrides all of them:

$ sadame hosts --limit web2.example.com --var workers=16
sadame 0.5.1 hosts: 1 host from hosts.rb

web2.example.com
  address   web2.example.com
  user      deploy
  port      (ssh default)
  sudo      yes
  ssh_opts  (none)
  groups    web, all
  vars
    env          "production"       group all
    workers      16                 --var
    server_name  "www.example.com"  group web
    backup_hour  1                  vars.rb

Selecting hosts

Without -h, -g or --limit, sadame runs on the machine it is started on. With any of them, it runs on the selected hosts over SSH (see Remote hosts).

Flag Selects
-g web,db the hosts of these groups, including groups they include
-h web1.example.com,admin@db9.example.com:2200 these hosts; a name found in the inventory keeps its options, with user and port from the flag when given; any other name works too
--limit "web*" only the selected hosts whose name matches the pattern (* any characters, ? one); alone, it filters every host of the inventory

Flags combine: -g db -h web1.example.com selects both, and -g all --limit "web*" narrows all down to the web servers. Hosts are taken in the order of the groups given, then the -h hosts; each runs once. sadame hosts accepts the same flags, which makes it the way to check a selection before running anything.

Using groups in recipes

A recipe learns the groups of the host it runs on from host_groups, a list of symbols that includes groups reached through include:. The example's site.rb picks roles that way:

examples/08-multi-role-inventory/site.rb
# Every host gets the base below; the roles follow the host's groups in
# hosts.rb. Run it on all of them with: sadame apply site.rb -g all
file "/etc/motd", template: "motd.erb", locals: { name: host_name, groups: host_groups }

# Each role's resources are tagged with the role, so --tags web plans or
# applies only the web role, without the base.
include_recipe "roles/web", tags: [:web] if host_groups.include?(:web)
include_recipe "roles/db", tags: [:db] if host_groups.include?(:db)

host_name is the host's name from the inventory or -h. For local runs they are "local" and [], unless set with --host-name and --host-groups, which is a way to try a role on a test machine:

sudo sadame plan site.rb --host-groups web,all

Dynamic inventories

From a file

examples/11-dynamic-inventory/hosts.rb
# A dynamic inventory: hosts.rb is Ruby, so it can build the host list from
# any data. This one reads servers.json, which another tool might write.
# Paths are relative to where you run sadame, so build them from __FILE__.
servers = JSON.parse(File.read(File.join(File.dirname(__FILE__), "servers.json")))

servers.group_by { |server| server["role"] }.each do |role, members|
  group role.to_sym do
    members.each do |server|
      options = { address: server["address"], user: "deploy" }
      options[:port] = server["port"] if server["port"]
      host server["name"], options
    end
  end
end

group :all, include: [:web, :db] do
  vars env: "production"
end
examples/11-dynamic-inventory/servers.json
[
  { "name": "web1.example.com", "address": "203.0.113.11", "role": "web" },
  { "name": "web2.example.com", "address": "203.0.113.12", "role": "web" },
  { "name": "db1.example.com", "address": "203.0.113.21", "role": "db", "port": 2222 }
]
$ sadame hosts -g db
sadame 0.5.1 hosts: 1 host from hosts.rb

db1.example.com
  address   203.0.113.21
  user      deploy
  port      2222
  sudo      no
  ssh_opts  (none)
  groups    db, all
  vars
    env  "production"  group all

From a cloud provider's CLI

command_output runs the provider's CLI on the machine running sadame, with whatever login that CLI already has. These three files are in the example directory; pass one with --inventory.

examples/11-dynamic-inventory/hosts-hcloud.rb
# Hetzner Cloud: every server, grouped by its "role" label.
#   sadame hosts --inventory hosts-hcloud.rb
servers = JSON.parse(command_output("hcloud server list -o json"))

servers.each do |server|
  role = server["labels"]["role"] || "unassigned"
  group role.to_sym do
    host server["name"], address: server["public_net"]["ipv4"]["ip"], user: "root"
  end
end
examples/11-dynamic-inventory/hosts-doctl.rb
# DigitalOcean: every Droplet, in one group per tag.
#   sadame hosts --inventory hosts-doctl.rb
droplets = JSON.parse(command_output("doctl compute droplet list --output json"))

droplets.each do |droplet|
  public_ip = droplet["networks"]["v4"].find { |network| network["type"] == "public" }
  next if public_ip.nil?

  droplet["tags"].each do |tag|
    group tag.to_sym do
      host droplet["name"], address: public_ip["ip_address"], user: "root"
    end
  end
end
examples/11-dynamic-inventory/hosts-aws.rb
# AWS EC2: running instances, named and grouped by their Name and Role tags.
#   sadame hosts --inventory hosts-aws.rb
reply = JSON.parse(command_output("aws ec2 describe-instances " \
                                  "--filters Name=instance-state-name,Values=running --output json"))

reply["Reservations"].each do |reservation|
  reservation["Instances"].each do |instance|
    tags = {}
    (instance["Tags"] || []).each { |tag| tags[tag["Key"]] = tag["Value"] }
    next if instance["PublicIpAddress"].nil?

    role = (tags["Role"] || "unassigned").to_sym
    group role do
      host tags["Name"] || instance["InstanceId"], address: instance["PublicIpAddress"], user: "ec2-user"
    end
  end
end

The tests run each of them against recorded CLI output, so a change in sadame that breaks them is noticed. A change in a CLI's output format is not; check sadame hosts --inventory ... after upgrading the CLI.

Common mistakes

  • Relative paths in hosts.rb. File.read("servers.json") is relative to the directory you run sadame from. Build paths from File.dirname(__FILE__), as the example does.
  • vars outside a group. Top-level vars is an error; use vars.rb for defaults and host ..., vars: {} for one host.
  • Expecting -g all without a group named all. Groups are only what hosts.rb declares. sadame hosts without flags lists every host.
  • Slow commands in a dynamic inventory. command_output runs every time the inventory loads, including for plan. Cache the result in a file if the CLI is slow.
  • Secrets in hosts.rb. It is evaluated on the machine running sadame, and its vars are sent to the targets. Read secrets from the environment (ENV["DB_PASSWORD"]) rather than committing them.
  • Facts in hosts.rb. For the same reason, hosts.rb cannot see what a machine has. Read facts in the recipe, which runs on the target, and keep in hosts.rb what you decide.

See also