Overview

What are Resources?

In the context of the Platform Orchestrator, “Resources” refers to all elements which need to be provisioned for the complete Deployment of one or several workloads. These elements include the workloads themselves as well as all their direct and indirect dependencies.

Resources can be either infrastructure-based, like an Amazon S3 bucket, or logical, such as an AWS IAM Policy. Each Resource is characterized by a type that signifies the technology it represents, irrespective of its implementation or provisioning method.

Resources are a common ground for both core personas:

  • Developers specify dependencies on types of Resources that their workloads require
  • Platform engineers provide definitions for provisioning types of Resources in various contexts

The Platform Orchestrator brings both worlds together with each Deployment it executes.

The following guide walks you through the core concepts around Resources.

Resources 101

Developers request Resources via Score

Developers request any Resources their workload requires through Score . They deploy the Score file to the Platform Orchestrator, usually via a CI/CD pipeline.

# Score file for a workload requesting a postgres resource
apiVersion: score.dev/v1b1
metadata:
  name: my-workload
...
resources:
  my-db:
    type: postgres
flowchart LR
  score[Score file] -->|deploy| platform_orchestrator{Platform<br/>Orchestrator}

The Resource Graph

On deployment, the Platform Orchestrator generates a dependency Graph of all the requested Resources, the Resource Graph . You can view and navigate the Graph in the Humanitec Portal after the deployment finished.

As Resources can depend on each other, they need to be provisioned in the correct sequence. The Graph is created to determine that sequence. It therefore must be free of cycles.

flowchart LR
  workload(Workload<br/>&quot;my-workload&quot;) --> postgres(Postgres<br/>&quot;my-db&quot;)

Once the Graph structure is generated, the Platform Orchestrator provisions every Resource in the Graph. This usually means provisioning a real-world resource like a PostgreSQL database which is managed through the Orchestrator going foward, but it can also mean just representing a real-world resource in the Graph without managing it - we provide more details further down .


Resource Graph per Environment

There is one Resource Graph per Application Environment . All workloads deployed into that Environment are added to the Graph as root nodes, along with any resource dependencies defined via Score. Each node represents an Active Resource created during deployment.

flowchart LR
  workload(Workload<br/>&quot;my-workload&quot;) --> postgres(Postgres<br/>&quot;my-db&quot;)
  workload_other(Workload<br/>&quot;my-other-workload&quot;) --> storage(S3 bucket<br/>&quot;my-bucket&quot;)

Resource Definitions

The Platform Orchestrator finds the instructions on how to provision each Resource in a Resource Definition of the respective Resource Type . The chosen Driver (more details next ) determines how a Definition is executed, e.g. by running IaC code or using templates to create Kubernetes manifests on the target cluster.

Platform engineers maintain the Resource Definitions.

There are a number of Default Resource Definitions in the Orchestrator, including one for the “workload”.

Platform Engineers also control which Resource Definition to use in which context through matching criteria . The exact Definition to use in a deployment may therefore differ by Application or Environment, or it can be the same everywhere.

flowchart TB
  subgraph resDefs[Resource Definitions]
    direction LR
    resDefWorkload[Resource Definition<br/>&quot;Workload&quot;] ~~~ resDefPostgres[Resource Definition<br/>&quot;Postgres&quot;]
  end
  subgraph resources[Resource Graph]
    direction LR
    workload(Workload<br/>&quot;my-workload&quot;) --> postgres(Postgres<br/>&quot;my-db&quot;)
  end
  resDefs -.-> resources

Drivers

As part of each Resource Definition, the specified Driver determines the technical means for provisioning a Resource. Drivers may execute an IaC tool to provision a real-world resource, produce Kubernetes manifests on the target cluster, or just provide output values.

flowchart TB
    postgres(Postgres<br/>&quot;my-db&quot;) -->|Terraform Driver| database@{ shape: cyl, label: "my-db" } 

Resource references add Resources to the Graph

A Resource may again depend on other Resources. For example, the PostgreSQL database may require a PostgreSQL instance in which the database is hosted.

