Overview

Modules describe how to provision a real-world resource of a resource type by referencing a Terraform/OpenTofu module.

The Orchestrator matches each request for a resource to a module which defines the Terraform/OpenTofu source code that should be used to provision it. Each module may also further expand the resource graph by defining dependency and coprovisioned resources that add additional nodes and edges to the resource graph. Once all nodes in the resource graph are matched to a module, a final Terraform file is generated internally and executed in the runner to provision the environment.

Modules can reference source code from either remote locations, files mounted in the runner, or inline source code stored in the Orchestrator itself.

Basic Example

The most basic module only needs to define a resource type and a source for the Terraform/OpenTofu code. In this example, this module may provision a Kubernetes namespace using source code in the github.com/acme-org/module-library repository (if it were to exist). The module source references a module in a subdirectory using the // notation.

canyon create module my-namespace-module \
  --set resource_type=k8s-namespace \
  --set module_source=git::https://github.com/acme-org/module-library//my-namespace-module

Configuration

A module configuration consists of these elements:

# User-supplied unique module id
id: my-module

# The resource type this module matches
resource_type: my-resource-type

# Optional module description
description: My module description

# Specify exactly one of the "module_" properties
# Option1: External module source
module_source: git::https://github.com/organization/repository//sub/directory
# Option2: Inline module source code, 2000 characters max. Not possible if module_source is used
module_source_code: |
  # Inline Terraform/OpenTofu code
  variable "input_var" {}

# Optional map of provider mappings
provider_mapping:
  google: google.us-central-1

# Optional map of module inputs to pass in to variables in the module code
module_inputs:
  input_var: "some-value"

# Optional map of dependencies
dependencies:
  other-resource:
    type: my-other-resource-type

# Optional list of coprovisioned resources
coprovisioned:
- type: some-resource-type

Refer to the individual sections below for more details.

The resource type

The module must be linked to a resource type that has already been configured in the Orchestrator. The resource type indicates what kind of behavior and outputs this module provides as its external contract in the resource graph.

The outputs of a module are provided by the Terraform/OpenTofu output blocks in the module source. The Orchestrator cannot validate whether these match the contract of the resource type, but errors will be thrown during deployment if an output expected by the resource type is not provided by the module.

Module source locations

The module source must be valid OpenTofu or Terraform source code. For more information about writing modules, refer to the OpenTofu Modules  and Terraform Modules  documentation.

A module may define its source using the module_source field with a git URL, mounted-file source, or using inline Terraform/OpenTofu code in the module_source_code field.

Git sources

Module source can be pulled from a git repository using either HTTP or SSH protocols by specifying the git::ssh:// or git::https:// prefixes. For example:

module_source: git::https://github.com/organization/repository

module_source: git::ssh://[email protected]/repo.git

module_source: git::https://bitbucket.org/organization/repo//sub/directory

Note that unlike in Terraform/OpenTofu, bare github.com or bitbucket.org domains must still use the git:: prefixes to avoid conflict with other source types.

The target repository or directory is expected to contain valid .tf files describing a Terraform/OpenTofu module.

Git sources may define a target branch, commit, or tag using the ref query argument on the url:

module_source: git::https://github.com/organization/repository?ref=v1-preview

Mounted files

Alternatively, module source can be pulled from a directory available within the runner container. This requires customization of the runner configuration to mount additional files and volumes at runtime but can allow for more complex environment-dependent configurations.

Mounted file sources are defined through a / prefix.

module_source: /mnt/some/directory/here

Mounted file sources do not support any kind of version reference.

Inline code

For short modules, demos, or testing, you may wish to store the module source code in the Orchestrator itself through the module_source_code field. This supports a multiline Terraform/OpenTofu formatted string to define the module.

A maximum of 2000 characters is supported.

module_source_code: |
    variable "input_var" {
      type = string
    }

    resource "random_id" "example" {
      length = 10
    }

    output "value" {
      value = random_id.example.hex
    }

    output "humanitec_metadata" {
      value = {
        "Example-Key" = "example value"
      }
    }

Switching the module source

Modules support updating the module_source or switching between module_source and module_source_code at any time. This only influences how the contents are pulled at deployment time and will not cause the resource to be recreated or deleted unless the Terraform resources defined within the source content change.

Provider mapping

Terraform/OpenTofu does not allow modules to contain their own Terraform provider configurations and these must be injected externally. To support this, the Orchestrator allows common providers to be configured with the appropriate configuration. This allows multiple modules that use the same cloud APIs or platforms to share configuration.

The provider_mapping field maps from a local provider name used within the module to a <provider_type>.<provider_id> tuple defined in the Orchestrator.

For example, if a module relies on the “google” and “kubernetes” providers, the provider mapping may be set as:

provider_mapping:
  google: google.us-central-1
  kubernetes: kubernetes.in-cluster

It is good practice to provide mappings for all providers used within the module even when using the default provider configuration.

Module inputs

If the module source defines Terraform/OpenTofu variables through variables blocks, the module may need to define values for any required variables or to override defaults. The module_inputs map is one way of passing values through to these. Note that any resource parameters defined as params by dependencies or coprovisioning in the resource graph will take precedence.

