Sachin Chaurasiya

DevOps Part 1 of 5 · Platform Engineering

Platform Engineering Foundations: Defining the Platform Contract

The operating model behind the CI templates, ApplicationSets, Kyverno guardrails and observability stack on this site: what a team hands over, what the platform returns, where the golden path ends and enforcement begins.

Author
Sachin Chaurasiya
Sachin Chaurasiya
Published
Reading time
11 min read
Difficulty
intermediate

Reviewed Tested with gitlab-ci-local 4.75.1, Argo CD 3.5.3, Kyverno 1.19.1, kube-prometheus-stack 91.4.1, Kubernetes 1.35 (kind 0.31)

On this page

Overview

An internal platform is a set of promises. The application team promises to hand over a few things in a known shape; the platform team promises that, given those things, a service is built, scanned, deployed to every environment, protected by the same rules as everything else, and visible when it breaks. Everything else about platform engineering is detail. This article writes the promises down, using the four implementations already on this site as the definition of what the platform actually does:

Nothing below describes a capability those four do not have. Where the platform could do more, the article says so and stops.

Diagram · The platform contract and its four components
The platform contract and its four componentsAn application team provides source code, a Dockerfile, environment overlays and workload metadata. The contract is the set of files and labels the platform reads. Four platform components consume it: CI templates build, test, scan and package the artifact; environment delivery generates one Argo CD Application per environment from the overlays; guardrails refuse workloads that break the baseline or production rules; observability collects metrics and logs from whatever runs. Underneath, the cluster and cloud runtime are owned by the platform, and the deployment status, logs, metrics and policy failures flow back to the application team unfiltered.Application team ownsPlatform team ownsprovidesartifactsyncadmissionfeedbackApplicationteamcode · overlays1Platformcontractfiles, labels, inputs2CI templatesbuild · test · scan3EnvironmentdeliveryApplicationSets4GuardrailsKyverno via GitOps5Observabilitymetrics · logs6Kubernetesruntimecluster · cloud7

An application team provides source code, a Dockerfile, environment overlays and workload metadata. The contract is the set of files and labels the platform reads. Four platform components consume it: CI templates build, test, scan and package the artifact; environment delivery generates one Argo CD Application per environment from the overlays; guardrails refuse workloads that break the baseline or production rules; observability collects metrics and logs from whatever runs. Underneath, the cluster and cloud runtime are owned by the platform, and the deployment status, logs, metrics and policy failures flow back to the application team unfiltered.

  1. The application team owns its code, its Dockerfile, its tests, and the per-environment overlay that says how many replicas and which configuration each environment gets.
  2. The contract is the set of files, labels and inputs the platform reads: a .gitlab-ci.yml that includes a template, an apps/<name>/envs/<env>/ directory with a config.json, a team label, resource requests, a readiness probe.
  3. CI templates turn the source into a tested, scanned, checksummed artifact and an SBOM, with the same jobs for every service.
  4. Environment delivery turns the overlays into one Argo CD Application per environment, automated in development and staging, gated by a person in production.
  5. Guardrails refuse a workload that breaks the baseline anywhere or the production rules in production, and say why in the error message.
  6. Observability collects metrics and logs from whatever runs, and alerts on the signals the platform decided matter.
  7. The runtime is owned by the platform, and everything it knows about a service flows back to the team that owns the service, unfiltered.

The contract

What each side provides, as implemented.

