4 Guide Repository Handling Multiple Environments
Baptiste Bonnot edited this page 2025-09-15 13:37:39 +02:00

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