Resource Graph
Resource Definitions can reference other Resource Definitions. This means that when a resource is provisioned, it might require other resources to be provisioned before it. The relationship between all the resources that need to be provisioned during a deployment is known as the Resource Graph. How a resource relates to or references other resources is defined by the Resource Definition.
The Resource Graph is a Directed Acyclic Graph (DAG) . This means that references that form a loop are not allowed. This is because a loop means that a resource would end up referencing itself. The graph is used to work out the order in which resources should be provisioned during a deployment. This is done by determining a Topological Order of the graph.
The shape of the Resource Graph for a particular deployment is affected by both the developer (via the Deployment Set or Score File) and by the platform team (via Resource Definitions). Developers can add resources to the graph via Resource Dependencies. Platform teams can add resources and additional connections in the graph via Resource References , Co-Provisioning and Resource Selectors .
Building and executing
For every deployment, a Resource Graph is first built and then executed on.
For each deployment into an Environment, the Platform Orchestrator builds the Resource Graph using in the following steps:
-
Gather Types and IDs of all resources to be provisioned for the Deployment Set being deployed.
This includes:
- all private and shared resource dependencies in the deployment set
- a
workload
resource type with IDmodules.<WORKLOAD ID>
- implicit resources such as
base-env
ork8s-cluster
Any resources that are referenced via placeholders in a workload are added as dependencies of the relevant
workload
resource in the Graph. -
For each resource to be provisioned, the Platform Orchestrator looks up the appropriate Resource Definition using Matching Criteria.
-
Each Resource Definition is analyzed to see if it adds new resources or connections to the Resource Graph.
If a new resource is added to the graph, steps 2 and 2 are related to that new resource.
When the graph is ready to execute, a Topological Order of the graph is determined. The resources are then provisioned in that order. This means that each resource is provisioned before any resources that depend on it.
Basic graphs
The simplest Resource Graph just involves implicit and dependent resources. Implicit resources are provisioned automatically by the Platform Orchestrator. These include: base-env
, workload
, k8s-cluster
and k8s-namespace
resources. Dependent resources are resources that are directly required by a workload. Examples of dependent resources could include a private postgres
database that a workload connects to or a shared dns
that the workload is exposed under.
Example
Consider a simple Application made up of a single workload with a private resource dependency on a PostgreSQL database. In this case, there is one dependent resources which is dependent by the workload
resource, a single base-env
, a single k8s-cluster
and a k8s-namespace
.
The base-env
, k8s-cluster
and k8s-namespace
are all unconnected. This means that they can be provisioned in any order. The postgres
resource is connected to the workload
resource. This means that it needs to be provisioned before the workload resource is provisioned.
Resource references
A Resource Reference is a Placeholder that can be used in a Resource Definition to use the outputs of another resource. Any Driver input in the Resource Definition can contain a resource reference placeholder. A resource that references another resource needs to be provisioned after the resource it references.
Placeholders
Resources are referenced by the tuple (combination of) Resource Type and Resource ID. If Resource ID is omitted, the resource ID of the referencing resource is used. The Placeholder has the following format:
resources.DESC.outputs.OUTPUT[.SUB_PROPERTY...]
where:
-
DESC
is the resource descriptor:TYPE[#ID]
where:
TYPE
is the type of the resource, e.g.dns
ID
is the optional Resource ID, e.g.shared.api-dns
-
OUTPUT
is an output from the resource, e.g.host
-
SUB_PROPERTY...
can be further sub properties if the value ofOUTPUT
is a complex type.
As ID
might contain .
, it is common to write the placeholder using []
notation for the TYPE#ID
tuple as follows:
${resources['dns#shared.dns'].outputs.host}
This reference resolves to the host
value as outputted by the dns
resource with Resource ID of shared.api-dns
.
Example
The previous example can be extended as follows:
- The
base-env
resource creates the PostgreSQL instance that thepostgres
resource needs. - The workload needs to run under a specific Kubernetes Service Account.
Both of these should not affect how the developer specifies their workload.
This can be modeled using Resource References.
1. Reference the base-env from the Postgres resource
To get the outputs from the base-env
the Resource Definition for the postgres
resource needs to reference the outputs of the base-env
resource:
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
id: postgres-def
entity:
type: postgres
name: postgres-def
driver_type: humanitec/postgres
driver_inputs:
values:
host: ${resources.base-env#base-env.outputs.pghost}
name: ${resources.base-env#base-env.outputs.pgname}
port: ${resources.base-env#base-env.outputs.pgport}
append_host_to_user: false
secrets:
dbcredentials:
username: ${resources.base-env#base-env.outputs.pguser}
password: ${resources.base-env#base-env.outputs.pgpassword}
criteria:
- app_id: example-app
In this case, the Resource Type is base-env
and the Resource ID is also base-env
.
2. Provision a service account for the workload
The workload needs to reference a resource of type k8s-service-account
and retrieve the name
output. This can then be used to set the serviceAccountName
field in the workload.
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
id: workload-with-sa
entity:
type: workload
name: workload-with-sa
driver_type: humanitec/echo
driver_inputs:
values:
update:
- op: add
path: /spec/serviceAccountName
value: ${resources.k8s-service-account.outputs.name}
criteria:
- app_id: example-app
k8s-service-account
resource is assumed to exist.
Compared to the first example:
- an additional resource of type
k8s-service-account
with a Resource ID that is the same as theworkload
is created. It is depended on by theworkload
resource. - This is also a dependency added on the
base-env
from thepostgres
resource.
3. More examples
Our
Example Library
contains further examples making use of Resource references. Inspect their code to see the ${resources..}
notation applied to solve the particular use case of each example.
Summary: Resource References
Resource References can be used to:
- chain resources together such that the output of one resource can be used as the input of another
- provision new resources by adding them into the Resource Graph
Co-provision Resources
Co-provisioning allows additional resources to be added to the Resource Graph without a Resource Reference required. These additional resources are listed in a Resource Definition and are added whenever that Resource Definition is matched.
It can be necessary to provision an additional resource that is not depended on by the resource it should be provisioned with. For example, an IAM access policy for a PostgreSQL database should depend on the PostgreSQL database rather than the other way around. This means that Resource References alone cannot be used to model this. Instead, a Resource Definition can define additional resources to be Co-provisioned. This co-provisioned resource can then use a Resource Reference to create a dependency in the opposite direction.
%% Using HTML formatting for the subgraph labels
%% due to broken layout in Firefox for markdown.
flowchart TB
subgraph fig1 ["<b>R</b> references <b>N</b>"]
direction LR
R2(R) --> N2(N)
end
subgraph fig2 ["<b>R</b> co-provisions <b>N</b>, <b>N</b> references <b>R</b>"]
direction RL
N1(N) --> R1(R)
end
fig1 ~~~ fig2
A Resource Definition can define that additional resources need to be provisioned when the one it defines is provisioned. These additional resources can be added with a range of different links to the resource being provisioned and its parent resources.
You define co-provisioning by adding a provision
section to the Resource Definition like this:
entity:
...
provision:
# Name the Resource Type of the resource that should be co-provisioned
aws-policy:
is_dependent: false
match_dependents: true
Specify these attributes to model the Resource Graph:
is_dependent
(true
/false
): states whether the co-provisioned resource depends on its co-provisioning resource.- Note that when the co-provisioned resource has a reference to its co-provisioning resource, there will be a dependency at any rate. Setting this attribute to
true
will introduce a dependency into the graph in all cases.
- Note that when the co-provisioned resource has a reference to its co-provisioning resource, there will be a dependency at any rate. Setting this attribute to
match_dependents
(true
/false
): states whether the resources which depend on the co-provisioning resource will depend on the co-provisioned resource as well.- This is useful if you want to traverse the Resource Graph using Resource selectors to query the co-provisioned resources.
In the following diagram, the resource R is depended on by its parent P. The resource R co-provisions an additional resource N.
%% Using HTML formatting for the subgraph labels
%% due to broken layout in Firefox for markdown.
flowchart TB
subgraph fig1 ["<b>R</b> co-provisions <b>N</b> with no additional links"]
direction LR
P1(P) ---> R1(R) ~~~ N1(N)
end
subgraph fig2 ["<b>R</b> co-provisions <b>N</b>, <b>N</b> has a reference to <b>R</b>"]
direction LR
P2(P) ---> R2(R)
N2(N) ---> R2(R)
end
subgraph fig3 ["<b>R</b> co-provisions <b>N</b> and specifies it matches its dependents, <b>N</b> has a reference to <b>R</b>"]
direction LR
P3(P) ---> R3(R)
P3(P) ---> N3(N)
N3(N) ---> R3(R)
end
fig1 ~~~ fig2
fig2 ~~~ fig3
classDef pClass stroke-width:1px
classDef rClass stroke-width:2px
classDef nClass stroke-width:2px,stroke-dasharray: 5 5
class R1 rClass
class R2 rClass
class R3 rClass
class N1 nClass
class N2 nClass
class N3 nClass
class P1 pClass
class P2 pClass
class P3 pClass
classDef subgraphClass white-space:nowrap
class fig1 subgraphClass
class fig2 subgraphClass
class fig3 subgraphClass
Example: PostgreSQL and IAM Policy
The previous example can be further extended as follows:
- An AWS IAM Policy (
aws-policy
) needs to be created for eachpostgres
resource created. This needs the name of the database and so must depend on thepostgres
This can be modeled by:
- co-provisioning an
aws-policy
in thepostgres
Resource Definition. This is done without specifying a Resource ID so that it uses the same Resource ID as thepostgres
resource. - In the Resource Definition for the
aws-policy
, a Resource Reference is used to retrieve the database name.
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
id: postgres-def
entity:
type: postgres
name: postgres-def
driver_type: humanitec/postgres
driver_inputs:
values:
host: ${resources.base-env#base-env.outputs.pghost}
name: ${resources.base-env#base-env.outputs.pgname}
port: ${resources.base-env#base-env.outputs.pgport}
append_host_to_user: false
secrets:
dbcredentials:
username: ${resources.base-env#base-env.outputs.pguser}
password: ${resources.base-env#base-env.outputs.pgpassword}
provision:
aws-policy:
is_dependent: false
match_dependents: true
criteria:
- app_id: example-app
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
id: iam-policy
entity:
type: aws-policy
name: iam-policy
driver_type: my-org/aws-policy
driver_inputs:
values:
db_name: ${resources.postgres.outputs.name}
criteria:
- app_id: example-app
Compared to the second example:
- An additional resource of type
aws-policy
with a Resource ID that is the same as thepostgres
is created. - The
aws-policy
has a resource reference to a resource of typepostgres
without specifying the Resource ID. This matches thepostgres
resource that co provisioned it because it defaults to use Resource ID of the resource making the Resource Reference
Example: S3 and IAM Policy
Our Example Library features another complete example on co-provisioning for S3 and IAM policies .
Summary: Co-provisioning
Co-provisioning can be used to:
- add additional resources to the resource graph,
- define additional edges between the co-provisioned resource and the resource that provisioned it and the resources that it depends on.
Resource selectors
Resource Selectors provides a way of querying the Resource Graph to find sets of resources that have a particular type and are depended on or depend on a particular resource. They are defined via Placeholders that expand to an array of values. Unlike Resource References , Resource Selectors do not add additional resources to the graph. They only add additional edges to the graph.
Placeholders
Resource Selectors extend the syntax of
Resource Reference Placeholders
by appending either a >
or a <
symbol to the DESC
:
resources.DESC.outputs.OUTPUT[.SUB_PROPERTY...]
where:
-
DESC
is the Resource Descriptor:TYPE[#ID][><]SELECTED_TYPE
where:
-
TYPE
is the type of the resource, e.g.dns
-
ID
is the optional Resource ID, e.g.shared.api-dns
-
><
is one of>
or<
indicating the direction of dependence:>
makes the selector output all the resources with typeSELECTED_TYPE
that are depended on by the resource identified by the Resource Descriptor.<
makes the selector output all the resources with typeSELECTED_TYPE
that are dependent on the resource identified by the Resource Descriptor.
-
-
OUTPUT
is an output from the resource, e.g.host
-
SUB_PROPERTY...
can be further sub properties if the value ofOUTPUT
is a complex type.
As ID
might contain .
, it is common to write the placeholder using []
notation for the TYPE#ID
tuple as follows:
${resources['dns#shared.dns<route'].outputs.path}
This reference resolves to an array of path
values as outputted by the route
resources that depend on a dns
with a Resource ID of shared.api-dns
. It is an array because there can more than one route
resource depending on the dns
.
This kind of selector is used internally as ${resources.dns<route.outputs.path}
to realize the
default Humanitec route handling
. Note that it shows more than one potential route
resource.
Default Humanitec route handling
Selectors are most easily understood right to left:
- the selector
TYPE#ID>SELECTED_TYPE
should be read as: All the resources with typeSELECTED_TYPE
that are depended on by resources with IDID
and typeTYPE
. - the selector
TYPE#ID<SELECTED_TYPE
should be read as: All the resources with typeSELECTED_TYPE
that are dependent on resources with IDID
and typeTYPE
.
Example
The previous example can be further extended as follows:
- An AWS IAM Role (
aws-role
) needs to be created that the service account references. The role needs to access the ARNs of all the AWS IAM Policies that the workload depends on.
This can be modeled by:
- Referencing an
aws-role
from thek8s-service-account
. This is done without specifying theID
in the Resource Reference Placeholder so that theaws-role
inherits the Resource ID from thek8s-service-account
. - In the Resource Definition for the
aws-role
, a Resource Selector is used to retrieve the ARNs of all the policies that theworkload
resource depends on.
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
id: iam-role
entity:
type: aws-role
name: iam-role
driver_type: my-org/aws-role
driver_inputs:
values:
arns: ${resources.workload>aws-policy.outputs.name}
criteria:
- app_id: example-app
Compared to the second example:
- An additional resource of type
aws-role
with a Resource ID that is the same as theworkload
is created. - The
aws-role
depends on all theaws-policy
resources that the workload depends on.
Summary: Resource Selector
The Resource Selector can be used to select sets of resources of a particular type out of the Resource Graph.
Resource Graph patterns
See our example page of Resource Graph patterns that can be used when building Resource Graphs.