GitOps

quality

GitOps

Definition and Overview

GitOps is an operational framework that uses Git repositories as the single source of truth for declarative infrastructure and application configuration. All changes to infrastructure, deployments, and operational state are made exclusively through Git commits, pull requests, and automated reconciliation loops. The core principle is that the desired state of every system is declared in version-controlled files, and automated agents continuously reconcile the actual state of deployed environments with the declared state in Git. GitOps provides complete audit trails, instant rollback capabilities, reproducible environments, and a unified workflow for both application code and infrastructure changes.

The GitOps model was formalized by Weaveworks in 2017, building on earlier practices of Infrastructure as Code (IaC) and Continuous Delivery. What distinguishes GitOps from general IaC is the emphasis on Git as the sole mechanism for making changes -- operators do not SSH into servers, click through dashboards, or run ad hoc scripts. Every change, no matter how small, flows through Git. This constraint transforms operations from an imperative activity ("run this command on this server") into a declarative one ("the desired state is X; the system will converge to X automatically").

The reconciliation loop is the heart of GitOps. An automated agent monitors the Git repository for changes and compares the declared state against the actual state of the target environment. When a difference is detected (a "drift"), the agent automatically applies changes to bring the environment into compliance. This model provides self-healing properties: if someone manually modifies a production server, the GitOps agent detects the drift and reverts the change to match the Git-declared state.

GitOps operates on two fundamental models: push-based and pull-based. In push-based GitOps (used by most CI/CD platforms including GitLab CI), the CI pipeline pushes changes to the target environment when commits are merged. In pull-based GitOps (used by ArgoCD, Flux), an agent running inside the target environment pulls changes from Git. Pull-based models provide stronger security guarantees because the target environment does not need to expose inbound access to the CI system.

Historical Context and Evolution

The trajectory from manual server administration to GitOps represents a fundamental shift in how infrastructure is conceived and managed:

EraApproachChange MechanismAudit TrailRollback
1990sManual administrationSSH + scriptsOperator memoryBackup restoration
2000sConfiguration management (Puppet, Chef)Declarative manifestsPartial (run logs)Re-converge to previous state
2010sInfrastructure as Code (Terraform, CloudFormation)Declarative + plan/applyState filesState file rollback
2017+GitOps (Flux, ArgoCD, GitLab)Git commits onlyComplete Git historygit revert

Each transition increased the declarativeness, auditability, and reproducibility of infrastructure management. GitOps represents the current apex of this progression, where the Git repository becomes the canonical representation of system state and Git operations become the sole mechanism for state changes.

Technical Deep Dive

Declarative State Management

The foundation of GitOps is declaring all system state in version-controlled files. For the Prismatic Platform, this encompasses multiple configuration layers:

