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:

  1. 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 ID modules.<WORKLOAD ID>
    • implicit resources such as base-env or k8s-cluster

    Any resources that are referenced via placeholders in a workload are added as dependencies of the relevant workload resource in the Graph.

  2. For each resource to be provisioned, the Platform Orchestrator looks up the appropriate Resource Definition using Matching Criteria.

  3. 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.

Example: Basic Graph

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 of OUTPUT 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:

  1. The base-env resource creates the PostgreSQL instance that the postgres resource needs.
  2. 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

Compared to the first example:

  • an additional resource of type k8s-service-account with a Resource ID that is the same as the workload is created. It is depended on by the workload resource.
  • This is also a dependency added on the base-env from the postgres resource.

Example: With 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
  classDef rClass fill:#fff,stroke:#000,stroke-width:2px
  classDef nClass fill:#fff,stroke:#000,stroke-width:2px,stroke-dasharray: 5 5
  class R1 rClass
  class R2 rClass
  class N1 nClass
  class N2 nClass
  classDef subgraphClass fill:#fff,stroke-width:0,white-space:nowrap
  class fig1 subgraphClass
  class fig2 subgraphClass

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.
  • 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 fill:#fff,stroke:#000,stroke-width:1px
  classDef rClass fill:#fff,stroke:#000,stroke-width:2px
  classDef nClass fill:#fff,stroke:#000,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 fill:#fff,stroke-width:0,white-space:nowrap
  class fig1 subgraphClass
  class fig2 subgraphClass
  class fig3 subgraphClass

Example

The previous example can be further extended as follows:

  • An AWS IAM Policy (aws-policy) needs to be created for each postgres resource created. This needs the name of the database and so must depend on the postgres

This can be modeled by:

  • co-provisioning an aws-policy in the postgres Resource Definition. This is done without specifying a Resource ID so that it uses the same Resource ID as the postgres 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 the postgres is created.
  • The aws-policy has a resource reference to a resource of type postgres without specifying the Resource ID. This matches the postgres resource that co provisioned it because it defaults to use Resource ID of the resource making the Resource Reference

Example: 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.
  • OUTPUT is an output from the resource, e.g. host

  • SUB_PROPERTY... can be further sub properties if the value of OUTPUT 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<routes'].outputs.route}

This reference resolves to an array of route values as outputted by the route resources that depends on a dns with Resource ID of shared.api-dns.

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 the k8s-service-account. This is done without specifying the ID in the Resource Reference Placeholder so that the aws-role inherits the Resource ID from the k8s-service-account.
  • In the Resource Definition for the aws-role, a Resource Selector is used to retrieve the ARNs of all the policies that the workload resource depends on.
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
  id: iam-role
entity:
  type: aws-policy
  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 the workload is created.
  • The aws-role depends on all the aws-policy resources that the workload depends on.

Example: Selectors

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.

Top