GitOps-Ready Namespaces with Flux
Overview
In our OpenShift clusters, namespaces are provisioned GitOps-ready by default. Each namespace is linked to a Git repository where configurations are stored and managed declaratively.
This setup enables:
-
Consistent GitOps workflow across tenants
-
Multi-environment support from a single repo
-
Overlay approach with minimal duplication
Each namespace is link to a git repository this combination is what we call a Tenant for this example we are going to take the example of 3 environments dev, qa, prod:
We forward the TENANT_NAMESPACE
variable holding the value of you tenant
name. This allows Kustomizations to dynamically target the right folder for
each environment.
Flux Kustomization e.g
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app
namespace: ${TENANT_NAMESPACE}
spec:
interval: 5m
path: ./overlays/${TENANT_NAMESPACE}
prune: true
sourceRef:
kind: GitRepository
name: my-app-repo
If the tenant is my-app-dev then it will target the folder ./overlays/my-app-dev
Overlays Structure and Patterns
Simple Patterns
We use the Kustomize overlays pattern to separate environment-specific differences from shared configuration. Given our vanilla tenant folder.
(venv) euler@HAL:~/.../tenants/tenant-tpl(main)$ tree
.
├── config
│ ├── app
│ │ └── limits-ranges.yaml
│ └── ks.yaml
├── echo-server
│ ├── app
│ │ └── helmrelease.yaml
│ └── ks.yaml
├── kustomization.yaml
├── README.md
├── renovate.json5
├── repos
│ ├── helm
│ │ └── bjw-s.yaml
│ └── ks.yaml
├── scripts
│ └── rewrap-secrets.sh
└── vars
├── ks.yaml
└── tenant-tpl
├── example.yaml
└── README.md
I will in this example spawn two namespaces link to that git repository test-tpl & test-tpl-dev
Query to check resources from flux perspective.
$ flux get all -n tenant-tpl
NAME REVISION SUSPENDED READY MESSAGE
gitrepository/tenant-repos main@sha1:0fcffb6b False True stored artifact for revision 'main@sha1:0fcffb6b'
NAME REVISION SUSPENDED READY MESSAGE
helmrepository/bjw-s False True Helm repository is Ready
NAME REVISION SUSPENDED READY MESSAGE
helmchart/tenant-tpl-echo-server 3.2.1 False True pulled 'app-template' chart with version '3.2.1'
NAME REVISION SUSPENDED READY MESSAGE
helmrelease/echo-server 3.2.1 False True Helm install succeeded for release tenant-tpl/echo-server.v1 with chart app-template@3.2.1
NAME REVISION SUSPENDED READY MESSAGE
kustomization/echo-server main@sha1:0fcffb6b False True Applied revision: main@sha1:0fcffb6b
kustomization/repos-sync main@sha1:0fcffb6b False True Applied revision: main@sha1:0fcffb6b
kustomization/tenant-apps main@sha1:0fcffb6b False True Applied revision: main@sha1:0fcffb6b
kustomization/tenant-config main@sha1:0fcffb6b False True Applied revision: main@sha1:0fcffb6b
kustomization/vars main@sha1:0fcffb6b False True Applied revision: main@sha1:0fcffb6b
$ flux get all -n tenant-tpl-dev
NAME REVISION SUSPENDED READY MESSAGE
gitrepository/tenant-repos main@sha1:0fcffb6b False True stored artifact for revision 'main@sha1:0fcffb6b'
NAME REVISION SUSPENDED READY MESSAGE
helmrepository/bjw-s False True Helm repository is Ready
NAME REVISION SUSPENDED READY MESSAGE
helmchart/tenant-tpl-dev-echo-server 3.2.1 False True pulled 'app-template' chart with version '3.2.1'
NAME REVISION SUSPENDED READY MESSAGE
helmrelease/echo-server 3.2.1 False True Helm install succeeded for release tenant-tpl-dev/echo-server.v1 with chart app-template@3.2.1
NAME REVISION SUSPENDED READY MESSAGE
kustomization/echo-server main@sha1:0fcffb6b False True Applied revision: main@sha1:0fcffb6b
kustomization/repos-sync main@sha1:0fcffb6b False True Applied revision: main@sha1:0fcffb6b
kustomization/tenant-apps main@sha1:0fcffb6b False True Applied revision: main@sha1:0fcffb6b
kustomization/tenant-config main@sha1:0fcffb6b False True Applied revision: main@sha1:0fcffb6b
kustomization/vars False False kustomization path not found: stat /tmp/kustomization-1496015889/vars/tenant-tpl-dev: no such file or directory
As you can see they are similar and have both deployed the echo server. However
as you can see there is a slight difference with the kustomzation/vars
. The
dev
environements is complaining about some missing directory. Let's inspect
the vars kustomization
$ cat vars/ks.yaml
---
# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: &app vars
namespace: ${TENANT_NAMESPACE}
spec:
targetNamespace: ${TENANT_NAMESPACE}
commonMetadata:
labels:
app.kubernetes.io/name: *app
path: ./vars/${TENANT_NAMESPACE}
prune: true
sourceRef:
kind: GitRepository
name: tenant-repos
wait: false
interval: 10m
retryInterval: 1m
timeout: 5m
As you can see this kustomization is defining a lot of field with the value
${TENANT_NAMESPACE}
. That's because the /vars
folder is already
preconfigured to handle overlays patterns. It will target the right directory
according to the namespace name he's running on. If we check our git repository
structure:
(venv) euler@HAL:~/.../tenants/tenant-tpl(main)$ tree vars/
vars/
├── ks.yaml
└── tenant-tpl
├── example.yaml
└── README.md
2 directories, 3 files
According to the kustomization definition the path:
./vars/${TENANT_NAMESPACE}
will not be found for the tenant tenant-tpl-dev
We are going to create the appropriate folder.
$ mkdir vars/tenant-tpl-dev
$ cp -r vars/tenant-tpl/example.yaml vars/tenant-tpl
tenant-tpl/ tenant-tpl-dev/
$ cp -r vars/tenant-tpl/example.yaml vars/tenant-tpl-dev/dev-example.yaml
$ vim vars/tenant-tpl-dev/dev-example.yaml
$ cat vars/tenant-tpl-dev/dev-example.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: example-vars-dev
data:
EXAMPLE: foo-dev
$ git commit -am 'Adding dev vars folder'
[main 07a29a5] Adding dev vars folder
1 file changed, 7 insertions(+)
create mode 100644 vars/tenant-tpl-dev/dev-example.yaml
$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 22 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 1.16 KiB | 1.16 MiB/s, done.
Total 5 (delta 1), reused 0 (delta 0), pack-reused 0
To ssh://git-ssh.kvant.cloud:2222/phoenix-oss/tenant-tpl.git
0fcffb6..07a29a5 main -> main
Verification
$ flux reconcile ks vars -n tenant-tpl-dev --with-source #One liner to force source update and reconciliation
► annotating GitRepository tenant-repos in tenant-tpl-dev namespace
✔ GitRepository annotated
◎ waiting for GitRepository reconciliation
✔ fetched revision main@sha1:07a29a5d22eae9be68fd9d9da141b3fa83f9f5d2
► annotating Kustomization vars in tenant-tpl-dev namespace
✔ Kustomization annotated
◎ waiting for Kustomization reconciliation
✔ applied revision main@sha1:07a29a5d22eae9be68fd9d9da141b3fa83f9f5d2
$ flux get all -n tenant-tpl-dev
NAME REVISION SUSPENDED READY MESSAGE
gitrepository/tenant-repos main@sha1:07a29a5d False True stored artifact for revision 'main@sha1:07a29a5d'
NAME REVISION SUSPENDED READY MESSAGE
helmrepository/bjw-s False True Helm repository is Ready
NAME REVISION SUSPENDED READY MESSAGE
helmchart/tenant-tpl-dev-echo-server 3.2.1 False True pulled 'app-template' chart with version '3.2.1'
NAME REVISION SUSPENDED READY MESSAGE
helmrelease/echo-server 3.2.1 False True Helm install succeeded for release tenant-tpl-dev/echo-server.v1 with chart app-template@3.2.1
NAME REVISION SUSPENDED READY MESSAGE
kustomization/echo-server main@sha1:07a29a5d False True Applied revision: main@sha1:07a29a5d
kustomization/repos-sync main@sha1:07a29a5d False True Applied revision: main@sha1:07a29a5d
kustomization/tenant-apps main@sha1:07a29a5d False True Applied revision: main@sha1:07a29a5d
kustomization/tenant-config main@sha1:07a29a5d False True Applied revision: main@sha1:07a29a5d
kustomization/vars main@sha1:07a29a5d False True Applied revision: main@sha1:07a29a5d
$ oc get configmap -n tenant-tpl-dev
NAME DATA AGE
example-vars-dev 1 3m32s
kube-root-ca.crt 1 36m
openshift-service-ca.crt 1 36m
tenant-settings 10 36m
Congratulations you now manage to handle simple overlays patterns and have the possibility to handle multiple environments using one git repository.
Advanced Patterns
Base
In the previous example we showed to you how to handle configmaps or secrets to be loaded for a given environments. In this example we are going to create a directory structure that allow us to *Have shared resources definition and environments specific one.
Taking back our echo-server example we are going to show how spawn different resources definitions base on the environments. Here the diagram of what we are trying to achieve.
flowchart TD
%% Node styles
classDef file fill:#ffffff,stroke:#9CA3AF,stroke-width:1px,color:#374151,rounded:10px;
classDef var fill:#FEF9C3,stroke:#F59E0B,stroke-width:1px,color:#78350F,rounded:8px;
classDef overlay fill:#DCFCE7,stroke:#22C55E,stroke-width:2px,color:#166534,rounded:10px;
classDef base fill:#E0F2FE,stroke:#3B82F6,stroke-width:2px,color:#1E40AF,rounded:10px;
classDef flux fill:#F3E8FF,stroke:#7C3AED,stroke-width:2px,color:#4C1D95,rounded:10px;
%% Git repository
subgraph GitRepo["📂 Git Repository"]
subgraph Base["Base (shared resources)"]
D1["Deployment.yaml"]:::file
HR["HelmRelease.yaml"]:::file
KB["kustomization.yaml"]:::base
end
subgraph Overlays["Overlays (tenant-specific)"]
subgraph Dev["🟢 Dev Overlay"]
P1["patch-replicas.yaml"]:::file
KD["kustomization.yaml"]:::overlay
end
subgraph Prod["🔴 Prod Overlay"]
P2["patch-resources.yaml"]:::file
KP["kustomization.yaml"]:::overlay
end
end
end
%% Flux Kustomization
subgraph Flux["⚡ FluxCD"]
KOverlay["Kustomization CR → overlays/${TENANT_NAMESPACE}"]:::flux
end
%% Kubernetes Cluster
subgraph Cluster["🖥️ Kubernetes Cluster"]
NS["${TENANT_NAMESPACE} namespace"]:::var
APP["myapp resources"]:::file
end
%% Connections
KOverlay --> Dev
KOverlay --> Prod
Dev --> Base
Prod --> Base
Dev --> APP
Prod --> APP
APP --> NS
In the given example we are going on the tenant-tpl-dev increase the number of replicas for the echo server. While we change the resources value for the production. This structure allow us to avoid duplicating the helmrelease.yaml base definition and only modify the value we want for base on each environment.
We end up with that structure for our echo-server.
tenant-tpl/echo-server(main)$ tree
.
├── base
│ ├── helmrelease.yaml
│ └── kustomization.yaml
├── ks.yaml
└── overlays
├── tenant-tpl
│ ├── kustomization.yaml
│ └── patch-resources.yaml
└── tenant-tpl-dev
├── kustomization.yaml
└── patch-replicas.yaml
Analysis and proof
Our goal was to define a main helmrelease for the echo server and change some parameters according to the environments. We increase the number of replicas for dev and change the amount of resources for prod.
First look to the flux side to proof reconciliation and versioning.
tenant-tpl
(venv) euler@HAL:~/.../tenant-tpl/echo-server(main)$ flux get all -n tenant-tpl
NAME REVISION SUSPENDED READY MESSAGE
gitrepository/tenant-repos main@sha1:3f1afa0f False True stored artifact for revision 'main@sha1:3f1afa0f'
NAME REVISION SUSPENDED READY MESSAGE
helmrepository/bjw-s False True Helm repository is Ready
NAME REVISION SUSPENDED READY MESSAGE
helmchart/tenant-tpl-echo-server 3.2.1 False True pulled 'app-template' chart with version '3.2.1'
NAME REVISION SUSPENDED READY MESSAGE
helmrelease/echo-server 3.2.1 False True Helm upgrade succeeded for release tenant-tpl/echo-server.v2 with chart app-template@3.2.1
NAME REVISION SUSPENDED READY MESSAGE
kustomization/echo-server main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
kustomization/repos-sync main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
kustomization/tenant-apps main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
kustomization/tenant-config main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
kustomization/vars main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
tenant-tpl-dev
(venv) euler@HAL:~/.../tenant-tpl/echo-server(main)$ flux get all -n tenant-tpl-dev
NAME REVISION SUSPENDED READY MESSAGE
gitrepository/tenant-repos main@sha1:3f1afa0f False True stored artifact for revision 'main@sha1:3f1afa0f'
NAME REVISION SUSPENDED READY MESSAGE
helmrepository/bjw-s False True Helm repository is Ready
NAME REVISION SUSPENDED READY MESSAGE
helmchart/tenant-tpl-dev-echo-server 3.2.1 False True pulled 'app-template' chart with version '3.2.1'
NAME REVISION SUSPENDED READY MESSAGE
helmrelease/echo-server 3.2.1 False True Helm upgrade succeeded for release tenant-tpl-dev/echo-server.v2 with chart app-template@3.2.1
NAME REVISION SUSPENDED READY MESSAGE
kustomization/echo-server main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
kustomization/repos-sync main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
kustomization/tenant-apps main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
kustomization/tenant-config main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
kustomization/vars main@sha1:3f1afa0f False True Applied revision: main@sha1:3f1afa0f
Both are at the same versioning on main@sha1:3f1afa0f'
Now inspecting the
echo-server helmrelease.
$ flux trace hr echo-server -n tenant-tpl-dev
Object: HelmRelease/echo-server
Namespace: tenant-tpl-dev
Status: Managed by Flux
---
Kustomization: echo-server
Namespace: tenant-tpl-dev
Target: tenant-tpl-dev
Path: ./echo-server/overlays/tenant-tpl-dev
Revision: main@sha1:3f1afa0f19882e2c6acdf318b6a6d0195e24d046
Status: Last reconciled at 2025-09-15 13:13:04 +0200 CEST
Message: Applied revision: main@sha1:3f1afa0f19882e2c6acdf318b6a6d0195e24d046
---
GitRepository: tenant-repos
Namespace: tenant-tpl-dev
URL: https://git.kvant.cloud/phoenix-oss/tenant-tpl
Branch: main
Revision: main@sha1:3f1afa0f19882e2c6acdf318b6a6d0195e24d046
Status: Last reconciled at 2025-09-15 13:12:46 +0200 CEST
Message: stored artifact for revision 'main@sha1:3f1afa0f19882e2c6acdf318b6a6d0195e24d046'
$ flux trace hr echo-server -n tenant-tpl
Object: HelmRelease/echo-server
Namespace: tenant-tpl
Status: Managed by Flux
---
Kustomization: echo-server
Namespace: tenant-tpl
Target: tenant-tpl
Path: ./echo-server/overlays/tenant-tpl
Revision: main@sha1:3f1afa0f19882e2c6acdf318b6a6d0195e24d046
Status: Last reconciled at 2025-09-15 13:12:21 +0200 CEST
Message: Applied revision: main@sha1:3f1afa0f19882e2c6acdf318b6a6d0195e24d046
---
GitRepository: tenant-repos
Namespace: tenant-tpl
URL: https://git.kvant.cloud/phoenix-oss/tenant-tpl
Branch: main
Revision: main@sha1:3f1afa0f19882e2c6acdf318b6a6d0195e24d046
Status: Last reconciled at 2025-09-15 13:12:07 +0200 CEST
Message: stored artifact for revision 'main@sha1:3f1afa0f19882e2c6acdf318b6a6d0195e24d046'
As you can see in the path
hold a different value based on the environments.
Thanks to our main kustomization that defined the path using
${TENANT_NAMESPACE}
value. Each overlays include the base but applied a patch
on it before sending it to kube.
Let's verify that our patch are correctly applied. On dev we wanted 4 replicas.
$ oc get pods -n tenant-tpl-dev
NAME READY STATUS RESTARTS AGE
echo-server-5bd6b558d6-4nngj 1/1 Running 0 10m
echo-server-5bd6b558d6-65zlb 1/1 Running 0 10m
echo-server-5bd6b558d6-ks2fq 1/1 Running 0 3d19h
echo-server-5bd6b558d6-mz2wf 1/1 Running 0 3d19h
echo-server-5bd6b558d6-rm64f 1/1 Running 0 10m
4 replicas ✔
On tenant-tpl we wanted to increase the resources.
DEV
$ oc get pods -n tenant-tpl-dev -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{range .spec.containers[*]}{.name}{" CPU Requests: "}{.resources.requests.cpu}{" Memory Requests: "}{.resources.requests.memory}{"\n"}{end}{end}'
echo-server-5bd6b558d6-4nngj
app CPU Requests: 10m Memory Requests: 64Mi
echo-server-5bd6b558d6-65zlb
app CPU Requests: 10m Memory Requests: 64Mi
echo-server-5bd6b558d6-ks2fq
app CPU Requests: 10m Memory Requests: 64Mi
echo-server-5bd6b558d6-mz2wf
app CPU Requests: 10m Memory Requests: 64Mi
echo-server-5bd6b558d6-rm64f
app CPU Requests: 10m Memory Requests: 64Mi
PROD
$ oc get pods -n tenant-tpl -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{range .spec.containers[*]}{.name}{" CPU Requests: "}{.resources.requests.cpu}{" Memory Requests: "}{.resources.requests.memory}{"\n"}{end}{end}'
echo-server-6cc5465c7d-5kg5r
app CPU Requests: 100m Memory Requests: 128Mi
echo-server-6cc5465c7d-dsmb6
app CPU Requests: 100m Memory Requests: 128Mi
We confirm that we have different resources. Our patch properly work and we now achieve to have a comon base and patching fields according to the environments ✔
Find the file reference
Base
Main Kustomization
Overlays
tenant-tpl
tenant-tpl-dev