The GitOps decision record: why the sync model you chose determines your infrastructure drift detection ceiling and your secret management blast radius
GitOps decisions are made when the team decides that Kubernetes deployments should be driven from a Git repository rather than from CI pipeline kubectl commands, when an engineer reads a blog post about ArgoCD and sets it up over a weekend to improve deployment traceability, and when a new platform engineer joins and replaces a collection of Helm chart CI jobs with a Flux Kustomization that watches the main branch and syncs to the staging cluster. The AI session that handles the initial GitOps setup is tool-installation-oriented and terminates at the working sync: it installs ArgoCD via the kubectl apply of the install manifest, configures an Application resource pointing at the Git repository and the target namespace, pushes a small manifest change to the repository, observes that ArgoCD detects the change and shows the application as Synced in the UI, and closes. The GitOps tool works. The session succeeds. The session closes.
What the AI session does not produce is the second half of the GitOps decision. The session answers "how do I get ArgoCD to deploy from a Git repository?" and delivers a working ArgoCD installation with a first successful sync visible in the UI. It does not ask: what happens to a direct kubectl apply made during an incident — does ArgoCD's self-healing behavior overwrite the manual change within the next reconciliation interval, and does the engineer making the change know that it will be overwritten; what is the canonical mechanism for managing secrets in the GitOps repository, and has any engineer been told that committing a Kubernetes Secret YAML file with base64-encoded values is equivalent to committing the plaintext value because base64 is not encryption; what is the reconciliation interval, and does the team know that the default ArgoCD resync period means a manual change will be reverted within 3 minutes without producing any error or log entry visible to the engineer who made the change; what is the GitOps rollback procedure for a bad deployment, and how does it differ from kubectl rollout undo (which produces drift that self-healing will immediately overwrite) and from ArgoCD's UI rollback button (which creates persistent drift between the cluster state and the Git repository HEAD); what resources are in scope for GitOps management and which are managed outside of GitOps (Terraform-managed IAM roles, manually applied CRDs from vendor Helm charts, manually configured cluster-level resources), and is the boundary between GitOps-managed and non-GitOps-managed resources documented so that a future engineer does not manage the same resource in both systems and produce a reconciliation conflict; what is the promotion model for environment progression (development to staging to production), and who is authorized to approve a production promotion. Each of these questions has an answer that is not derivable from "an application shows as Synced in the ArgoCD UI" as the session completion criterion. Each answer determines whether an on-call engineer's emergency kubectl patch survives to stabilize the system or is silently overwritten by the next reconciliation cycle while the engineer investigates why the fix isn't taking effect, whether a database credential committed to the GitOps repository as a Kubernetes Secret YAML exposes that credential to every person and system with read access to the repository and its git history in perpetuity, whether a misconfigured manifest pushed to the main branch produces a PagerDuty alert within 5 minutes or goes undetected for hours because reconciliation failure notification was never configured, and whether the rollback procedure in the incident runbook is "git revert the offending commit and push" or "kubectl rollout undo deployment/api" — the second of which is silently overwritten by ArgoCD within the reconciliation interval. The answers are in the AI sessions. They are almost never written down.
Two ways GitOps decisions produce the wrong outcome in production
The reconciliation loop that silently reverted every incident fix for two hours
A developer tools SaaS adopts ArgoCD in month 14 after launch. The team of nine engineers is deploying via a GitHub Actions workflow that runs helm upgrade --install on every push to the main branch, which works but provides no visibility into the current cluster state and no audit trail linking deployed versions to Git commits. A senior engineer spends a Saturday afternoon setting up ArgoCD: installs ArgoCD into the cluster's argocd namespace using the stable install manifest, creates Application resources for the api, worker, and frontend deployments, configures automated sync with self-healing enabled as part of the standard recommended configuration in the ArgoCD getting-started guide, verifies that pushing a change to the Helm values in the repository causes ArgoCD to detect the diff and apply the change within 3 minutes, and updates the GitHub Actions workflow to remove the helm upgrade step. The ADR she writes for the migration covers the tool selection rationale (ArgoCD over Flux based on the team's preference for a UI), the repository structure (a monorepo with a helm/ directory containing the chart and a values/ directory with per-environment values files), and the access control model (ArgoCD syncs using a ServiceAccount with cluster-admin permissions scoped to the application namespaces). The ADR does not document the self-healing behavior, the reconciliation interval, or the implication that direct kubectl changes to the cluster will be overwritten by ArgoCD during the next reconciliation cycle. The session concludes that "ArgoCD is now managing deployments" and does not record that "all cluster changes must now be made via Git commits, including emergency changes during active incidents, because self-healing will revert any direct kubectl changes within 3 minutes."
Seven weeks later, the api Deployment experiences a memory leak in a background job processor that was introduced in a dependency upgrade. The P99 memory usage of the api pods spikes from 320MB to 940MB over a 45-minute window on a Wednesday evening. The on-call engineer receives a PagerDuty alert for pod OOMKills. He investigates the logs, identifies the background job processor as the source of the memory growth, and determines that the fastest mitigation is to set the PROCESS_BACKGROUND_JOBS environment variable to false on the running Deployment — disabling the background job processor on the running pods while the engineering team prepares a proper fix. He runs kubectl set env deployment/api PROCESS_BACKGROUND_JOBS=false -n production. Kubernetes applies the change, the pods restart with the updated environment variable, the OOMKills stop, the alert clears. The engineer updates the PagerDuty incident with the mitigation applied and begins investigating the root cause in the dependency changelog.
Three minutes and twelve seconds later, ArgoCD completes its resync cycle. The ArgoCD controller compares the cluster state (api Deployment with PROCESS_BACKGROUND_JOBS=false) against the Git repository state (Helm values file with no PROCESS_BACKGROUND_JOBS override, which means the environment variable is not set at all). ArgoCD detects a diff and, because self-healing is enabled, applies the Git-defined state to the cluster. The api pods restart with the environment variable removed — equivalent to PROCESS_BACKGROUND_JOBS=true, the default — and the memory leak resumes. ArgoCD logs a sync event with the message "synced successfully" and marks the application as Synced. The engineer's PagerDuty incident, which was marked as resolved after the mitigation, receives a new OOMKill alert 8 minutes later as the memory usage climbs again.
The engineer re-applies the kubectl set env command. ArgoCD reverts it again 3 minutes later. This loop repeats three times over the next 40 minutes. The engineer escalates to a second engineer, describing "a system issue where my kubectl changes are not persisting — the pods keep reverting to the bad configuration." The second engineer checks the Kubernetes event log, the Deployment revision history, and the container environment variables across restarts, none of which show ArgoCD's involvement because ArgoCD's sync applies as a clean Kubernetes apply with no distinguishing annotation. The second engineer checks the ArgoCD UI and sees the application showing as Synced with a sync timestamp of 2 minutes ago. She checks the ArgoCD sync history and finds a pattern: the application synced 3 minutes after the first kubectl set env, 3 minutes after the second, and 3 minutes after the third. The sync is overwriting the manual changes. The mitigation is to push a commit to the Git repository with PROCESS_BACKGROUND_JOBS: false in the Helm values file, which ArgoCD detects and syncs, applying the change persistently because it now matches the Git state. Total incident duration: 2 hours 14 minutes, of which 1 hour 40 minutes was consumed by the investigation of why manual changes were not persisting and the identification of ArgoCD self-healing as the cause. The root cause of the extended incident was not the memory leak — the initial mitigation was correct and took effect in under 2 minutes — but the absence of a documented operational constraint: that self-healing was enabled, what the reconciliation interval was, and that the correct emergency procedure was to commit to Git rather than kubectl apply.
The base64-encoded database password that lived in the GitOps repository for eleven months
A B2B analytics platform adopts Flux GitOps for its Kubernetes deployments six months after launch. The platform engineer sets up a gitops/ repository containing Flux Kustomizations for each application, a base/ directory with shared resources, and environment overlays for staging and production. The setup session installs the Flux CLI, runs flux bootstrap github to configure the Flux controllers and connect them to the repository, creates Kustomization resources pointing at the application directories, and verifies that a change to a Deployment manifest in the repository is applied to the cluster within 60 seconds. The session is focused on the Flux bootstrap configuration and the Kustomization structure. It does not address secret management. The platform engineer notes "secrets handling TBD" in the repository's README and moves on to migrating the application manifests into the repository.
Two weeks later, a backend engineer needs to deploy a new database connection configuration to the staging environment. The database connection string includes a username and password for the staging PostgreSQL instance. The engineer searches the repository for examples of how secrets are handled in the GitOps setup and finds the "secrets handling TBD" note in the README. She searches online for "Kubernetes secret GitOps" and reads a tutorial that creates a Secret resource YAML file with base64-encoded values in the data field. The tutorial note that "Kubernetes Secrets are base64 encoded, not encrypted" does not register as significant — the engineer interprets "base64 encoded" as a form of encoding that is part of how Kubernetes handles secrets internally. She creates a secret.yaml file in the gitops/overlays/staging/ directory containing the database connection string base64-encoded in the data field, commits it to the repository, and pushes. Flux applies the Secret to the staging cluster. The database connection works. The deployment succeeds. The secret.yaml file is committed to the repository.
Four months later, the platform team rotates the staging database password after a security review recommends quarterly credential rotation. A backend engineer updates the secret.yaml file with the new base64-encoded password, commits the change, and pushes. The Secret is updated in the cluster. The old password is no longer valid in the database. The commit history in the gitops/ repository now contains both the old and new password values as base64-encoded strings in consecutive commits. The git log retains both values permanently.
Two months after the password rotation, a new engineer joins the team. During his first week, he clones the gitops/ repository to understand the infrastructure setup. He reads the secret.yaml files in the repository. He knows that base64 can be decoded with a single command. He decodes the values in the current secret.yaml out of curiosity to understand what the encoding looks like — and realizes that what appears as a string of random characters is the plaintext database password. He reports this to the platform team. The platform team investigates the repository history and finds three versions of the staging database password, two versions of a Redis connection string, and one version of a third-party API key, all committed as base64-encoded values at various points over the preceding eleven months. The credentials are rotated immediately. The git history is scrubbed using git filter-repo, which requires all existing clones of the repository to be deleted and re-cloned from the scrubbed origin. The remediation takes two days of engineering time, coordination with all engineers who have cloned the repository, verification that no CI/CD system has a local clone with unrevoked authentication, and a security review to assess whether the credentials were accessed by anyone with repository read access who was not authorized. The initial GitOps setup session's "secrets handling TBD" note was the root cause: there was no documented policy specifying that Kubernetes Secret YAML files must not be committed to the repository in any form — plaintext or base64 — and no mechanism specified for handling secrets in a GitOps-compatible way. Any engineer who needed to deploy a secret did what the first engineer did: searched for examples, found the standard Kubernetes Secret YAML format, and used it.
Three structural properties that GitOps decisions determine
The sync model and the drift detection ceiling
The sync model specifies the reconciliation trigger (continuous poll on an interval versus event-driven sync on Git webhook), the reconciliation interval for poll-based sync, the self-healing behavior when the cluster state drifts from the Git-defined state, the manual change override policy during incidents, and the scope of resources under GitOps management. Without a documented sync model, the team's understanding of GitOps is "the cluster reflects what is in Git" — a model that is accurate for normal operations and silently wrong during incidents, because the team's muscle memory for infrastructure changes (kubectl apply, kubectl edit, kubectl set) produces changes that are overwritten within the reconciliation interval without the engineer being notified. The sync model must specify six properties. First, the reconciliation trigger and interval: ArgoCD polls the Git repository on a configurable interval (default 3 minutes, set by application.resync-period in the argocd-cm ConfigMap); webhook-triggered sync reacts to Git push events via a webhook configured in the Git hosting provider and processed by ArgoCD's API server, producing near-instant sync without polling overhead; the reconciliation interval must be specified in the decision record because it determines the maximum drift window — a cluster change that is not reflected in Git will be reverted within one reconciliation interval, and the engineer making the change must know the interval to predict when the revert will occur. Second, the self-healing behavior: ArgoCD's syncPolicy.automated.selfHeal setting controls whether ArgoCD reverts cluster state to match Git state after a drift is detected; Flux's default behavior is always self-healing (Flux continuously reconciles the cluster to match the Git state without a separate self-healing toggle); the self-healing behavior must be documented explicitly as "any direct kubectl change to a managed resource will be reverted within N minutes" rather than as "ArgoCD automated sync is enabled," because the operational implication of self-healing is different from the operational implication of automated sync, and engineers who have worked in non-GitOps Kubernetes environments will not infer the self-healing behavior from "automated sync enabled." Third, the manual change policy for incidents: the decision record must specify the correct emergency procedure when the cluster needs to be changed immediately and the Git repository CI pipeline approval process would take too long — whether the team suspends ArgoCD's automated sync during the incident (via argocd app set
The secret management model and the plain-text exposure surface
The secret management model specifies the mechanism for managing secret values in the context of a GitOps workflow where all Kubernetes resources are defined as YAML manifests in a Git repository, the scope of which secret types use which mechanism, the rotation procedure in the GitOps context, and the recovery procedure if the cluster-bound decryption key (Sealed Secrets private key, SOPS age private key) is lost. Without a documented secret management model, the GitOps repository's convention for secrets is whatever each engineer did when they first needed to commit a secret resource — which, in the absence of a specified mechanism, is the path of least resistance: a Kubernetes Secret YAML with base64-encoded values, which are not encrypted and which expose the plaintext value to anyone with read access to the repository or its git history. The secret management model must specify six properties. First, the canonical secret mechanism for the platform: the choice between Sealed Secrets, External Secrets Operator (ESO), SOPS, and Vault Agent Injector must be made once and applied consistently across all applications and environments; using multiple mechanisms produces a secret management audit surface where some secrets are managed by Sealed Secrets (encrypted ciphertexts in Git, decryptable only by the cluster controller), some by ESO (references to external secret store paths, no values in Git), and some by ad hoc base64-encoded Secret YAMLs (plaintext values in Git), and the security properties of the ad hoc group determine the platform's actual security posture regardless of the controls applied to the other groups. Second, the secret type taxonomy: different secret types have different lifecycle characteristics that affect mechanism selection; long-lived infrastructure credentials (database passwords, TLS private keys, third-party API keys) change infrequently and benefit from the External Secrets Operator's centralized rotation model where a single secret store update propagates to all consuming clusters; application configuration values that are treated as secrets for policy reasons but rarely contain true credentials (feature flag override values, internal service URLs marked as sensitive) may be appropriate for Sealed Secrets because they do not need the full external secret store infrastructure; the type taxonomy must specify which mechanism applies to which category of secret. Third, the rotation procedure in the GitOps context: for ESO-managed secrets, rotation requires updating the value in the external secret store (AWS Secrets Manager, Vault) and the ExternalSecret resource's refreshInterval determines when the ESO controller refreshes the Kubernetes Secret from the store; if the interval is 1 hour, a rotated value in the secret store takes up to 1 hour to propagate to the cluster Secret; the rotation procedure must specify whether the team uses the ESO force-refresh annotation to trigger immediate refresh after rotation or whether the interval-based refresh is sufficient; for Sealed Secrets, rotation requires re-encrypting the new value with kubeseal, committing the updated SealedSecret to Git, and waiting for ArgoCD/Flux to sync; the rotated ciphertext in Git history reveals that the previous ciphertext is no longer valid, but does not expose the previous plaintext value because the ciphertext is encrypted rather than base64-encoded. Fourth, the cluster decryption key backup: Sealed Secrets stores its private key in a Kubernetes Secret in the sealed-secrets namespace; if the cluster is destroyed and the Secret was not backed up, all SealedSecrets in the repository become permanently undecryptable, and every sealed secret must be re-sealed with a new cluster key; the backup procedure for the Sealed Secrets controller key must be documented, scheduled, and tested with a restoration exercise, because a key loss that is discovered during cluster recovery after a catastrophic failure is the worst time to discover that the key was not backed up; similarly, a SOPS age private key that is stored only as a Kubernetes Secret without an external backup produces the same irrecoverable state if the Secret is deleted. Fifth, the secret scope management: Kubernetes Secrets are namespace-scoped; a Secret in the production namespace cannot be referenced by a resource in the staging namespace; in a GitOps monorepo with per-environment overlay directories, the same logical secret (database password for the api service) must be defined separately in the staging and production overlays; the secret management model must specify how the same logical secret is managed across environments — whether the ESO ExternalSecrets reference different paths in the secret store for each environment (preferred because each environment has an independent value and independent rotation), or whether the same value is shared across environments via a shared secret store path (which means that staging and production share the same database password, a security policy violation in most compliance frameworks). Sixth, the secret access audit model: who can read the secret values in the canonical secret store is not the same as who can read the Kubernetes Secret that the ESO controller creates in the cluster; the access audit model must specify both the secret store access control (which IAM roles, Vault policies, or GCP IAM bindings can read which secret paths) and the Kubernetes RBAC bindings that control which service accounts, users, and controllers can read Kubernetes Secret objects in each namespace; in a GitOps environment where the ESO controller has broad secret store read access, a misconfigured ExternalSecret that references a production secret path while being deployed to the staging namespace creates a cross-environment secret exposure that does not require any IAM misconfiguration — the ExternalSecret resource's path reference is the exposure mechanism. The secrets management decision record specifies the secret store technology and the credential lifecycle model; the GitOps secret management model is the integration layer between the secret store and the Kubernetes workloads, and must be specified as a joint concern of the secrets management decision record and the GitOps decision record, because the two decisions determine together whether secret values are ever committed to Git in any form, whether the cluster decryption key is backed up and tested, and whether secret rotation requires a GitOps commit, a secret store update, or both.
The reconciliation failure model and the incident response floor
The reconciliation failure model specifies the alert configuration for GitOps controller failures, the escalation path when a manifest push produces a reconciliation error, the rollback procedure for bad deployments, and the promotion model for environment progression. Without a documented reconciliation failure model, a broken manifest pushed to the GitOps repository produces a silent failure: ArgoCD or Flux enters a Degraded state, stops applying changes from the repository, and waits; the broken application continues running on the previous configuration (which may or may not be the desired behavior during the failure window); no alert fires; the failure is discovered when an engineer checks the ArgoCD UI for an unrelated reason or when a second manifest push also fails and an engineer investigates why the change did not apply. The reconciliation failure model must specify six properties. First, the alert configuration for controller health: ArgoCD exposes Prometheus metrics for application health (argocd_app_info with a health_status label), sync status (argocd_app_sync_total with a phase label), and controller errors; Flux exposes Prometheus metrics for reconciliation success and failure per Kustomization and HelmRelease; both controllers must have alerting configured that fires when any managed application is in a non-healthy or non-synced state for longer than two reconciliation intervals — a threshold that allows for transient git connectivity issues or API server load spikes without paging, while ensuring that a persistent reconciliation failure produces an alert within 10-15 minutes; the alert routing must specify whether the alert goes to PagerDuty (for production environment failures) or Slack (for staging environment failures), and the on-call runbook must include the steps to diagnose a GitOps reconciliation failure. Second, the rollback procedure: the GitOps rollback procedure is a new Git commit that reverts the breaking change — either a git revert
Three AI session types that embed GitOps decisions without documenting them
The initial GitOps setup session is where the sync model, self-healing behavior, and secret management policy are established without being named as decisions. The session is tool-installation-oriented and terminates at the working sync: install ArgoCD or run flux bootstrap, configure a repository connection, create an Application or Kustomization resource, verify that a manifest change is applied to the cluster, and close. The self-healing configuration is applied as part of the recommended setup — ArgoCD's getting-started guide, the Flux quick-start documentation, and most AI-generated setup guides include automated sync with self-healing as the default configuration because it produces the intended GitOps behavior; but the operational implication — that manual kubectl changes will be overwritten — is never explained to the engineer receiving the setup guide, because the session terminates at "it works" and not at "here is what changes in how your team makes cluster changes." The secret management decision surfaces as a TODO: the initial setup focuses on the GitOps tooling configuration and does not provision a secret management mechanism; the first engineer who needs to commit a secret to the repository encounters the TODO or the README gap and implements whatever mechanism is locally available, which in the absence of a specified mechanism is the standard Kubernetes Secret YAML with base64-encoded values. The initial setup session result is "manifests in Git are applied to the cluster" — not "automated sync is enabled with self-healing every 3 minutes, meaning all emergency cluster changes must be committed to Git; secrets are managed via External Secrets Operator using the production/staging paths in AWS Secrets Manager with the ESO controller's IAM role arn:aws:iam::123456789:role/eso-reader, and no secret values may be committed to the repository in any encoding; the reconciliation failure alert is configured as a PagerDuty Low priority alert firing when any production application is in a non-synced state for more than 6 minutes." The second result requires treating each configuration choice as an operational commitment with documented implications for the team's incident response, secret handling, and deployment workflow, rather than as a technical setup step with a binary working/not-working completion criterion. The new CTO onboarding problem with GitOps is that the incoming technical leader inherits a GitOps configuration — automated sync enabled, self-healing enabled, reconciliation interval undocumented, secret management undocumented — without knowing whether the cluster has ever experienced the "manual fix that disappeared" incident, whether there are base64-encoded secrets in the git history that pre-date the current secret management convention, or whether the reconciliation failure alert goes to a Slack channel that is monitored during business hours only or to PagerDuty that pages at 3am.
The infrastructure migration session embeds GitOps scope decisions as resource-by-resource additions without documenting the cumulative scope or its boundaries. The session is triggered by the need to migrate an existing application from manual kubectl deployments to GitOps management: the engineer exports the existing Deployment, Service, and ConfigMap resources from the cluster using kubectl get -o yaml, cleans up the cluster-assigned fields (resourceVersion, uid, creationTimestamp), commits the cleaned manifests to the GitOps repository, creates an ArgoCD Application resource pointing at the directory, and confirms that ArgoCD shows the application as Synced. The session succeeds in migrating one application. It does not document which resources are now under GitOps management versus which remain managed by other systems (the Helm releases still managed by the previous CI pipeline, the cluster-level resources managed by Terraform, the CRDs managed by the cert-manager and external-secrets Helm charts that were installed separately). The boundary between GitOps-managed and non-GitOps-managed resources grows more complex with each migration session and is never explicitly documented. The consequence is a conflict: an engineer who notices that the cert-manager Certificate resource in the cluster does not match the Certificate resource committed to the GitOps repository (because cert-manager updated the Certificate's status fields and ArgoCD is comparing the full resource including status fields rather than only the spec fields) opens an ArgoCD Application that shows OutOfSync due to the status field difference, triggers a sync that overwrites the Certificate resource, and inadvertently disrupts the cert-manager renewal cycle. The infrastructure decision record specifies what infrastructure is managed by Terraform versus other mechanisms; the GitOps scope boundary is the infrastructure decision complement for Kubernetes resources, specifying which resource types and namespaces are under GitOps management and which are excluded from the scope, so that ArgoCD's sync policies are not applied to resources that have other controllers managing their state.
The incident response session embeds GitOps operational decisions as on-call discoveries that are never written back to documentation. The session is triggered by a production incident where the on-call engineer needs to make an emergency cluster change. The engineer applies the change directly with kubectl, observes that it is reverted (or does not observe this and is confused about why the incident symptoms are persisting), escalates to a second engineer, and together they discover the self-healing behavior and develop an ad hoc workaround — suspending automated sync, applying the fix, re-enabling sync, committing the fix to Git. The workaround works. The incident resolves. The post-mortem produces an action item: "document the emergency change procedure for GitOps environments." The action item may or may not be completed. If it is completed, it is added to a Confluence page or a README file in the GitOps repository that is not linked from the incident runbook for the specific alert type that fired. The next on-call engineer who encounters a GitOps self-healing reversion during an incident finds the same confusion, because the documentation exists but is not accessible in the context of the specific incident. The decisions never written down in a GitOps setup are the self-healing behavior that determines whether emergency kubectl changes persist or are silently reverted, the secret management mechanism that prevents database credentials from being committed to the Git history in a decodable encoding, and the rollback procedure that specifies git revert as the correct emergency action rather than kubectl rollout undo which self-healing will immediately overwrite.
The five sections of a GitOps decision record
The first section documents the GitOps tool selection and repository structure. The tool selection rationale must specify why ArgoCD, Flux, or an alternative was chosen and what was ruled out — ArgoCD provides a rich UI for visibility into application sync status and health, supports multi-cluster management from a single control plane, and has an active ecosystem of integrations (Argo Rollouts for progressive delivery, Argo Workflows for pipeline orchestration); Flux's design philosophy is CLI-first and UI-optional, favoring composable Kubernetes controllers (Kustomize controller, Helm controller, Image Automation controller) over a monolithic operator; the choice affects the operational model (ArgoCD requires running the ArgoCD API server as a dependency; Flux's controllers are lighter-weight but require CLI-based management in the absence of a UI), the multi-cluster support model (ArgoCD has native multi-cluster management from the ArgoCD UI; Flux requires running a Flux controller in each target cluster), and the image update automation model (Flux's Image Automation controller automatically commits image tag updates to the repository when a new image is detected in the container registry; ArgoCD does not have native image update automation and requires a third-party tool like Argo CD Image Updater or a CI pipeline step). The repository structure must specify whether the team uses a monorepo (single repository for all environments and applications) or a multi-repo (separate repositories per environment, per application, or per cluster); the Kustomize base-and-overlay model or the Helm-chart-per-environment model for managing environment-specific configuration; the directory structure for the repository including the naming conventions for environment directories, application directories, and shared resource directories; and the branch model (single main branch with environment directories versus environment-named branches that each represent the desired state of the corresponding environment). The tooling selection must specify the versions of ArgoCD, Flux, and associated CLI tools that are installed and the upgrade policy — ArgoCD and Flux both have active release cadences with breaking changes between minor versions, and the upgrade policy must specify whether version upgrades are applied as part of a quarterly maintenance window, as automated Dependabot or Renovate PRs, or manually after a major release review. The build system decision record specifies the CI/CD pipeline configuration; the GitOps tool selection determines the integration point between the CI/CD pipeline and the cluster deployment — in a GitOps model, the CI pipeline produces a container image and pushes it to the registry, then either commits an image tag update to the GitOps repository or triggers an ArgoCD sync, and the specific integration pattern (image tag commit versus sync trigger) must be specified to avoid parallel workflows where both the CI pipeline and ArgoCD apply changes to the cluster simultaneously.
The second section documents the sync model specification. The sync model must specify all six properties described in the structural properties section above: the reconciliation trigger and interval, the self-healing behavior with its operational implication stated explicitly, the manual change policy for incidents including the exact commands for suspending and re-enabling automated sync, the GitOps scope boundary with the resource types and namespaces under management, the drift detection alert configuration including the threshold and routing, and the resync and force-sync procedures for incident response. The sync model specification is the section of the decision record most likely to be consulted by an on-call engineer at 2am, and must therefore be written with the operational consumer in mind: procedural steps, not architectural descriptions; the exact commands, not a reference to the ArgoCD or Flux documentation; the expected output of each command that confirms the step succeeded; and the rollback step for each procedure in case the step has unintended consequences. The access control model decision record specifies the team's RBAC policy for Kubernetes cluster access; the GitOps model changes the relationship between Git repository access and cluster deployment permissions — in a GitOps environment, cluster deployment requires Git repository write access rather than Kubernetes RBAC cluster-admin; the access control decision record must be updated to reflect this change, specifying which engineers can push to which branches of the GitOps repository (which determines who can deploy to which environment), and what the emergency access procedure is for engineers who need to make direct cluster changes without going through the Git workflow.
The third section documents the secret management model. The secret management model must specify the canonical mechanism (Sealed Secrets, ESO, SOPS, or Vault Agent Injector) with the rationale for the choice, the scope of each mechanism if multiple mechanisms are used (e.g., ESO for database credentials and third-party API keys, Sealed Secrets for application configuration values that are sensitive but not credential-grade), the explicit prohibition on committing Kubernetes Secret YAML files with base64-encoded values to the repository, the rotation procedure for each secret type including the expected propagation time from secret store update to cluster Secret refresh, the cluster decryption key backup procedure and test schedule, the cross-environment isolation policy specifying that staging and production secrets must reference separate paths in the secret store with separate values, and the audit policy for who can read which secret store paths and who can read which Kubernetes Secrets in each namespace. The secret management model must also specify what to do with existing base64-encoded secrets that were committed to the repository before the secret management policy was established — the remediation process (identify all Kubernetes Secret YAML files in the repository history, rotate all committed credentials, remove the files from git history using git filter-repo, require all engineers to re-clone the repository, verify that no CI/CD cache or artifact store has a copy of the repository with the pre-remediation history). The security ADR and threat model decision record specifies the team's threat model; the GitOps secret management model must be evaluated against the threat model by considering the blast radius of each attack surface: compromise of the Git repository read access (who can read the repository, and what information is exposed if they can — with base64-encoded secrets committed, the blast radius is all committed credentials; with ESO references, the blast radius is the ESO controller's IAM role permissions), compromise of the cluster decryption key (Sealed Secrets private key or SOPS age key), and compromise of the ESO controller's IAM role or service account.
The fourth section documents the rollback and environment promotion model. The rollback model must specify the procedure for each rollback scenario: application image rollback (git revert the image tag commit, or argocd app rollback for immediate cluster-to-previous-state with subsequent git revert to resolve the drift), configuration rollback (git revert the configuration change), database migration rollback (the migration rollback procedure if the application version rollback is backward-compatible with the rolled-back schema, or the schema rollback procedure if it is not), and cluster-level resource rollback (restoring the previous version of a CRD or cluster-level configuration). The promotion model must specify the workflow for each environment transition: who initiates the promotion, what validation gates the staged deployment must pass before promotion (smoke tests, load tests, integration tests, manual review), what the PR review requirements are for the production promotion PR, what the expected time from staging deployment to production deployment is under the normal promotion workflow, and what the fast-path promotion procedure is for urgent security fixes that cannot wait for the standard review cycle. The rollback and promotion model must address the interaction between GitOps and database schema migrations: a GitOps deployment that includes a database migration job (Kubernetes Job that runs the migration before the application Deployment is updated) creates a promotion unit where the migration and the application version are applied together; rolling back the application version without rolling back the migration leaves the database in the migrated state that the previous application version may not tolerate; the decision record must specify whether the application version and the schema migration are designed as a backward-compatible unit (the previous application version works with the new schema), whether the database migration is run before or after the application deployment in the sync wave configuration, and what the rollback procedure is if the migration was applied but the application deployment failed. The distributed tracing decision record specifies the OTEL Collector DaemonSet configuration; OTEL Collector configuration changes managed by the GitOps repository — changes to the collector pipeline, the sampling processor policy, the exporter endpoints — are subject to the same GitOps rollback procedure as application changes, and the self-healing behavior that prevents direct kubectl changes to collector configuration must be noted in the OTEL Collector runbook alongside the GitOps emergency change procedure.
The fifth section documents the reconciliation failure model and observability. The observability model must specify the Prometheus metrics exported by ArgoCD or Flux that are scraped and stored in the team's metrics platform, the alert rules configured on those metrics, and the Grafana dashboards or equivalent visibility into the current sync status and health of each managed application. ArgoCD exports metrics including argocd_app_info (labels: name, namespace, dest_namespace, dest_server, project, repo, health_status, sync_status — useful for alert rules that fire when health_status != Healthy or sync_status != Synced), argocd_app_sync_total (sync attempt count with phase label), argocd_app_reconcile_count (reconciliation count per application), and argocd_cluster_api_resource_objects (the number of managed resources per cluster). Flux exports metrics including gotk_reconcile_duration_seconds (reconciliation duration histogram per Kustomization and HelmRelease), gotk_reconcile_condition (reconciliation result with a type label, Ready condition with a status label — False means failure), and gotk_suspend_status (whether a Kustomization is suspended). The alert rules must be specified by condition, threshold, severity, and routing: a production ArgoCD application with health_status="Degraded" for more than 5 minutes is a PagerDuty High alert; a production ArgoCD application with sync_status="OutOfSync" for more than 10 minutes is a PagerDuty Low alert; a staging application with either condition is a Slack alert to the #platform channel. The reconciliation failure runbook must specify the diagnosis steps for each error category: a manifest syntax error (kubectl apply --dry-run=client produces an error message identifying the problematic field); a resource conflict (ArgoCD's UI shows the specific resource that failed to apply with the Kubernetes API error); an image pull error (the pod enters an ImagePullBackOff state, which is visible in argocd_app_info health_status as "Degraded" and diagnosable via kubectl describe pod); a sync timeout (the sync operation exceeded the ArgoCD configurable sync timeout, defaulting to 20 minutes — common for database migration jobs that run during the sync phase). The access control review model must specify the cadence for reviewing who has write access to the GitOps repository, who has access to the GitOps controller's ServiceAccount credentials, and whether any changes to the repository access control model (new engineers joining, contractors given temporary access) require a corresponding review of the scope of access granted. The new CTO onboarding problem with the GitOps observability model is that the incoming technical leader inherits a GitOps setup where ArgoCD is running and applications are showing as Synced in the UI, but the reconciliation failure alert is configured to go to a Slack channel that is no longer monitored, the ArgoCD API server TLS certificate expired two months ago and was renewed by an engineer without documenting the renewal procedure, the ArgoCD admin password was set during the initial setup session and has never been rotated, and the ArgoCD controller's ServiceAccount has cluster-admin permissions that were granted during the setup session's "simplest configuration that works" approach and never scoped to the minimum required permissions.
The initial GitOps setup session that installed ArgoCD with automated sync and self-healing enabled without documenting that manual kubectl changes would be overwritten within 3 minutes, the infrastructure migration session that moved application manifests to the GitOps repository without specifying a secret management mechanism and leaving a README note of "secrets handling TBD" that the first engineer who needed to commit a secret interpreted as permission to use base64-encoded Kubernetes Secret YAML files, and the incident response session where the on-call engineer discovered the self-healing behavior by experiencing it and produced an ad hoc workaround that was never written into the runbook — each produced GitOps decisions whose operational consequences (a 2-hour incident extension caused by manual kubectl mitigations being silently overwritten by the reconciliation loop; eleven months of base64-encoded database credentials visible in git history to every engineer who cloned the repository; reconciliation failure alerts configured to a Slack channel with no after-hours monitoring, producing a window of hours between a bad manifest push and its detection) exceed what documenting the self-healing behavior with its operational implication as an incident response constraint, specifying the secret management mechanism before the first secret was committed to the repository, and configuring reconciliation failure alerts with the same escalation path as application health alerts would have cost to specify at the time the founding GitOps decisions were made. The decisions are in the AI sessions: the self-healing configuration applied as part of the recommended setup without noting that it converts all emergency cluster changes into a Git workflow requirement, the base64-encoding used in the first Kubernetes Secret YAML without the engineer knowing that base64 is not encryption and that the value is readable by any party with repository read access or git history access, the reconciliation interval left at the default without documenting it as the maximum duration for a manual kubectl fix before ArgoCD reverts it. WhyChose's open-source extractor surfaces these initial setup sessions, infrastructure migration sessions, and incident response sessions as structured decision records before an on-call engineer spends two hours investigating why their manual kubectl fix keeps disappearing, before a new engineer decodes the base64-encoded values in a secret.yaml file and discovers that the database password is visible in plain text and in the git history, and before a bad manifest push produces a reconciliation failure that goes undetected for six hours because the alert routing was left at the default Slack channel that nobody monitors at midnight. The decisions never written down in a GitOps setup are the self-healing behavior that determines whether emergency kubectl changes persist or are silently overwritten, the secret management policy that determines whether base64-encoded credentials accumulate in the git history, and the reconciliation failure alert that determines whether a broken manifest push is detected in 5 minutes or in the morning standup when someone notices that last night's deployment did not apply.
Further reading
- The container orchestration decision record — GitOps operates on Kubernetes manifests; the cluster configuration, RBAC model, and namespace structure specified in the container orchestration decision record determine the scope of resources available for GitOps management and the permissions that must be granted to the GitOps controller's ServiceAccount
- The secrets management decision record — the GitOps secret management model is the integration layer between the secret store and the Kubernetes workloads; the choice between Sealed Secrets, External Secrets Operator, SOPS, and Vault Agent Injector must be made before the first secret is needed in the GitOps repository, because the git history permanently retains any values committed in any encoding until a git filter-repo scrub
- The deployment strategy decision record — GitOps is a deployment model; the rollback procedure, the backward compatibility constraint for rolling deployments, and the blue/green flip procedure each have GitOps-specific implementations that differ from the kubectl rollout undo and kubectl apply equivalents in non-GitOps environments
- The infrastructure decision record — GitOps manages Kubernetes resources; Terraform manages cloud infrastructure; the boundary between GitOps-managed resources and Terraform-managed resources must be documented to prevent the same resource from being managed by both systems and producing a perpetual reconciliation conflict
- The security ADR and threat model decision record — a GitOps repository containing base64-encoded secrets is a critical security finding in any SOC 2, ISO 27001, or PCI-DSS audit; the GitOps adoption decision must be evaluated against the threat model by assessing the blast radius of each secret management mechanism before the first secret is committed
- The access control model decision record — GitOps converts cluster deployment authorization into Git repository write access authorization; the access control model decision record must be updated to reflect this change and specify the mapping from Git branch write permissions to cluster environment deployment permissions
- The build system decision record — the CI/CD pipeline produces the container images that the GitOps repository references; the image tag update strategy — whether the CI pipeline commits image tag updates to the GitOps repository or uses Flux ImageUpdateAutomation or ArgoCD Image Updater — is the integration point between the build system and the GitOps sync model
- The distributed tracing decision record — the OTEL Collector DaemonSet and its configuration are managed by the GitOps repository; the self-healing behavior that prevents direct kubectl changes applies to the collector configuration, and the emergency change procedure for collector configuration during a tracing incident must reference the GitOps emergency change workflow rather than direct kubectl apply
- The new CTO onboarding problem — the incoming technical leader inherits a GitOps configuration that shows all applications as Synced in the ArgoCD UI without documentation of whether self-healing is enabled, what the reconciliation interval is, whether the secret management mechanism prevents base64-encoded credentials from accumulating in the git history, or whether the reconciliation failure alert routes to PagerDuty or to an unmonitored Slack channel
- Decisions never written down — the self-healing behavior that silently reverts manual kubectl changes within the reconciliation interval, the secret management policy that determines whether the git history permanently retains base64-encoded credentials, and the reconciliation failure alert routing that determines whether a broken manifest is detected in five minutes or the following morning are founding GitOps decisions made in the initial setup session, the first infrastructure migration session, and the first incident response session whose operational consequences only manifest when an on-call engineer's fix disappears three minutes after being applied, when a new engineer decodes a secret.yaml and reads the database password, or when a bad manifest push goes undetected for six hours