My goals are to
1. Maintain a single source of truth for each environment. I want to deploy an entire environment as an artifact. I don't roll back single services, I roll back environments. This minimizes the danger that I ever deploy an environmental state that hasn't been integration tested.
2. Maintain an easy API for devs to use. I like shift left things as much as the next person... but I find devs to be less than enthusiastic about learning tools like helm, kustomize, kubectl, etc.
The way I've solved this is by having org wide helm charts that the DevOps team maintains. Each microservice uses that chart. The API to the charts are tailored to maintain a good balance of extensibility/simplicity. The helm charts make certain assumptions like we'll be using Istio, external dns will provision IP addresses, etc. These assumptions help to simplify the API that devs need to use considerably, and it keeps their implementations DRY and easily refactorable.
From there I have an aggregate repo where my CI/CD pipelines put the generated helm values files for every deployment in a directory structure like: environment > service (I'm mainly just updating image tags, the values files don't change much). Then I use Argo to deploy everything. As a side note, when Argo deploys helm templates, it just uses helm as a manifest generator, it doesn't use helm's state tracking etc. It pretty much just runs a
helm template . > manifests.yaml
to generate the manifests and ignores the rest.
Finally, in the repo I have documentation/scripts and such for manual deployment of an entire environment by bypassing Argo for break glass situations.