The adventures of GitOps with a Monorepo
Ah, GitOps. The magical buzzword that everyone pretends they've been using for years. Because, obviously, if you're not managing your infrastructure like it's just another codebase, are you even DevOps-ing?
Gone are the days of logging into servers, running ad-hoc scripts, and praying nothing breaks. Now, we push YAML to Git and let automation take the wheel. Simple, right? Sure—until you realize your cluster is on fire because someone merged a "quick fix" at 5 PM on a Friday.
But hey, at least with GitOps, everything is version-controlled, auditable, and (in theory) rollback-able. So, let's dive into why GitOps is the best thing since sliced CI/CD pipelines.
Why ArgoCD? UI Simplicity and CRD Integration
When selecting a GitOps tool, ArgoCD stood out due to its user-friendly UI and seamless integration with Kubernetes Custom Resource Definitions (CRDs). The UI provides clear visibility into application states, making it easier to track deployments and identify issues without relying solely on CLI commands. Additionally, its CRD-based approach allows for declarative management of applications, aligning well with Kubernetes-native workflows.
The Plan: Refactoring the Deployment Repository for ArgoCD
Currently, I manage a single deployment repository for all my applications. While this centralized approach works, it needs to be restructured to take full advantage of ArgoCD. The goal is to improve automation, maintainability, and visibility while keeping everything in one place.
Steps for the Transition:
- Restructure the repository to use ArgoCD’s
Application
andAppProject
CRDs, ensuring clear separation between different applications. - Define ArgoCD applications declaratively so that each application has its own dedicated configuration within the repository.
- Enable automated synchronization, allowing ArgoCD to continuously monitor the repository and apply changes as they are merged.
- Establish a structured deployment workflow, making it easier to track, audit, and roll back changes if necessary.
By refining the deployment repository, I aim to streamline operations, reduce manual intervention, and ensure a more scalable and reliable deployment process.
Defining the Structure for Application Values
The first step in the transition was to establish a clear and scalable structure for defining application deployment values. To maintain organization and simplify management, the repository is structured as follows:
repo/
│── apps/
│ ├── <namespace>/
│ │ ├── <application>/ # Kustomize application
│ │ ├── <another-app>/
│ │ ├── _helm/ # Holds all Helm values files for this namespace
│ │ │ ├── <helm-chart-values>.yaml
│ │ │ ├── <another-chart-values>.yaml
Structure Breakdown:
apps/
: The top-level directory containing all deployed applications, organized by namespace.<namespace>/
: Each Kubernetes namespace has its own subfolder, ensuring clear separation of applications.<application>/
: Each deployed application has its own folder containing its Kustomize base and overlays._helm/
: A dedicated folder inside each namespace to store values files for Helm charts deployed within that namespace.
This structure ensures that application configurations are modular, easy to maintain, and compatible with both Kustomize and Helm-based deployments. With this foundation in place, the next step is integrating it into ArgoCD for automated synchronization and deployment.
Telling ArgoCD How to Load Applications
With the repository structure in place, the next step is to instruct ArgoCD on how to discover and manage applications. Since all applications belong to a single deployment repository, we start by defining an AppProject to manage them under a unified scope.
Defining the AppProject
To ensure proper access control and organization, we define an AppProject that includes all our applications. This project acts as a logical boundary for our deployments.
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: home-cluster
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Main Home cluster configuration
sourceRepos:
- '*'
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: '*'
kind: '*'
orphanedResources:
warn: false
Automating Application Creation with an ApplicationSet
Rather than manually creating ArgoCD Application
resources for each deployed app, we leverage ApplicationSets to automate the process. This allows ArgoCD to dynamically generate application definitions based on the repository structure.
Bootstrapping ArgoCD with a Root Application
To enable the automation, we define a root application that is applied manually via CI/CD. This application is responsible for syncing the entire ArgoCD configuration, including the ApplicationSets
that define how individual applications are created.
The root application is placed in:
repo/
│── argocd/
│ ├── apps/ # Contains ApplicationSet definitions
│ ├── root-app.yaml # Root ArgoCD application applied manually
Root Application YAML Definition
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: argocd-apps
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: home-cluster
# Source of the application manifests
source:
repoURL: ssh://git@gitlab.xxx.xx/xxx/repo.git
targetRevision: HEAD
path: argocd/apps
# directory
directory:
recurse: true
jsonnet: {}
destination:
server: https://kubernetes.default.svc
namespace: argocd
# Sync policy
syncPolicy:
automated:
prune: false
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
- RespectIgnoreDifferences=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
ApplicationSet for Automatic Application Management
Inside the argocd/apps/
directory, we place ApplicationSet definitions that dynamically generate ArgoCD applications based on the repository structure. This allows all applications to be automatically discovered and managed by ArgoCD.
With this setup, managing deployments becomes seamless:
- Apply the root application manually via CI/CD.
- The root app syncs all configurations inside
argocd/apps/
. - The ApplicationSet discovers and registers all application deployments automatically.
This approach ensures a scalable and maintainable GitOps workflow with minimal manual intervention.
Automating Application Management with ApplicationSets
To dynamically create ArgoCD applications, we define two ApplicationSets:
- Kustomize ApplicationSet – Handles Kustomize-based deployments.
- Helm ApplicationSet – Manages Helm chart releases based on a predefined configuration.
Kustomize ApplicationSet
The Kustomize ApplicationSet is straightforward. It uses the Git generator to scan the repository and iterate over the apps/
directory, excluding the _helm/
folder. Each subfolder inside a namespace becomes an ArgoCD Application
with a Kustomize source.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: kustomize-releases
spec:
goTemplate: true
goTemplateOptions: [ "missingkey=error" ]
generators:
- git:
repoURL: ssh://git@gitlab.xxx.xx/xxx/repo.git
revision: HEAD
directories:
- path: apps/**/**
- path: apps/**/_helm
exclude: true
template:
metadata:
name: "{{ .path.basename }}"
spec:
project: home-cluster
source:
repoURL: ssh://git@gitlab.xxx.xx/xxx/repo.git
targetRevision: HEAD
path: "{{ .path.path }}"
destination:
server: https://kubernetes.default.svc
namespace: "{{ index .path.segments 1 }}"
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
- RespectIgnoreDifferences=true
- ApplyOutOfSyncOnly=true
- ServerSideApply=true
Helm ApplicationSet
The Helm ApplicationSet is more complex since it needs to determine which Helm charts to deploy and with what values. To achieve this, we use a Matrix generator that combines:
- A Git generator to retrieve the
charts.yml
file insideargocd/apps/
, which defines the Helm charts to deploy. - A List generator to parse the
charts.yml
and iterate over it.
Example charts.yml
File
charts:
- repoURL: https://kubernetes-sigs.github.io/external-dns/
chartName: external-dns
targetRevision: 1.15.1
releaseName: external-dns
namespace: networking
- repoURL: https://argoproj.github.io/argo-helm
chartName: argo-cd
targetRevision: 7.8.2
releaseName: argocd
namespace: argocd
Using the data from charts.yml
, the ApplicationSet combines the Helm chart information with the corresponding _helm/
values files to define Helm-based applications.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: helm-releases
spec:
goTemplate: true
goTemplateOptions: [ "missingkey=error" ]
generators:
- matrix:
generators:
- git:
repoURL: ssh://git@gitlab.xxx.xx/xxx/repo.git
revision: HEAD
files:
- path: argocd/apps/charts.yml
- list:
elements: []
elementsYaml: "{{ .charts | toJson }}"
template:
metadata:
name: "{{ .releaseName }}"
spec:
project: home-cluster
sources:
- repoURL: "{{ .repoURL }}"
targetRevision: "{{ .targetRevision }}"
chart: "{{ .chartName }}"
helm:
releaseName: "{{ .releaseName }}"
valueFiles:
- "$values/apps/{{ .namespace }}/_helm/{{ .releaseName }}.yml" # (1)
ignoreMissingValueFiles: false
- repoURL: ssh://git@gitlab.xxx.xx/xxx/repo.git # (2)
targetRevision: HEAD
ref: values
destination:
server: https://kubernetes.default.svc
namespace: "{{ .namespace }}"
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
- RespectIgnoreDifferences=true
- ApplyOutOfSyncOnly=true
- ServerSideApply=true
- Here you can see how we utilize the folder structure from above to dynamiclly build the values files path from the namespace and the releaseName.
- I am using here the ability to define a remote values files location since the git generator above only loads the
charts.yml
Summary
- The Kustomize ApplicationSet discovers and deploys Kustomize applications automatically.
- The Helm ApplicationSet pulls Helm chart definitions from charts.yml and combines them with the values stored in _helm/.
- Both ApplicationSets ensure that all applications in the repository are automatically managed by ArgoCD.
With these ApplicationSets in place, applications are deployed dynamically without requiring manual ArgoCD configuration for each one.
Automating Dependency Updates with Renovate
Keeping dependencies up to date is critical for security, stability, and feature improvements. Rather than manually tracking updates or relying on latest
tags (which can introduce unexpected issues), I use Renovate to automate dependency updates in a controlled and auditable way.
How Renovate Works
Renovate scans the repository, detects outdated dependencies, and creates pull requests with updated versions. It follows a set of predefined rules to manage updates efficiently, reducing the manual effort required to keep everything current.
Renovate Configuration (renovate.json
)
The repository is configured with the following key settings:
- Automated PRs with dependency grouping: Renovate creates pull requests for dependency updates, grouping them intelligently.
- Preserve semantic versioning ranges: Ensures updates respect defined constraints.
- Security alerts: Automatically labels PRs with security fixes.
- Automerge for patches: Patch updates are automatically merged to reduce maintenance overhead.
- Rate limits disabled: Allows efficient batch processing of updates.
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":preserveSemverRanges",
":dependencyDashboard",
":rebaseStalePrs",
":enableVulnerabilityAlertsWithLabel('security')",
"group:recommended",
":automergePatch",
":combinePatchMinorReleases",
":disableRateLimiting"
],
"enabledManagers": [
"kustomize",
"argocd",
"helm-values",
"custom.regex"
],
"prHourlyLimit": 25,
"branchConcurrentLimit": 100,
"argocd": {
"fileMatch": [
"\\.yml$"
]
},
"helm-values": {
"commitMessageTopic": "helm values {{depName}}",
"fileMatch": [
"(^|/)_helm/.*\\.ya?ml$"
],
"pinDigests": false
}
}
Custom Managers for Kustomize, ArgoCD, and Helm
To fully support our GitOps workflow, I use custom managers to detect and update dependencies across different deployment formats:
- Kustomize & ArgoCD: Renovate scans and updates version references inside YAML files related to ArgoCD and Kustomize configurations.
- Helm Values: The helm-values manager updates Helm values files stored in _helm/, ensuring our Helm deployments use the latest versions.
- Custom Regex for Helm Charts (charts.yml): Since Helm charts are defined in a charts.yml file, a regex-based manager extracts version details and updates them automatically.
Custom Regex Managers for charts.yml
{
"customManagers": [
{
"customType": "regex",
"fileMatch": [
"(^|/)charts.ya?ml$"
],
"matchStrings": [
"repoURL:\\s*(?<repoURL>https:\\/\\/.*)\\n\\s*chartName:\\s*(?<chartName>.*)\\n\\s*targetRevision:\\s*(?<targetRevision>.*)"
],
"depNameTemplate": "{{chartName}}",
"currentValueTemplate": "{{targetRevision}}",
"datasourceTemplate": "helm",
"registryUrlTemplate": "{{repoURL}}"
},
{
"customType": "regex",
"fileMatch": [
"(^|/)charts.ya?ml$"
],
"matchStrings": [
"repoURL:\\s*(?<repoURL>[a-zA-Z.]+\\/[a-zA-Z\\/]+)\\n\\s*chartName:\\s*(?<chartName>.*)\\n\\s*targetRevision:\\s*(?<targetRevision>.*)"
],
"datasourceTemplate": "docker",
"packageNameTemplate": "{{repoURL}}/{{chartName}}",
"currentValueTemplate": "{{targetRevision}}"
}
]
}
Benefits of Using Renovate
By leveraging Renovate, I ensure that:
- All Kustomize overlays, Helm charts, and ArgoCD configurations stay up to date.
- Updates are handled automatically without manual intervention.
- Changes are tracked through pull requests, allowing for validation before merging.
- The repository remains secure and stable by promptly applying security fixes.
This fully automated approach eliminates the need to manually monitor dependencies, making GitOps maintenance more efficient and scalable. 🚀