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.
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:
- Golden delivery path: Reusable GitLab CI Templates and Components
- Environment delivery: Argo CD ApplicationSets for Multi-Environment Delivery
- Guardrails: Platform Guardrails with Kyverno and GitOps
- Observability: Kubernetes Observability with Prometheus, Grafana and Loki
Nothing below describes a capability those four do not have. Where the platform could do more, the article says so and stops.
- 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.
- The contract is the set of files, labels and inputs the platform reads: a
.gitlab-ci.ymlthat includes a template, anapps/<name>/envs/<env>/directory with aconfig.json, a team label, resource requests, a readiness probe. - CI templates turn the source into a tested, scanned, checksummed artifact and an SBOM, with the same jobs for every service.
- Environment delivery turns the overlays into one Argo CD Application per environment, automated in development and staging, gated by a person in production.
- Guardrails refuse a workload that breaks the baseline anywhere or the production rules in production, and say why in the error message.
- Observability collects metrics and logs from whatever runs, and alerts on the signals the platform decided matter.
- 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 provides | Implemented by |
|---|---|
Source, a lockfile, npm test and npm run lint that mean something | The template’s test_command and lint_command inputs |
| A Dockerfile that builds from the packaged artifact | Container Security path; the template stops at package |
apps/<name>/base/ with a Deployment, a Service and probes | The ApplicationSet’s Kustomize base |
apps/<name>/envs/<env>/ with replicas, image tag, configuration and a config.json | The Git file generator |
A platform.example.com/team label, requests and a memory limit on every container | Baseline guardrails |
| In production: at least two replicas and a readiness probe | Production guardrails |
| A merge request, with a reason and an end date, for anything that must differ | exceptions/ |
| The platform provides | Implemented by |
|---|---|
| One pipeline definition per delivery shape: build, unit tests, lint, secrets scan, dependency scan, SBOM, package | templates/node-service.yml, versioned by tag |
| Artifact conventions: one tarball per commit, a checksum beside it, an expiry | The template’s package job |
| One Application per environment, created and pruned from the directory layout | The ApplicationSet, applicationsSync: create-update |
| Namespaces with the platform’s labels, which the team cannot change | managedNamespaceMetadata |
| Automated sync with self-heal in development and staging; a manual gate in production | templatePatch on automated |
| A small set of rules, audited before they are enforced, with a written exception path | Two policy layers, Audit → Deny 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 fix | messageExpression 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:
| Control | Where it is enforced | Why there |
|---|---|---|
| Which jobs run in a consumer pipeline | CODEOWNERS on .gitlab-ci.yml; compliance pipelines on Ultimate | The include cannot do it; review or a higher-tier feature can |
| Which directory may create which resource | Argo CD AppProject whitelists and destinations | A team’s Application cannot ship a policy or touch another namespace |
| What a workload must declare | Kyverno Deny, at admission | The refusal is at the last point before the cluster, and names the fix |
| Who may grant an exception | The platform-exceptions namespace, written only by the platform project | An exception anywhere else is ignored by Kyverno |
| Which version of the pipeline a repository runs | A Git tag the consumer pins | The 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 applicationsand 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 --listor 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
includewith a tag and inputs. - Add a test stage: one job on
.platform:test. - Create an environment: one directory with a
kustomization.yamland aconfig.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.jsonkeys and the policy set are interfaces in the same sense and deserve the same discipline. - Documentation that is the artifact. The
spec:inputsblock with its descriptions, the policy annotations, theREADMEper 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.0unaffected byv1.1.0; the platform’s promise is that this stays true until a major version says otherwise. - Feedback loops. The
PolicyReportbefore enforcement, thegit 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