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.