Platform engineers can define this dependency by placing a Resource reference to a PostgreSQL instance into the Definition for the “postgres” database Resource. Now whenever a “postgres” is requested, a “postgres instance” will automatically be added to the Graph as a dependent Resource.

flowchart LR
  workload(Workload<br/>&quot;my-workload&quot;) --> postgres(Postgres<br/>&quot;my-db&quot;)
  postgres --> postgres_instance(Postgres instance<br/>for &quot;my-db&quot;)

Infrastructure abstraction

Developers do not have to be concerned with underlying infrastructure details. They just request the “postgres” database via Score. The infrastructure behind it is abstracted away from them. The Postgres instance does not appear in Score.

flowchart LR
  workload(Workload<br/>&quot;my-workload&quot;) --> postgres(Postgres<br/>&quot;my-db&quot;)
  postgres -.-> postgres_instance(Postgres instance<br/>for &quot;my-db&quot;):::dashed

  classDef dashed stroke-dasharray:5,5

Resource Types

Resources are strongly typed. Each Resource in the Graph has exactly one Resource Type , e.g. workload, postgres, or postgres-instance.

Each Resource Definition covers exactly one Resource Type. There may be more than one Definition for a Resource Type. The Definition’s matching criteria determine which one of those is picked in a specific deployment context , e.g. for a specific Application or Environment.

flowchart TB
  subgraph resDefs[Resource Definitions]
    direction LR
    resDefWorkload@{ shape: processes, label: "Resource Definition<br/>type: <span style="font-family:Courier">workload</span>" } ~~~ resDefPostgres@{ shape: processes, label: "Resource Definition<br/>type: <span style="font-family:Courier">postgres</span>"}
    resDefPostgres ~~~ resDefPostgresInstance@{ shape: processes, label: "Resource Definition<br/>type: <span style="font-family:Courier">postgres-instance</span>"}
  end
  subgraph resources[Resource Graph]
    direction LR
    workload(Workload<br/>&quot;my-workload&quot;<br/>type: <span style="font-family:Courier">workload</span>) --> postgres(Postgres<br/>&quot;my-db&quot;<br/>type: <span style="font-family:Courier">postgres</span><br/>&lpar;direct&rpar;)
    postgres --> postgres_instance(Postgres instance<br/>for &quot;my-db&quot;<br/>type: <span style="font-family:Courier">postgres-instance</span><br/>&lpar;indirect&rpar;)
  end
  resDefs -.-> resources

Direct and indirect Resource Types

Types of Resources that developers may request via Score such as postgres are called direct Resource Types . Other types used further downstream such as the postgres-instance are called indirect Resource Types .


Implicit Resource Types

The workload itself is a so-called implicit Resource Type . One Resource of each implicit Type is automatically added to each deployment by the Platform Orchestrator. Other implicit types include the k8s-cluster and k8s-namespace, representing the target Kubernetes cluster and namespace for the workload deployment.

flowchart LR
    workload(Workload<br/>&quot;my-workload&quot;<br/>type: <span style="font-family:Courier">workload</span><br/>&lpar;implicit&rpar;) --> postgres(Postgres<br/>&quot;my-db&quot;<br/>type: <span style="font-family:Courier">postgres</span><br/>&lpar;direct&rpar;)
    postgres --> postgres_instance(Postgres instance<br/>for &quot;my-db&quot;<br/>type: <span style="font-family:Courier">postgres-instance</span><br/>&lpar;indirect&rpar;)
    k8sCluster(Kubernetes cluster<br/>type: <span style="font-family:Courier">k8s-cluster</span><br/>&lpar;implicit&rpar;)
    k8sNamespace(Kubernetes namespace<br/>type: <span style="font-family:Courier">k8s-namespace</span><br/>&lpar;implicit&rpar;)

Using Resource outputs in the Graph

Resources can produce outputs and provide them to upstream Resources. E.g. the postgres-instance can provide its host value as an output to the postgres Resource so that the Platform Orchestrator may use that data to provision the database in the proper instance.

In the Resource Graph, i.e. between Resources, a Resource Definition has access to any output another referenced Resource may produce.

