My experience trying to manage longer lived systems like robot computers over time with Ansible has been that it quickly becomes a nightmare as your playbook grows cruft to try to account for the various states the target may be coming from.
My experience trying to manage longer lived systems like robot computers over time with Ansible has been that it quickly becomes a nightmare as your playbook grows cruft to try to account for the various states the target may be coming from.
I still use it as it allows more portability across OS releases and families (as in easier migration), but it also increases the complexity when creating a new task/role/playbook.
In that sense, Dockerfiles with shell-based RUN commands are much easier to manage.
Another advantage of config management over shell might be a better integration with the underlying cloud provider. For instance Ansible supports AWS SSM parameter store, which allows me to use dynamic definitions of some configuration data (RDS database endpoints, for instance) or secrets (no need for Ansible vault)
Even in the run-once case, though, I think there's a benefit to Ansible's role-based approach to modularization. And again for the dev scenario, it's much easier to run only portions of a playbook than it is to run portions of a shell script.
And finally, the diagnostics and overall failure story are obviously way better for Ansible, too.
Now, all this said, I do still go back and forth. For example, literally right now in another window I'm working a small wrapper that prepares clean environments to build patched Ubuntu kernels in— and it's all just debootstrap, systemd-nspawn, and a bunch of shell script glue.