Learn › Salt Configuration Management › State Files
Create /srv/salt, write your first .sls state file, and apply it to a minion.
Everything you have sent to minion1 so far has been an order. Install this. Restart that. Set this grain. Each order ran, printed a result, and then existed nowhere except your shell history.
Now picture that same machine wiped and rebuilt tomorrow morning after a disk failure. The orders are gone with it. To get the machine back you would have to remember every command you ever aimed at it, in the right sequence, and type them all again correctly.
There is another way to work, and it is not a script. Instead of sending a machine a list of actions, you write down what the machine should look like and hand that description to Salt. This lesson is where you write your first one, in two lines of text.
The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. Every screen in this lesson was captured in one sitting on this exact lab, in the order you are about to walk through. The clock times printed on them run forward as you read. The lab user is saltops, the master is named saltmaster, and there is exactly one minion, named minion1.
There are two ways to use Salt, and the difference between them is the whole lesson.
Execution is what you have been doing. sudo salt 'minion1' pkg.install nginx performs one action, right now. It is imperative, which means you say exactly what to do. Salt does it, prints what happened, and the moment is over.
A state is a description. You write a file that says nginx should be installed on this machine, and Salt works out what to do about it. It is declarative, which means you describe the end result and leave the steps to the tool.
The naming convention carries the difference, and once you see it you cannot unsee it. Execution functions are present-tense verbs: pkg.install, service.start, file.copy. State functions are past tense: pkg.installed, service.running, file.managed. The past tense reads like a description of a machine that is already finished.
That is why production runs on states. A description is repeatable, because applying it twice is the same as applying it once. It is documentation, because the file says what the server is. And it goes in version control, so a change to a server becomes a change someone can review.
Salt shipped in 2011 as a remote execution tool. Thomas S. Hatch wrote it to get answers from a whole fleet in under a second, and for a while that was all it did. The state system came later, once it was clear that people were pasting the same handful of commands into the same machines over and over, mostly to rebuild something they had built before.
The file extension records that history. A state file ends in .sls, short for SaLt State. Inside, the format is YAML, a plain-text way of writing data that a person can read without knowing any programming language.
Declarative is the half that takes getting used to. In a shell script you own the order and the branching: check whether the package is there, and if it is not, install it. In a state file you write only the destination. The checking, the branching and the do-nothing case all belong to Salt. That is the trade. You give up control of the steps and get repeatability back in exchange.
Two engineers set up the same web server on Monday. The first runs sudo salt 'minion1' pkg.install nginx from the command line and watches it return. The second writes a state file and applies it. Both machines end Monday with nginx on them.
On Tuesday the machine is wiped and rebuilt from a blank image. Take a position before you read on.
>>> Only the second one. The command ran, returned its result, and that was the end of it. Salt does keep a record of every job on the master, so you can look up what a command returned months later, but a job record is history. It is not a description of a machine and nothing replays it for you. The state file is different in kind: it says what the machine should be, so it is still true after the machine is gone. If you picked the first answer, that is the assumption worth unlearning early, because nothing in Salt automatically re-runs old commands against a rebuilt machine. If you picked the third, notice which machine was rebuilt. The minion was wiped. The master, and the file root you are about to look inside, were never touched.
A state file only counts if it sits somewhere the master looks. That place is called the file root, and by default it is /srv/salt on the master.
On a master you build yourself, creating that directory is the first thing you do. On this box it already exists and the master's configuration already points at it, so your job is to look inside and see what you have been handed.
Use the long listing, ls -l, rather than a bare ls. The long form prints one line per entry with the permissions, the owner and the group in front of the name, and in a second you will need to know who owns this directory.
Before you press Enter, commit to an answer: is there a state file in there already, or are you starting from nothing?
ls -l /srv/salt
prompt: saltops@saltmaster:~$ answer: ls -l /srv/salt ||| ls -l /srv/salt/ output: total 4 drwxr-xr-x 2 saltops saltops 4096 Jul 31 09:59 files hint: The long listing is ls -l, and the file root is the path /srv/salt: ls -l /srv/salt
One entry, and it is not a state file. files is a directory, which the leading d on drwxr-xr-x tells you. There is no .sls in here at all, so this master currently describes nothing. That empty recipe book is the starting point, and having it in place is the first of the five things your lab grades.
Now read the two columns in the middle: saltops saltops is the owner and the group of that entry. That is you. The file root holding it belongs to saltops too, which this listing does not show you: ls -l prints what is inside a directory, and ls -ld /srv/salt is the form that prints the directory's own line. It matters more than it looks, because the very next thing you do is write a file in here, and a directory you own is a directory you can write to without sudo.
total 4 is just the disk space the listing covers, in kilobyte blocks. Ignore it.
You will meet the same path written a second way later in this track: salt://webserver.sls means the file webserver.sls inside the file root. The salt:// prefix is Salt's way of saying start at /srv/salt and work down from there.
Now create the state file. A heredoc is the shell's way of typing several lines into a file in one go: everything between the << 'EOF' marker and the closing EOF becomes the content of the file. Type it exactly as it appears, including the two spaces at the start of the second line:
cat > /srv/salt/webserver.sls << 'EOF'
nginx:
pkg.installed
EOF
No sudo anywhere in that command, and that is deliberate. You saw who owns /srv/salt a moment ago. On this box sudo is granted for the salt commands and nothing else, so a state file gets written the ordinary way, as yourself.
Two lines, two jobs:
nginx: is the state ID. It is a label you choose and it has to be unique inside the file. When you give the function no package name of its own, Salt uses the ID as the name, so this one label is doing double duty: it names the block and it names the package. pkg.installed is the state module and function. The pkg module manages packages. installed is the condition you want to be true. Past tense, exactly as promised.That is the whole file. If the ID and the package name ever need to differ, you add a parameter underneath the function, indented one more level, in the form - name: nginx. Here they are the same word, so there is nothing left to say.
YAML indentation is the number one source of Salt errors. Each level is exactly two spaces, and never a tab. pkg.installed sits two spaces in, under the ID. Get it wrong and Salt does not say the indentation is bad. It says the SLS failed to render, which sends most people hunting in the wrong place. Check your spacing first, every time.
A heredoc is silent. It prints nothing when it works and nothing when the shell swallowed a line, so the only honest way to know what is in that file is to read it back off the disk.
No command is shown for this one. You already know the tool that prints a file's contents to the screen; you met it in the very first foundations lessons. Point it at /srv/salt/webserver.sls and check two things in what comes back: that both lines are there, and that the second one starts two spaces in.
prompt: saltops@saltmaster:~$ answer: cat /srv/salt/webserver.sls output: nginx: pkg.installed hint: The tool that dumps a whole file to the screen is cat, and the file is the one you just wrote: cat /srv/salt/webserver.sls
Two lines, exactly as written. nginx: flush against the left margin, pkg.installed two spaces in underneath it. That indentation is the only thing tying the function to the ID, so it is worth the second look every single time.
There is now a .sls file in the file root that was not there when you ran ls -l, and that file is the second of the five things your lab grades. Read it back like this before every apply. Salt will not tell you that you meant three spaces until it fails to render, and by then you are debugging the wrong thing.
The file is written and saved on the master. You have not applied it. minion1 is connected, its key is accepted, and it is answering.
>>> Nothing new. A state file is inert. It is text in a directory until a command tells Salt to compile it and send the work out, and until then the minion has no idea it exists. If you picked the second answer, it is a fair guess. Some tools do work that way, with an agent that polls for changes, and Salt can be configured to apply states on a schedule too. It just does not do that by default. If you picked the third, imagine sleeping next to a tool where saving a file changes production the instant you hit write. The gap between writing and applying is deliberate, and it is where review and version control live.
One command applies a state file. state.apply takes the file name without the .sls on the end:
sudo salt 'minion1' state.apply webserver
It is the same three-part shape as every other Salt command. The target picks the machine, state is the module, apply is the function, and webserver is the argument. Salt finds /srv/salt/webserver.sls, compiles it into work, publishes that work to the target, and the minion does whatever is needed to make the description true.
That minion has never had a web server on it. Before you press Enter, commit to two numbers. Your file names one package, so how many things are about to change on that machine? And roughly how long should this take, compared to the commands you have run so far, which all came back in a blink?
prompt: saltops@saltmaster:~$ answer: sudo salt 'minion1' state.apply webserver ||| sudo salt "minion1" state.apply webserver ||| sudo salt minion1 state.apply webserver ||| sudo salt '*' state.apply webserver ||| sudo salt "*" state.apply webserver output: minion1: ---------- ID: nginx Function: pkg.installed Result: True Comment: The following packages were installed/updated: nginx Started: 10:02:16.356517 Duration: 9745.879 ms Changes: ---------- nginx: ---------- new: 1.22.1-9+deb12u9 old: nginx-common: ---------- new: 1.22.1-9+deb12u9 old:
Summary for minion1 ------------ Succeeded: 1 (changed=1) hint: Target the minion by name in quotes, then the module and function, then the file name without .sls: sudo salt 'minion1' state.apply webserver
Read it from the top. minion1: is the machine reporting back. The row of dashes opens one state block, and there is exactly one block here because your file held one state.
ID: nginx is the label you chose. Function: pkg.installed is what ran. Result: True is the only field that says whether it worked. Comment: is the human sentence Salt wrote for this exact case: The following packages were installed/updated: nginx. Software that was not on that machine is on it now.
Duration: 9745.879 ms is nearly ten seconds, which is why the command sat there. That is not Salt being slow. That is a package manager downloading and unpacking real software on the other end.
Then the part worth staring at. Under Changes: there are two entries, not one. nginx went from an empty old value to new: 1.22.1-9+deb12u9, and so did nginx-common, which you never mentioned anywhere. Your file named one package. The minion's package manager pulled in what that package depends on, and Salt reported every change it made, not only the one you asked for. That is what Changes is for: it is the record of what this run did to that machine.
Succeeded: 1 (changed=1) counts state blocks, not packages: one block ran, and one block had to change something. The screen above is trimmed at that line, and you will read the full count block underneath it on the next run. Running state.apply is the third of the five things your lab grades.
nginx is on the minion now. You change nothing about the file, and you send the exact same command a second time.
>>> It checks and leaves the machine alone. This property has a name worth learning: idempotency. Applying the same state twice leaves the machine exactly where applying it once did. That makes a state file safe to run on a timer, in a pipeline, or by a colleague who is not sure whether someone already ran it. If you picked the reinstall, that is the expensive answer. It is how a plain script behaves: the script does the work every time, because it has no idea what the machine already looks like. A state module checks the current state first and acts only when reality does not match the description. If you picked the refusal, that is an imperative habit talking. Nothing has failed here, so there is nothing to refuse. What all of this looks like on screen is the next thing you type.
Type it again, character for character. Nothing about the command changes, so watch what changes in the answer. Three fields are worth your attention, and you just read all three on the previous screen: Comment, Duration and Changes.
prompt: saltops@saltmaster:~$ answer: sudo salt 'minion1' state.apply webserver ||| sudo salt "minion1" state.apply webserver ||| sudo salt minion1 state.apply webserver ||| sudo salt '*' state.apply webserver ||| sudo salt "*" state.apply webserver output: minion1: ---------- ID: nginx Function: pkg.installed Result: True Comment: All specified packages are already installed Started: 10:02:27.201363 Duration: 23.399 ms Changes:
Summary for minion1 ------------ Succeeded: 1 Failed: 0 ------------ Total states run: 1 Total run time: 23.399 ms hint: You are re-running the apply from the previous step, unchanged: sudo salt 'minion1' state.apply webserver
Same command, same Result: True, and three fields moved.
Comment now reads All specified packages are already installed. That is Salt telling you it looked. Duration fell from 9745.879 ms to 23.399 ms, which is the difference between installing software and asking a package database one question. And Changes: has nothing under it at all.
That empty Changes is the most valuable line in configuration management. It means the machine already matched the description, so Salt touched nothing. When you apply a file across two hundred machines, the ones with an empty Changes are the ones you can stop thinking about, and the ones with content underneath are the story.
Now the summary block, in full this time. Succeeded: 1 with no (changed=1) after it: one block ran and it did not have to change anything. Failed: 0 is the number you are really looking for. Total states run: 1 is how many blocks your file had. Total run time: 23.399 ms adds up the durations, which with one block is just that block's duration again. On a real file with thirty blocks in it, this is the part you read first.
Your lab's fifth check wants two things true at once: a state file of your own sitting in the file root, and a state.apply in your history. You have now done both, twice. Reading the output is the part no grader can score for you, and the part that decides whether you trust the run.
Stop and count what changed. You wrote two lines of YAML on one machine, and a different machine you have never logged into now has a web server package on it that was not there when this lesson started.
The package is not the achievement. The file is. That file is now the definition of that machine. Hand it to a colleague, apply it to a hundred machines, or apply it to a rebuilt one tomorrow morning. You get the same result every time, because it describes an outcome instead of a sequence.
Every lesson after this one adds vocabulary to a file that looks exactly like the one you just wrote. Files, services, variables, templates, ordering. The shape on the page stays the same.
Scaffolding off. No command is printed for this step.
Salt reported that it installed nginx. That report is Salt describing its own work, and a tool grading its own homework is the weakest evidence there is. Make the machine answer for itself instead.
Ask minion1 to run a shell command that prints where the nginx program lives on disk, and read the path that comes back. You met the module that runs shell commands on a minion in the Modules and Functions lesson. Quote the shell command so your own shell leaves it alone.
prompt: saltops@
Practice First State in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.