Back to Blogs
July 30, 2025

FluxCD GitOps Made Simple: A Follow-up

Kubernetes GCP Cloud Native GitOps FluxCD

In my previous post, "FluxCD GitOps Made Simple: My Journey to Automated Kubernetes Deployments", I mentioned wanting to follow up on how to setup the ConfigMap automatically following GitOps principles. This post discusses the various next steps I took in order to achieve that.

Some required techniques and technologies

To achieve a proper GitOps setup with automated ConfigMap management, I leveraged three key technologies that work together seamlessly:

1. Kustomize configMapGenerator

Kustomize's configMapGenerator is a powerful feature that automatically creates ConfigMaps from files, literals, or environment files. According to the official Kustomize documentation, this generator creates ConfigMaps by reading files, environment files, or literal key-value pairs. The key advantage is that it automatically handles ConfigMap naming and updates when the source files change.

The configMapGenerator supports several sources:

  • Files: Direct file references that get embedded as ConfigMap data
  • Environment files: Key-value pairs from .env files
  • Literals: Direct key-value pairs specified inline

This approach eliminates the need to manually create and maintain ConfigMaps, ensuring they're always synchronized with your Git repository.

2. Git Submodules for Component Management

Git submodules allow you to include other Git repositories as subdirectories within your main repository. As documented in the Git Submodules guide, this feature enables you to:

  • Reference external repositories: Include component-specific configurations from separate repositories
  • Maintain version control: Each submodule can be pinned to specific commits or branches
  • Separate concerns: Keep component configurations isolated from the main FluxCD repository
  • Enable team collaboration: Different teams can manage different components independently

In my setup, I used submodules to reference separate repositories containing Helm chart values and configurations for each component (static-website, api-java, chatbot-ui, etc.). This approach was particularly effective for managing the previous values.yaml content that I had accumulated.

3. FluxCD HelmRelease valuesFrom

FluxCD's valuesFrom feature in HelmRelease resources allows you to reference multiple ConfigMaps and merge their values. According to the FluxCD HelmRelease documentation, this feature supports:

  • Multiple ConfigMap references: Combine values from several ConfigMaps
  • Ordered merging: Values are merged in the order specified, with later values overriding earlier ones
  • Inline value overrides: Direct values in the HelmRelease take final precedence
  • Secret references: Also supports referencing Kubernetes Secrets for sensitive data

I implemented a layered configuration approach using this feature:

  • Base ConfigMap: Contains common values shared across all environments
  • Environment ConfigMap: Contains environment-specific overrides
  • Inline values: Contains frequently changing values like image tags

This layered approach provides the flexibility to maintain consistency across environments while allowing for environment-specific customizations and rapid updates to frequently changing values.

A new folder structure

Here's the complete folder structure I implemented, required after some Kustomization validation errors with the proper path to the Git submodules folders:

fluxcd-gitops-repo/
├── .gitmodules                    # Git submodule definitions
├── clusters/
│   ├── base/                      # Base configurations
│   │   ├── apps/examples/         # Base HelmRelease definitions
│   │   └── infrastructure/        # Base infrastructure components
│   ├── components/                # Git submodules for each component
│   │   ├── static-website/        # Static website component
│   │   │   ├── base.values.yaml   # Base configuration
│   │   │   ├── lcl.values.yaml    # Local environment values
│   │   │   └── Chart.yaml         # Helm chart definition
│   │   ├── api-java/             # Java API component...
│   │   ├── chatbot-ui/           # Chatbot UI component...
│   ├── local/                     # Local environment
│   │   ├── apps/examples/        # Local-specific HelmRelease patches
│   │   ├── infrastructure/       # Local infrastructure patches
│   │   └── kustomization.yaml    # Local environment kustomization
│   └── sandbox/                   # Sandbox environment
│       ├── apps/examples/        # Sandbox-specific HelmRelease patches
│       ├── infrastructure/       # Sandbox infrastructure patches
│       └── kustomization.yaml    # Sandbox environment kustomization
└── README.md

The Layered Configuration Approach

One of the key insights I discovered was implementing a layered configuration approach that follows GitOps principles while maintaining flexibility across different environments. Here's how it works and its benefits.

Benefits of This Approach

  • Environment Consistency: Base configurations ensure consistency across environments
  • Environment Flexibility: Environment-specific values allow for customization
  • Component Reusability: Components can be easily shared and reused
  • GitOps Compliance: All configurations are version-controlled and follow GitOps principles
  • Automated Updates: Image automation and ConfigMap generation reduce manual work
  • Scalability: Easy to add new components and environments

1. Base Configuration Layer

The foundation of my approach starts with base.values.yaml files in each component submodule. These contain values that are common across all environments and should rarely change:

# Example: clusters/components/static-website/base.values.yaml
replicaCount: 1
image:
  pullPolicy: Always
service:
  type: ClusterIP
  port: 80
  targetPort: 80
resources:
  requests:
    cpu: 1m
    memory: 48Mi
autoscaling:
  minReplicas: 1
  maxReplicas: 2
  targetCPUUtilizationPercentage: 80

2. Environment-Specific Layer

Each environment (local, sandbox, production) has its own values file that overrides only what's necessary:

# Example: clusters/components/static-website/lcl.values.yaml (local)
image:
  repository: static-web-example
  imagePullSecrets:
environment:
  LOG_LEVEL: "debug"
autoscaling:
  enabled: false
# Example: clusters/components/static-website/snbx.values.yaml (sandbox)
image:
  repository: registry.gitlab.com/your-org/static-web-example
  imagePullSecrets: "registry-key"