The application team providesImplemented by
Source, a lockfile, npm test and npm run lint that mean somethingThe template’s test_command and lint_command inputs
A Dockerfile that builds from the packaged artifactContainer Security path; the template stops at package
apps/<name>/base/ with a Deployment, a Service and probesThe ApplicationSet’s Kustomize base
apps/<name>/envs/<env>/ with replicas, image tag, configuration and a config.jsonThe Git file generator
A platform.example.com/team label, requests and a memory limit on every containerBaseline guardrails
In production: at least two replicas and a readiness probeProduction guardrails
A merge request, with a reason and an end date, for anything that must differexceptions/
The platform providesImplemented by
One pipeline definition per delivery shape: build, unit tests, lint, secrets scan, dependency scan, SBOM, packagetemplates/node-service.yml, versioned by tag
Artifact conventions: one tarball per commit, a checksum beside it, an expiryThe template’s package job
One Application per environment, created and pruned from the directory layoutThe ApplicationSet, applicationsSync: create-update
Namespaces with the platform’s labels, which the team cannot changemanagedNamespaceMetadata
Automated sync with self-heal in development and staging; a manual gate in productiontemplatePatch on automated
A small set of rules, audited before they are enforced, with a written exception pathTwo policy layers, AuditDeny by commit
Metrics for every container, logs for every pod, a triage dashboard, and alert rules (a receiver is still the next step)kube-prometheus-stack, Loki, Alloy, PrometheusRule
Error messages that name the fixmessageExpression in every policy

Two properties make this a contract rather than a wish list. Both columns are files: an engineer can open the template, the ApplicationSet and the policy directory and read exactly what will happen to their service. And the platform’s side is enforced by the same mechanisms it delivers with: an Argo CD project decides what each directory may create, Kyverno decides what a workload may omit, and a Git tag decides which version of the pipeline a repository runs.

What the platform does not provide

The list of things a platform deliberately leaves alone is as important as the list it takes on, because every item it takes on becomes something the platform team is paged for.

  • Application correctness. The pipeline runs npm test; it does not know whether the tests are good. A service with no tests passes the golden path.
  • The Dockerfile. The template packages the built artifact; the image build and its hardening are the application team’s, with the Container Security path as the reference. A platform that owns every Dockerfile owns every base-image upgrade for every language.
  • Runtime configuration values. The overlay holds them; the platform holds the mechanism. Secrets are not in the overlay at all, and the platform does not read them: Secrets and Configuration Security explains where they live.
  • Database migrations, data, and rollback of either. The rollback article is explicit that the platform can roll back a Deployment and cannot roll back a schema.
  • A developer portal. There is no Backstage or equivalent in this implementation. Self-service is the repository and the pull request, and the section below explains why that is a complete answer at this scale.
  • Cloud identity and network boundaries. The platform assumes them and does not implement them here; that is the Cloud Security Foundations path.

A team that needs one of these gets the relevant path, not a platform feature. The platform team saying “not ours” clearly is a service; saying “we will look into it” is a queue.

Golden path, not the only path

The template article ends on a finding that decides this section: a consumer can override a required job. Including the template and then writing dependencies: { allow_failure: true } is valid GitLab CI, and it worked when tried. An include is composition, not enforcement.

That result is the correct design, not a defect to engineer away. The golden path is the default that requires no thought: five lines of .gitlab-ci.yml, a directory per environment, a label. Teams that need more extend it on the seams the platform left for the purpose: the .platform:test hidden job for an extra test stage, a new environment directory for a preview environment, an exception merge request for a leader-only process. None of those require a conversation with the platform team, and none of them weaken anyone else’s service.

Enforcement belongs in exactly the places where a team’s shortcut would affect someone other than that team, and the implementations put it there:

ControlWhere it is enforcedWhy there
Which jobs run in a consumer pipelineCODEOWNERS on .gitlab-ci.yml; compliance pipelines on UltimateThe include cannot do it; review or a higher-tier feature can
Which directory may create which resourceArgo CD AppProject whitelists and destinationsA team’s Application cannot ship a policy or touch another namespace
What a workload must declareKyverno Deny, at admissionThe refusal is at the last point before the cluster, and names the fix
Who may grant an exceptionThe platform-exceptions namespace, written only by the platform projectAn exception anywhere else is ignored by Kyverno
Which version of the pipeline a repository runsA Git tag the consumer pinsThe platform cannot change a consumer’s pipeline without the consumer moving

Everything above the line of “affects other teams” is convention; everything at or below it is a control. Confusing the two in either direction is the common failure: enforcing conventions makes teams route around the platform, and treating controls as conventions makes the platform a suggestion.

