Continuous deployment with Flux and Argo CD
tsg render reads the deployment configuration and writes Kubernetes
manifests. It does not contact a cluster, registry, or release feed. The same
inputs and CLI version produce the same bytes, which makes rendering a suitable
CI build step.
deployment.yaml
↓ tsg render CI or a local build
manifests/ → Git
↓ Flux / Argo CD apply, health checks, prune
cluster
Flux or Argo CD owns the deployment after the render. Do not run tsg deploy
against the same resources.
What the controller takes over
The controller replaces the cluster-facing part of the CLI workflow. Its apply, health, and prune rules now determine what happens in the cluster.
| Concern | tsg deploy | Flux or Argo CD |
|---|---|---|
| Apply | Server-side apply in ordered waves | Controller-specific apply and ordering |
| Readiness | Waits after each wave | Health checks per Kustomization or Application |
| Pruning | Uses an inventory ConfigMap written by the CLI | Uses the desired state and controller inventory |
| Data during pruning | Retains CNPG resources and PVCs | Deletes resources unless you configure an exception |
| Secret checks | Stops before apply when a referenced Secret is missing | The dependent workload fails to start |
| Status and uninstall | Reads the CLI inventory | No CLI inventory exists |
CLI inventory is not used
tsg apply and tsg deploy write an inventory ConfigMap named
tsg-inventory-<deployment>-<set>. tsg render does not. A deployment managed
only by Flux or Argo CD therefore has no CLI inventory. tsg status reports
nothing, and tsg uninstall has nothing to delete. Remove resources through
Git and let the controller prune them.
Protect data-bearing resources
The CLI retains CloudNativePG Cluster, Database, and DatabaseRole
resources and any rendered PersistentVolumeClaim during prune. A GitOps
controller does not know about that policy.
TSG sets the CloudNativePG reclaim policy to retain on Database and
DatabaseRole. Deleting either custom resource leaves its PostgreSQL object in
place. That protection does not apply to the Cluster custom resource or its
operator-created PVCs. Protect the Cluster itself. Also protect the Namespace,
because deleting it cascades to every namespaced resource, and protect any PVCs
that the TSG bundle renders.
Add these global patches to the deployment configuration:
patches:
- target:
kind: Namespace
merge:
metadata:
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled
argocd.argoproj.io/sync-options: Prune=false,Delete=false
- target:
kind: Cluster
group: postgresql.cnpg.io
allowMultiple: true
optional: true
merge:
metadata:
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled
argocd.argoproj.io/sync-options: Prune=false,Delete=false
- target:
kind: PersistentVolumeClaim
allowMultiple: true
optional: true
merge:
metadata:
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled
argocd.argoproj.io/sync-options: Prune=false,Delete=false
Flux documents the kustomize.toolkit.fluxcd.io/prune: disabled annotation in
its Kustomization reference.
For Argo CD, Prune=false protects a resource during sync and Delete=false
protects it when the Application is deleted. See the
Argo CD sync options.
Keep backups outside the cluster. A prune annotation prevents an accidental controller deletion, but it is not a backup or a disaster recovery plan. Protected resources also remain in the cluster after you remove them from Git. Inspect and delete them manually when removal is intentional and a backup has been verified.
Render into the repository
Write the bundle to a directory managed only by the CLI:
tsg render participant.yaml -o gitops/tsg
gitops/tsg/
bundle.json # CLI metadata, not a Kubernetes object
manifests/
infra/manifests.yaml
stacks/
alfa/manifests.yaml
Point the controller at a directory below manifests/. Do not point it at the
bundle root. bundle.json records the resolved images, CLI version, and input
apiVersion, but it is not a Kubernetes object.
The CLI replaces the complete output directory on each render. Removing a stack from the configuration therefore removes its manifests from Git, and the controller can prune them. Keep patches, extra manifests, and secret material outside the output directory.
Pin inputs that change the output
Two inputs can change many rendered objects at once:
- If an image has no
tagordigest, the CLI derives its tag from the CLI version. Setspec.images.<component>.digestfor committed bundles. Seespec.images. - Every rendered object has an
app.kubernetes.io/versionlabel. A CLI upgrade changes this label across the bundle. Pin the CLI in CI and upgrade it in a separate commit so the resulting diff is easy to review.
Render in CI
This example pins the CLI, renders the bundle, and commits a changed result:
npm install -g @tsg-dsp/cli@0.19.0
tsg render deployment.yaml -o gitops/tsg
git add gitops/tsg
git diff --cached --quiet || git commit -m "chore(tsg): render $(tsg -v | head -1)"
The merge request contains the exact manifests that the controller will read. If contributors should commit generated output themselves, have CI render the bundle and fail when the working tree is not clean.
Flux
Create one Flux Kustomization for each apply set. Make every stack depend on
the shared infrastructure set. Flux generates a kustomization.yaml when a
source path contains plain Kubernetes manifests, so the rendered directories
work without extra files.
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: tsg-infra
namespace: flux-system
spec:
interval: 10m
prune: true
wait: true
sourceRef:
kind: GitRepository
name: tsg
path: ./gitops/tsg/manifests/infra
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: tsg-alfa
namespace: flux-system
spec:
interval: 10m
prune: true
wait: true
timeout: 10m
dependsOn:
- name: tsg-infra
sourceRef:
kind: GitRepository
name: tsg
path: ./gitops/tsg/manifests/stacks/alfa
dependsOn stops Flux from reconciling the stack until the infrastructure
Kustomization is ready. This matters when database.mode: shared, because the
stack's database resources refer to the shared PostgreSQL Cluster.
Flux can decrypt SOPS-encrypted Secrets from the same repository:
spec:
decryption:
provider: sops
secretRef:
name: sops-age
Argo CD
Create one Argo CD Application for each apply set. If an app-of-apps owns the
Applications, use sync waves to apply infrastructure first. Otherwise, sync the
infrastructure Application before the stack Applications.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: tsg-alfa
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
project: default
destination:
server: https://kubernetes.default.svc
namespace: tsg-alfa
source:
repoURL: https://gitlab.example.com/ops/tsg.git
targetRevision: main
path: gitops/tsg/manifests/stacks/alfa
directory:
recurse: true
include: "*.yaml"
syncPolicy:
automated:
prune: true
selfHeal: true
The path contains only Kubernetes YAML. The include rule makes that
restriction explicit. Copy the Application for the infrastructure set, change
its path to gitops/tsg/manifests/infra, and assign it sync wave "0".
Make resource tracking explicit
Set annotation-based tracking in argocd-cm:
data:
application.resourceTrackingMethod: annotation
Current Argo CD releases use annotation tracking by default, while older
releases used the app.kubernetes.io/instance label. TSG assigns that label to
its components. Setting the tracking method explicitly keeps behavior consistent
across Argo CD versions and prevents a label collision. See the
Argo CD resource tracking documentation.
Render during sync
Argo CD can render the configuration through a Config Management Plugin.
tsg render --stdout writes a single YAML document stream. The plugin sidecar
needs Node.js 22 and the CLI.
apiVersion: argoproj.io/v1alpha1
kind: ConfigManagementPlugin
metadata:
name: tsg
spec:
version: v1.0
discover:
fileName: "./deployment.tsg.yaml"
generate:
command: [tsg]
args: [render, deployment.tsg.yaml, --stdout]
Save this configuration as
/home/argocd/cmp-server/config/plugin.yaml in the plugin sidecar. It resembles
a Kubernetes resource, but you do not apply it to the cluster. The
Argo CD plugin guide
documents the required sidecar mounts and entrypoint.
This approach reviews the configuration but not the rendered manifests. The CLI version also moves into the plugin image, so an image update can change the render without a configuration change. Prefer a committed bundle when reviewers need to inspect the final Kubernetes objects.
Ordering within an apply set
Neither controller applies resources in file order. The CLI normally waits between its database, configuration, workload, and routing waves. A controller does not reproduce those waits within one apply set.
A Deployment may start before CloudNativePG has created its DatabaseRole.
The workload can restart until the role becomes available, after which the
controller should converge. A missing Secret is different because reconciliation
cannot create secret material. Deliver referenced Secrets through SOPS, External
Secrets, Sealed Secrets, or another secret manager before the first sync.
Move an existing deployment to GitOps
Do not use the CLI and a GitOps controller to apply the same stack. They use different ownership records, and each may undo or prune the other's work.
To hand over an existing deployment:
-
Add the data-protection and Argo CD tracking settings described above.
-
Render the current configuration into the GitOps repository and commit it.
-
Sync the infrastructure set first, then the stack sets. The resource names stay the same, so a normal sync updates the existing objects.
-
After a successful sync, remove only this deployment's CLI inventories:
kubectl delete configmap -n <namespace> \-l 'app.kubernetes.io/managed-by=tsg-cli,tsg.dataspac.es/deployment=<deployment>'
The label selector deliberately includes the deployment name. A namespace can contain inventories for more than one deployment.
After the handover, use tsg render to update the committed bundle. tsg diff
can still compare rendered objects with the live cluster, but prune information
comes from CLI inventory and is no longer available. Leave apply, status,
uninstall, and pruning to the controller.
Related documentation
- CLI reference lists every command and option.
- Configuration reference documents every field, including patches.
- Deployment guide covers prerequisites, Secrets, Gateway API, and ongoing operations.