Agent skill
module-writing
Create and manage Facets IaC modules with Terraform. Covers facets.yaml structure, Terraform file conventions, output types, provider configuration, deep merge patterns, and the complete module development workflow using Raptor CLI.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/module-writing
SKILL.md
Facets Module Development Guide
A comprehensive reference for LLMs and developers to create Facets.cloud IaC modules.
Table of Contents
- Overview
- Core Concepts
- Module Structure
- The facets.yaml File
- Terraform Files
- Type System and Outputs
- Provider Passing
- Spec Schema Design
- UI Extensions (x-ui-*)
- Best Practices and Anti-Patterns
- Module Lifecycle
- Complete Examples
- Quick Reference
1. Overview
What is Facets?
Facets.cloud is a self-serve platform that enables:
- Platform/Infra engineers to create reusable infrastructure modules
- Developers to compose blueprints without Terraform knowledge
- Teams to deploy consistent environments across clouds
What is a Facets Module?
A Facets module is a Terraform module enhanced with a facets.yaml metadata file that:
- Defines a developer-friendly DSL (the
spec) - Declares typed inputs and outputs for inter-module connectivity
- Controls UI rendering for the Facets portal
- Enables provider passing between modules
Key Terminology
| Term | Description |
|---|---|
| Intent (Kind) | The technology/purpose (e.g., postgres, service, kubernetes_cluster) |
| Flavor | The implementation variant (e.g., ovh, aws, gcp, k8s) |
| Version | Semantic version of the module |
| Blueprint | Collection of resource definitions (JSON/YAML) that developers create |
| Resource | An instance of a module in a blueprint |
| Spec | The developer-facing configuration DSL defined by the module |
| Project Type | Template defining which modules are available to a project |
| Output Type | JSON schema defining the contract for module outputs |
The Big Picture
Output Types (contracts) → Modules (templates) → Blueprint Resources (instances)
- Output Types: JSON schemas defining what data modules produce (attributes + interfaces)
- Modules: Reusable Terraform templates that consume inputs and produce typed outputs
- Blueprint Resources: YAML declarations instantiating modules and wiring dependencies
Key flow: Types ensure compatibility → Modules implement infrastructure → Blueprints compose actual systems using ${type.name.out.path} references.
2. Core Concepts
2.1 The Module Contract
Every Facets module establishes a contract:
INPUTS (dependencies) → MODULE (terraform) → OUTPUTS (typed values + providers)
- Inputs: Other modules this module depends on (e.g., postgres needs network, cloud_account)
- Outputs: Typed values and providers this module exposes to others
2.2 Intent Interoperability
Modules of the same intent (kind) but different flavors should expose compatible outputs. This allows:
- Swapping implementations (e.g.,
postgres/ovh→postgres/aws) without changing dependent resources - Blueprint portability across clouds
Example: All postgres flavors should expose @outputs/postgres with reader and writer interfaces containing host, port, username, password, connection_string.
2.3 Two Ways to Wire Modules
There are two distinct methods for connecting modules:
Method 1: Via inputs (Compile-Time Dependency)
- Declared in
facets.yamlinputs:section - Creates a hard dependency edge in the blueprint graph
- Module receives data via
var.inputs.<name> - Use for: Providers, required infrastructure dependencies (network, cloud account)
# facets.yaml
inputs:
kubernetes_details:
type: "@facets/ovh-kubernetes"
providers:
- kubernetes
- helm
Method 2: Via ${} in spec (Runtime Reference)
- Developer types or selects via
x-ui-output-typedropdown - No compile-time dependency declared
- Expression stored in spec, resolved at deployment
- Use for: Optional references, dynamic wiring (env vars, config values)
# Blueprint resource spec
spec:
env:
DB_HOST: "${postgres.petsdb.out.interfaces.writer.host}"
DB_PASS: "${postgres.petsdb.out.interfaces.writer.password}"
Key Difference:
inputs: Module developer declares the dependency; platform ensures it's satisfied${}in spec: Blueprint developer creates the reference; more flexible but less enforced
2.4 Resource Reference Syntax
Module Output References:
Format: ${<kind>.<resource_name>.out.<attributes|interfaces>.<path>}
${postgres.main-db.out.interfaces.writer.host}
${postgres.main-db.out.interfaces.writer.password}
${postgres.main-db.out.attributes.database_name}
${service.petclinic.out.attributes.service_name}
${helm.cert-manager.out.attributes.release_name}
Blueprint-Level References:
These reference project-level configuration:
${blueprint.self.artifacts.<artifact_name>} # Artifact URI (docker image, etc.)
${blueprint.self.secrets.<secret_name>} # Project secret value
${blueprint.self.variables.<variable_name>} # Project variable value
Examples:
spec:
release:
image: "${blueprint.self.artifacts.my-service}" # Resolves to image URI
env:
API_KEY: "${blueprint.self.secrets.API_KEY}" # Resolves to secret value
BASE_URL: "${blueprint.self.variables.BASE_URL}" # Resolves to variable value
Discovery Command:
List all available expressions for a project:
raptor get resource-output-expressions -p myproject
2.5 Project Types and Input Discovery
Always identify which project type you're targeting. Input types must be exposed by modules available in that project type.
To discover valid input types for a project type:
# List all output types available in a project type (with provider info)
PT=myprojecttype
raptor get resource-type-mappings $PT -o json | jq -r '.[].id' | while read tf; do
ver=$(raptor get resource-types -o json | jq -r --arg tf "$tf" \
'.[] | select("\(.name)/\(.flavor)" == $tf) | "\(.name)/\(.flavor)/\(.version)"' \
| sort -t/ -k3 -Vr | head -1)
if [ -n "$ver" ]; then
echo "=== $ver ==="
raptor get resource-type-outputs "$ver" -o json | jq -r '.[].type' | while read ot; do
providers=$(raptor get output-type "$ot" -o json 2>/dev/null | jq -r '.providers[]?.name // empty' | tr '\n' ',' | sed 's/,$//')
[ -n "$providers" ] && echo " $ot [providers: $providers]" || echo " $ot"
done
fi
done
3. Module Structure
3.1 Required Files
my-module/
├── facets.yaml # Module metadata, schema, inputs, outputs (REQUIRED)
├── variables.tf # Terraform variables with specific structure (REQUIRED)
├── main.tf # Terraform resources (REQUIRED)
├── outputs.tf # output_attributes and output_interfaces locals (REQUIRED)
└── versions.tf # Terraform version constraint (OPTIONAL)
3.2 File Purposes
| File | Purpose |
|---|---|
facets.yaml |
Defines the module's identity, spec schema, inputs, outputs, and UI behavior |
variables.tf |
Declares standard Facets variables (instance, instance_name, environment, inputs) |
main.tf |
Contains the actual Terraform resources |
outputs.tf |
Defines output_attributes and output_interfaces locals (NO output blocks) |
versions.tf |
Terraform version constraint only (NO provider versions) |
3.3 Critical Terraform Rules
| Rule | Reason |
|---|---|
| Terraform v1.5.7 or OpenTofu | Use versions prior to license changes |
| No provider blocks | Providers come from inputs via providers: field |
| No provider version constraints | Versions defined in output type schemas |
| No output blocks | Use local.output_attributes and local.output_interfaces |
Use lookup() with defaults |
Never use try() |
Add prevent_destroy = true |
For stateful resources (databases, storage, CDNs) |
4. The facets.yaml File
This is the heart of a Facets module. It defines everything the platform needs to integrate your module.
4.1 Complete Structure
# === IDENTITY ===
intent: postgres # The kind/technology (REQUIRED)
flavor: ovh # Implementation variant (REQUIRED)
version: "1.0" # Semantic version, quoted (REQUIRED)
description: | # Human-readable description (REQUIRED)
Creates a managed PostgreSQL database on OVH Cloud
clouds: # Compatible clouds (REQUIRED for now)
- aws # Valid: aws, azure, gcp, kubernetes
# Note: Legacy field, will be removed. When in doubt, include all.
# === INPUTS (Dependencies) ===
inputs:
cloud_account: # Input name (used in var.inputs.cloud_account)
type: "@facets/aws_cloud_account" # Output type this input accepts
optional: false # Whether this input is required
displayName: AWS Cloud Account # UI display name
description: AWS cloud account for provisioning
providers: # Providers this input supplies to the module
- aws
network: # Input without provider (data only)
type: "@mycompany/vpc-details"
optional: false
displayName: VPC Network
description: Network configuration
# === OUTPUTS ===
outputs:
# REQUIRED: 'default' is the primary output, used for UI autocomplete
default:
type: "@outputs/postgres" # Output type contract
title: PostgreSQL Database
# If exposing providers, define them here:
providers:
kubernetes:
source: hashicorp/kubernetes
version: 2.23.0
attributes:
host: attributes.cluster_endpoint
cluster_ca_certificate: attributes.cluster_ca_certificate
# OPTIONAL: Expose attribute subset for x-ui-output-type wiring
attributes:
type: "@mycompany/postgres_attributes"
title: PostgreSQL Attributes
# OPTIONAL: Expose nested fields with generic types for cross-module reuse
attributes.read_only_iam_policy_arn:
type: "@outputs/iam_policy_arn"
title: Read-Only IAM Policy
# === ARTIFACT INPUTS (for deployable modules) ===
artifact_inputs: # Optional: for modules that deploy artifacts
primary:
attribute_path: spec.release.image # Path in spec where artifact URI goes
artifact_type: docker_image # "docker_image" or "freestyle"
# === SPEC SCHEMA ===
spec:
title: PostgreSQL Configuration
description: Configure your PostgreSQL database
type: object
properties:
version:
type: string
title: PostgreSQL Version
enum: ["14", "15", "16"]
default: "15"
# ... more properties
required:
- version
x-ui-order:
- version
# === SAMPLE RESOURCE ===
sample:
kind: postgres
flavor: ovh
version: "1.0"
disabled: false
spec:
# ONLY include fields that have defaults defined in spec.properties
# Never fabricate values for fields without defaults
version: "15"
4.2 Identity Fields
| Field | Required | Description |
|---|---|---|
intent |
Yes | The kind/technology. Use existing intents for interoperability. |
flavor |
Yes | Your implementation variant. Often the cloud provider name. |
version |
Yes | Semantic version. Must be quoted: "1.0" not 1.0 |
description |
Yes | Clear description of what the module creates |
clouds |
Yes (legacy) | Array of: aws, azure, gcp, kubernetes. Will be deprecated. |
4.3 Inputs Section
Inputs declare dependencies on other modules. The platform wires these automatically based on blueprint connections.
inputs:
<input_name>:
type: "<output_type>" # The output type to accept
optional: true|false # Default: false
displayName: "Display Name" # Shown in UI
description: "Description" # Help text
providers: # Providers this input supplies (if any)
- kubernetes
- helm
Key Points:
- Input names become keys in
var.inputs.<input_name> - The
typemust match an output type from another module in the same project type providerslist declares which Terraform providers this input supplies- Inputs without
providersare data-only (no provider configuration)
Optional Inputs:
Use optional: true when a dependency is not always needed:
inputs:
prometheus:
type: "@facets/prometheus"
optional: true # Only needed if metrics export is enabled
displayName: Prometheus
description: Prometheus instance for metrics export
In variables.tf, mark the corresponding input as optional:
variable "inputs" {
type = object({
prometheus = optional(object({
attributes = object({
endpoint = string
})
}))
})
}
In main.tf, check if the optional input is provided:
locals {
metrics_enabled = var.inputs.prometheus != null
}
4.4 Outputs Section
Outputs declare what this module exposes to others.
Understanding the default Output
The default output represents the complete module output - the combination of all output_attributes and output_interfaces your module produces. It is:
- Required for every module
- Used for UI autocomplete when developers reference outputs via
${kind.name.out...} - The identity of what your module exposes
The output type you assign to default is a design decision:
Option 1: Flavor-specific type (when implementation details matter to consumers)
# kubernetes_cluster/eks/facets.yaml
outputs:
default:
type: "@facets/eks_details" # EKS-specific: includes EKS-only attributes
Option 2: Generic/interoperable type (when implementation is irrelevant to consumers)
# mysql/rds/facets.yaml
outputs:
default:
type: "@outputs/mysql" # Generic: any MySQL works the same for consumers
Designing for Interoperability with Sub-paths
You can expose sub-paths of your output with different (often more generic) types. This enables cross-flavor compatibility:
# kubernetes_cluster/eks/facets.yaml
outputs:
default:
type: "@facets/eks_details" # Full EKS-specific output
title: EKS Cluster
providers:
kubernetes: ...
helm: ...
attributes.kubernetes:
type: "@facets/k8s_details" # Generic Kubernetes interface
title: Kubernetes Access
# kubernetes_cluster/gke/facets.yaml
outputs:
default:
type: "@facets/gke_details" # Full GKE-specific output
providers:
kubernetes: ...
helm: ...
attributes.kubernetes:
type: "@facets/k8s_details" # Same generic type as EKS!
title: Kubernetes Access
Result: A service module that only needs Kubernetes access can declare:
inputs:
k8s:
type: "@facets/k8s_details" # Works with both EKS and GKE!
This pattern allows consumers to:
- Wire to
defaultwhen they need flavor-specific features - Wire to a sub-path when they only need generic capabilities
- Swap implementations (EKS ↔ GKE) without changing dependent modules
Key insight: Designing output type contracts is a critical part of module development. It determines interoperability and flexibility for consumers.
Output Declaration Syntax
outputs:
# Primary output (required)
default:
type: "<output_type>" # Type contract
title: "Human Title"
providers: # Optional: provider configurations
<provider_name>:
source: hashicorp/kubernetes
version: 2.23.0
attributes:
host: attributes.cluster_endpoint
# Optional: Subset for x-ui-output-type dropdowns
attributes:
type: "<attribute_output_type>"
title: "Attributes for wiring"
# Optional: Nested path for generic type reuse
attributes.some_field:
type: "@outputs/generic_type"
title: "Specific Field"
Nested Output Paths
Nested paths like attributes.read_only_iam_policy_arn enable generic type reuse:
- A Loki module might create an S3 bucket internally with
output_attributes.s3_bucket_arn - Declare
attributes.s3_bucket_arnwith type@outputs/s3_bucket_arn - Now any module with input type
@outputs/s3_bucket_arnsees Loki in the dropdown - Schema at that path must match the declared output type
4.5 Artifact Inputs Section
For modules that deploy container images or other artifacts (like services, jobs), use artifact_inputs to declare which spec fields receive artifact URIs.
artifact_inputs:
primary:
attribute_path: spec.release.image # Dot-path to the spec field
artifact_type: docker_image # "docker_image" or "freestyle"
Artifact Types:
docker_image: Container images (Docker, OCI)freestyle: Any other deployable artifact (zip files, binaries, etc.)
How it works:
- Users register artifacts in the Facets portal with URIs per environment
- In spec, use
${blueprint.self.artifacts.<artifact_name>}to reference the artifact - At deployment, the expression resolves to the registered URI for that environment
Example in spec:
spec:
properties:
release:
type: object
properties:
image:
type: string
title: Container Image
default: "${blueprint.self.artifacts.my-service}"
4.6 Sample Section
Provides a template resource for users when they add this module to a blueprint.
sample:
kind: postgres
flavor: ovh
version: "1.0"
disabled: false # false = enabled, true = disabled (module not invoked)
spec:
# CRITICAL: Only include fields that have 'default' defined in spec.properties
# Never fabricate values - if no default exists, omit the field
version: "15"
The disabled field:
disabled: false- Resource is enabled; module will be invoked during deploymentdisabled: true- Resource is disabled; module is skipped entirely during deployment
Users can override disabled per environment to enable/disable resources selectively.
5. Terraform Files
5.1 Understanding var.inputs (Dependencies)
var.inputs represents your module's dependencies - the prerequisites that must exist before your module can be provisioned.
When you declare an input in facets.yaml:
inputs:
network:
type: "@facets/ovh-network"
optional: false
You're saying: "This module cannot be provisioned without a network. Give me the outputs from a module that exposes @facets/ovh-network."
The structure of var.inputs.<name> is determined by the output type schema you're consuming.
Discovering Input Structure
To know what fields are available in var.inputs.network, look up the output type schema:
raptor get output-type @facets/ovh-network -o json
This returns something like:
{
"properties": {
"type": "object",
"properties": {
"attributes": {
"type": "object",
"properties": {
"network_id": { "type": "string" },
"region": { "type": "string" },
"db_subnet_id": { "type": "string" },
"network_cidr": { "type": "string" }
}
},
"interfaces": {
"type": "object",
"properties": {}
}
}
}
}
This tells you var.inputs.network will have:
var.inputs.network.attributes.network_idvar.inputs.network.attributes.regionvar.inputs.network.attributes.db_subnet_idvar.inputs.network.attributes.network_cidr
Translating to variables.tf
Use the output type schema to construct your var.inputs type:
variable "inputs" {
type = object({
# Each key matches an input name from facets.yaml
network = object({
# Structure comes from @facets/ovh-network schema
attributes = object({
network_id = string
region = string
db_subnet_id = string
network_cidr = string
})
# interfaces = object({}) # Empty in this case, can omit
})
})
}
The attributes vs interfaces Pattern
Output types typically have two sections:
attributes: Flat key-value data (IDs, names, ARNs, configuration values)interfaces: Connection parameters (host, port, username, password for databases)
# Accessing attributes (simple values)
var.inputs.ovh_provider.attributes.project_id
var.inputs.network.attributes.region
# Accessing interfaces (connection details)
var.inputs.postgres.interfaces.writer.host
var.inputs.postgres.interfaces.writer.password
Inputs with Providers vs Data-Only Inputs
Some inputs provide Terraform providers, others just provide data:
inputs:
# This input provides the 'ovh' provider to your module
ovh_provider:
type: "@facets/ovh-provider"
providers:
- ovh
# This input only provides data (no providers list)
network:
type: "@facets/ovh-network"
Both are accessed the same way via var.inputs.<name>, but only inputs with providers will configure Terraform providers for your module to use.
Nested Output Paths and Input Structure
Important: When a module declares an output with a nested path like attributes.k8s_details:
# EKS module facets.yaml
outputs:
attributes.k8s_details:
type: '@facets/eks'
The consumer receives that sub-object directly, not wrapped in .attributes/.interfaces.
# Consumer module - kubernetes_details IS the attributes content
local.kubernetes_details.k8s_details # Correct
local.kubernetes_details.default_tags # Correct
local.kubernetes_details.attributes.xxx # WRONG - no .attributes wrapper
Rule of thumb: Always check how existing modules access their inputs to understand the actual structure being passed.
5.2 Understanding var.instance (Resource Configuration)
The var.instance.spec structure must mirror facets.yaml spec.properties.
EXCEPTION: x-ui-output-type fields - When a spec field uses x-ui-output-type, the types differ:
| Location | Type | Reason |
|---|---|---|
| facets.yaml spec | type: string |
Stores expression like ${s3.bucket.out.attributes} |
| variables.tf | object({...}) |
Expression resolves to actual object at runtime |
# facets.yaml - string because it holds the wiring expression
s3_bucket:
type: string
x-ui-output-type: "@mycompany/s3_bucket_attributes"
# variables.tf - object because expression resolves at runtime
s3_bucket = object({
bucket_name = string
bucket_arn = string
bucket_regional_domain_name = string
})
Full Example:
variable "instance" {
type = object({
kind = string
flavor = string
version = string
spec = object({
# Simple fields - mirror spec.properties
index_document = optional(string, "index.html")
versioning_enabled = optional(bool, false)
price_class = optional(string, "PriceClass_100")
# Map type for patternProperties
origins = optional(map(object({
s3_bucket = object({ # x-ui-output-type field - object type
bucket_name = string
bucket_arn = string
bucket_regional_domain_name = string
})
path_pattern = optional(string, "default")
})), {})
# Free-form maps (x-ui-yaml-editor fields)
env = optional(map(string), {})
labels = optional(map(string), {})
})
})
}
variable "instance_name" {
type = string
description = "Unique architectural name from blueprint"
}
variable "environment" {
type = object({
name = string # Logical name (e.g., "dev", "prod")
unique_name = string # Globally unique (includes project)
cloud_tags = optional(map(string), {})
})
}
variable "inputs" {
type = object({
cloud_account = object({
attributes = object({
aws_region = string
aws_iam_role = string
external_id = string
session_name = string
})
})
# Input from another module (matches output type schema)
s3_bucket = optional(object({
bucket_name = string
bucket_arn = string
bucket_regional_domain_name = string
}))
})
}
5.3 Resource Naming Convention
Use var.instance_name and var.environment for resource names:
locals {
# Pattern 1: Globally unique (recommended)
name = "${var.instance_name}-${var.environment.unique_name}"
# Pattern 2: Unique within project
name = "${var.instance_name}-${var.environment.name}"
# Pattern 3: Just instance name (when uniqueness handled elsewhere)
name = var.instance_name
}
Note: var.environment.unique_name includes project name, making resources globally unique across all projects and environments.
5.4 main.tf
Standard Terraform resources using the variables:
locals {
name = "${var.instance_name}-${var.environment.unique_name}"
}
resource "aws_s3_bucket" "this" {
bucket = local.name
force_destroy = lookup(var.instance.spec, "force_destroy", false)
tags = var.environment.cloud_tags
}
# Access inputs from other modules
resource "aws_s3_bucket_policy" "this" {
bucket = var.inputs.s3_bucket.bucket_name
# ...
}
# Stateful resources MUST have prevent_destroy
resource "aws_rds_cluster" "this" {
cluster_identifier = local.name
# ...
lifecycle {
prevent_destroy = true
}
}
5.5 outputs.tf
CRITICAL: Do NOT use Terraform output blocks. Facets automatically extracts outputs from two special locals:
locals {
output_attributes = { ... } # Facets reads this automatically
output_interfaces = { ... } # Facets reads this automatically
}
Facets handles the output mechanism internally. You just define the locals - never write output "..." { } blocks.
Understanding attributes vs interfaces
| Field | Purpose | Structure | Use For |
|---|---|---|---|
output_attributes |
All non-network outputs | Flat key-value map | IDs, ARNs, names, config values, everything else |
output_interfaces |
Network endpoints only | <endpoint_name> → {host, port, ...} |
Database connections, API endpoints, service URLs |
Basic Module (no network endpoints)
locals {
output_attributes = {
bucket_name = aws_s3_bucket.this.id
bucket_arn = aws_s3_bucket.this.arn
bucket_regional_domain_name = aws_s3_bucket.this.bucket_regional_domain_name
}
output_interfaces = {} # Empty - no network endpoints
}
Module with Network Endpoints (databases, services)
locals {
output_attributes = {
cluster_id = aws_rds_cluster.this.id
cluster_arn = aws_rds_cluster.this.arn
api_key = var.api_key
secrets = ["api_key"] # Fields hidden in UI unless user has permission
}
# interfaces: <endpoint_name> → {connection details}
output_interfaces = {
primary = { # Endpoint name - can be anything meaningful
host = aws_rds_cluster.this.endpoint
port = tostring(aws_rds_cluster.this.port)
username = aws_rds_cluster.this.master_username
password = aws_rds_cluster.this.master_password
connection_string = "postgres://${aws_rds_cluster.this.endpoint}:${aws_rds_cluster.this.port}"
secrets = ["password", "connection_string"]
}
readonly = { # Another endpoint
host = aws_rds_cluster.this.reader_endpoint
port = tostring(aws_rds_cluster.this.port)
}
}
}
interfaces Endpoint Naming
The endpoint name (key) is flexible - choose names meaningful to your module:
| Module Type | Example Endpoint Names |
|---|---|
| Database | primary, readonly, writer, reader |
| API Service | api, admin, internal, public |
| Message Queue | producer, consumer, management |
| Cache | primary, replica |
The secrets Key (Hiding Sensitive Fields)
The secrets key is a special reserved field that lists which sibling fields contain sensitive data. Fields listed in secrets are hidden in the Facets UI unless the user has permission to view secrets.
output_attributes = {
cluster_id = aws_rds_cluster.this.id # Visible to all
api_key = var.api_key # Hidden - listed in secrets
secrets = ["api_key"] # Declares which fields are sensitive
}
output_interfaces = {
primary = {
host = "db.example.com" # Visible
port = "5432" # Visible
username = "admin" # Visible
password = "secret123" # Hidden - listed in secrets
connection_string = "postgres://..." # Hidden - listed in secrets
secrets = ["password", "connection_string"]
}
}
Rules:
secretsis an array of strings naming the sensitive sibling fields- Can be used in both
output_attributesand within eachoutput_interfacesendpoint - Only lists field names at the same level (not nested paths)
Key Points Summary
output_attributes: Everything that isn't a network endpointoutput_interfaces: Reserved for network connection details only- Endpoint names are your choice - NOT limited to
reader/writer - Each endpoint object typically has:
host,port, optional credentials secretsarray: Lists field names containing sensitive data (hidden in UI)- Both locals MUST be present, even if empty
- All values should be strings (use
tostring()for numbers)
5.6 versions.tf
Only specify Terraform version, NOT provider versions:
terraform {
required_version = ">= 1.5.0, < 2.0.0"
# NO required_providers block - providers come from inputs
}
6. Type System and Outputs
6.1 Output Types
Output types define contracts between modules. They enable:
- Type checking when connecting modules
- Automatic provider configuration
- Blueprint validation
- UI dropdowns for module selection
Design Consideration: The choice of output type for default affects interoperability. Use flavor-specific types when consumers need implementation details; use generic types when the implementation is transparent to consumers. See Section 4.4 for detailed examples.
Naming Conventions:
@outputs/<intent> # Standard intent outputs (postgres, redis, service)
@facets/<name> # Facets-specific types (aws_cloud_account, kubernetes-details)
@<company>/<name> # Organization-specific types
6.2 Output Type Schema
Output types are JSON Schema documents with an additional providers array for Terraform provider configuration.
raptor create output-type @namespace/name -f schema.json
Schema Structure (JSON Schema + providers):
{
"properties": { ← Standard JSON Schema wrapper
"type": "object",
"properties": {
"attributes": {...}, ← JSON Schema for flat key-value outputs
"interfaces": {...} ← JSON Schema for network endpoint outputs
}
},
"providers": [...] ← Non-JSON-Schema: Terraform provider configs
}
Schema without providers (most modules):
{
"properties": {
"type": "object",
"properties": {
"attributes": {
"type": "object",
"properties": {
"bucket_name": { "type": "string" },
"bucket_arn": { "type": "string" }
}
},
"interfaces": {
"type": "object",
"properties": {}
}
}
},
"providers": []
}
Schema with interfaces (for modules exposing network endpoints):
{
"properties": {
"type": "object",
"properties": {
"attributes": {
"type": "object",
"properties": {
"cluster_id": { "type": "string" },
"cluster_arn": { "type": "string" }
}
},
"interfaces": {
"type": "object",
"properties": {
"primary": {
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "string" },
"username": { "type": "string" },
"password": { "type": "string" },
"connection_string": { "type": "string" }
}
},
"readonly": {
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "string" }
}
}
}
}
}
},
"providers": []
}
Schema with providers (for modules that expose Terraform providers):
{
"properties": {
"type": "object",
"properties": {
"attributes": {
"type": "object",
"properties": {
"aws_region": { "type": "string" },
"aws_iam_role": { "type": "string" }
}
},
"interfaces": {
"type": "object",
"properties": {}
}
}
},
"providers": [
{
"name": "aws",
"source": "hashicorp/aws",
"version": "5.0.0"
}
]
}
Key Rules:
- The
propertiessection follows standard JSON Schema - The
providersarray is Facets-specific (not JSON Schema) - If
facets.yamloutputs section hasproviders:, the output type schema MUST have matchingprovidersarray
6.3 Understanding interfaces in Output Types
The interfaces section is reserved for network endpoints. Each key is an endpoint name, and the value is an object with connection details.
Structure:
interfaces.<endpoint_name>.host
interfaces.<endpoint_name>.port
interfaces.<endpoint_name>.username
interfaces.<endpoint_name>.password
interfaces.<endpoint_name>.connection_string
Endpoint names are flexible - choose names meaningful to your module:
| Intent | Common Endpoint Names |
|---|---|
| Database (postgres, mysql) | primary, readonly or writer, reader |
| Cache (redis, valkey) | primary, replica |
| Message Queue | producer, consumer |
| API/Service | api, admin, internal |
6.4 Standard Output Contracts
When implementing common intents, follow these output structures:
Database (@outputs/postgres, @outputs/mysql):
output_interfaces = {
primary = { # Or "writer" - endpoint for read-write access
host = string
port = string
username = string
password = string # secret
connection_string = string # secret
secrets = ["password", "connection_string"]
}
readonly = { # Or "reader" - endpoint for read-only access
host = string
port = string
username = string
password = string
connection_string = string
secrets = ["password", "connection_string"]
}
}
Cache (@outputs/redis, @outputs/valkey):
output_interfaces = {
primary = {
host = string
port = string
password = string # secret
connection_string = string # secret (redis://...)
secrets = ["password", "connection_string"]
}
}
Service/API:
output_interfaces = {
api = {
host = string # Service hostname
port = string # Service port
}
}
6.5 Reusing Output Types
Prefer existing output types for interoperability.
Before creating new types, check what's available:
# List all output types in your project type
PT=myprojecttype
raptor get resource-type-mappings $PT -o json | jq -r '.[].id' | while read tf; do
ver=$(raptor get resource-types -o json | jq -r --arg tf "$tf" \
'.[] | select("\(.name)/\(.flavor)" == $tf) | "\(.name)/\(.flavor)/\(.version)"' \
| sort -t/ -k3 -Vr | head -1)
[ -n "$ver" ] && raptor get resource-type-outputs "$ver" -o json | jq -r '.[].type'
done | sort -u
When to reuse:
- Your module outputs similar data (e.g., another S3 bucket → use existing S3 type)
- Generic types exist (e.g.,
@outputs/iam_policy_arn,@outputs/endpoint) - Cross-module wiring is desired (reusing types makes modules appear in dropdowns)
When to create new:
- No existing type matches your output structure
- Module has unique attributes not covered by generic types
6.6 Validating Outputs Before Publishing
Critical: Ensure local.output_attributes and local.output_interfaces match the declared output type schema.
# 1. Get your declared output type schema
raptor get output-type @mycompany/my_type -o json | jq '.properties.properties.attributes.properties'
# 2. Compare with your outputs.tf local.output_attributes keys
# They MUST match - same field names, compatible types
Validation checklist:
- Every field in output type schema exists in
local.output_attributesorlocal.output_interfaces - Field names match exactly (case-sensitive)
- If output type has
providers, facets.yaml outputs section declares them -
secretsarray lists all sensitive fields
7. Provider Passing
Facets automatically configures providers. Modules never define provider blocks.
7.1 How It Works
- Module A (e.g., cloud_account) exposes providers in its output
- Module B (e.g., postgres) declares it needs those providers in its input
- Facets constructs provider blocks and passes them to Module B
7.2 Exposing Providers (in outputs)
# kubernetes_cluster/facets.yaml
outputs:
default:
type: "@facets/kubernetes-details"
title: Kubernetes Cluster
providers:
kubernetes: # Provider alias
source: hashicorp/kubernetes # Terraform provider source
version: 2.23.0 # Provider version
attributes: # Map output attributes to provider config
host: attributes.cluster_endpoint
cluster_ca_certificate: attributes.cluster_ca_certificate
client_certificate: attributes.client_certificate
client_key: attributes.client_key
helm:
source: hashicorp/helm
version: 2.11.0
attributes:
kubernetes: # Nested provider config
host: attributes.cluster_endpoint
cluster_ca_certificate: attributes.cluster_ca_certificate
7.3 Consuming Providers (in inputs)
# service/facets.yaml
inputs:
kubernetes_details:
type: "@facets/kubernetes-details"
optional: false
displayName: Kubernetes Cluster
providers: # List providers you need
- kubernetes
- helm
7.4 Provider Chain Example
cloud_account → network → kubernetes_cluster → service
↓ ↓ ↓ ↓
aws (data) kubernetes/helm (uses both)
8. Spec Schema Design
The spec section defines the developer-facing DSL using JSON Schema.
8.1 Basic Structure
spec:
title: Module Configuration
description: Configure your resource
type: object
properties:
# ... property definitions
required:
- required_field
x-ui-order:
- field1
- field2
8.2 Property Types
String with enum:
version:
type: string
title: Version
default: "15"
enum: ["14", "15", "16"]
Integer with range:
nodes_count:
type: integer
title: Number of Nodes
minimum: 1
maximum: 10
default: 3
Boolean:
enable_ssl:
type: boolean
title: Enable SSL
default: true
Nested object:
advanced_config:
type: object
title: Advanced Configuration
x-ui-toggle: true # Collapsible section
properties:
timeout:
type: string
default: "300"
max_connections:
type: integer
default: 100
Map with patternProperties (use instead of arrays):
Maps are the default choice for collections. Arrays break environment overrides. See Section 10.1 for details.
ports:
type: object
title: Ports
patternProperties:
"^[a-zA-Z0-9_-]+$": # Key pattern (regex)
type: object
properties:
port:
type: string
protocol:
type: string
enum: [tcp, udp]
required:
- port
- protocol
Output type reference (for wiring modules):
s3_bucket:
type: string # String in spec (holds expression)
title: S3 Bucket
x-ui-output-type: "@mycompany/s3_bucket_attributes"
8.3 Design for Deep Merge
Facets applies environment overrides using deep merge. Arrays are replaced entirely; maps merge at key level.
Problem with arrays:
# Base: volumes: [{name: config, path: /etc}, {name: data, path: /var}]
# Override: Must repeat ENTIRE array to change one item
Solution - Use patternProperties:
volumes:
type: object
patternProperties:
"^[a-zA-Z0-9_-]+$":
type: object
properties:
path:
type: string
Result:
# Base
volumes:
config: # Key: stable identifier
path: /etc/config
data:
path: /var/data
# Override - only changes what's needed
volumes:
data:
path: /mnt/fast-data # Only this changes; config inherited
8.4 x-ui-yaml-editor vs patternProperties
| Feature | patternProperties | x-ui-yaml-editor |
|---|---|---|
| Schema validation | Yes | No |
| Deep merge | Yes | No |
| UI form generation | Yes | YAML editor |
| Use case | Structured objects | Free-form key-value (env, labels, tags) |
# Use patternProperties for structured data
ports:
type: object
patternProperties:
"^[a-zA-Z0-9_-]+$":
type: object
properties:
port: { type: string }
protocol: { type: string }
# Use x-ui-yaml-editor for free-form maps
env:
type: object
title: Environment Variables
x-ui-yaml-editor: true
9. UI Extensions (x-ui-*)
Facets extends JSON Schema with x-ui-* properties to control portal UI rendering.
9.1 Complete Reference
| Extension | Purpose | Example |
|---|---|---|
x-ui-order |
Field display order | ["name", "version", "size"] |
x-ui-toggle |
Collapsible section | true |
x-ui-visible-if |
Conditional visibility | See below |
x-ui-overrides-only |
Only in environment overrides | true |
x-ui-override-disable |
Cannot be overridden per env | true |
x-ui-secret-ref |
Reference project-level secrets | true |
x-ui-variable-ref |
Reference project-level variables | true |
x-ui-output-type |
Dropdown of modules with this output | "@outputs/s3" |
x-ui-output |
Dropdown of fields of modules with this output | See below |
x-ui-placeholder |
Input placeholder text | "Enter domain" |
x-ui-error-message |
Custom validation error | "Invalid CIDR" |
x-ui-textarea |
Multi-line text input | true |
x-ui-yaml-editor |
YAML editor for free-form maps | true |
x-ui-editor |
Code editor widget | true |
x-ui-command |
Command array input | true |
x-ui-typeable |
Allow typing in dropdowns | true |
x-ui-unique |
Value must be unique | true |
x-ui-no-sort |
Don't sort enum values | true |
x-ui-skip |
Skip in UI (internal use) | true |
x-ui-dynamic-enum |
Options from other fields | "spec.ports.*.port" |
x-ui-disable-tooltip |
Tooltip when disabled | "No ports" |
x-ui-artifact |
Attach artifact | docker_image |
x-ui-array-input-validation |
Validation of item in an array | See below |
x-ui-compare |
Cross-field validation | See below |
9.2 Critical Extensions for Module Design
x-ui-overrides-only - Field MUST be set per-environment:
region:
type: string
title: Deployment Region
x-ui-overrides-only: true
Use for: regions, CIDRs, credentials, environment-specific endpoints Don't use for: SKUs, scaling params, access rules (can have safe defaults)
x-ui-override-disable - Value locked at blueprint level:
service_type:
type: string
enum: [application, cronjob, job]
x-ui-override-disable: true
Use for: service type, fundamental architecture choices, ports
x-ui-output-type - Dropdown of modules exposing an output type:
This extension renders a dropdown in the UI showing all blueprint resources that expose the specified output type. When the user selects a resource, the UI auto-generates the ${kind.name.out...} expression.
# facets.yaml spec
cert_manager_release:
type: string # String in schema (stores the expression)
title: Cert Manager Release
x-ui-output-type: "@facets/helm_release_name"
x-ui-visible-if:
field: spec.cert_manager_config.enabled
values:
- true
What happens:
- UI shows dropdown of all resources exposing
@facets/helm_release_name - User selects
cert-manager(a helm resource) - Field value becomes:
${helm.cert-manager.out.attributes.release_name} - At deployment, expression resolves to actual value
In variables.tf - use the resolved object type, not string:
# The expression resolves at runtime, so declare the actual type
cert_manager_release = optional(string) # If it resolves to a simple value
# OR for complex outputs:
s3_bucket = optional(object({
bucket_name = string
bucket_arn = string
}))
Works in nested fields and patternProperties.
x-ui-output - Dropdown of modules exposing an output type:
This extension renders a dropdown in the UI showing field of all blueprint resources that expose the specified output type.
# facets.yaml spec
pubsub:
title: Topic Name
description: The topic name of the Pub.Sub
type: string
x-ui-output:
type: @outputs/pubsub
field: 'attributes.id'
What happens:
UI shows dropdown of field of all resources exposing @facets/helm_release_name
Works in nested fields and patternProperties.
x-ui-array-input-validation - Dropdown of modules exposing an output type:
This extension allows validation to be applied to each item in an array-type field by defining a custom pattern and corresponding error message within the field, which is shown when the pattern validation fails.
# facets.yaml spec
times:
type: array
items:
type: string
x-ui-array-input-validation:
pattern: "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$"
error: "Invalid time. Please enter a valid time in HH:MM format."
What happens: Each item of the array needs to follow the pattern mentioned and failing to do so gives the error mentioned.
9.3 Conditional Visibility
readiness_port:
type: string
x-ui-visible-if:
field: spec.health_checks.type
values:
- PortCheck
- HttpCheck
In patternProperties, use {{this}} for current key:
custom_tls:
patternProperties:
"^[a-zA-Z0-9_-]+$":
properties:
certificate:
x-ui-visible-if:
field: spec.domains.{{this}}.custom_tls.enabled
values:
- true
9.4 Cross-Field Comparison
cpu:
type: string
x-ui-compare:
field: spec.size.cpu_limit
comparator: "<="
x-ui-error-message: "CPU cannot exceed limit"
10. Best Practices and Anti-Patterns
10.1 DO: Use Maps Instead of Arrays (Critical)
Arrays should almost never be used in spec schemas. They don't deep-merge, which breaks environment overrides.
When an environment override is applied:
- Maps: Individual keys are merged (change one item, others inherited)
- Arrays: Entire array is replaced (must repeat everything to change one item)
# BAD - Array (breaks environment overrides)
ports:
type: array
items:
type: object
properties:
port: { type: string }
# GOOD - Map with patternProperties (default choice)
ports:
type: object
patternProperties:
"^[a-zA-Z0-9_-]+$":
type: object
properties:
port: { type: string }
Only use arrays when ALL of these are true:
- Order of items is semantically significant
- Items will never need individual overrides per environment
- The entire list is always replaced as a unit
Examples where arrays are acceptable:
command: ["/bin/sh", "-c", "echo hello"]- order matters, never partially overriddenargs: ["--config", "/etc/config.yaml"]- same reason
Examples where maps are required:
- Ports, volumes, environment variables, rules, domains - anything users might override individually
10.2 DO: Mark Override-Only Fields Appropriately
# Environment-specific - no sensible default
region:
type: string
x-ui-overrides-only: true
# Has safe default - can be overridden
instance_size:
type: string
default: "small"
# No x-ui-overrides-only
10.3 DO: Reuse Output Types
Check existing types before creating new ones:
raptor get output-type @outputs/postgres -o json
10.4 DO: Add prevent_destroy for Stateful Resources
resource "aws_rds_cluster" "this" {
# ...
lifecycle {
prevent_destroy = true
}
}
10.5 DO: Mark Sensitive Outputs
output_attributes = {
api_key = var.api_key
secrets = ["api_key"]
}
output_interfaces = {
primary = {
password = aws_db.password
secrets = ["password"]
}
}
10.6 DO: Use lookup() with Defaults
# GOOD
force_destroy = lookup(var.instance.spec, "force_destroy", false)
# BAD - Never use try()
force_destroy = try(var.instance.spec.force_destroy, false)
10.7 DON'T: Define Provider Blocks
# BAD - Never do this
provider "aws" {
region = var.region
}
# GOOD - Providers come from inputs automatically
10.8 DON'T: Use Output Blocks
# BAD
output "bucket_name" {
value = aws_s3_bucket.this.id
}
# GOOD - Use locals only
locals {
output_attributes = {
bucket_name = aws_s3_bucket.this.id
}
}
10.9 DON'T: Fabricate Sample Values
# BAD - fabricating values for fields without defaults
sample:
spec:
region: "us-east-1" # No default defined!
bucket_name: "my-bucket" # No default defined!
# GOOD - only include fields with defaults
sample:
spec:
versioning_enabled: false # Has default: false in properties
10.10 DON'T: Use x-ui-yaml-editor for Structured Data
# BAD - loses schema validation and deep merge
ports:
type: object
x-ui-yaml-editor: true
# GOOD - proper schema with deep merge
ports:
type: object
patternProperties:
"^[a-zA-Z0-9_-]+$":
type: object
properties:
port: { type: string }
11. Module Lifecycle
11.1 Development Workflow
- Survey existing output types in target project type
- Design outputs - what does the module expose? Reuse types where possible
- Create output types only if needed
- Write facets.yaml - spec, inputs, outputs, sample
- Write Terraform files - variables.tf, main.tf, outputs.tf
- Validate outputs match schema before publishing
- Upload and test - dry-run, then preview stage
- Publish when ready
11.2 Raptor Commands
Output Types:
# View output type schema
raptor get output-type @namespace/name
# Create/update output type
raptor create output-type @namespace/name -f schema.json
Module Management:
# Download existing module for reference
raptor get iac-module <type/flavor/version>
raptor get iac-module <type/flavor/version> -o ./modules/
# Validate module (dry-run)
raptor create iac-module -f <module_dir> --dry-run
# Upload module as PREVIEW
raptor create iac-module -f <module_dir> --auto-create
# Publish to PUBLISHED stage
raptor publish iac-module <type/flavor/version>
# View module details and usages
raptor get iac-module <type/flavor/version> --details
raptor get iac-module <type/flavor/version> --usages
# Delete module
raptor delete iac-module <type/flavor/version>
raptor delete iac-module <type/flavor/version> --force
List Modules:
raptor get iac-module # List all
raptor get iac-module --source CUSTOM # Custom modules only
raptor get iac-module --stage PREVIEW # Preview stage only
11.3 Testing Workflow
PREVIEW modules are only available in dedicated testing projects where unpublished modules can be used.
Recommended workflow:
-
Upload as PREVIEW:
bashraptor create iac-module -f ./my-module --auto-create # Module is now in PREVIEW stage -
Test in a testing project:
- Add a resource using your module to the testing project's blueprint
- Run a plan to see Terraform output:
bash
raptor create release -p testing-project -e dev --plan -w - If plan looks good, deploy:
bash
raptor create release -p testing-project -e dev -w
-
Check release logs for errors:
bashraptor get releases -p testing-project -e dev raptor logs release -p testing-project -e dev -f <RELEASE_ID> -
Iterate: Fix issues, re-upload with
--auto-create, test again -
Publish when ready:
bashraptor publish iac-module <type/flavor/version> # Module is now available to all projects
Direct publish: For new modules with low blast radius, you can publish directly:
raptor create iac-module -f ./my-module --auto-create
raptor publish iac-module <type/flavor/version>
Blueprint commands for testing:
# Apply resource to blueprint (validate)
raptor apply -f <resource.yaml> -p <project> --dry-run
# Check environment overrides
raptor get resource-overrides -p <project> -e <env> <kind/name>
# Get runtime outputs after deployment
raptor get resource-outputs -p <project> -e <env> <kind/name>
11.4 Versioning Strategy
- Patch (1.0.x): Bug fixes, documentation
- Minor (1.x.0): New optional features, non-breaking changes
- Major (x.0.0): Breaking changes to spec or outputs
12. Complete Examples
12.1 Cloud Account Module (Provider Source)
A module that provides cloud provider configuration.
facets.yaml:
intent: cloud_account
flavor: ovh
version: "1.0.0"
description: Configures OVH Terraform provider with application key authentication
clouds:
- kubernetes
spec:
title: OVH Provider Configuration
description: Configure OVH API access credentials
type: object
properties:
endpoint:
type: string
title: OVH API Endpoint
enum:
- ovh-eu
- ovh-ca
default: ovh-eu
x-ui-overrides-only: true
application_key:
type: string
title: Application Key
x-ui-secret-ref: true
application_secret:
type: string
title: Application Secret
x-ui-secret-ref: true
consumer_key:
type: string
title: Consumer Key
x-ui-secret-ref: true
project_id:
type: string
title: Cloud Project ID
x-ui-overrides-only: true
required:
- endpoint
- application_key
- application_secret
- consumer_key
- project_id
outputs:
default:
type: "@facets/ovh-provider"
title: OVH Provider Configuration
providers:
ovh:
source: ovh/ovh
version: 2.7.0
attributes:
endpoint: attributes.endpoint
application_key: attributes.application_key
application_secret: attributes.application_secret
consumer_key: attributes.consumer_key
sample:
kind: cloud_account
flavor: ovh
version: "1.0.0"
disabled: false
spec:
endpoint: ovh-eu
variables.tf:
variable "instance" {
type = object({
kind = string
flavor = string
version = string
spec = object({
endpoint = string
application_key = string
application_secret = string
consumer_key = string
project_id = string
})
})
}
variable "instance_name" {
type = string
}
variable "environment" {
type = object({
name = string
unique_name = string
})
}
variable "inputs" {
type = object({})
}
main.tf:
# Cloud account is a provider-only module
# No resources to create - just passes through configuration
outputs.tf:
locals {
output_attributes = {
endpoint = var.instance.spec.endpoint
application_key = var.instance.spec.application_key
application_secret = var.instance.spec.application_secret
consumer_key = var.instance.spec.consumer_key
project_id = var.instance.spec.project_id
secrets = ["application_key", "application_secret", "consumer_key"]
}
output_interfaces = {}
}
12.2 Database Module (PostgreSQL)
A module with inputs (dependencies) and standard outputs.
facets.yaml:
intent: postgres
flavor: ovh
version: "1.0"
description: Creates managed PostgreSQL database on OVH Cloud
clouds:
- kubernetes
inputs:
ovh_provider:
type: "@facets/ovh-provider"
optional: false
displayName: OVH Provider
providers:
- ovh
network:
type: "@facets/ovh-network"
optional: false
displayName: OVH Network
outputs:
default:
type: "@outputs/postgres"
title: PostgreSQL Database
spec:
title: PostgreSQL Configuration
type: object
properties:
version:
type: string
title: PostgreSQL Version
enum: ["14", "15", "16"]
default: "15"
plan:
type: string
title: Service Plan
enum: [essential, business, enterprise]
default: essential
flavor:
type: string
title: Instance Size
enum: [db1-4, db1-7, db1-15, db1-30]
default: db1-4
nodes_count:
type: integer
title: Number of Nodes
minimum: 1
maximum: 10
default: 1
disk_size:
type: integer
title: Disk Size (GB)
minimum: 20
maximum: 2000
default: 80
required:
- version
- plan
- flavor
- nodes_count
- disk_size
x-ui-order:
- version
- plan
- flavor
- nodes_count
- disk_size
sample:
kind: postgres
flavor: ovh
version: "1.0"
disabled: false
spec:
version: "15"
plan: essential
flavor: db1-4
nodes_count: 1
disk_size: 80
variables.tf:
variable "instance" {
type = object({
kind = string
flavor = string
version = string
spec = object({
version = string
plan = string
flavor = string
nodes_count = number
disk_size = number
})
})
validation {
condition = contains(["14", "15", "16"], var.instance.spec.version)
error_message = "Version must be 14, 15, or 16."
}
}
variable "instance_name" {
type = string
}
variable "environment" {
type = object({
name = string
unique_name = string
})
}
variable "inputs" {
type = object({
ovh_provider = object({
attributes = object({
project_id = string
})
})
network = object({
attributes = object({
region = string
openstack_network_id = string
db_subnet_id = string
network_cidr = string
})
})
})
}
main.tf:
locals {
region = replace(var.inputs.network.attributes.region, "/[0-9]+$/", "")
database_name = "${var.environment.unique_name}-${var.instance_name}"
}
resource "ovh_cloud_project_database" "postgres" {
service_name = var.inputs.ovh_provider.attributes.project_id
description = "PostgreSQL: ${local.database_name}"
engine = "postgresql"
version = var.instance.spec.version
plan = var.instance.spec.plan
flavor = var.instance.spec.flavor
disk_size = var.instance.spec.disk_size
nodes {
region = local.region
network_id = var.inputs.network.attributes.openstack_network_id
subnet_id = var.inputs.network.attributes.db_subnet_id
}
ip_restrictions {
description = "Private network only"
ip = var.inputs.network.attributes.network_cidr
}
lifecycle {
prevent_destroy = true
}
}
resource "ovh_cloud_project_database_postgresql_user" "admin" {
service_name = var.inputs.ovh_provider.attributes.project_id
cluster_id = ovh_cloud_project_database.postgres.id
name = "admin"
}
outputs.tf:
locals {
output_attributes = {
database_id = ovh_cloud_project_database.postgres.id
database_name = "defaultdb"
engine = "postgresql"
version = var.instance.spec.version
host = ovh_cloud_project_database.postgres.endpoints[0].domain
port = tostring(ovh_cloud_project_database.postgres.endpoints[0].port)
}
# interfaces: endpoint_name → connection details
output_interfaces = {
primary = { # Read-write endpoint (could also be named "writer")
host = ovh_cloud_project_database.postgres.endpoints[0].domain
port = tostring(ovh_cloud_project_database.postgres.endpoints[0].port)
username = ovh_cloud_project_database_postgresql_user.admin.name
password = ovh_cloud_project_database_postgresql_user.admin.password
connection_string = format(
"postgresql://%s:%s@%s:%d/defaultdb",
ovh_cloud_project_database_postgresql_user.admin.name,
ovh_cloud_project_database_postgresql_user.admin.password,
ovh_cloud_project_database.postgres.endpoints[0].domain,
ovh_cloud_project_database.postgres.endpoints[0].port
)
secrets = ["password", "connection_string"]
}
readonly = { # Read-only endpoint (could also be named "reader")
host = ovh_cloud_project_database.postgres.endpoints[0].domain
port = tostring(ovh_cloud_project_database.postgres.endpoints[0].port)
username = ovh_cloud_project_database_postgresql_user.admin.name
password = ovh_cloud_project_database_postgresql_user.admin.password
connection_string = format(
"postgresql://%s:%s@%s:%d/defaultdb",
ovh_cloud_project_database_postgresql_user.admin.name,
ovh_cloud_project_database_postgresql_user.admin.password,
ovh_cloud_project_database.postgres.endpoints[0].domain,
ovh_cloud_project_database.postgres.endpoints[0].port
)
secrets = ["password", "connection_string"]
}
}
}
12.3 Kubernetes Cluster Module (Provider Exposer)
A module that exposes kubernetes and helm providers.
facets.yaml:
intent: kubernetes_cluster
flavor: ovh
version: "1.0"
description: Creates OVH Managed Kubernetes cluster
clouds:
- kubernetes
inputs:
ovh_provider:
type: "@facets/ovh-provider"
optional: false
displayName: OVH Provider
providers:
- ovh
network:
type: "@facets/ovh-network"
optional: false
displayName: OVH Network
outputs:
default:
type: "@facets/ovh-kubernetes"
title: OVH Kubernetes Cluster
providers:
kubernetes:
source: hashicorp/kubernetes
version: 2.23.0
attributes:
host: attributes.cluster_endpoint
cluster_ca_certificate: attributes.cluster_ca_certificate
client_certificate: attributes.client_certificate
client_key: attributes.client_key
helm:
source: hashicorp/helm
version: 2.11.0
attributes:
kubernetes:
host: attributes.cluster_endpoint
cluster_ca_certificate: attributes.cluster_ca_certificate
client_certificate: attributes.client_certificate
client_key: attributes.client_key
spec:
title: Kubernetes Cluster Configuration
type: object
properties:
version:
type: string
title: Kubernetes Version
default: "1.28"
node_pool:
type: object
title: Default Node Pool
x-ui-overrides-only: true
properties:
flavor:
type: string
title: Node Flavor
default: b3-8
min_nodes:
type: integer
default: 3
max_nodes:
type: integer
default: 10
x-ui-order:
- version
- node_pool
sample:
kind: kubernetes_cluster
flavor: ovh
version: "1.0"
disabled: false
spec:
version: "1.28"
outputs.tf:
locals {
output_attributes = {
cluster_id = ovh_cloud_project_kube.cluster.id
cluster_endpoint = ovh_cloud_project_kube.cluster.kubeconfig_attributes[0].host
cluster_ca_certificate = base64decode(ovh_cloud_project_kube.cluster.kubeconfig_attributes[0].cluster_ca_certificate)
client_certificate = base64decode(ovh_cloud_project_kube.cluster.kubeconfig_attributes[0].client_certificate)
client_key = base64decode(ovh_cloud_project_kube.cluster.kubeconfig_attributes[0].client_key)
secrets = ["client_key"]
}
output_interfaces = {}
}
13. Quick Reference
Module Checklist
-
facets.yamlwith intent, flavor, version (quoted), description -
specwith properties, required, x-ui-order -
inputsfor all dependencies with correct types -
outputswithdefaultand correct type -
samplewith only defaulted fields -
variables.tfwith instance, instance_name, environment, inputs -
main.tfwith resources,prevent_destroyfor stateful -
outputs.tfwithoutput_attributesandoutput_interfaceslocals - NO provider blocks, NO provider versions, NO output blocks
- Output type schema matches
output_attributes/output_interfaces
Common x-ui Extensions
x-ui-overrides-only: true # Must set per environment
x-ui-override-disable: true # Cannot change per environment
x-ui-secret-ref: true # Reference secrets
x-ui-variable-ref: true # Reference variables
x-ui-output-type: "@type" # Module dropdown
x-ui-toggle: true # Collapsible section
x-ui-yaml-editor: true # Free-form key-value only
x-ui-visible-if: # Conditional display
field: spec.type
values: [value1, value2]
Key Rules Summary
| Rule | Details |
|---|---|
| Terraform version | v1.5.7 or OpenTofu (pre-license change) |
| Provider blocks | Never define - come from inputs |
| Provider versions | Never constrain - defined in output types |
| Output blocks | Never use - only locals |
| Arrays in spec | Almost never use - maps (patternProperties) are default |
| Stateful resources | Always add prevent_destroy = true |
| try() function | Never use - use lookup() with defaults |
| Sample values | Only include fields with defaults defined |
Raptor Commands
# Output types
raptor get output-type @namespace/name
raptor create output-type @namespace/name -f schema.json
# Modules
raptor get iac-module type/flavor/version
raptor create iac-module -f . --dry-run # Validate
raptor create iac-module -f . --auto-create # Upload
raptor publish iac-module type/flavor/version # Publish
raptor delete iac-module type/flavor/version
# Project types
raptor get project-types
raptor create project-type NAME --description "Description"
raptor get resource-type-mappings PROJECT_TYPE
raptor create resource-type-mapping PROJECT_TYPE --resource-type type/flavor
# Testing
raptor apply -f resource.yaml -p project --dry-run
raptor create release -p project -e env --plan -w
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?