What stays visible

A platform that hides the system underneath it produces engineers who cannot debug their own service. The four implementations keep the following visible without any platform-specific tooling, and a platform that removed any of them would be worse:

  • Deployment status: kubectl -n argocd get applications and the Application’s operation state, which carries Kyverno’s rejection message verbatim when a sync is refused.
  • Policy findings: kubectl get policyreport -n <namespace>, readable by the team before enforcement starts, and the admission error afterwards.
  • Logs and metrics: the same Grafana, the same Loki labels (namespace, pod, container, app), the same PromQL for every service; the triage dashboard is a starting point, not a wall.
  • Environment state: the environment directory in Git is the state, and the Application’s diff is the distance from it.
  • The pipeline that actually ran: gitlab-ci-local --list or GitLab’s merged YAML, not the five-line consumer file.

The abstraction is in the defaults, not in the visibility. A team that never reads a policy file still gets a message that names the label to add; a team that wants to read the policy file finds it in policies/ with the owner and layer in its annotations.

Self-service, as implemented

Self-service here means that an application team can do the following without a ticket, and the platform team finds out afterwards, through Git:

  • Adopt the pipeline: one include with a tag and inputs.
  • Add a test stage: one job on .platform:test.
  • Create an environment: one directory with a kustomization.yaml and a config.json; the ApplicationSet generates the Application, the namespace and its labels.
  • Promote a version: one line in the next environment’s overlay, one merge, and for production one sync by a named person.
  • Request an exception: one merge request against exceptions/ with a reason, an owner and an end date.
  • Diagnose a failure: the dashboard, the logs and the reports, with no access request.

Each of those is a Git operation on a repository the team already works in. A portal that wraps them would add a form in front of a file; at this scale it would also add a second place where the truth can drift from Git. That is why no portal is part of this implementation, and it is not a statement that one is never useful: a portal earns its place when the number of teams makes the repository layout itself a discovery problem, and its job is then to show the contract, not to replace it.

The platform as a product

The obligations that follow from publishing a contract, each already carried by one of the implementations:

  • Versioned interfaces. The template is tagged; a consumer pins a tag; a new job is a minor release and a renamed input is a major one. The ApplicationSet’s config.json keys and the policy set are interfaces in the same sense and deserve the same discipline.
  • Documentation that is the artifact. The spec:inputs block with its descriptions, the policy annotations, the README per template. If the documentation is elsewhere it is already wrong.
  • Upgrade paths. A changelog per tag, an “upgrade” section that says what a consumer changes, and an audit window before any new Deny.
  • Compatibility. Old tags keep working. The article showed a consumer on v1.0.0 unaffected by v1.1.0; the platform’s promise is that this stays true until a major version says otherwise.
  • Feedback loops. The PolicyReport before enforcement, the git log exceptions/ after it, the rendered pipeline listing, the Alertmanager route. Each is a signal about whether the contract fits the teams.
  • Reliability. Kyverno’s webhooks fail closed; Argo CD’s controller is the deployment path; the template repository is the most privileged CI configuration in the organisation. The platform’s own components need the platform’s own guardrails and observability, and it is the first consumer of both.

None of this comes with numbers. Adoption, lead time and developer satisfaction are measurable and worth measuring once there is something to measure; this site’s implementation ran on one lab cluster with one sample service, and any figure attached to it would be invented.

Where this leaves the platform

Four components, one contract, and a repository layout that any engineer can read in an afternoon. The pieces that a next iteration would add, in the order the gaps appeared while building the current ones: a deployment template to pair with the build template, so the identity that deploys is the platform’s and short-lived; a guardrail on namespace creation itself, so the label scheme cannot be bypassed by hand; tagged releases of the policy repository once there is more than one cluster; and a real Alertmanager receiver, which the observability article left at null. Each is a merge request against a directory that already exists, which is the test of whether the foundation holds.

References

Keep reading