Skip to main content
Version: v0.20.1

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.

Concerntsg deployFlux or Argo CD
ApplyServer-side apply in ordered wavesController-specific apply and ordering
ReadinessWaits after each waveHealth checks per Kustomization or Application
PruningUses an inventory ConfigMap written by the CLIUses the desired state and controller inventory
Data during pruningRetains CNPG resources and PVCsDeletes resources unless you configure an exception
Secret checksStops before apply when a referenced Secret is missingThe dependent workload fails to start
Status and uninstallReads the CLI inventoryNo 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 tag or digest, the CLI derives its tag from the CLI version. Set spec.images.<component>.digest for committed bundles. See spec.images.
  • Every rendered object has an app.kubernetes.io/version label. 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:

  1. Add the data-protection and Argo CD tracking settings described above.

  2. Render the current configuration into the GitOps repository and commit it.

  3. Sync the infrastructure set first, then the stack sets. The resource names stay the same, so a normal sync updates the existing objects.

  4. 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.