Configuration LayerFilePurpose
CI/CD Pipeline.gitlab-ci.ymlBuild, test, quality gate, deployment stages
Container ImageDockerfileApplication packaging and runtime environment
Deployment Targetfly.tomlFly.io deployment specification (scaling, regions, services)
Application Configconfig/*.exsElixir application configuration per environment
Infrastructuredocker-compose.ymlLocal development environment specification
Quality Gates.credo.exsStatic analysis configuration
Git Hooks.githooks/*Pre-commit, commit-msg, pre-push enforcement scripts
Agent Definitions.aiad/agents/*.agent.mdAIAD agent specifications and configurations

Each of these files is version-controlled, reviewed through merge requests, and applied automatically by the reconciliation pipeline. No configuration exists only in a deployment dashboard or SSH session.

Reconciliation Architecture

The GitOps reconciliation loop follows a four-phase cycle:

Phase 1: Observe          Phase 2: Diff             Phase 3: Act              Phase 4: Verify
+--------------+         +--------------+          +--------------+          +--------------+
| Monitor Git  |-------->| Compare      |--------->| Apply        |--------->| Health Check |
| repository   |         | desired vs   |          | changes to   |          | and confirm  |
| for changes  |         | actual state |          | target env   |          | convergence  |
+--------------+         +--------------+          +--------------+          +--------------+
       ^                                                                            |
       |                                                                            |
       +----------------------------------------------------------------------------+
                                    Continuous Loop

In the Prismatic Platform's push-based model, GitLab CI serves as the reconciliation agent. When a commit is merged to the main branch, the pipeline executes the full quality enforcement sequence, builds a new container image, and deploys it to the target environment. The pipeline only proceeds to deployment if all quality gates pass, ensuring that the deployed state always reflects a quality-verified commit.

Drift Detection and Correction

Drift occurs when the actual state of a deployed environment diverges from the declared state in Git. GitOps systems handle drift through detection and correction:

defmodule PrismaticGitOps.DriftDetector do
  @moduledoc """
  Detects configuration drift between Git-declared state and deployed state.
  Reports discrepancies for automated or manual correction.
  """

  @type drift_report :: %{
    component: String.t(),
    expected: term(),
    actual: term(),
    severity: :info | :warning | :critical,
    detected_at: DateTime.t()
  }

  @spec detect_drift(String.t()) :: {:ok, list(drift_report())} | {:error, term()}
  def detect_drift(environment) do
    with {:ok, declared} <- load_declared_state(environment),
         {:ok, actual} <- query_actual_state(environment) do
      drifts =
        declared
        |> Map.keys()
        |> Enum.flat_map(fn key ->
          case {Map.get(declared, key), Map.get(actual, key)} do
            {same, same} -> []
            {expected, actual_val} ->
              [%{
                component: key,
                expected: expected,
                actual: actual_val,
                severity: classify_severity(key),
                detected_at: DateTime.utc_now()
              }]
          end
        end)

      {:ok, drifts}
    end
  end

  defp classify_severity("security_" <> _), do: :critical
  defp classify_severity("scaling_" <> _), do: :warning
  defp classify_severity(_), do: :info

  defp load_declared_state(environment) do
    config_path = "config/#{environment}.exs"
    fly_config = "fly.toml"

    with {:ok, app_config} <- parse_elixir_config(config_path),
         {:ok, deploy_config} <- parse_fly_toml(fly_config) do
      {:ok, Map.merge(app_config, deploy_config)}
    end
  end

  defp query_actual_state(environment) do
    case System.cmd("fly", ["status", "--app", "prismatic-#{environment}", "--json"]) do
      {output, 0} -> Jason.decode(output)
      {error, _code} -> {:error, {:fly_status_failed, error}}
    end
  end
end

Immutable Deployments

GitOps pairs naturally with immutable deployments, where each deployment creates a new container image rather than modifying an existing one. The Prismatic Platform's Dockerfile defines the complete runtime environment:

# Multi-stage build for reproducible, minimal images
FROM hexpm/elixir:1.19.0-erlang-27.0-debian-bookworm AS build

WORKDIR /app
ENV MIX_ENV=prod

# Dependencies (cached layer)
COPY mix.exs mix.lock ./
COPY apps/*/mix.exs ./apps/
RUN mix deps.get --only prod && mix deps.compile

# Application code
COPY . .
RUN mix release prismatic

# Runtime (minimal image)
FROM debian:bookworm-slim AS runtime
COPY --from=build /app/_build/prod/rel/prismatic ./
CMD ["bin/prismatic", "start"]

Every Git commit that passes quality gates produces a new immutable image tagged with the commit SHA. Rollback is trivial: deploy the image from the previous commit. The Git log provides a complete history of every image that has ever been deployed.

Secrets Management in GitOps

A critical challenge in GitOps is managing secrets (API keys, database credentials, tokens) that cannot be stored in Git. The Prismatic Platform addresses this through GitLab CI variables:

Secret CategoryStorageAccess Control
API TokensGitLab CI Variables (masked, protected)Protected branches only
Database URLsGitLab CI Variables (masked, protected)Protected branches only
Deployment KeysGitLab CI Variables (protected)Deployment jobs only
Encryption KeysFly.io SecretsRuntime environment only

Secrets never appear in Git history, CI logs, or container images. They are injected at deployment time through environment variables, maintaining the GitOps principle while protecting sensitive data.

GitOps and the Pre-Commit Pipeline

The Prismatic Platform extends GitOps principles to the developer workstation through an 11-phase pre-commit pipeline that enforces quality before code even reaches the repository:

defmodule PrismaticGitOps.PreCommitPipeline do
  @moduledoc """
  11-phase pre-commit pipeline ensuring every commit meets
  GitOps quality standards before entering the repository.
  """

  @phases [
    {1, :compilation, "mix compile --warnings-as-errors"},
    {2, :formatting, "mix format --check-formatted"},
    {3, :credo, "mix credo --strict"},
    {4, :dialyzer, "mix dialyzer"},
    {5, :tests, "mix test"},
    {6, :forbidden_patterns, "mix quality.forbidden_patterns"},
    {7, :quality_gates, "mix quality.gates"},
    {8, :template_validation, "scripts/validate-promo-templates.sh"},
    {9, :quality_standard, "mix quality.enforce_standard"},
    {10, :design_consistency, "scripts/validate-design-consistency.sh"},
    {11, :secret_scanning, "scripts/scan-secrets.sh"}
  ]

  @spec run_pipeline(list(String.t())) :: :ok | {:error, {integer(), atom(), String.t()}}
  def run_pipeline(staged_files) do
    @phases
    |> Enum.reduce_while(:ok, fn {phase_num, phase_name, command}, :ok ->
      case execute_phase(phase_name, command, staged_files) do
        :ok -> {:cont, :ok}
        {:error, msg} -> {:halt, {:error, {phase_num, phase_name, msg}}}
      end
    end)
  end
