In previous articles, we talked about the importance of platform backends and why platform teams shouldn’t start building their Internal Developer Platform (IDP) from the frontend. We also discussed different kinds of backends for Internal Developer Platforms and why graph based backends are a superior design to pipeline based backends.
In this article, we will dive deeper into the core logic of graph-based backends, Resource Graphs, and how the Humanitec Platform Orchestrator utilizes them to standardize your infrastructure setup and automate software delivery.
What is a Resource Graph?
A Resource Graph in the context of Humanitec’s Platform Orchestrator is a representation of the relationship between a workload and all its dependent resources (like database, storage, DNS, etc.) that need to be provisioned during a deployment.
This graph is built using Resource Definitions which can in turn reference other Resource Definitions. So if resource A has a dependency on resource B, the graph will represent such dependency, ensuring that B gets provisioned before A.
The Resource Graph is a Directed Acyclic Graph (DAG), which means that references that form a loop are not allowed, i.e. resource B is not allowed to back-reference resource A.
The Resource Graph is used by the Platform Orchestrator to work out the order in which resources should be provisioned during a deployment. This is done by determining the topological ordering (the linear sequence) of the graph.
The sequence for a particular deployment is affected by both the developer and the platform team. Developers can add resources to the graph by specifying Resource Dependencies in their Score workload specification. Platform teams can add resources to the graph via Resource References to additional resources in the Resource Definitions. They can also use advanced mechanisms like Co-Provisioning and Resource Selectors for more elaborate setups.
Developers are shielded from most of the potential complexity of the graph. All they are concerned about are the Resources they directly require, and that they request via their Score file (e.g. a database). The set of additional backdrop resources (e.g. principals, permissions, firewall rules etc.) is modeled by the Platform team, and will unfurl automatically at deployment time. That's separation of concerns, and massively reduced cognitive load for developers.
For every deployment, a Resource Graph is first built and then executed by the Platform Orchestrator, which is a key difference to stage by stage pipeline backends. The Platform Orchestrator builds the Resource Graph in a series of steps, including gathering Types and IDs of all resources to be provisioned, looking up the appropriate Resource Definitions using Matching Criteria, and analyzing each Resource Definition to see if it adds new resources or connections to the Resource Graph. Resource References can be used to chain resources together such that the output of one resource can be used as the input of another, so new resources can be easily added to the Resource Graph.
Examples
Workload with a database of type Postgres
Let’s start with a very simple example. The Resource Graph is created based on the resources declared by the developer in the workload specification, e.g. in a Score file.
A Score file is a YAML file that developers use to declare their workloads and the resources they need in order to run. This file acts as the single source of truth for a workload’s runtime requirements. It describes dependencies like databases, storage, and caches.
Here is an example of a Score file:
apiVersion: score.dev/v1b1
metadata:
name: product-service
containers:
main:
image: . # Set by CI pipeline
variables:
CONNECTION_STRING: postgresql://${resources.db.user}:${resources.db.password}@${resources.db.host}:${resources.db.port}/${resources.db.name}
resources:
db:
type: postgres
Here the db resource of type postgres is defined in the Score file. The CONNECTION_STRING variable uses a substitution pattern to reference the db resource.
When the Platform Orchestrator processes this particular Score file, it matches the requested Resource Dependencies to the Resource Definitions defined by the platform team. It then creates a graph-based representation of the workload and its dependencies that looks like this:
Crucially, the developer doesn’t have to worry about any implementation detail of the Postgres DB, nor where the workload gets deployed to. The Platform Orchestrator will simply check the Resource Definitions against the respective Matching Criteria and context (e.g. by environment type), and it’ll automate all steps to deployment. In this example the context may say that environment type = ephemeral.
Workload with a Postgres database and a Redis cache
Let’s assume that, as a next step, a developer wants to add an additional Redis cache to this workload.
The Score file now includes a cache Resource of type: redis.
apiVersion: score.dev/v1b1
...
resources:
db:
type: postgres
cache:
type: redis
The graph based representation will look like this:
Workload with Postgres, Redis, storage of type s3 and DNS
Our example workload so far is missing a DNS entry to access it. Let’s add that and also an S3 bucket to it. The Score file will now look like this:
apiVersion: score.dev/v1b1
...
resources:
db:
type: postgres
cache:
type: redis
dns:
type: dns
storage:
type: s3
The graph-based representation will look like this:
Advanced: workload requests HTTP routing and DNS entry
Now that we dissected simpler examples, let’s see what a more advanced use case would look like. Our workload is now requesting HTTP routing and a DNS entry. The route uses the hostname from the DNS resource via another placeholder.
apiVersion: score.dev/v1b1
...
resources:
api-dns:
type: dns
users-route:
type: route
params:
host: ${resources.api-dns.host}
path: /users
port: 80
The platform engineering team used Resource Definitions to instruct the Platform Orchestrator to automatically provision an Ingress resource and a TLS certificate with any DNS entry. The Resource Graph would look like this:
All this complexity is shielded away from developers, and it again takes only a few and simple lines of code to request the resources the workload depends on.
Very advanced: two workloads request the same S3 storage, but different classes
An even more advanced example would include two workloads that request a resource type of S3, but of a different class. Classes can be used to specify different flavors of a given type. In this example both workloads use the same S3 bucket, but the first workload uses the class admin, while the second workload uses the class read only because it does not require full access to the S3 bucket.
Score file A:
apiVersion: score.dev/v1b1
...
resources:
storage:
type: s3
class: admin
id: main-s3
Score file B:
apiVersion: score.dev/v1b1
...
resources:
storage:
type: s3
class: read-only
id: main-s3
The platform team has used Resource Definitions for instructing the Platform Orchestrator to automatically provision an appropriate IAM policy for the requested class. Because both workloads requested the same id main-s3, they will effectively get the same concrete S3 storage. This pattern is called "Delegator resource" and you can read more about it, and other advanced patterns, at Resource Graph Patterns.
Looking at the Score file we can see once more that developers are completely shielded from the increased complexity of even very advanced provisioning use cases. Instead of dealing with different flavors of S3 buckets and IAM policies, they simply declare the class of the resource the workload depends on. This is all they need to do to stay on the golden path, without having to worry about implementation details or violating policies.
Conclusion
Resource Graphs and graph-based backends are superior to pipeline-based designs because they ensure standardization and automation at scale, in a secure and compliant way. Developers can interact with any environment or infrastructure autonomously, shielded from the underlying complexity of the setup.
Zooming out and looking at the big picture makes this even more obvious. The same Score file can be deployed to different environments, and each time the Platform Orchestrator will create a specific Resource Graph for each context.
In the example below you see the workload running in an ephemeral environment on AWS:
No matter which degree of complexity the underlying infrastructure and resource system has, the developer experience, e.g. in the form of a Score file, remains simple and intuitive. The complexity of the workload spec file doesn’t grow linearly with the complexity of the respective Resource Graph.
Resource Graphs enable infrastructure platform engineers to create an abstraction layer on top of existing infra, which in turn lets infra and operations teams ensure resources are provisioned and consumed in a standardized and compliant way.
Check our documentation for a more in depth look at the Resource Graph and its patterns. To start building a graph-based backend for your IDP, join our upcoming workshops or talk to our platform architects.