Infrastructure as Code and GitOps Questions
Defining and managing infrastructure and delivery state declaratively: provisioning as code (Terraform, CloudFormation, Pulumi, Ansible, Puppet), configuration management, idempotency, drift detection and remediation, and version control for infrastructure definitions, extended by GitOps where git is the source of truth for deployment and infrastructure state. Covers keeping environments consistent, treating config as a first-class versioned artifact, pull-based deployment and continuous reconciliation toward the committed state (ArgoCD, Flux, and similar controllers), Kubernetes manifest and configuration delivery via git, secrets handling for IaC and GitOps pipelines, policy-as-code guardrails (OPA, Sentinel), Terraform state management and locking, and auditable change through version control: branching strategy, pull request review, commit conventions, and code review policy for infrastructure code. Distinct from the CI/CD pipeline design topic, which owns generic pipeline structure and platform-scale release orchestration (build, test, artifact publishing, runner mechanics) and the architectural choice between push-based CI/CD and pull-based GitOps, even when the payload is infrastructure code. Distinct from the safe deployment and rollback strategies topic, which owns deployment-strategy mechanics: canary and blue-green traffic shifting, automated rollback triggered by metrics or SLOs, feature-flag progressive delivery, Kubernetes rollout mechanics (maxSurge, maxUnavailable, health-check gating), and database or schema migration safety as it gates a release, even when the delivery mechanism is GitOps. Distinct from the automation and scripting topic, which owns operational-scripting disciplines (retry and backoff logic, CLI tool design, generic file, checksum, or diff utilities) when the task is not specifically about declarative infrastructure or configuration state. This topic keeps the GitOps reconciliation loop itself, drift detection and remediation, and IaC state and module lifecycle management regardless of which adjacent discipline a question also touches.
Compare implementing complex lifecycle logic via a custom Kubernetes operator (controller + CRD) versus using GitOps patterns and existing controllers. For what classes of problems does authoring an operator make sense? Discuss development/testing overhead, operational burden, versioning, and how to handle CRD migrations safely.
Sample Answer
Direct answer
Authoring a custom Kubernetes operator (a controller plus a CRD, custom resource definition) makes sense when the desired behavior is genuinely STATEFUL and REACTIVE in a way plain manifest reconciliation cannot express: the system needs to make ONGOING decisions based on live, changing conditions (not just "does live state match this static YAML"), coordinate MULTI-STEP workflows with intermediate states, or encapsulate deep domain-specific operational knowledge (how to safely fail over THIS specific database, how to scale THIS specific stateful system) that a generic GitOps controller has no way to know. For everything else, the overwhelming majority of application deployment, plain GitOps patterns (a GitOps controller applying versioned manifests) are simpler, cheaper to build, and cheaper to operate, and reaching for a custom operator by default is a common, expensive over-engineering mistake.
Structured elaboration
Classes of problems where an operator makes sense. Managing a system whose lifecycle has genuine STATE MACHINES beyond "exists or doesn't" (a database cluster that needs a specific bootstrap-then-join sequence, a specific safe failover procedure, and specific backup/restore orchestration); systems that need to REACT to external, non-Git-sourced signals (autoscaling based on a custom metric, remediating a specific failure mode detected at runtime); and situations where the SAME complex operational logic needs to run repeatedly and consistently across many instances (a platform team building a PostgresCluster CRD once so every team can request a correctly-operated database without re-learning the operational playbook each time). The common thread: the logic is genuinely PROCEDURAL and CONDITION-DEPENDENT, not a static desired-state comparison.
Development and testing overhead. Writing a correct controller (using controller-runtime or a similar framework) means implementing a reconcile loop, handling partial failures and requeueing correctly, managing CRD schema evolution, and testing against realistic failure injection (what happens if the reconcile function is killed mid-operation and restarted, since Kubernetes controllers must be idempotent and safe to re-run from any partial state); this is meaningfully more engineering investment than writing a set of plain manifests, closer to building a small distributed system than authoring configuration.
Operational burden. A custom operator is itself a piece of software that needs monitoring, on-call ownership, its own release process, and its own bugs to fix; every operator a platform adds is a new, indefinitely-maintained liability, not a one-time build cost, and the team introducing it needs to own that burden for the operator's entire lifetime, not just its initial development.
Versioning and CRD migrations. CRDs have their OWN API versioning story (v1alpha1 -> v1beta1 -> v1, with conversion webhooks needed to translate existing stored objects between versions), separate from and additional to the application's own versioning; a breaking CRD schema change requires either a conversion webhook (translating old stored CRs to the new schema on read) or a coordinated migration of every existing CR instance, both genuinely hard problems that plain-manifest GitOps never has to solve, since plain manifests have no equivalent "stored schema" concept to migrate.
Trade-offs and pitfalls
- Common mistake: building an operator because "it feels more sophisticated" rather than because the problem genuinely requires ongoing reactive logic. Most application deployment (stateless services, most databases-as-a-managed-service usage) is fully served by plain declarative manifests reconciled by Argo CD/Flux; adding a custom operator for something that could be a static Deployment manifest adds real, ongoing maintenance cost for no corresponding benefit.
- A common, workable middle ground before committing to a full custom operator is checking whether an EXISTING community operator already covers the need (a well-maintained Postgres, Kafka, or cert-manager-style operator), since building and maintaining bespoke operational logic for a widely-used piece of infrastructure duplicates effort the broader ecosystem has often already solved and battle-tested.
- CRD migrations are the single most underestimated cost of the operator path. A team that builds
v1alpha1and never plans for its evolution paints itself into a corner: every future breaking change to the CRD schema now needs a conversion webhook or a painful manual migration of every live custom resource across every cluster running it, a cost that scales with adoption success, the MORE the operator is used, the harder it becomes to change. - An operator and GitOps are not mutually exclusive, and the common real-world pattern combines them: the CRD (a
PostgresClusterobject, for instance) is itself STILL managed declaratively through Git and reconciled by Argo CD/Flux like any other manifest, while the CUSTOM OPERATOR watches that CRD and performs the complex, stateful, reactive work of actually standing up and operating the database; the operator handles the "how," GitOps still governs the "what was requested and when," which is the right way to think about the two working together rather than as competing choices.
Describe RBAC best practices when granting Kubernetes permissions to GitOps controllers (e.g., Argo CD, Flux). Explain how to minimize blast radius via namespacing, Argo CD Projects, scoped service accounts, and integration with an external identity provider (SSO). Include how to handle cluster-scoped resources when necessary.
Sample Answer
Direct answer
Minimize blast radius by scoping every permission a GitOps controller holds to the smallest set of namespaces, resource kinds, and clusters that its actual applications need, using Argo CD Projects (or Flux's per-tenant Kustomization/GitRepository scoping) as the primary isolation boundary, backing service accounts with narrowly-scoped Kubernetes RBAC (role-based access control) rather than cluster-admin, federating human access through an external identity provider so authorization decisions track real organizational identity, and treating any CLUSTER-scoped resource as an explicit, separately-reviewed exception rather than a default capability.
Structured elaboration
Namespacing as the default isolation unit. Scope each Argo CD AppProject (or Flux tenant) to a specific set of destination namespaces, and back it with a Kubernetes Role (namespace-scoped) rather than a ClusterRole wherever the application's resources are entirely namespace-local (Deployments, Services, ConfigMaps, most application workloads). This means a compromised or misconfigured project can, at worst, damage its own namespaces, not the whole cluster.
Argo CD Projects specifically. An AppProject constrains three things simultaneously: which Git repositories a project's Applications may source from (sourceRepos), which cluster/namespace pairs they may target (destinations), and which Kubernetes resource kinds they may create, at both namespace and cluster scope (namespaceResourceWhitelist / clusterResourceWhitelist, defaulting to deny). A team's project should whitelist only the specific kinds its applications actually use (Deployment, Service, ConfigMap, Ingress) rather than *, so even a maliciously crafted manifest committed to that team's repo cannot create, say, a ClusterRoleBinding granting itself broader access.
Scoped service accounts. The Kubernetes service account backing each project's sync operations should hold a Role/RoleBinding (not ClusterRole/ClusterRoleBinding) matching exactly the project's whitelisted destinations and resource kinds; this is the enforcement mechanism that makes the AppProject's declared intent actually binding, since the AppProject config alone is an Argo-CD-level policy, not a Kubernetes-API-server-level one, an attacker with direct API access (bypassing Argo CD entirely) is only actually blocked by the underlying Kubernetes RBAC.
External identity provider integration. Map identity-provider (IdP) groups to Argo CD RBAC roles so that project membership tracks the organization's real team structure (someone added to the "payments-team" IdP group automatically gets payments-project access, and loses it automatically on removal), rather than a separately maintained Argo-CD-local user list that drifts out of sync with actual employment/team status.
Handling cluster-scoped resources. Some legitimate needs (CustomResourceDefinitions, cluster-wide policy objects, namespace creation itself) genuinely require cluster scope. The safe pattern: carve these into a SEPARATE, tightly-restricted "platform" project managed by the platform team alone, with its own dedicated service account and mandatory review process, explicitly NOT available to application teams' projects; a team's own AppProject should have an empty clusterResourceWhitelist by default, and any addition to it should be a deliberate, reviewed, logged exception, not a convenience default.
Trade-offs and pitfalls
- Common mistake: granting a project's service account
cluster-admin"temporarily, to unblock a deploy," and never revisiting it. This is the single most common way blast-radius scoping erodes in practice; a scoped-RBAC-by-default policy needs an expiry or review mechanism for exceptions, not just a documented intent that nobody re-audits. AppProjectresource whitelisting is an Argo-CD-enforced policy, not a Kubernetes-API-server-enforced one. Anyone with directkubectlaccess to the cluster (bypassing the GitOps controller entirely) is unaffected by it; the actual security boundary is the underlying Kubernetes RBAC bound to the service account, which is why both layers need to be configured consistently, not just the Argo CD-level policy.- Over-scoping "for future flexibility" defeats the purpose just as effectively as no scoping at all. A whitelist of
*resource kinds "in case we need it later" gives an attacker (or a mistake) the same blast radius as no whitelist; the discipline has to be adding permissions when a genuine new need arises, not front-loading them speculatively. - Namespace-per-project isolation does not by itself stop cross-namespace attacks that exploit shared cluster-scoped resources (a shared
IngressClass, a sharedStorageClass, a shared admission webhook); genuinely strong tenant isolation needs the namespace boundary reinforced by NetworkPolicies and, for the highest-sensitivity tenants, physically separate clusters, not namespace RBAC alone.
High-level: what approaches do you recommend for handling secrets and sensitive data when storing infrastructure code in Git? Compare at least three options (e.g., cloud secret manager, encrypted files with SOPS/git-crypt, committing to Git with vault references) and explain trade-offs in security, auditability, and developer ergonomics.
Sample Answer
Direct answer
Comparing exactly the three options the question names: a CLOUD SECRET MANAGER (Vault, or a cloud-native equivalent) keeps secret values entirely OUT of Git, fetched live at deploy or runtime; ENCRYPTED FILES using SOPS or git-crypt commit an encrypted BLOB directly to Git, decrypted locally or in CI by whoever holds the key; and COMMITTING TO GIT WITH VAULT REFERENCES commits only a PATH or POINTER to Git (no secret material at all, encrypted or otherwise), resolved to a real value only when something with backend access reads it. The strongest default for most teams is the third pattern for its combination of good auditability and minimal Git-side attack surface, with encrypted-files-in-Git as a reasonable, simpler alternative for teams not ready to operate a secrets backend, and a cloud secret manager alone (no Git-tracked reference at all) as the right fit only when the consuming system already integrates with it natively.
Structured elaboration
Cloud secret manager (fetched directly, no Git-tracked reference at all). The application or deployment tooling calls the secret manager's API directly at runtime, with NOTHING about the secret, not even a path, tracked in Git. Security: strongest, nothing secret-adjacent ever touches version control. Auditability: strong at the SECRET-ACCESS level (the secret manager's own access logs), but weak at the INFRASTRUCTURE-CHANGE level, since "this deployment now reads secret X" is not itself a reviewable Git diff, it is implicit in application code or deploy scripts. Developer ergonomics: requires every consuming system to integrate with the secret manager's specific API/SDK directly, real integration work per consumer.
Encrypted files with SOPS or git-crypt. The secret VALUE is encrypted client-side and the encrypted blob IS committed to Git. Security: the value is opaque in Git, but the encryption key (a GPG key, a KMS-backed key for SOPS) becomes the entire security boundary, anyone who can decrypt can read every secret ever committed this way. Auditability: strong at the CHANGE level (a Git diff shows exactly when a secret's encrypted blob changed, tied to a commit and PR, even though the reviewer cannot see the plaintext), weaker at the ACCESS level (decrypting locally leaves no centralized log the way an API call to a secret manager does). Developer ergonomics: straightforward once the encryption tooling is set up, git-crypt unlock or SOPS-aware editors make the workflow feel close to normal file editing.
Committing to Git with Vault references (a path, not a value). Only a REFERENCE (vault:secret/data/prod/db-password#value) is committed, resolved by an External Secrets Operator or equivalent at apply/sync time. Security: no secret material, encrypted or not, ever exists in Git at all, the smallest possible Git-side attack surface of the three. Auditability: strongest combination, the REFERENCE change is a normal, reviewable Git diff (when did this deployment start pointing at a different secret path), AND the secret backend's own access logs cover actual value reads independently. Developer ergonomics: requires the secrets backend to be genuinely available and reliable, since every apply/sync now has a live runtime dependency on it.
Worked example
A concrete recommendation matrix by team maturity and constraint:
| Situation | Recommended approach | Why |
|---|---|---|
| Team already operates Vault or a cloud secret manager reliably | Vault references committed to Git | Best combination of change-auditability and zero Git-side exposure |
| Team has no secrets backend yet, needs something working THIS WEEK | Encrypted files (SOPS, KMS-backed) | Lower operational lift; no new runtime dependency; still keeps plaintext out of Git |
| A single, narrowly-scoped consuming system with native secret-manager integration already built in (e.g., a managed service that pulls its own credentials directly) | Direct secret-manager fetch, no Git reference at all | Avoids maintaining a redundant reference layer when the consumer already integrates natively |
Trade-offs and pitfalls
- Common mistake: treating "encrypted" as automatically equivalent to "as safe as no secret material in Git at all." An encrypted blob in Git is safe only as long as the encryption key itself is properly access-controlled and never compromised; a Vault-reference approach removes this entire class of risk by never putting secret material (even encrypted) in Git in the first place, a meaningfully different security posture, not just a different implementation detail.
- The Vault-reference approach's biggest real cost is the NEW runtime dependency it introduces, every apply or sync now needs the secrets backend reachable; a team choosing this approach needs to have already solved (or be ready to solve) Vault's own availability and disaster-recovery story, not just its access-control story.
- Encrypted-files-in-Git's key-management story needs the SAME rigor as a secret manager's access policy would need, teams sometimes under-invest here specifically because "it's just a GPG key" feels like less of an operational surface than standing up Vault, when the actual security property it provides depends entirely on that key being managed with equal care.
- A direct secret-manager fetch with NO Git-tracked reference at all trades away change auditability for simplicity, this is the right trade for a narrow, well-integrated consumer, but applied broadly across many systems it leaves "which deployment reads which secret" undiscoverable from Git alone, a real loss for an organization that wants its infrastructure history to be a complete record.
Design an infrastructure codebase to support multiple tenants (teams or customers) that require isolation and shared services. Discuss repository layout, use of modules, parameterization, environment management, access controls, onboarding flow for new tenants, and how to use Git constructs (branches, repos, codeowners) to enforce tenancy boundaries.
Sample Answer
Direct answer
The right unit of reuse for a multi-tenant infrastructure codebase is the MODULE, not the repository or the environment: shared platform modules (network, IAM baseline, common services) are authored once and consumed by parameterized per-tenant instantiations, while tenancy BOUNDARIES (who can change what for which tenant) are enforced through Git constructs, CODEOWNERS scoping and branch protection on a per-tenant DIRECTORY structure, rather than through application-layer access control alone. This is a distinct concern from the runtime GitOps question of how a controller reconciles multiple tenant CLUSTERS; this question is about the CODEBASE and REPOSITORY structure that PRODUCES each tenant's infrastructure in the first place.
Structured elaboration
Repository layout. A single repo (shared platform modules genuinely benefit from atomic, coordinated updates) with a clear top-level split: modules/ for shared, versioned, parameterized building blocks, and tenants/<tenant-id>/ for each tenant's specific instantiation of those modules with its own parameter values.
Use of modules and parameterization. Each shared module exposes a deliberately MINIMAL parameter surface (tenant name/ID, environment tier, any genuinely tenant-specific sizing or feature flags), everything else is fixed inside the module so a tenant's own directory cannot accidentally diverge from the platform's baseline security or architecture posture; a tenant directory is then just a short module-invocation file passing that tenant's specific parameter values, not a full copy of the module's internals.
Environment management. Each tenant's directory further splits by environment (tenants/<tenant-id>/prod/, /staging/), following the same overlay-and-promotion pattern used for multi-environment GitOps generally, so a tenant's prod and staging environments are independently reconciled and independently promotable.
Access controls. Two layers, matching how the modules-versus-tenants split above works: shared module changes require PLATFORM-team review (CODEOWNERS on modules/**), since a change here can affect every tenant at once; a tenant's own parameter file changes require review from whoever owns that tenant relationship (could be the platform team for internal tenants, or a delegated team for larger external/enterprise tenants with their own dedicated engineering contact), scoped via CODEOWNERS on tenants/<tenant-id>/** specifically, so tenant A's team cannot approve (or, depending on repo permissions, even SEE) tenant B's parameter changes if isolation requires that level of separation.
Onboarding flow for new tenants. A SCRIPTED or TEMPLATED process, not a hand-copied directory: a generator (a small CLI, or a documented terraform scaffold with placeholder substitution) creates a new tenants/<new-tenant-id>/ directory from a template, pre-populated with sane defaults and the CODEOWNERS entry for the new tenant automatically added, opened as a single PR for platform-team review; this is what keeps onboarding tenant number 50 exactly as consistent and low-error as onboarding tenant number 2, rather than degrading as ad hoc copy-paste drift accumulates.
Using Git constructs to enforce tenancy boundaries. Branch protection with path-scoped CODEOWNERS is the PRIMARY mechanism (as above); for organizations with a genuinely hard isolation requirement between specific tenants (regulatory separation, a competitive-conflict pairing of customers), a full separate REPO per that specific tenant, rather than a directory in the shared repo, is the appropriate exception, accepting the coordination cost of losing atomic, cross-tenant module updates in exchange for repo-level (not merely path-level) access isolation.
Worked example
infra-repo/
modules/
network/ # CODEOWNERS: @org/platform-team
iam-baseline/ # CODEOWNERS: @org/platform-team
tenants/
acme-corp/
prod/main.tf # module "network" { source = "../../../modules/network" tenant_id = "acme-corp" ... }
staging/main.tf
CODEOWNERS-scoped: @org/platform-team, @acme-account-team
globex-inc/
prod/main.tf
staging/main.tf
CODEOWNERS-scoped: @org/platform-team, @globex-account-team
A change to modules/network/ requires platform-team approval and, because the module is shared, triggers a validation pass checking it does not break any CONSUMING tenant's plan. A change to tenants/acme-corp/prod/main.tf requires only the platform team and Acme's own account team, never Globex's, and Globex's team has no approval authority (and, if the repo's permission model supports directory-scoped visibility, potentially no visibility) into Acme's parameters.
Trade-offs and pitfalls
- Common mistake: letting a tenant's directory drift into a full copy of the module's internals "just this once" for a special requirement, rather than extending the shared module's parameter surface (or, if genuinely a one-off, explicitly forking with a documented reason). Once one tenant directory diverges from pure parameterization, every subsequent tenant with a similar special need copies the same pattern, and the module's promised consistency erodes tenant by tenant.
- The onboarding template needs an owner and periodic review, an unmaintained scaffold generates new tenants against an increasingly outdated baseline (missing a security control added to the platform since the template was last updated), silently under-serving every tenant onboarded after that gap opened.
- Path-scoped CODEOWNERS visibility depends on the Git hosting platform's actual capabilities, some platforms only scope APPROVAL rights via CODEOWNERS while still leaving the whole repo readable to anyone with repo access; if genuine READ isolation between tenants is required, this needs to be verified against the specific platform's actual permission model, not assumed from CODEOWNERS alone.
- Full separate repos for hard-isolation tenants should be the deliberate EXCEPTION, not the default, defaulting every tenant to its own repo forfeits the atomic, coordinated shared-module updates that are the main structural advantage of the monorepo-plus-modules design in the first place, for isolation most tenants do not actually require.
Discuss the trade-offs between using immutable image tags (digests) versus mutable tags (like 'latest' or 'v1') in a GitOps workflow. Explain how immutable tagging affects reconciliation, security (CVE remediation), reproducibility, and developer iteration. Propose a recommended tagging policy for production and for developer environments.
Sample Answer
Direct answer
Immutable digests (image@sha256:...) guarantee that the manifest committed to Git and the bytes actually pulled and run are the SAME image forever, which is exactly what reconciliation, reproducibility, and rollback correctness all depend on; mutable tags (:latest, :v1 reused across builds) let the SAME manifest silently resolve to DIFFERENT bytes at different times, breaking the core GitOps guarantee that Git fully describes what is running. The practical policy: digests (or at minimum strictly immutable, never-reused tags) in every environment a GitOps controller actually reconciles, with a separate, explicit "update the digest" commit as the mechanism for shipping a new version, never implicit re-resolution of a mutable tag; mutable tags are acceptable ONLY in inner-loop developer environments that are explicitly outside the GitOps-reconciled path.
Structured elaboration
Effect on reconciliation. A GitOps controller's reconciliation loop compares the manifest's declared image reference against the cluster's running state. With a digest, "does the running Pod match desired state" is an unambiguous, stable comparison, the digest either matches or it doesn't, and once it matches, reconciliation correctly does nothing further. With a mutable tag, the CONTROLLER sees no drift (the tag string in the manifest hasn't changed), but the ACTUAL running bytes can differ from what was originally deployed if a Pod restarts and re-pulls a tag that has since been overwritten upstream, a form of drift the reconciliation loop is structurally blind to because it only compares the STRING in the manifest, not the resolved content.
Effect on security (CVE, Common Vulnerabilities and Exposures, remediation). A patched image (fixing a CVE) pushed under the SAME mutable tag changes what NEW pods pull without any Git commit recording that change, so there is no audit trail of when the fix actually rolled out, and existing running pods do not automatically pick it up (they keep running the old, pulled bytes until they restart), creating an unpredictable, unrecorded window of mixed-version exposure. A digest-pinned deployment makes CVE remediation an explicit, auditable act: bump the digest in Git, and reconciliation deploys exactly that patched image, deterministically, everywhere, with a Git commit as the permanent record of when the fix went out.
Effect on reproducibility. "Reproducible" means the SAME Git commit always produces the SAME running system. Digests give this property unconditionally. Mutable tags give it only until someone pushes a new image under that tag, at which point the SAME Git commit now resolves differently than it did before, which specifically breaks rollback (reverting to an OLD commit that references :v1 does not guarantee you get back the ORIGINAL :v1 bytes if :v1 has since been overwritten) and breaks any forensic "what was actually running at time T" investigation.
Effect on developer iteration. For an INNER-LOOP dev environment (a developer's own sandbox, rapidly rebuilding and testing), requiring a fresh digest and a Git commit for every single iteration is genuine friction that does not serve any of the guarantees above, since nobody is trying to prove reproducibility or audit a dev sandbox's history the way they would a production deployment.
Worked example
A recommended tiered policy:
| Environment | Tag policy | Rationale |
|---|---|---|
| Developer/sandbox | Mutable tag (:dev) OK, often OUTSIDE GitOps reconciliation entirely (direct kubectl/local tooling) | Fast iteration matters more than audit trail; not a GitOps-reconciled environment in the first place |
| Staging/CI-integration | Immutable, unique tag per build (:sha-<commit> or :build-<n>), digest-equivalent in practice since each is never reused | Needs traceability back to the exact commit/build without full production rigor |
| Production | Digest (@sha256:...) required, enforced by policy (admission-controller rule rejecting mutable tags or bare tags without a resolved digest) | Full reconciliation correctness, CVE-remediation auditability, and rollback guarantees required |
The transition from a build artifact to a production digest reference should be an EXPLICIT step in the pipeline (CI resolves the newly built image's digest and commits a manifest update referencing it), not something a human copies by hand, since a manually-typed digest is exactly the kind of error-prone step this policy exists to eliminate.
Trade-offs and pitfalls
- Common mistake: using a "unique-looking" tag (a commit SHA or build number) and assuming it is equivalent to a digest. It usually is, in practice, AS LONG AS the registry and CI pipeline genuinely never reuse it; but this is a PROCESS guarantee (nobody re-pushes that tag), not a CRYPTOGRAPHIC one the way a digest is (the digest IS the content hash, so it cannot silently point to different bytes by definition). A digest removes the reliance on that process discipline entirely.
- Common mistake: enforcing digest-only policy in production manifests but leaving the ADMISSION path open to mutable tags, so a manifest applied outside the normal GitOps flow (a manual
kubectl apply, a different pipeline) can still introduce a mutable-tag image; an admission-controller policy (Kyverno/Gatekeeper) rejecting any Pod spec without a resolved digest closes this gap regardless of HOW the manifest arrived. - Digests make manifests harder for a human to read at a glance (
sha256:a1b2c3...conveys no version information the way:v2.3.1does); a common, workable mitigation is keeping a human-readable tag in the commit message or a companion annotation/label while the actualimage:field uses the digest, giving both machine-verifiable immutability and human-readable context. - Rollback via Git revert only fully works if EVERY commit in history referenced digests, not mutable tags. A rollback to a historical commit that referenced
:v1is not a true rollback if:v1has since been overwritten; digest pinning is what makes "revert the commit" and "actually get back the old running state" the same operation.
Unlock Full Question Bank
Get access to all Infrastructure as Code and GitOps interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.