Building a Facets Module
This section explains how to turn a module concept into a working Facets module. You'll define the module interface using facets.yaml
, connect it to Terraform logic, and use the Facets CLI to scaffold and validate your work.
We follow the recommended Facets module development workflow:
1. Plan the Capability
The goal is to model a reusable capability — like provisioning an S3 bucket. You should define what parts of the configuration developers should control, and what should be embedded as opinionated logic.
For example, this S3 bucket module lets developers:
- Set a bucket name
- Choose whether it is public or private
- Select a lifecycle policy using simple enums — like
standard
,short
, orlongterm
These enums are an example of organizational context and abstraction. Instead of asking developers to input raw retention periods, the module translates these values internally (e.g., short
= 30 days, standard
= 90 days, longterm
= 365 days). This ensures consistency and simplifies configuration for consumers.
2. Define the Module Contract (facets.yaml
)
facets.yaml
)Use facets.yaml
to define your module’s public interface and metadata. This includes:
intent
,flavor
,version
, and cloud provider- Developer-facing
spec
inputs - Cross-module
inputs
typed via@outputs/...
- Structured
outputs
(bucket, policies, etc.)
This file serves as the module's single source of truth for configuration and wiring.
You can use the Facets CLI to scaffold and validate this YAML structure.
intent: aws-s3-bucket
flavor: secure-bucket
version: '1.0'
description: Provision a secure S3 bucket with lifecycle and access policies.
clouds:
- aws
spec:
title: S3 Bucket Settings
description: Inputs to configure bucket behavior.
type: object
properties:
bucket_name:
type: string
title: Bucket Name
is_public:
type: boolean
title: Make Public
default: false
lifecycle_policy:
type: string
title: Lifecycle Policy
enum: [standard, short, longterm]
default: standard
required:
- bucket_name
inputs:
cloud_account:
type: "@outputs/aws_account"
providers:
- aws
outputs:
attributes.bucket_name:
title: Bucket Name
type: "@outputs/bucket_name"
attributes.read_policy:
title: Read IAM Policy
type: "@outputs/iam_policy"
attributes.write_policy:
title: Write IAM Policy
type: "@outputs/iam_policy"
3. Write the Terraform Logic
Your Terraform module must only use the standard variables injected by the Facets engine. These map directly to your facets.yaml
:
variable "instance" {
description = "Developer-supplied configuration."
type = object({
kind = string
flavor = string
version = string
spec = object({
bucket_name = string
is_public = bool
lifecycle_policy = string
})
})
}
variable "instance_name" {
description = "Globally unique resource name."
type = string
}
variable "environment" {
description = "Environment metadata."
type = object({
name = string
unique_name = string
})
}
variable "inputs" {
description = "Cross-module inputs."
type = object({
cloud_account = object({
region = string
})
})
}
Do not define additional input variables.
4. Generate Outputs via Locals
Facets modules expose outputs using the output_attributes
object. This is a flat key-value map where each key represents an output field that can be consumed by other modules.
You may optionally define output_interfaces if your module exposes an interface that developers can connect to — such as a Postgres reader or writer — which typically includes standard attributes like url, user, and password.
locals {
output_interfaces = {}
output_attributes = {
bucket_name = aws_s3_bucket.this.bucket
read_policy = aws_iam_policy.read_policy.policy
write_policy = aws_iam_policy.write_policy.policy
}
}
Mark sensitive outputs using the
sensitive(...)
wrapper — for example,sensitive(aws_s3_bucket.this.bucket)
. Never expose secrets as plain text; marking them as sensitive ensures they are not rendered in logs or UI.. Never expose secrets as plain text — ensure any secret values are flagged to prevent them from being rendered or logged.
5. Providers
Facets modules do not define provider blocks internally. Instead, they consume providers through upstream modules using typed inputs.
To declare that your module depends on a provider, use the providers
key inside the inputs:
block in facets.yaml
:
inputs:
cloud_account:
type: "@outputs/aws_account"
providers:
- aws
This declares that the module expects a cloud_account
input which includes a usable aws
provider configuration.
This approach allows each building block to independently receive and use the provider it needs. It also enables gradual upgrades — not every module must migrate to a new provider version at the same time.
Updated 4 days ago