flowchart LR
    workload(Workload<br/>&quot;my-workload&quot;) --> postgres(Postgres<br/>&quot;my-db&quot;)
    postgres --> postgres_instance(Postgres instance<br/>for &quot;my-db&quot;)
    postgres_instance -.->|host| postgres

Using Resource outputs in Score

Direct Resources can pass the outputs defined in their Resource Type schema - and only those - to the workload in Score. E.g. the postgres can pass on its host value as an output to the workload to help it create a database connection.

apiVersion: score.dev/v1b1
...
container:
  my-container:
    variables:
      # Reference the postgres resource to read the "host" output
      DB_HOST: ${resources.my-db.host}
resources:
  my-db:
    type: postgres
flowchart LR
    workload(Workload<br/>&quot;my-workload&quot;) --> postgres(Postgres<br/>&quot;my-db&quot;)
    postgres --> postgres_instance(Postgres instance<br/>for &quot;my-db&quot;)
    postgres_instance -.->|host| postgres
    postgres -.->|host| workload

Graph resources and real-world resources

A real-world Resource represented in the Graph may or may not be managed by the Platform Orchestrator. E.g. the Orchestrator may provision the “my-db” database in a PostgreSQL instance, while the instance itself is maintained externally.

You will still want to reflect the PostgreSQL instance in the Resource Graph to pass required outputs such as the host to the Resources of postgres databases located in that PostgreSQL instance. You can use the Echo Driver in the instance Resource Definition for that purpose.

flowchart TB
  subgraph resDefs[Resource Graph]
    direction LR
    postgres(Postgres<br/>&quot;my-db&quot;) --> postgresInstance(Postgres instance<br/>for &quot;my-db&quot;)
    postgresInstance -.->|host| postgres
  end
  subgraph realWorld[Real-world resources]
    direction LR
    database@{ shape: cyl, label: "&quot;my-db&quot; database<br/>managed by<br>Platform<br>Orchestrator" } ~~~ postgresInstanceReal(PostgreSQL instance<br/>managed<br/>externally)
  end
    resDefs -->|Terraform Driver for PostgreSQL database| realWorld
    resDefs -->|Echo Driver for PostgreSQL instance| realWorld

Workload Resources

The workloads themselves are deployed to a Kubernetes cluster using the container image defined via Score. The Platform Orchestrator creates a Kubernetes Deployment by default, but you can customize this behavior using a different Workload Profile .

The cluster Resource

The target cluster is expressed by the Resource of the Type k8s-cluster. That is an implicit Type , so one such Resource is always added. Like for all other Resource Definitions, the matching criteria control which specific Definition, and therefore which cluster, is used in a deployment context.

The cluster Resource Definition provides the data required to access the cluster .

The real-world Kubernetes cluster is always managed externally, not by the Platform Orchestrator.

flowchart LR
    workload(Workload<br/>&quot;my-workload&quot;<br/>type: <span style="font-family:Courier">workload</span><br/>&lpar;implicit&rpar;)
    k8sCluster(Kubernetes cluster<br/>type: <span style="font-family:Courier">k8s-cluster</span><br/>&lpar;implicit&rpar;)
    subgraph k8sClusterRealWorld[Kubernetes cluster]
        workloadOnK8s(my-workload)
    end
    k8sCluster -->|defines| k8sClusterRealWorld
    workload -->|deployed to| workloadOnK8s

Shared Resources

When several Resources need to access the same Resource, you can make it a Shared Resource .

E.g. given that the PostgreSQL databases requested by two workloads reside in the same PostgreSQL instance, then both postgres Resources in the Graph can reference the same postgres-instance Resource.

flowchart LR
    workload(Workload<br/>&quot;my-workload&quot;) --> postgres(Postgres<br/>&quot;my-db&quot;)
    workload2(Workload<br/>&quot;my-other-workload&quot;) --> postgres2(Postgres<br/>&quot;my-other-db&quot;)
    postgresInstance(Postgres instance<br/>&quot;Shared instance&quot;)
    postgres --> postgresInstance
    postgres2 --> postgresInstance