autoscaling:
  enabled: true

3. Dynamic Values Layer

The final layer consists of values that change frequently and are managed directly in the HelmRelease:

# In HelmRelease values section
values:
  image:
    tag: "1.0.1" # {"$imagepolicy": "flux-system:static-web-imgpolicy:tag"}
  replicaCount: 1

This is only an example. Each component must be reviewed to determine the best option for each value item.

Git Submodules for Component Management

I implemented Git submodules to manage individual component configurations separately from the main FluxCD repository. This approach provides several benefits and follows the FluxCD repository structure guidelines:

  • Separation of Concerns: Each component has its own repository with its own lifecycle
  • Independent Versioning: Components can be updated independently
  • Reusability: Components can be shared across different projects
  • Security: Different teams can have different access levels to different components

Here's how the submodules are configured in .gitmodules:

[submodule "clusters/components/static-website"]
    path = clusters/components/static-website
    url = https://gitlab.com/your-org/static-web-chart.git
[submodule "clusters/components/chatbot-ui"]
    path = clusters/components/chatbot-ui
    url = https://gitlab.com/your-org/chatbot-chart.git

ConfigMapGenerator Implementation

The configMapGenerator in Kustomization is the key to automatically creating ConfigMaps from the values files. This follows the Kustomize kustomization.yaml specification. Here's how it's implemented:

configMapGenerator:
  - name: static-web-base-values
    namespace: default
    files:
      - base.values.yaml=../components/static-website/base.values.yaml
  - name: static-web-level-values
    namespace: default
    files:
      - lcl.values.yaml=../components/static-website/lcl.values.yaml
  - name: api-java-base-values
    namespace: default
    files:
      - base.values.yaml=../components/api-java/base.values.yaml
  - name: api-java-level-values
    namespace: default
    files:
      - lcl.values.yaml=../components/api-java/lcl.values.yaml

This approach automatically generates ConfigMaps for each component and environment combination, ensuring that the values are always up-to-date with the Git repository.

FluxCD HelmRelease Integration

The valuesFrom feature in FluxCD HelmRelease allows us to reference multiple ConfigMaps and merge their values. This approach is inspired by the FluxCD Kustomize Helm example:

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: static-web-helmrelease
  namespace: default
spec:
  valuesFrom:
    - kind: ConfigMap
      name: static-web-base-values
      valuesKey: base.values.yaml
    - kind: ConfigMap
      name: static-web-level-values
      valuesKey: lcl.values.yaml
  values:
    image:
      repository: registry.gitlab.com/your-org/static-web-example
      tag: "1.0.1" # {"$imagepolicy": "flux-system:static-web-imgpolicy:tag"}

The values are merged in order: base values are applied first, then environment-specific values override them, and finally, inline values take precedence.

Environment-Specific Kustomization

Each environment has its own Kustomization file that references the appropriate values files:

  • Local Environment: Uses lcl.values.yaml for development-specific settings
  • Sandbox Environment: Uses snbx.values.yaml for testing configurations
  • Production Environment: Would use prod.values.yaml for production settings

In other words, the folders for each environment in FluxCD contains a Kustomization.yaml file referring to all the base files to include, the patches files to include (which refers to the proper level since it is known from the file perspective which environment I'm building) and an item for each ConfigMap to generate from which file. Although a better Kustomization could potentially simplify that file, reducing the duplication between each environment, I chose to keep it simple and referencing directly the files to be explicit; since I am weak technically on Kustomize, I have found it easier for me!

A few gotchas

One thing to note, in FluxCD and using GitLab, you have to use a submodule using the HTTPS instead of the SSH due to the authentication mechanism of FluxCD. This is related to how FluxCD GitRepository sources handle authentication.

Kustomization is a pain! I had to change and re-change my FluxCD Git repo folder structure to ensure I was meeting all security requirements. And even then, I'm still failing on one validation which for now, doesn't seem to have an impact. More to investigate here!!

When the content of one of the base or level values file changed, it is a bit more cumbersome to bring it updated. You have to first update the component Git repo and commit the changes. then, in the submodule repo folder, which is under the main FluxCD git repo, do a git pull. Then commit those changes to the FluxCD git repo and reconcile everything, which means the git source as well as the helmrelease.

Additional Challenges Encountered

  • FluxCD Reconciliation: After updating submodules, you need to trigger reconciliation of both the GitSource and HelmRelease
  • Image Automation: Ensuring that image automation works correctly with the layered approach requires careful configuration

Conclusion

The current setup marks a significant step forward in adopting robust GitOps principles. By layering configurations through Git submodules, configMapGenerator, and FluxCD’s valuesFrom, the system now supports better automation, clarity, and environment-specific overrides—all driven by version-controlled changes.

The layered configuration approach with Git submodules and ConfigMapGenerator has significantly improved my GitOps workflow. While there are still challenges to overcome, particularly around secret management and infrastructure components, the foundation is now in place for a robust, scalable GitOps implementation.

Next Steps

  • Secret Management: Evaluate and implement secure, GitOps-friendly secret handling mechanisms such as Sealed Secrets or External Secrets Operator
  • Infrastructure GitOps: Refactor critical services like cert-manager and Istio into fully managed GitOps deployments

References and Further Reading

This implementation draws inspiration from several official examples and documentation from FluxCD or Git:

AI Usage Disclosure

This document was created with assistance from AI tools. The content has been reviewed and edited by a human. For more information on the extent and nature of AI usage, please contact the author.