Score: Developer-Centric Workload Specification
Here’s a configuration problem most teams hit eventually: you have a docker-compose.yaml for local development and a Helm chart (or raw manifests) for Kubernetes. They describe the same workload, but diverge constantly. A developer adds an environment variable — it needs to go in three places. You add a new service dependency — it needs to be wired up differently in each environment. The files drift apart. Problems appear in staging that weren’t present locally.
Score is a CNCF Sandbox project (accepted August 2024) that attacks this at the source. Instead of maintaining environment-specific files, you write a single score.yaml that describes what your workload needs — containers, dependencies, ports, volumes — and Score’s CLI tools generate the environment-specific output from it. One score.yaml, one score-compose generate for Docker Compose, one score-k8s generate for Kubernetes manifests.
The Problem in Concrete Terms
A typical developer workflow without Score looks like this:
Application code
├── docker-compose.yaml ← local dev
├── k8s/
│ ├── deployment.yaml ← staging/prod
│ ├── service.yaml
│ ├── configmap.yaml
│ └── secret.yaml
└── helm/
└── values.yaml ← production overrides
Every one of these files describes the same workload. When something changes — new environment variable, new dependency, port change — you update it in multiple places and hope you didn’t miss one. Score collapses this to:
Application code
├── score.yaml ← single source of truth
├── docker-compose.yaml ← generated by score-compose
└── k8s/manifests/ ← generated by score-k8s
The generated files are artifacts. The score.yaml is the source.
The score.yaml Format
A complete score.yaml for a web service with a database and cache:
|
|
The key elements:
containers — one or more containers in the workload. Each has an image, optional command override, environment variables, resource limits, and health checks. The ${resources.db.host} syntax is how you reference provisioned resource outputs — the actual connection details get injected at generation time based on what the target platform’s provisioners provide.
service — ports the workload exposes. Named ports (web, metrics) let Score generate the right service type for each target platform.
resources — dependencies the workload needs. type: postgres, type: redis, type: s3 are the declarations. How they’re actually provisioned is the platform team’s concern, not the developer’s.
metadata.name — the workload’s identity, referenced with ${metadata.name} in variables.
score-compose: Local Development
Install:
|
|
Initialize a project:
|
|
This creates a .score-compose/ directory with a state.yaml and default provisioner definitions. Add .score-compose/ to your .gitignore — it’s a local state directory.
Generate and run:
|
|
generate reads score.yaml, loads provisioners, and writes docker-compose.yaml. The default provisioners handle postgres, redis, mongodb, mysql, rabbitmq, and s3 (via MinIO) automatically. For postgres, you get a postgres container and the connection details injected as environment variables in your service. You didn’t configure any of that — the provisioner handled it.
Multiple workloads in one compose stack:
|
|
Each generate call appends to the compose file. Services see each other on the Docker network.
Overrides for local tweaks:
|
|
Overrides let you change values for local development without touching the canonical score.yaml.
score-k8s: Kubernetes Manifests
Install:
|
|
Initialize:
|
|
Generate Kubernetes manifests:
|
|
From the same score.yaml that generated your Docker Compose file, score-k8s generate produces:
- Deployment — pod spec with containers, environment variables, resource limits, health checks
- Service — ClusterIP/LoadBalancer based on the
service.portsdefinition - ConfigMap — non-sensitive environment variables
- Secret — sensitive values from provisioners (database passwords, API keys)
- PersistentVolumeClaim — if volumes are declared
The generated manifests are complete and apply directly. No templating engine required, no Helm values to maintain.
How Resource Provisioning Works
The provisioning system is where Score gets interesting and where the platform team’s work happens.
When score-compose or score-k8s encounters resources: db: type: postgres, it looks for a provisioner that matches type: postgres. The provisioner defines how to create the resource and what outputs to expose. Those outputs (host, port, username, password, name) get injected into the container’s environment variables wherever ${resources.db.*} appears.
Default provisioners (included out of the box):
| Resource type | score-compose | score-k8s |
|---|---|---|
postgres |
Docker postgres container | External/in-cluster postgres, credentials via Secret |
redis |
Docker redis container | External/in-cluster redis |
mongodb |
Docker mongo container | External/in-cluster MongoDB |
mysql |
Docker mysql container | External/in-cluster MySQL |
s3 |
MinIO container | S3 bucket (AWS or compatible) |
rabbitmq |
Docker rabbitmq container | External/in-cluster RabbitMQ |
Custom provisioners let platform teams add anything. A provisioner is a YAML template placed in the state directory:
|
|
Developers reference type: kafka in score.yaml. The provisioner starts a Kafka container and injects the broker URL. The developer never configures Kafka — they just declare that they need it.
For Kubernetes, provisioners can generate actual cloud resources (RDS instances, ElastiCache clusters) via Terraform or CloudFormation, then inject the resulting connection strings as Secrets. This is the platform team’s domain: write the provisioner once, every workload that needs postgres gets a properly provisioned database without touching infrastructure code.
Complete Workflow
Developer side:
|
|
CI/CD pipeline:
|
|
The same score.yaml that the developer tested locally is the source for the Kubernetes manifests deployed to production. The outputs differ (Docker Compose vs Kubernetes manifests), but the workload specification is identical.
Multi-Container Workloads
Score supports multiple containers per workload by defining multiple entries under containers:
|
|
Both containers share the same pod in Kubernetes and the same Docker network in Compose. Init container semantics and Kubernetes sidecar restart policies are implemented at the platform level — score-k8s generates a Deployment that the Kubernetes scheduler handles natively.
Score and Internal Developer Platforms
Score’s design makes it a natural fit as the workload specification layer in an Internal Developer Platform (IDP). The separation of concerns maps directly to the platform engineering model:
Developers own: score.yaml — what the workload needs (image, env vars, dependencies, ports)
Platform teams own: Provisioners — how those needs are satisfied in each environment (which database tier, which security group, which naming convention, which cloud region)
This means a developer can declare type: postgres without knowing whether that’s a Docker container locally, an RDS instance in staging, or an Aurora cluster in production. The provisioner layer makes that decision, consistently, without developer involvement.
The workload is portable across environments not because magic made it so, but because the interface between the developer’s specification and the platform’s implementation is cleanly defined.
Humanitec (Score’s creator) built their commercial Platform Orchestrator on this model. But the spec and the CLI tools are open-source and CNCF-governed — you can build your own provisioners for any target platform without touching Humanitec’s product.
Comparison with Alternatives
| Score | Docker Compose | Helm | Acorn | Waypoint | |
|---|---|---|---|---|---|
| Multi-platform | Yes | No (Compose only) | No (K8s only) | No (K8s only) | Yes |
| Developer-facing | Yes (simple spec) | Yes (but env-specific) | No (complex templating) | Yes | Partial |
| Provisioner system | Yes | No | No | Yes | Plugin-based |
| GitOps-friendly | Yes | Partial | Yes | Yes | Partial |
| CNCF | Sandbox | No | No | No | No |
| Lock-in | Low (open spec) | Low | Low | Medium | Medium |
Score’s unique position: it’s the only tool that provides a platform-agnostic workload spec without requiring you to adopt a specific runtime or platform. It generates artifacts for your existing tools rather than replacing them.
Current Maturity
Stable and production-ready:
- The
score.dev/v1b1specification — stable, versioned score-compose— production-ready for local developmentscore-k8s— production-ready for Kubernetes deployments- Core resource types: postgres, redis, mongodb, mysql, s3, rabbitmq
Growing/experimental:
- Additional platform implementations (Cloud Run, Nomad, ECS, Fly.io — community projects)
- Advanced provisioner features
- Init container and sidecar specification at the Score level (currently delegated to implementations)
Known limitations:
- Only supports OCI containers — not serverless functions, VMs, or other workload types
- Platform teams must build and maintain provisioners — Score doesn’t auto-provision infrastructure
- Assumes an existing orchestration platform (Docker or Kubernetes) — Score doesn’t run workloads itself
- The ecosystem is growing but small compared to Helm’s plugin library
When Score Makes Sense
Score is a good fit when:
- Your team maintains parallel configs for local, staging, and production environments that keep diverging
- You’re building or operating an IDP and need a clean workload specification interface for developers
- You want developer portability — developers should be able to describe workloads without knowing Kubernetes YAML
- Your stack is containers on Kubernetes — Score’s strongest support is for this combination
It’s probably not worth the investment when:
- You have a small team with a simple, stable stack where configuration drift isn’t a real problem
- You need non-container workloads (functions, managed services, bare-metal)
- You don’t have a platform team to build and maintain provisioners
Getting Started
The fastest path to understanding Score is running it against a real service:
|
|
The manifests are readable, standard Kubernetes YAML. Score doesn’t abstract Kubernetes away — it generates idiomatic Kubernetes resources that you can inspect, modify, and apply with standard tooling.
The Bottom Line
Score solves configuration sprawl by making the workload specification the single source of truth instead of making environment-specific configuration files the source of truth. The developer writes what they need once; platform teams define how those needs are fulfilled per environment.
It’s not magic and it doesn’t eliminate platform complexity — the provisioner system still requires platform engineering work. But it puts that complexity in the right place: platform teams handle it once, and developers stop maintaining parallel configuration files.
For teams already investing in platform engineering and IDPs, Score is worth evaluating as the workload specification layer. The CNCF backing and Apache 2.0 license mean you’re not betting on a vendor’s continued interest.
Comments