Advanced Resource topics

This section covers topics around Resources you will not need to know initially, but will be helpful in more advanced scenarios.


Co-provision other Resources

Sometimes a Resource needs another Resource to be created along with it, but with the dependency reversed. E.g. an AWS IAM policy may need to be created for each postgres database Resource. The policy needs the name of the database and so it must depend on the postgres.

flowchart LR
    subgraph justForLayout[ ]
      workload(Workload<br/>&quot;my-workload&quot;) --> iamPolicy(IAM policy<br/>for &quot;my-db&quot;)
    end
    iamPolicy --> postgres
    workload --> postgres(Postgres<br/>&quot;my-db&quot;)
    postgres -.->|name| iamPolicy

    classDef invisible fill:#00000000,stroke:#00000000
    class justForLayout invisible

A Resource Reference from the postgres to an IAM Policy would create a new IAM policy Resource, but the dependency would be directed the wrong way.

Instead, the postgres Resource can co-provision the IAM policy and have it depend on itself, i.e. on the postgres. The IAM policy may thus read the name output of the postgres Resource.

The co-provisioning can also have the workload depend on the IAM policy to facilitate advanced Graph lookups like the one shown next.


Perform lookups with Resource Selectors

You can perform lookups into the Graph from a Resource to locate other Resources somewhere else and read their outputs. You can even go round corners.

Consider this Graph: on top of the postgres and the IAM policy, there is now a Kubernetes service account and an IAM role. Both were created using Resource references : the workload referenced and thus created the service account, and the service account referenced and thus created the IAM role.

flowchart LR
    iamPolicy --> postgres
    workload --> postgres(Postgres<br/>&quot;my-db&quot;)
    workload(Workload<br/>&quot;my-workload&quot;) ----> iamPolicy(IAM policy<br/>for &quot;my-db&quot;)
    workload --> k8sServiceAccount(K8s service account<br/>for &quot;my-workload&quot;) --> iamRole(IAM Role<br/>for &quot;my-workload&quot;)

The IAM role now needs the names of all IAM policies for the postgres Resources of its upstream workload.

It can perform a lookup using a Resource Selector in the IAM Resource Definition stating “Please give me the names of all IAM policies that my upstream workload depends on”. This selector finds the IAM policy for “my-db” by traversing the dependencies of the Graph.

The Resource selector will add a new dependency from the IAM role to the IAM policy so that the IAM role can read the name output.

flowchart LR
    subgraph justForLayout[ ]
      direction LR
      workload(Workload<br/>&quot;my-workload&quot;) --> k8sServiceAccount(K8s service account<br/>for &quot;my-workload&quot;) --> iamRole(IAM Role<br/>for &quot;my-workload&quot;)
    end
    iamPolicy --> postgres
    iamRole --> iamPolicy
    workload ----> iamPolicy(IAM policy<br/>for &quot;my-db&quot;)
    workload --> postgres(Postgres<br/>&quot;my-db&quot;)
    iamPolicy -.->|name| iamRole

    classDef invisible fill:#00000000,stroke:#00000000
    class justForLayout invisible

Specialize Resources with Resource Classes

Resource Classes provide a way of offering different flavors or specializations of a Resource Type. Platform engineers define available classes and provide either separate Resource Definitions per class or differentiate between classes in the IaC code within one Definition.

Developers can then request Resources of a particular class via Score:

# Score file for a workload requesting resources with classes
apiVersion: score.dev/v1b1
metadata:
  name: my-workload
...
resources:
  external-bucket:
    type: s3
    # Class for a bucket open to the public internet
    class: external
  sensitive-bucket:
    type: s3
    # Class for a protected, encrypted bucket 
    class: sensitive
flowchart LR
    workload(Workload<br/>&quot;my-workload&quot;) --> s3BucketExternal(S3 bucket<br/>&quot;external-bucket&quot;<br/>class: external)
    workload --> s3BucketSensitive(S3 bucket<br/>&quot;sensitive-bucket&quot;<br/>class: sensitive)

Propagate Resource Classes to associated Resources