If the module source defines a Terraform/OpenTofu variable with no declared default, it must be provided through either the module_inputs or resource parameters (params).

module_inputs is defined as a structure, for example:

module_inputs:
  input_var: Some literal string
  complex_variable:
    key: value
  example_a: "${bare-placeholder}"
  example_b: "an ${interpolated-placeholder} within a string"

The structure may contain objects, arrays, strings, booleans, numbers, and null values.

Placeholder support in module inputs

Module inputs also support placeholders inside strings using the ${ prefix and } suffix. Module inputs support context placeholders, resource placeholders, and selector placeholders.

If the placeholder is the only content of the string, as in example_a above, then the expression type is maintained and passed through to the module. So if the expression referenced an integer value, the variable would be set as an integer. Similarly with objects, arrays, booleans, and null values. However, if the expression is surrounded by other string literal content, then the referenced value will be encoded as JSON before being embedded in the string.

Dependencies

While the Terraform/OpenTofu source code backing a module allows it to provision arbitrarily complex resources, modules are most efficient if they consume and subsume behaviour from dependency resources and other nodes in the resource graph.

The dependencies map allows a module to specify other resources that must be provisioned before the current resource. Dependencies thus effectively create additional resources.

The current resource may then use placeholders in the module_inputs to refer to outputs of these dependencies. This is the core of the resource graph: a resource that has a dependency on another resource is represented by two nodes joined by a directional edge.

flowchart LR
    currentResource(Current resource) -->|params| someresource(Other resource)
    someresource -.->|outputs| currentResource

Each dependency is specified by an alias key and a resource type with optional resource ID, class, and resource parameter settings:

dependencies:
  some-alias:
    type: some-resource-type
    class: optional-class
    id: optional-id
    params:
      key: value
  another-alias:
    type: another-resource-type
    params:
      ref: "${resources.some-alias.outputs.x}"

The alias, “some-alias” in the example above, is used as a local name within the module. This alias may be used in resource placeholders to refer to this dependency. For example, ${resources.some-alias.outputs.x}.

The resource type must be a resource type available within the deployment environment. If it cannot be matched to a module, the graph construction will fail with a related error.

Using a resource placeholder in the params for another resource creates a dependency in the resource graph between the two resources. The example shown above will yield this graph:

flowchart LR
    currentResource(Current resource) --> someResource(Some resource)
    currentResource --> anotherResource(Another resource)
    anotherResource --> someResource

Resource classes in dependencies

The dependencies class is a specialization of the resource type. It defaults to a class of “default” if not provided.

The symbol @ may by used to signify the current resource’s class. For example, if the current resource was a type postgres of class standard, a dependency with class @-beta would resolve to a class of standard-beta.

Resource IDs in dependencies

The dependencies id may be used to refer to a specific instance of the resource in the graph. It defaults to inheriting the id of current resource but can be specified as any fixed value.

Like the class, the @ symbol can be used to modify the inherited ID. For example, if the current resource had an ID of main, a dependency with ID @-primary would resolve as main-primary.

Resource uniqueness

As with all resources, the resource type, class, and ID together uniquely identify a single instance of a resource.

Coprovisioned resources

A module may use coprovisioned resources to add resources to the graph that are dependent on the current resource, or just to ensure they exist somewhere in the graph.

Coprovisioned resources are defined as a list in the coprovisioned section of a module. They do not have an alias and cannot be used in resource placeholders.

coprovisioned:
- type: some-resource-type
  class: optional-class
  id: optional-id
  params:
    key: value
  is_dependent_on_current: false
  copy_dependents_from_current: false

Like the resources in dependencies, coprovisioned resources also have a type, class, ID, and resource parameters. These operate with the same conventions as dependencies including support for @ referencing the current resource.

By default, coprovisioned resources are added as nodes to the resource graph with no relation or edge to the current resource node.

flowchart TB
    currentResource(Current resource) ~~~ coprovisionedResource(Coprovisioned resource)

The is_dependent_on_current and copy_dependents_from_current booleans may be used to anchor them.

is_dependent_on_current: true adds a dependency relationship between the coprovisioned resource and the current resource so that the current resource must be provisioned before the coprovisioned one. This may be used because the coprovisioned resource has placeholders that may refer to outputs of the current node.

flowchart LR
    coprovisionedResource(Coprovisioned resource) --> currentResource(Current resource)

copy_dependents_from_current: true takes affect after the resource graph has been created. This flag makes all resources that depend on the current resource also depend on the coprovisioned resource.

flowchart LR
    coprovisionedResource(Coprovisioned resource)
    parentResource(Parent resource) --> coprovisionedResource
    parentResource(Parent resource) --> currentResource(Current resource)

Setting both flags to true will yield this graph:

flowchart LR
    coprovisionedResource(Coprovisioned resource) --> currentResource(Current resource)
    parentResource(Parent resource) --> currentResource
    parentResource(Parent resource) --> coprovisionedResource

Coprovisioned resources are frequently used with selector placeholder expressions to produce aggregate behaviors.

Top