TLDR;

If you want something simple which works well and only applies the difference, go with Git Repository Sync; if you want something more flexible, and that offers the possibility to use Binary Auth, for example, go with OCI Artifacts.

In both scenarios teams can individually manage the lifecycle of what gets synced. OCI Artifacts are usually a better choice for a bigger number of teams and Repository Sync works well for smaller/less teams and/or lower environments.

The Post

Here are a few options on how to use ACM to sync repos or artifacts to a GKE cluster.

Sync Option Description Authentication Support
Git Repository Sync configs from a Git repository (GitHub, GitLab, Bitbucket, etc.) SSH, HTTPS with token/password, GCP IAM
OCI Artifact (Image) Sync from an OCI image (container registry like Artifact Registry) GCP Workload Identity, image pull secrets

There are two other options which I’m not covering here: syncing to a GCS bucket and syncing to helm charts. Honestly, syncing to helm charts might get superseded by OCI artifact in most cases and syncing to a bucket doesn’t feel like a full solution because if yaml files make into a bucket why are they not being version controlled using Git?

Repository Structure (kustomize)

Refresh your knowledge in kustomize and take a look at this suggested repo structure:

acm/
└── root-sync/
    ├── base/
    │   ├── cluster/
    │   │   ├── clusterrole.yaml
    │   │   ├── namespace-team-a.yaml
    │   │   ├── namespace-team-b.yaml
    │   │   └── kustomization.yaml
    │   └── kustomization.yaml
    ├── overlays/
    │   ├── us-east4-dev/
    │   │   ├── kustomization.yaml
    │   │   └── patch-clusterrole.yaml
    │   ├── us-east4-staging/
    │   │   ├── kustomization.yaml
    │   │   └── patch-clusterrole.yaml
    │   ├── us-east4-prod/
    │   │   ├── kustomization.yaml
    │   │   └── patch-clusterrole.yaml
    │   ├── europe-west1-dev
    │   │   ├── kustomization.yaml
    │   │   └── patch-clusterrole.yaml
    │   ├── europe-west1-staging/
    │   │   ├── kustomization.yaml
    │   │   └── patch-clusterrole.yaml
    │   └── europe-west1-prod/
    │       ├── kustomization.yaml
    │       └── patch-clusterrole.yaml
    └── rendered/
        ├── us-east4-dev/
        ├── us-east4-staging/
        └── us-east4-prod/
        ├── europe-west1-dev/
        ├── europe-west1-staging/
        └── europe-west1-prod/
  • base: is where the yamls that are common across the clusters go (for consistency).
  • overlays: is where we overwrite values from base or where we add things to specific clusters (for flexibility).
  • rendered: is where kustomize outputs base + overlays.

My suggestion is to simply have rendered folders for each cluster in every environment and when calling oras push push each one of the rendered folders individually as a separate OCI Artifact OR sync each of the clusters to the folder path for that specific cluster if you are Syncing to a Git Repository.

Sync to Git Repository

Syncing to a Git Repository can be a good fit if you want a simpler pipeline. One thing you must consider here is that we can:

  • Sync a repo for the entire cluster (use only RootSync).
  • Sync individual namespaces (use RootSync and RepoSync).

The above doesn’t actually change how we do the git sync but what changes is how the cluster is tracking the changes in that repository. I have seem 3 different ways to sync to the repo:

  • branch (head in that branch)
  • commit hash
  • tag

Tag is actually the closes it gets to OCI Artifact as it’s more “static”. Commit hash is equivalent to the tag approach as it’s also pretty static and both need a change in the cluster RootSync/RepoSync to sync to the new version. Syncing to the head can be very risky, but for lower environments (dev and maybe staging) it’s actually a great fit.

Syncing Cluster or ns to HEAD

Repository sync to head

Syncing Cluster or ns to Tag or Commit Hash

Repository sync to tag or commit hash

Sync to OCI Artifact

Here you actually need more tooling to make it work, but it might be worth it… you need to run pipelines to push your artifacts somewhere (GAR in this case) but it also opens up the possibility to use Binary Auth to sign the OCI Artifact and then attest before pulling it into your clusters.

Dealing with the OCI Artifact

Creating the OCI Artifact should look very similar to building a container image:

OCI Artifact Lifecycle

Deploying it can also be similar to it. Which might be an advantage if your team is already well versed in building images…

One thing you must consider here is that we can:

  • Build one artifact for the entire cluster (use only RootSync).
  • Build artifacts and sync to cluster and individual namespaces (use RootSync and RepoSync).

Syncing Cluster to Single OCI Artifact

ACM treats OCI artifacts as immutable snapshots, so the cluster:

  • Pulls the entire artifact
  • ⚠️ Deletes previously synced resources
  • Applies the full new configuration set

Cluster with OCI Artifact Lifecycle

Syncing Cluster’s namespaces to OCI Artifacts

The previous example applies in this scenario as well because if you want to use RepoSync for individual namespaces you should be using RootSync for cluster wide types of resources or configurations.

Great news is that we can actually reuse what we just built for the cluster wide resources but apply it to individual namespaces (small tweaks to the pipeline to update a RepoSync instead of RootSync)

Namespaces with OCI Artifact Lifecycle

Summary

A lot of the following pros and cons are really relative. Sometimes it’s a “pro” to be immutable and some other times it isn’t. To me TLDR is: if you want something simple go with Git Repo Sync; if you want something more flexible and reusable, specially in a scenario you might want to reuse these artifacts across many clusters go with OCI Artifact.

Aspect OCI Artifact Sync Git Repository Sync
Source of Truth Immutable container image (e.g., stored in Artifact Registry) Version-controlled Git repository
Immutability ✅ Immutable once pushed (tagged by digest), which ensures consistent config versioning ⚠️ Git branches/tags can change unless locked by commit hash
Release Promotion ✅ Easy to promote artifacts between environments (e.g., dev → staging → prod) ⚠️ Harder to promote Git-based releases without extra automation
Atomic Updates ✅ OCI pulls entire snapshot as a single unit ⚠️ Git sync can partially apply changes if not managed carefully
Binary Signing ✅ Can sign and verify OCI artifacts ⚠️ No native signing of Git content (though PGP commit signing exists)
Air-gapped Support ✅ Better suited for air-gapped environments ⚠️ Requires Git access; harder in isolated setups
Readability / Developer UX ⚠️ Artifact is not easily human-readable; but the code to build it is ✅ It’s code in the repository
Change History ⚠️ No native diff/history without extra tooling; only the history in repo which sometimes it’s not accesible if the artifacts are just being distributed and not the repo/code ✅ Native Git versioning, history, blame, etc.
Automation / GitOps Compatibility ⚠️ We need CI to build and push image ✅ Full GitOps flow with pull-based sync
Tooling Ecosystem ⚠️ Requires OCI tooling like cosign, oras, registry access ✅ Broad Git tooling support and integrations
Initial Setup Complexity ⚠️ Requires container registry setup and CI image creation ✅ Easier to get started with simple repo
Real-time Change Tracking ⚠️ No automatic change detection unless you push new tags/digests (need CD to change the version) ✅ Option to auto-sync when commits are made to target branch