If you’re managing infrastructure across multiple environments and still using SSH to manually apply changes, it’s time to rethink your approach. A GitOps guide for sysadmins isn’t about replacing your skills—it’s about scaling your impact through version control and automation. In this comprehensive guide, we’ll walk through what GitOps actually means, why it matters for your infrastructure, and how to implement it without breaking your existing workflows.
GitOps has become the de facto standard for infrastructure management in cloud-native environments, and for good reason. But before you start migrating everything to Git, you need to understand the principles, the tools, and—most importantly—how to avoid the common pitfalls that trap teams in perpetual firefighting mode.
What Is GitOps, Really?
GitOps is a set of practices that use Git as the single source of truth for infrastructure and application deployments. Instead of manually SSH-ing into servers or running ad-hoc Terraform commands, you commit your infrastructure changes to a Git repository, and automated tooling ensures your systems match that desired state.
The core principle is simple: Git becomes your deployment mechanism. Your infrastructure configuration, application manifests, and policy definitions all live in version control, and a reconciliation engine continuously ensures your live environment matches what’s declared in Git.
Here’s what makes GitOps different from traditional Infrastructure as Code (IaC):
- Declarative, not imperative — You describe what the desired state should be, not how to achieve it
- Version-controlled everything — Every change has a commit history, author, and timestamp
- Automated reconciliation — Tools continuously verify that reality matches your Git declarations
- Audit trail by default — Who changed what, when, and why? Check Git history
- Easy rollbacks — Reverting a deployment is just reverting a commit
It sounds straightforward in theory. In practice, the implementation details determine whether GitOps saves you hours each week or introduces new complexity that frustrates your team.
The GitOps Workflow: How It Actually Works
Let’s walk through a realistic scenario. You’re managing a Kubernetes cluster and need to deploy a new version of your payment service.
Traditional workflow (non-GitOps):
1. Dev pushes new code to your container registry
2. You SSH into the Kubernetes cluster
3. You manually update the deployment YAML
4. You run kubectl apply -f payment-service.yaml
5. You verify the pods are running
6. You document this change in a Slack message or ticket
GitOps workflow:
1. Dev pushes new code to the container registry
2. An automation tool detects the new image
3. It automatically creates a pull request updating the manifest in your Git repo
4. You review and approve the PR
5. The GitOps operator applies the manifest automatically
6. Everything is logged in Git history
The second approach is auditable, reversible, and doesn’t require you to be awake at 2 AM to apply a critical patch.
The Pull-Based Model
Most modern GitOps tools use a “pull-based” approach rather than push-based CI/CD pipelines. Here’s why this matters:
In a push-based model (traditional CI/CD):
– Your CI pipeline has credentials to deploy to production
– If the pipeline is compromised, your production is compromised
– The pipeline must have network access to all your environments
In a pull-based model (GitOps):
– A reconciliation agent runs inside your cluster/infrastructure
– It pulls the desired state from Git and applies it
– Your CI system only needs to push to the container registry
– The agent only needs read access to your Git repository
– You have fewer secrets scattered across your infrastructure
This architectural difference is why GitOps is considered more secure than traditional CI/CD for infrastructure deployments.
Key GitOps Tools for Infrastructure Management
Flux CD
Flux is a lightweight GitOps operator designed specifically for Kubernetes. It’s CNCF-incubated and excellent for teams wanting a minimal, declarative approach.
Strengths:
– Extremely lightweight (runs in a few hundred MB)
– Excellent documentation for Kubernetes-native workflows
– Built-in support for Helm, Kustomize, and plain YAML
– Strong focus on multi-tenancy and security
Install Flux on a Kubernetes cluster:
curl -s https://fluxcd.io/install.sh | sudo bash
flux bootstrap github \
--owner=YOUR_GITHUB_USERNAME \
--repo=fleet-infra \
--path=clusters/production \
--personal
This command creates a Git repository and installs Flux, which will then manage deployments based on what’s in that repo.
ArgoCD
ArgoCD is more feature-rich and provides a web UI for managing deployments. It’s better suited for teams managing multiple clusters or complex deployment workflows.
Strengths:
– Web UI for monitoring and managing deployments
– Excellent multi-cluster management
– Deep Helm integration
– Sync policies and pruning options
Installing ArgoCD:
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
After installation, you’d create an Application manifest:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-service
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/infrastructure
targetRevision: main
path: apps/payment-service
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
ArgoCD would continuously ensure that what’s in the apps/payment-service directory matches the production cluster.
Terraform Cloud / Terraform Enterprise
If you’re primarily using Terraform for infrastructure, these platforms provide GitOps-style workflows without requiring a separate reconciliation engine.
When you commit Terraform to your Git repository, Terraform Cloud automatically:
– Runs terraform plan
– Waits for approval
– Runs terraform apply
This gives you many GitOps benefits without additional tooling.
Building Your GitOps Repository Structure
The structure of your Git repository dramatically affects how maintainable your infrastructure becomes. Here’s a practical layout for a team managing multiple environments:
infrastructure/
├── clusters/
│ ├── production/
│ │ ├── kustomization.yaml
│ │ ├── flux-system/
│ │ └── apps/
│ │ ├── payment-service/
│ │ ├── api-gateway/
│ │ └── monitoring/
│ ├── staging/
│ │ ├── kustomization.yaml
│ │ └── apps/
│ └── development/
├── apps/
│ ├── payment-service/
│ │ ├── base/
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ └── kustomization.yaml
│ │ └── overlays/
│ │ ├── production/
│ │ ├── staging/
│ │ └── development/
├── infrastructure/
│ ├── networking/
│ ├── storage/
│ └── monitoring/
└── docs/
└── DEPLOYMENT_GUIDE.md
This structure uses Kustomize, which lets you maintain a single base application configuration while overlaying environment-specific changes. For example, production might require more replicas, different resource limits, or additional security policies.
Example Kustomize overlay for production:
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
replicas:
- name: payment-service
count: 5
commonLabels:
environment: production
patches:
- target:
kind: Deployment
name: payment-service
patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources
value:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
This approach keeps your base configuration DRY while allowing each environment to diverge slightly.
Implementing GitOps: A Step-by-Step Approach
Phase 1: Start Small and Pilot
Don’t migrate your entire infrastructure to GitOps in one sprint. Pick a single, non-critical application or service.
Choose your tool — For Kubernetes-native teams, start with Flux or ArgoCD. For mixed environments, consider Terraform Cloud.
Create your repository structure — Set up a basic directory layout and initialize it with your first application’s manifests.
Deploy the GitOps operator — Install Flux, ArgoCD, or your chosen tool into a staging cluster.
Deploy one application — Push your first application manifests to Git and watch the operator deploy them.
Example with Flux:
# Install Flux
flux bootstrap github \
--owner=myorg \
--repo=fleet-infra \
--path=clusters/staging
# Create a deployment manifest
mkdir -p clusters/staging/apps/web-app
cat > clusters/staging/apps/web-app/deployment.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 2
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web-app
image: myregistry.azurecr.io/web-app:v1.0.0
ports:
- containerPort: 8080
EOF
# Create a Flux source and kustomization
flux create source git web-app-repo \
--url=https://github.com/myorg/infrastructure \
--branch=main
flux create kustomization web-app \
--source=web-app-repo \
--path=clusters/staging/apps/web-app
Phase 2: Establish Workflow Patterns
Once your pilot is successful, establish patterns for your team:
- Who can commit to main? — Require pull requests and reviews for infrastructure changes.
- Testing and validation — Add CI checks that validate your manifests before merging.
- Promotion paths — Define how changes flow from development → staging → production.
Example GitHub Actions workflow for validating manifests:
name: Validate Manifests
on:
pull_request:
paths:
- 'clusters/**'
- 'apps/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install kustomize
run: |
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
- name: Validate Kubernetes manifests
run: |
kustomize build clusters/production > /tmp/manifests.yaml
kubectl apply --dry-run=client -f /tmp/manifests.yaml
- name: Check image tags
run: |
# Fail if any image uses 'latest' tag
if grep -r "image:.*:latest" clusters/; then
echo "❌ Found images with 'latest' tag. Use explicit versions."
exit 1
fi
Phase 3: Automate Image Updates
GitOps shines when you automate image updates. After your container image is built and pushed, automatically update your manifests.
Using Flux Image Automation:
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageRepository
metadata:
name: web-app
namespace: flux-system
spec:
image: myregistry.azurecr.io/web-app
interval: 1m
---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImagePolicy
metadata:
name: web-app
namespace: flux-system
spec:
imageRepositoryRef:
name: web-app
policy:
semver:
range: '>=1.0.0 <2.0.0'
---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: web-app
namespace: flux-system
spec:
interval: 1m
sourceRef:
kind: GitRepository
name: infrastructure
git:
checkout:
ref:
branch: main
commit:
author:
email: [email protected]
name: Flux
update:
path: ./clusters/production
strategy: Setters
This configuration automatically updates your deployment manifests when a new image is pushed to your registry.
Common GitOps Pitfalls and How to Avoid Them
Pitfall 1: Treating Git as the Ultimate Truth, But Forgetting About Reality
Your Git repository should be the source of truth, but you need to actively verify that your live environment actually matches it.
Prevention: Use drift detection tools:
– Flux has built-in drift detection
– ArgoCD shows drift in the UI
– Set up periodic syncs that correct drift automatically
Pitfall 2: Making Git Too Complex
Teams often over-engineer their GitOps repository structure, creating layers of Helm charts, Kustomize bases, and custom tooling that only one person understands.
Prevention: Keep it simple:
– Start with plain YAML or Kustomize overlays
– Add complexity only when needed
– Document your conventions clearly
Pitfall 3: Secrets in Git
Your GitOps repository should be public (for transparency) or internal, but never containing secrets like database passwords or API keys.
Prevention: Use a secrets management tool:
– Sealed Secrets (Kubernetes-native, integrates with Flux)
– SOPS (Mozilla’s Secrets Operations tool)
– External Secrets Operator (integrates with AWS Secrets Manager, HashiCorp Vault, etc.)
Example with Sealed Secrets:
# Install sealed-secrets
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.18.0/controller.yaml
# Seal a secret
echo -n "mypassword" | kubectl create secret generic db-secret \
--dry-run=client \
--from-file=password=/dev/stdin \
-o yaml | \
kubeseal -f - > sealed-secret.yaml
# Commit sealed-secret.yaml to Git
# Only your cluster's Sealed Secrets controller can decrypt it
Pitfall 4: Manual Changes to Live Systems
The entire GitOps model breaks if someone SSHes into a server and makes changes. You need governance to prevent this.
Prevention:
– Use RBAC to restrict direct cluster access
– Require all infrastructure changes to go through Git
– Audit and alert on manual changes that bypass GitOps
– Make it easier to commit to Git than to manually change systems
GitOps for Non-Kubernetes Environments
GitOps isn’t just for Kubernetes. You can apply the same principles to VMs, cloud infrastructure, and on-premises systems.
For infrastructure managed with Terraform:
– Use Terraform Cloud with VCS integration
– Commit your .tf files to Git
– Require pull requests for approval
– Terraform Cloud automatically plans and applies changes
For VMs and on-premises:
– Use configuration management tools (Ansible, Puppet, Chef) triggered by Git webhooks
– Store your playbooks and configurations in Git
– Use similar approval workflows
Example Ansible approach:
# In your CI/CD pipeline triggered by Git pushes
---
- name: Apply infrastructure configuration
hosts: all
vars:
git_repo: "https://github.com/myorg/infrastructure"
git_version: "{{ lookup('env', 'GIT_COMMIT') }}"
tasks:
- name: Clone infrastructure repo
git:
repo: "{{ git_repo }}"
dest: /opt/infrastructure
version: "{{ git_version }}"
- name: Apply configurations
include_role:
name: "{{ item }}"
loop:
- networking
- monitoring
- security
Comparing GitOps Tools: Quick Reference
| Tool | Best For | Learning Curve | Web UI | Multi-Cluster |
|---|---|---|---|---|
| Flux CD | Kubernetes-native, minimal teams | Low | CLI-first | Good |
| ArgoCD | Complex deployments, UI preference | Medium | Excellent | Excellent |
| Terraform Cloud | Infrastructure-heavy environments | Low | Good | N/A |
| GitLab CI/CD | GitLab shops wanting built-in solution | Low | Built-in | Good |
| GitHub Actions | Simple workflows, GitHub-native | Low | Limited | Possible |
Moving Your Sysadmin Workflows to GitOps
If you’ve spent years managing infrastructure manually, GitOps might feel foreign. Here’s how to transition:
Your version control skills translate directly — You probably already use Git. GitOps just extends that to infrastructure.
Your troubleshooting skills are still crucial — GitOps is a deployment tool, not a debugging tool. When things break, you’ll still need to SSH into systems, check logs, and investigate.
You’re not being replaced — GitOps automation handles routine changes. Your expertise matters more for designing systems, preventing outages, and handling edge cases.
Start documenting infrastructure as code — Every manual configuration you have should be captured in your Git repository.
Practical Next Steps
Choose your first tool — For Kubernetes teams, start with Flux (lightweight) or ArgoCD (feature-rich). Both have excellent documentation.
Set up a pilot — Pick one non-critical service and manage it with GitOps for a month.
Build your repository — Create the directory structure we outlined and commit your first application manifests.
Establish approval workflows — Use GitHub’s branch protection rules to require reviews before changes are deployed.
Iterate and expand — Once the pilot is successful, gradually migrate more services and eventually entire environments.
The shift to GitOps isn’t about adopting the latest trendy technology—it’s about making your infrastructure auditable, reversible, and scalable. Once you experience the peace of mind that comes from having a complete, version-controlled history of every infrastructure change, you’ll wonder why you ever managed systems any other way.