Learn › Salt Configuration Management › State Files
Use file.managed to deploy configuration files from the Salt master to minions.
There is a file on minion1 called /etc/motd. It holds seven lines of boilerplate that came with the Debian image. Nobody has ever edited it. You are about to replace every one of those lines from where you are sitting, and you are not going to open a session on that machine to do it.
Copying a file is not the interesting part. Any tool can copy a file. The interesting part is what Salt hands back afterwards. It does not say done. It shows you, line by line, what that file used to say and what it says now, in the minion's own words.
By the end of this page a directory will also exist on that machine that does not exist right now.
The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. Both screens in this lesson were captured from a real Salt master with one real minion attached, which is exactly the machine your lab boots. The lab user is saltops, the master is named saltmaster, and there is exactly one minion, named minion1. Timestamps and durations came from that capture, so your own numbers will differ.
In the last lesson you made a package appear on a minion by describing the end result instead of typing the steps. A state is that description, written in a file. file.managed is the state that describes a file.
It makes one promise: this path on the minion holds this content. Salt keeps the promise whichever way it has to.
That is why the naming is past tense. file.managed is not an order to copy something. It is a statement about how the world should look, and Salt closes whatever gap it finds.
Every file state has two halves in two different places. The source is content sitting on the master, where you can read it, edit it and keep it in version control. The destination is a path on the minion. Your job is to write down both, once.
Thomas S. Hatch released Salt in 2011. Pushing commands to a fleet was the headline, but he ran into the same wall everyone does about a week in: commands are not enough. Real servers need content. Configuration files, certificates, scripts. The tools of the day answered that with a second machine, an HTTP server or an rsync host, plus another set of credentials to look after.
So he built a small file server into the master itself. The directories listed under file_roots are published to the minions, and on this lab box that is /srv/salt, the same directory your state files live in.
Content in the file root gets its own address scheme, written like a web URL: salt://. It always means the master's file root and nothing else. salt://motd is the file named motd in /srv/salt. The minion pulls that content down over the connection it already holds to the master, so there is no second service to run and no second password to manage.
Order matters here. Salt cannot ship content it does not have, so the source file comes first. Write it into the file root:
cat > /srv/salt/motd << 'EOF'
Managed by Salt
EOF
Read that command in two pieces. cat > /srv/salt/motd opens the file for writing. << 'EOF' says take everything up to the next line that reads EOF and treat it as the text to write. The quotes around EOF matter: they tell the shell to copy your text literally, without touching anything that looks like a variable.
One line of content, Managed by Salt. That is the whole file. Nothing about it is special yet; it is an ordinary file in an ordinary directory. What makes it Salt content is the directory it sits in.
answer: /srv/salt/motd hint: The salt:// prefix means the master's file root, which on this box is /srv/salt. Add the file name to it.
Now the description. State files are YAML, they end in .sls, and they live in the same file root:
cat > /srv/salt/motd.sls << 'EOF'
/etc/motd:
file.managed:
- source: salt://motd
- user: root
- group: root
- mode: "0644"
EOF
Four things are going on in those six lines.
/etc/motd: is the state ID, the label for this block of work. You can put any label you like there. Put a path there and Salt uses it as the destination as well, which saves you a line. This is the shape you will see most often in the wild.file.managed: is the module and the function. file is the part of Salt that deals with files. managed is the promise described above.- source: salt://motd is where the content comes from: the file you just wrote. The leading dash and space mark a list item in YAML.- user: root, - group: root and - mode: describe who owns the file on the minion and what its permissions are. Salt sets those too, not just the bytes.Always put quotes around the mode. Write mode: "0644", never mode: 0644. YAML reads a bare 0644 as a number rather than as the four characters you typed, and the leading zero does not survive the trip. Quote it and Salt receives exactly what you wrote. This one is the most common beginner bug in Salt state files, and the error it causes shows up much later, as a file with permissions nobody asked for.
answer: mode: "0644"|||mode: '0644'|||- mode: "0644"|||- mode: '0644' hint: The key, a colon, then the four digits wrapped in quotes.
Both files are on the master. Nothing has reached the minion yet. Before you send anything, take a position on what comes back.
>>> A diff. Salt reports a file change the way a version control system does, minus for what left and plus for what arrived, and it prints it in the return you are already reading. If you picked the second answer, the minion does keep a log. But a return that only says True sends you off to read logs on every machine you touched, which defeats the point of working from one place. If you picked the third, think about scale for a second: a state that rewrites a four thousand line configuration file on two hundred machines would bury your terminal. A diff prints only what moved, so a run that changes nothing prints nothing.
Every Salt command keeps the same three-part shape you already know: sudo salt, then the target in quotes, then the work. The work here is state.apply followed by the name of the state file without the .sls on the end. Salt finds /srv/salt/motd.sls, reads it, and hands the job to the minion.
sudo salt 'minion1' state.apply motd
prompt: saltops@saltmaster:~$ answer: sudo salt 'minion1' state.apply motd|||sudo salt "minion1" state.apply motd|||sudo salt minion1 state.apply motd|||sudo salt 'minion1' state.sls motd|||sudo salt '*' state.apply motd output: minion1: ---------- ID: /etc/motd Function: file.managed Result: True Comment: File /etc/motd updated Started: 09:10:03.473828 Duration: 7.782 ms Changes: ---------- diff: --- +++ @@ -1,7 +1 @@ - -The programs included with the Debian GNU/Linux system are free software; -the exact distribution terms for each program are described in the -individual files in /usr/share/doc/*/copyright. - -Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent -permitted by applicable law. +Managed by Salt
Summary for minion1 ------------ Succeeded: 1 (changed=1) Failed: 0 ------------ Total states run: 1 Total run time: 7.782 ms hint: Target first, then the function, then the state file name with no .sls: sudo salt 'minion1' state.apply motd
Read it in three bands.
The header. minion1: names who is talking, then a row of dashes opens one block per state that ran. ID: /etc/motd is the label you wrote. Function: file.managed is what ran. Result: True is the verdict. Comment: File /etc/motd updated is Salt's own sentence about it. Notice what is missing: there is no Name: line. Salt prints one only when the destination differs from the ID, and you put the path in the ID, so there was nothing extra to print.
The changes. This is the band that teaches. @@ -1,7 +1 @@ says the old file was 7 lines starting at line 1, and the new one is 1 line. Every line below it starting with a minus left the file. The single line starting with a plus arrived: +Managed by Salt. You are not reading Salt's opinion of what it did. You are reading the minion's before and after. It is still Salt reporting on Salt, though, so the next step is to make the machine answer for itself.
The summary. Succeeded: 1 (changed=1) counts states that worked and, in the brackets, states that actually changed something. Those two numbers are different questions and the gap between them is the useful signal. Failed: 0 is the one you scan for first on a bad day. Started and Duration are per state, so you can see which part of a long run cost you the time.
Salt just told you the file changed. Salt is also the thing that changed it. Before you trust any tool's report of its own work, ask the machine directly.
cmd.run runs a plain shell command on the minion and hands you back what it printed. The command that reads a file is cat, so cat /etc/motd is the read-back you want. Same three parts as always: sudo salt, the target in quotes, then the work.
answer: sudo salt 'minion1' cmd.run 'cat /etc/motd'|||sudo salt "minion1" cmd.run 'cat /etc/motd'|||sudo salt 'minion1' cmd.run "cat /etc/motd"|||sudo salt "minion1" cmd.run "cat /etc/motd"|||sudo salt minion1 cmd.run 'cat /etc/motd' hint: The module is cmd and the function is run, with the shell command in quotes: sudo salt 'minion1' cmd.run 'cat /etc/motd'
What comes back is the single line you wrote on the master, Managed by Salt, read off the minion's own disk. That is the difference between a report and a proof, and the lab scores it as one of its five tasks.
Count what changed. Everything you have sent minion1 until now was an instruction to run: ping, report a grain, install a package. This was different. You moved content.
A file that lives on your master, that you can read, edit, review and keep in version control, is now the file that lives on that machine. That is configuration management in one sentence, and the rest of Salt is variations on it.
Nothing on the minion has changed since the run finished. You press the up arrow and send the identical command a second time.
>>> It checks first. A state names a destination, not a step, so the first thing Salt does is compare what is there against what you asked for. When the two already agree there is nothing to close, and Salt's contract is that a run with nothing left to do still reports success and reports no changes. The property has a name worth knowing: idempotency. It is what makes states safe to run on a schedule and safe to run when you are not sure whether someone else already ran them. If you picked the second answer, that is exactly the behaviour states were invented to get away from, and it is why the function is managed and not copy. If you picked the third, Salt keeps no memory of past runs on the minion. It looks at the machine in front of it, every time.
Not every file state is about content. Sometimes a service just needs somewhere to put things, and the directory has to exist before anything else will work.
file.directory makes that promise: this path on the minion is a directory. Write a second state file for it:
cat > /srv/salt/webdir.sls << 'EOF'
/srv/www:
file.directory:
- user: root
- group: root
- mode: "0755"
- makedirs: True
EOF
Same shape as before, and one thing missing. The ID is the path again, file.directory replaces file.managed, and ownership and mode work the same way. There is no - source: line, because there is no content to fetch. A directory is not made of anything.
- makedirs: True is the new setting. It is the state file version of mkdir -p: create any parent directories that are missing on the way down.
That last setting looks like a detail until the day it is not. Take a position on it before you move on.
>>> It fails, and the comment on the failed state points at the missing parent. Without makedirs, file.directory will create exactly one level and no more. That is deliberate rather than lazy: a typo in a long path would otherwise build a whole tree of empty directories nobody asked for, quietly, on every machine you targeted. If you picked the second answer, Salt would rather stop and tell you than guess. Add - makedirs: True and it will build the parents, but only because you said so. If you picked the third, note that Salt never moves your path somewhere else. The path you write is the path you get, or you get an error.
Scaffolding off. Nothing is shown from here on.
Your next job on this master is an nginx configuration. You have already put the source file in the file root, at /srv/salt/nginx.conf, and you are writing the file.managed block that will carry it to the minion. Every other line is done. The one line left is the one that tells Salt where to fetch the content from.
answer: - source: salt://nginx.conf|||source: salt://nginx.conf hint: Same address scheme as the last state file, pointed at the new file name in the master's file root.
Still no command shown.
minion1 needs /srv/www to exist. You wrote that state a few minutes ago and saved it as /srv/salt/webdir.sls. So far it has done nothing at all. A state file sitting on the master is just a file until you put it to work, so put it to work on the one minion this master trusts.
prompt: saltops@saltmaster:~$ answer: sudo salt 'minion1' state.apply webdir|||sudo salt "minion1" state.apply webdir|||sudo salt minion1 state.apply webdir|||sudo salt 'minion1' state.sls webdir|||sudo salt '*' state.apply webdir output: minion1: ---------- ID: /srv/www Function: file.directory Result: True Comment: Started: 09:10:04.542262 Duration: 3.261 ms Changes: ---------- /srv/www: ---------- directory: new
Summary for minion1 ------------ Succeeded: 1 (changed=1) Failed: 0 ------------ hint: Same three parts as the last apply, with the other state file's name and no .sls.
directory: then new. That is the whole change, and it is a different shape from the last one on purpose. A file state reports a diff because a file has contents that can differ. A directory state reports what it made, because a directory either is there or is not.
Look at the Comment: field: it is empty. Salt writes a sentence there when it has something to add beyond the change itself, and here it did not. An empty comment next to Result: True is not a problem, it is a state that had nothing to explain.
This capture stops at the second dashed rule, so the totals rows that closed the first screen are simply not shown here.
You moved content onto a machine you never logged into, and then made a directory appear on it. Here is every form this lesson put in your hands:
| Form | What it does |
|---|---|
cat > /srv/salt/motd << 'EOF' | Write source content into the master's file root |
cat > /srv/salt/motd.sls << 'EOF' | Write the state file that carries it |
file.managed | This path on the minion holds this content |
- source: salt://motd | Fetch that content from /srv/salt/motd on the master |
- user: / - group: | Set ownership of the file on the minion |
- mode: "0644" | Set permissions, always quoted |
sudo salt 'minion1' state.apply motd | Apply one state file by name, with no .sls |
sudo salt 'minion1' cmd.run 'cat /etc/motd' | Make the minion read the deployed file back to you |
file.directory | This path on the minion is a directory |
- makedirs: True | Create the missing parents on the way down |
sudo salt 'minion1' state.apply webdir | Apply the directory state |
And the four facts underneath them:
salt:// means the master's file root, which on this box is /srv/salt.Name: line only when the two differ.Result and Changes answer two different questions, and reading them as one is the mistake to avoid. Result says whether the state worked. Changes says whether anything on the machine actually moved. True with an empty Changes block is the quietest good news in Salt: the machine was already right.
The practice terminal walked you through the whole loop. You read the two writes off the page, the source file and the state file. Then you typed the parts that matter yourself: the path salt://motd resolves to, the quoted mode line, and the apply that put the state to work. You also typed the read-back that made the minion prove it, and the apply that made a directory appear.
The lab is that same loop on a real Salt master with one real minion attached. It scores five things, and this lesson has shown you every one. A source file in the master's file root, a file.managed state written, the state applied, the deployed file read back o
Practice File States in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.