end

Architecture and Implementation

Multi-Environment GitOps Pipeline

The Prismatic Platform operates a three-environment GitOps pipeline:

Developer Workstation          Staging Environment         Production Environment
+--------------------+        +---------------------+    +---------------------+
| Pre-commit hooks   |        | prismatic-staging    |    | prismatic-prod      |
| Local quality gates|        | .fly.dev             |    | .fly.dev            |
| Unit tests         |        |                      |    |                      |
|                    |   CI   | Auto-deploy on merge |  M | Manual trigger after |
| git push ----------|------->| to main branch       |--->| staging validation   |
|                    |        |                      |    |                      |
+--------------------+        | Smoke tests          |    | Health checks        |
                              | Integration tests    |    | Canary deployment    |
                              +---------------------+    +---------------------+
                                                    M = Manual Gate

Staging receives automatic deployments on every merge to main. Production requires explicit manual triggering after staging validation. This two-phase approach balances speed (automatic staging) with safety (manual production gate).

GitOps Compliance Validator

defmodule PrismaticGitOps.ComplianceValidator do
  @moduledoc """
  Validates that all infrastructure and deployment changes follow GitOps principles.
  Ensures no manual modifications exist outside Git-tracked configuration.
  """

  @required_files [
    ".gitlab-ci.yml",
    "Dockerfile",
    "fly.toml",
    "docker-compose.yml",
    ".credo.exs"
  ]

  @spec validate_gitops_compliance() :: {:ok, :compliant} | {:error, list(String.t())}
  def validate_gitops_compliance do
    violations =
      check_required_files() ++
      check_no_manual_overrides() ++
      check_pipeline_coverage() ++
      check_secret_isolation()

    case violations do
      [] -> {:ok, :compliant}
      violations -> {:error, violations}
    end
  end

  defp check_required_files do
    @required_files
    |> Enum.reject(&File.exists?/1)
    |> Enum.map(&"Missing required GitOps file: #{&1}")
  end

  defp check_no_manual_overrides do
    case System.cmd("fly", ["config", "show", "--app", "prismatic-prod", "--json"]) do
      {output, 0} ->
        deployed = Jason.decode!(output)
        declared = parse_fly_toml()

        if configs_match?(deployed, declared),
          do: [],
          else: ["Drift detected: deployed config differs from fly.toml"]

      _ ->
        ["Unable to verify deployment state"]
    end
  end

  defp check_pipeline_coverage do
    case File.read(".gitlab-ci.yml") do
      {:ok, content} ->
        stages = extract_stages(content)
        required = ["compile", "analyze", "test", "quality", "deploy"]

        missing = required -- stages
        Enum.map(missing, &"Missing required pipeline stage: #{&1}")

      {:error, _} ->
        ["Missing .gitlab-ci.yml"]
    end
  end

  defp check_secret_isolation do
    # Verify no secrets in tracked files
    {output, 0} = System.cmd("git", ["ls-tree", "-r", "--name-only", "HEAD"])

    output
    |> String.split("\n", trim: true)
    |> Enum.filter(&secret_file?/1)
    |> Enum.map(&"Secret file tracked in Git: #{&1}")
  end

  defp secret_file?(path) do
    String.ends_with?(path, ".env") or
    String.contains?(path, "credentials") or
    String.contains?(path, "secret")
  end
end

Usage in Prismatic Platform

The Prismatic Platform implements GitOps as its sole operational methodology. No infrastructure or deployment change occurs outside of Git.

GitOps-Managed Components

