I would use Terraform for anything that is provisioning and probably SaltStack for everything else.
There are some scenarios where there is no 'one tool for everything', like creating a complex OCI image, a VM image using Packer, a VM using Vagrant, configuring a network appliance or configuring an end-user workstation.
Example: super complex OCI images that attach to a CI pipeline would be better served by native packaging tools (like Jib for Java projects), but re-packaging of existing applications might depend on how green the build is. You could do a bunch of shell scripting in a Dockerfile, but once that gets big enough you'll start to need to use functions and perhaps even function libraries that you source in, but at that point you're doing a re-invention of something like Ansible and SaltStack, so depending on existing modules, knowledge, time, experience etc. you might use those instead.
Compare that to non-packaged-deliverable things like network switches and routers; those tend to be long-lived (non-ephemeral) systems, and they tend to have various inconsistent APIs between vendors, models and generations. But you may want to exchange configuration inputs and outputs with other systems so your Cloudflare rules, AWS ALBs, AWS SGs, on-prem Palo Alto firewall and on-prem Cisco switch agree on the configuration, and in such a way that it can be validated and audited constantly and consistently. Not a single named vendor does it all, especially not in a consistent and useful way. But as long as there are providers for Terraform, you can (with Terraform).
Perhaps the best way to describe how I balance it (or try to) is: the more modern the API, the more declarative I'd want to manage it in code. Cloud APIs tend to sit on the most-declarative end of the spectrum and end-user workstations on the least-declarative (or: most-imperative) end.
There are some scenarios where there is no 'one tool for everything', like creating a complex OCI image, a VM image using Packer, a VM using Vagrant, configuring a network appliance or configuring an end-user workstation.
Example: super complex OCI images that attach to a CI pipeline would be better served by native packaging tools (like Jib for Java projects), but re-packaging of existing applications might depend on how green the build is. You could do a bunch of shell scripting in a Dockerfile, but once that gets big enough you'll start to need to use functions and perhaps even function libraries that you source in, but at that point you're doing a re-invention of something like Ansible and SaltStack, so depending on existing modules, knowledge, time, experience etc. you might use those instead.
Compare that to non-packaged-deliverable things like network switches and routers; those tend to be long-lived (non-ephemeral) systems, and they tend to have various inconsistent APIs between vendors, models and generations. But you may want to exchange configuration inputs and outputs with other systems so your Cloudflare rules, AWS ALBs, AWS SGs, on-prem Palo Alto firewall and on-prem Cisco switch agree on the configuration, and in such a way that it can be validated and audited constantly and consistently. Not a single named vendor does it all, especially not in a consistent and useful way. But as long as there are providers for Terraform, you can (with Terraform).
Perhaps the best way to describe how I balance it (or try to) is: the more modern the API, the more declarative I'd want to manage it in code. Cloud APIs tend to sit on the most-declarative end of the spectrum and end-user workstations on the least-declarative (or: most-imperative) end.