A Resource can propagate its class to other associated Resources it creates so that this new Resource can be provisioned with a matching configuration. E.g. each S3 bucket of a certain classs can co-provision an IAM policy of the same class. The Resource Definition code for the IAM policy can then query the class of the current Resource and provision the proper policy implementation.

flowchart LR
    iamPolicyExternal(IAM policy<br/>for &quot;external-bucket&quot;<br>class: external) --> s3BucketExternal
    workload(Workload<br/>&quot;my-workload&quot;) ---> s3BucketExternal(S3 bucket<br/>&quot;external-bucket&quot;<br/>class: external)
    workload ---> s3BucketSensitive(S3 bucket<br/>&quot;sensitive-bucket&quot;<br/>class: sensitive)
    iamPolicySensitive(IAM policy<br/>for &quot;sensitive-bucket&quot;<br>class: sensitive) --> s3BucketSensitive

The same mechanism works for Resources being created via Resource References .


Use Resource IDs to structure the Graph

Each Resource in the Graph has a permanent, non-semantic Globally Unique Resource ID ( GUResID ) to identify the Resource throughout its lifecycle. 

Each Resource also has a semantic Resource ID . Its value depends on how the Resource was created. By default, it is derived from the name of the upstream workload. Shared Resources have a shared.* ID.

flowchart LR
    workload(Workload<br/>&quot;my-workload&quot;<br/>ID: modules.my-workload) --> postgres(Postgres<br/>&quot;my-db&quot;<br/>ID: modules.my-workload.externals.my-db)
    workload2(Workload<br/>&quot;my-other-workload&quot;<br/>ID: modules.my-other-workload) --> postgres2(Postgres<br/>&quot;my-other-db&quot;<br/>ID: modules.my-other-workload.externals.my-other-db)
    postgresInstance(Postgres instance<br/>&quot;Shared instance&quot;<br/>ID: shared.my-db-instance)
    postgres --> postgresInstance
    postgres2 --> postgresInstance

The combination of Resource Type, Resource Class, and Resource ID uniquely identifies a Resource in the Graph.

Just like with Resource classes, a Resource may propagate its ID to Resources it creates via Resource References or co-provisioning so that related sets of Resources share an ID. This allows a Resource to locate associated Resources in the Graph using Resource Selectors .

Context

Each Resource is provisioned within a specified context, comprising these elements:

The context uniquely identifies a provisioned Resource. This combination allows differentiating between active instances of the same Resource across various Applications and Environments.

You can query each element of the context in a Resource Definition using Placeholders .

Example

A developer has added a shared Resource Dependency on a Resource of type dns and given it the ID api-dns. This makes its Resource ID shared.api-dns.

If there are three environments development, staging, and production, then there are three instances of this Resource - one in each environment. Each Active Resource has the same Application ID and Resource ID, but they contain different Environment IDs. Therefore, they each have a unique context:

Application ID Environment ID Resource ID
awesome-app development shared.api-dns
awesome-app staging shared.api-dns
awesome-app production shared.api-dns

Resource provisioning

During deployment, the Platform Orchestrator needs to provision every Resource in the Resource Graph using the Driver named in the associated Resource Definition .

The term provision is used broadly to denote making Resources available. Often, this involves creating a new real-world entity - a new DNS name, a new database in an existing instance, etc. However, there are situations where you might want to use an existing entity without the Orchestrator creating a new one.

For instance, in a Production environment, you might want your Application to be accessible via a fixed DNS name that you manage outside of the Platform Orchestrator. Instead of having the Orchestrator automatically create a new subdomain, the production environment should be accessible via a pre-defined DNS name.

You still need to have a dns Resource in the Resource Graph in Production. However, the provisioning of this Resource can just mean returning a pre-configured DNS name, and do nothing more. The Echo Driver is useful for this purpose.

In practice, this means providing two Resource Definitions for the type dns:

  • One Definition matching the non-production Environments and actually creating a DNS entry, e.g. using the Wildcard DNS Driver
  • One Definition matching the Production Environment and just returning the configured DNS name using the Echo Driver
Top