ComponentGit-Tracked FileReconciliation Method
CI Pipeline.gitlab-ci.ymlGitLab CI auto-applies on push
Container BuildDockerfileCI builds new image on merge
Deployment Configfly.tomlfly deploy in CI pipeline
Quality Enforcement.credo.exs, .githooks/*Pre-commit + CI validation
Agent Definitions.aiad/agents/*.agent.mdAIAD indexer on commit
Application Configconfig/*.exsCompiled into release
Documentationsites/promo/content/*.mdZola build + GitHub Pages deploy

Session Discipline Integration

GitOps principles are enforced in every development session through the mandatory session discipline protocol:

defmodule PrismaticGitOps.SessionCompliance do
  @moduledoc """
  Ensures development sessions follow GitOps principles.
  All changes must flow through Git with proper tracking.
  """

  @spec validate_session_compliance() :: :ok | {:error, list(String.t())}
  def validate_session_compliance do
    checks = [
      check_no_uncommitted_infra_changes(),
      check_pipeline_config_valid(),
      check_deployment_config_tracked(),
      check_hooks_enabled()
    ]

    violations = Enum.flat_map(checks, fn
      :ok -> []
      {:error, msg} -> [msg]
    end)

    case violations do
      [] -> :ok
      violations -> {:error, violations}
    end
  end

  defp check_hooks_enabled do
    if File.exists?(".githooks/pre-commit"),
      do: :ok,
      else: {:error, "Pre-commit hooks not configured"}
  end
end

Rollback Procedure

GitOps enables instant rollback through Git history:

# View deployment history
git log --oneline --format="%h %s" -- fly.toml Dockerfile .gitlab-ci.yml

# Rollback to previous deployment
git revert HEAD --no-edit
git push origin main
# CI pipeline automatically deploys the reverted state

# Emergency rollback (bypasses CI, requires manual trigger)
fly deploy --image registry.fly.io/prismatic-prod:PREVIOUS_SHA

GitOps Maturity Model

Organizations adopt GitOps progressively. The following maturity model describes the stages of adoption:

LevelNameCharacteristicsPrismatic Status
L0ManualSSH administration, undocumented changesSuperseded
L1Version ControlledConfig in Git, but manually appliedSuperseded
L2CI-AutomatedCI applies changes on mergeActive (push-based)
L3Self-HealingDrift detection and automatic correctionPartially implemented
L4Policy-DrivenOPA/Rego policies gate all changesPlanned
L5AutonomousAI-driven infrastructure optimizationResearch phase

The Prismatic Platform operates at L2 with L3 capabilities for specific components. The session discipline protocol and pre-commit pipeline provide additional enforcement layers that go beyond typical L2 implementations.

Best Practices

Declare everything in Git. Every configuration file, deployment specification, pipeline definition, and operational procedure should be version-controlled. If a configuration exists only in a dashboard or environment variable, it is not GitOps-compliant. Document external configurations and track their expected values in Git.

Use merge requests for all changes. Never push directly to the main branch. Merge requests provide review, discussion, and audit trail for every change. Configure branch protection rules to enforce this workflow. The review process catches configuration errors before they reach production.

Implement staged deployment. Deploy to staging automatically on merge, then require manual approval for production. This provides a validation stage where integration tests, smoke tests, and manual verification can occur before production changes. The staging environment should mirror production as closely as possible.

Version container images with commit SHAs. Tag every container image with the Git commit SHA that produced it. This creates a bidirectional link between Git history and deployed artifacts. Given any deployed image, you can immediately find the exact source code and configuration that produced it.

Monitor drift continuously. Even with GitOps, drift can occur through manual interventions, provider-side changes, or automation failures. Implement drift detection that compares deployed state against Git-declared state and alerts on discrepancies.

Enforce GitOps at the developer workstation. Pre-commit hooks that validate configuration files, scan for secrets, and enforce quality standards extend GitOps principles to the earliest stage of the development lifecycle.

Common Pitfalls

Storing secrets in Git. The most dangerous GitOps anti-pattern is committing API keys, database credentials, or tokens to the repository. Even if removed later, secrets remain in Git history. Use external secret management (GitLab CI variables, Vault, cloud provider secret managers) and verify through pre-commit hooks that no secrets are tracked.

Incomplete state declaration. If only part of the infrastructure is declared in Git, the benefits of GitOps are undermined. Manual configurations that exist outside Git become invisible, undocumented, and unrecoverable. Audit all configuration sources and ensure they are tracked.

Ignoring drift after deployment. GitOps does not end at deployment. Without continuous drift detection, manual changes can accumulate in production, creating an ever-widening gap between the declared and actual state. When the next Git-driven deployment occurs, unexpected interactions between the Git state and the drifted state can cause failures.

Overly complex pipeline configuration. GitLab CI YAML has a 10-level nesting limit. Complex pipelines should extract logic to scripts rather than embedding it in YAML. Pipeline configuration should be readable and maintainable, not a demonstration of YAML's capabilities.

Missing rollback procedures. GitOps makes rollback possible through git revert, but teams must practice and document the procedure before a production incident. An untested rollback process is not a rollback process.

Treating GitOps as a tool rather than a discipline. GitOps is not a product you install; it is a set of principles you follow. Installing ArgoCD or Flux does not make an organization GitOps-compliant if teams continue to make manual changes outside of Git.

  • Continuous Integration -- Validation pipeline triggered by Git commits in the GitOps workflow
  • Continuous Deployment -- Automated deployment driven by GitOps reconciliation
  • GitLab CI/CD -- CI/CD platform implementing push-based GitOps reconciliation
  • Docker -- Container runtime producing immutable deployment artifacts
  • Quality Gates -- Automated enforcement ensuring only quality-verified code deploys
  • Fly.io -- Deployment target managed through GitOps configuration
  • Release -- Elixir release packaging integrated into the GitOps deployment pipeline
  • Infrastructure -- Foundational systems managed through GitOps practices

See Also


Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL