A domain-specific language makes a big difference in the amount of code you write. For example, you could argue that there's not much difference between:
chmod 640 /my/file
and
file { "/my/file":
mode => 640,
}
but there's a great deal of difference between these:
FILE=/my/file
chmod 640 $FILE
chown foo $FILE
chgrp bar $FILE
wget -O $FILE "http://my.puppet.server/dist/$FILE"
# where the URL contains "Hello world"
and
file { "/my/file":
mode => 640,
owner => foo,
group => bar,
content => "Hello world",
}
What happens if the wget fails? How will your script handle that? And what happens if there's something after that in your script that requires $FILE to be there with the correct contents?
You might argue that one could just put echo "Hello world" > $FILE
in the script, except that in the first example the script must be run on the client, whereas puppet compiles all of this on the server. So if you change the content, you only have to change it on the server and it changes it for as many systems as you want to put it on. And puppet handles dependencies and transfer problems for you automatically.
There's just no comparison - proper configuration management tools save you time and complexity. The more you try to do, the more shell scripts seem inadequate, and the more effort you will save by doing it with puppet.