Salt Vagrant - automatically provision one master and two slaves

Configuration management let's you control hundreds of computers. You can even configure machines that don't exist yet!

In this article, you'll use ready-made network of three virtual computers to play with Salt, a popular configuration management system.

These notes have not been extensively tested yet.

Background & Prerequisites

I cleaned up this configuration from a setup I've used for a long time on Linux. You can probably adapt it to Windows, and I think some of my students have.

These notes are pretty dense. As prerequisite, you should know Linux command line and directory structure. Here, we prepare to administer a lot of computers. It would help if you could already administer one.

Install Virtualization Environment

$ sudo apt-get update
$ sudo apt-get -y install virtualbox vagrant micro

$ mkdir saltdemo; cd saltdemo
$ micro Vagrantfile

Copy-paste the ready-made Vagrantfile in place.

Ready made Vagrantfile for three computers

# -*- mode: ruby -*-
# vi: set ft=ruby :
# Copyright 2014-2023 Tero Karvinen

$minion = <<MINION
sudo apt-get update
sudo apt-get -qy install salt-minion
echo "master:">/etc/salt/minion
sudo service salt-minion restart
echo "See also:"

$master = <<MASTER
sudo apt-get update
sudo apt-get -qy install salt-master
echo "See also:"

Vagrant.configure("2") do |config| = "debian/bullseye64"

	config.vm.define "t001" do |t001|
		t001.vm.provision :shell, inline: $minion "private_network", ip: ""
		t001.vm.hostname = "t001"

	config.vm.define "t002" do |t002|
		t002.vm.provision :shell, inline: $minion "private_network", ip: ""
		t002.vm.hostname = "t002"

	config.vm.define "tmaster", primary: true do |tmaster|
		tmaster.vm.provision :shell, inline: $master "private_network", ip: ""
		tmaster.vm.hostname = "tmaster"

Run Three Computers

$ vagrant up

It takes 3-5 minutes, and the three computers boot up. The slaves t001 and t002 will automatically contact master, because the slave daemon (salt-minion) was installed and configured with a three-line script in Vagrantfile.

Accept the Slaves

Log into your master computer.

$ vagrant ssh tmaster

The salt commands below are run inside the virtual machine tmaster. It's prompt is very long, so I've replaced it with "$". But it's a different computer from your host OS.

The slave computers have already sent their keys to master. Just approve them

$ sudo salt-key -A
The following keys are going to be accepted:
Unaccepted Keys:
Proceed? [n/Y] y
Key for minion t001 accepted.
Key for minion t002 accepted.

And test that the connection works. This command contacts slaves using the secured channel created by Salt.

$ sudo salt '*'

Command Slaves

You can run regular shell commands

$ sudo salt '*' 'hostname -I'

Collect Information

$ sudo salt '*' grains.items
$ sudo salt '*' grains.item osfinger ipv4

The Goal - Idempotent

Idempotent commands are much more powerful. They just describe the end state. Salt will only make changes if needed. These idempotent commands are the ones we'll mostly use later.

$ sudo salt '*' state.single file.managed '/tmp/see-you-at-terokarvinen-com'
Succeeded: 1 (changed=1)
Failed:    0

If you run it many times, you'll see that it's really idempotent. In the following rounds, the "(changed=1)" will disappear.

To make success messages shorter, you can use --state-output=terse

$ sudo salt --state-output=terse '*' state.single file.managed '/tmp/see-you-at-terokarvinen-com'

Install some software

$ sudo salt '*' state.single pkg.installed apache2

Make sure a daemon is running

$ sudo salt '*' state.single service.running apache2

Let's test that it's actually running

$ sudo apt-get -y install curl
$ curl -s|grep title
<title>Apache2 Debian Default Page: It works</title>

It works! So let's shut it down

$ sudo salt '*' state.single service.dead apache2
$ curl
curl: (7) Failed to connect to port 80: Connection refused

We can control users

$ sudo salt '*' state.single user.present terote01

Modify users

$ sudo salt '*' state.single user.present terote01 shell="/bin/bash"

And make sure they don't exist

$ sudo salt '*' state.single user.absent terote01

If none of the others work, we can use state. But we must make it idempotent ourselves.

$ sudo salt '*' state.single 'touch /tmp/tero' creates="/tmp/tero"

You should prefer these to package, file, service, user.

Infra as Code - Your wishes as a text file

$ sudo mkdir -p /srv/salt/hello
$ sudoedit /srv/salt/hello/init.sls

Write these contents to init.sls. Note that this syntax is YAML. Indentation matters, and its two spaces. No tabs.

$ cat /srv/salt/hello/init.sls

$ sudo salt '*' state.apply hello

top.sls - What Slave Runs What States

Top file defines what states are run for which slaves.

$ sudo salt '*' state.apply hello^C
$ sudoedit /srv/salt/top.sls
$ cat /srv/salt/top.sls
    - hello

Now you don't have to name any modules on state.apply:

$ sudo salt '*' state.apply

Bye bye

We have cattle not pets. When it's time to say goodbye to this installation, you can destroy the machines and their contents. There is no undo.

$ exit
$ vagrant destroy	# destroys all files in all three virtual computers