The main difference between shell scripts and Ansible playbooks is that shell scripts are imperative, while playbooks are declarative and idempotent.
A shell script will be something like
install this package
create this file
change this line in a configuration file
While a playbook is like
make sure this package is installed
make sure this file exists
make sure this line is like this in this config file
What happens if you want change something in your configuration script? In most cases you can't just replay a shell script, because it will try to do a lot of things that are already done and it is very hard to write idempotent commands.
Ansible on the other hand only performs the tasks in the playbook that need to be performed: if the condition is already satisfied, there's nothing to do.
This is especially convenient if your script fails at some point: you can fix the directive and replay the playbook, and it will restart from where it left off. With shell scripts you have to do it manually (comment out the first part?), which is error-prone and time-consuming.
I will, however, offer that a well-designed shell script should be able to be run many times in a row without causing any problems. The above script is an example of this.
If I wanted to change something in my configuration script, that's totally fine! In fact, I do change it all the time. Since all it does is ask you whether you want homebrew to install something, there is very little risk (if any) of something "going wrong."
To do the same thing using pure shell scripting, you have to write a large number of helper functions to avoid boilerplate. Things like asserting — in an idempotent way — the existence of files (with the right content, the right ownership and mode flags, etc.), services, packages and so on.
Tools like Ansible and Puppet already provide that set of functionality. If you write it yourself, you pretty much end up with something like Ansible, except it's specific only to your use case. Better to focus on commonality.
I'm a Puppet guy myself (although I appreciate the simplicity Ansible can bring to the table), and make extensive use not just of the primitives, but of the ability to bundle primitives as reusable modules. For example, rather than explicitly putting files in /etc/logrotate.d, I define a "logrotate class" and do:
However, this is not merely a macro that is expanded in-place. Rather, it's an object which can be referenced (for example, I can have something else which requires that the object Logrotate::Rotation[postgresql] runs first) as well as "inventoried" (I can ask the system about all the log rotations that have been declared, and use that to drive a UI, for example).
The expressiveness and flexibility of tools like Ansible (which must have something similar) and Puppet is best seen in a multi-node server environment, however.
A shell script will be something like
While a playbook is like What happens if you want change something in your configuration script? In most cases you can't just replay a shell script, because it will try to do a lot of things that are already done and it is very hard to write idempotent commands.Ansible on the other hand only performs the tasks in the playbook that need to be performed: if the condition is already satisfied, there's nothing to do.
This is especially convenient if your script fails at some point: you can fix the directive and replay the playbook, and it will restart from where it left off. With shell scripts you have to do it manually (comment out the first part?), which is error-prone and time-consuming.