Docs
Open the console →
Guides

Build your own Terraform blueprint

Write a custom Terraform blueprint for Cushy: native .tf blueprint folders (main/variables/outputs/tfvars) in Git, the typed fields schema, the module rules, and how it flows through plan, policy, approval and apply.

Cushy ships a built-in blueprint registry (S3 bucket, EC2 instance, VPC, and their GCP/Azure/Alibaba peers). The blueprint catalog extends it: the platform team publishes managed blueprints from a curated Git repository, and your organization can add its own blueprints on the Blueprints screen (/blueprints). A custom blueprint is a Terraform module with typed input variables plus a little catalog metadata — and it deploys through exactly the same plan → policy → approve → apply pipeline as the built-ins.

The definition format — a native .tf folder

In the Git catalog source, each blueprint is its own folder of native `.tf` files (not JSON): main.tf (the resource/module blocks plus a locals { cc_blueprint = { slug, title, description, provider, resource_type } } metadata carrier), variables.tf (the input variables — the deploy form is derived from them), an optional outputs.tf, and a generated terraform.tfvars example. The folder name is the blueprint's slug. The catalog's View definition shows exactly these .tf files.

hcl
# blueprints/team-s3-bucket/main.tf
locals {
  cc_blueprint = {
    slug          = "team-s3-bucket"
    title         = "Team S3 bucket"
    description   = "Our standard private bucket with mandatory tags."
    provider      = "AWS"
    resource_type = "storage"
  }
}
resource "aws_s3_bucket" "this" {
  bucket = var.name
  acl    = "private"
  tags   = var.tags
}

# blueprints/team-s3-bucket/variables.tf
variable "name" { type = string }
variable "tags" {
  type    = map(string)
  default = { env = "prod" }
}

The parser is the faithful inverse of Cushy's HCL emitter over a closed blueprint subset (variable/locals/resource/module/output blocks; comments allowed). Anything outside that subset — function calls, for expressions, ternaries — fails closed: the file is reported invalid, never mis-parsed. Legacy flat <slug>.tf files and .tf.json/.json definitions are still read for back-compat (a folder wins over a flat file with the same slug); edited blueprints are re-written in the folder form. The in-app own-blueprint form on /blueprints takes the same information as metadata inputs plus a fields-JSON and a module (HCL-JSON) editor — the JSON below documents that equivalent object shape.

json
{
  "slug": "team-s3-bucket",
  "title": "Team S3 bucket",
  "description": "Our standard private bucket with mandatory tags.",
  "provider": "AWS",
  "resourceType": "storage",
  "tfType": "aws_s3_bucket",
  "fields": [
    { "key": "name",   "label": "Bucket name", "type": "string", "required": true,
      "pattern": "^[a-z][a-z0-9-]{1,40}$" },
    { "key": "region", "label": "Region", "type": "select", "required": true,
      "default": "us-east-1", "options": ["us-east-1", "us-west-2"] },
    { "key": "public", "label": "Publicly readable", "type": "boolean", "default": false },
    { "key": "tags",   "label": "Tags", "type": "tags", "default": "env=prod" }
  ],
  "module": {
    "variable": {
      "name":   { "type": "string" },
      "public": { "type": "bool", "default": false },
      "tags":   { "type": "map(string)", "default": {} }
    },
    "resource": {
      "aws_s3_bucket": {
        "this": { "bucket": "${var.name}", "acl": "private", "tags": "${var.tags}" }
      }
    }
  }
}
KeyRules
slugUnique id within its catalog: lowercase letters, digits, hyphens (2–41 chars). Immutable once created. The full blueprint id becomes custom/org/<slug> (your own) or custom/platform/<slug> (managed).
title / descriptionHuman names for the deploy forms (title ≤ 80 chars).
providerAWS, GCP, AZR or ALI — the cloud account the workspace must target.
resourceTypeThe normalized inventory type the applied resource lands as: compute, storage, network, database, loadbalancer, kubernetes or serverless.
tfTypeThe primary Terraform resource type (e.g. aws_s3_bucket) — by convention the primary resource address is <tfType>.this, matching the built-in registry.
fieldsThe typed variables schema (max 20) — drives the deploy form and server-side validation.
moduleThe self-contained HCL-JSON module template. Must contain at least one resource block.

The fields schema

Each field is { key, label, type, required?, default?, options?, help?, pattern? }. key is snake_case and becomes the Terraform variable name. Submitted values are validated server-side before any plan: a missing required field, an option outside options, or a pattern mismatch is a 400.

Field typeForm controlInjected as
stringText input (optionally regex-pattern-checked)string variable
selectDropdown over optionsstring variable
booleanCheckboxbool variable
tagskey=value,key2=v2 tag editormap(string) variable

Module template rules

  • Allowed top-level keys: `resource`, `variable`, `output`, `locals` only. The platform stamps its own terraform block (required_providers + the encrypted Cushy state backend) and the per-cloud provider block — a template that carries terraform, provider or any backend block is rejected.
  • At least one `resource` block is required (a template without one is rejected with a 400 / an invalid sync report entry).
  • Name the primary resource `this` (<tfType>.this) to match the registry convention; extra resources may use any names (max 20 blocks).
  • Reference your fields as `${var.<key>}`. On plan, each submitted value is injected as that variable's default, so the module is standalone-applyable and the committed GitOps HCL stays value-complete.
  • Declare `output` blocks for anything downstream modules need (e.g. "output": { "vpc_id": { "value": "${aws_vpc.this.id}" } }). Applied output values persist with the workspace's state and other workspaces can BIND their variables to them ("← from output" — see *Chain modules* in the Terraform guide). sensitive: true outputs are never surfaced.
  • Never put secrets in a template. Definitions are stored and echoed verbatim in catalogs and hydrated artifacts — use variables and your cloud's secret manager instead.
Chain modules with output bindings

A custom blueprint's variable can consume ANOTHER workspace's applied output instead of a literal: deploy the producer (e.g. a VPC module that outputs vpc_id), then in the consumer's Configure form bind its vpc_id field via ← from output. The plan resolves the real value server-side, the dependency shows as *Depends on / Used by* on both workspaces, and the producer refuses destroy while the consumer is live.

Adding a blueprint

  1. Your own (Blueprints screen)

    Open Blueprints → + New blueprint, fill the metadata, paste the fields JSON and the module JSON, and save. Validation errors are shown inline. You can also import a definition file from your connected infra repo (pick the connection + the file path). Managing blueprints needs deploy rights (mutate_infra); Viewers see the catalog read-only.

  2. Managed (published by Cushy)

    The Cushy platform team publishes managed blueprints from a curated Git catalog. Every valid blueprint folder of native .tf files is published to all organizations with a *Managed* badge; a folder removed from the catalog disables (never deletes) its blueprint, and invalid definitions are never published.

  3. Deploy it

    Custom blueprints appear beside the built-ins in the Terraform + New workspace form and the Inventory + New resource wizard. Pick it, fill the typed fields, then plan → review diff/policy/cost → approve → apply.

How it flows through the pipeline

A custom blueprint's plan derives one planned resource per resource block in the template. The standard policy set applies: region-allowlist and no-public-bucket always, plus mandatory-tags when the blueprint declares a tags field. Approval rules (production separation-of-duties vs standard self-approve), plan-hash integrity, run logs, state versions, the hydrated GitOps commit and the failed-apply recovery model are identical to registry blueprints. When real provisioning is enabled for the target account, the same composed module (rendered to native HCL) is what the real OpenTofu runner applies.

Limitations

  • Applies are recorded-only unless real provisioning is enabled for the target account — same as every blueprint.
  • Import-adopt (importNativeId) is not supported on custom-blueprint workspaces — use the Import screen's generic codegen instead.
  • Cost estimation for custom resources uses the normalized resourceType base rate (no per-size multipliers unless the attribute names match the built-ins).
  • slug is immutable; disabling (or deleting your own) blueprint keeps existing workspaces' history but blocks new plans against it.
  • The managed-catalog sync is manual (Sync now) — there is no webhook/cron auto-sync yet.
Where the definition lives

Custom blueprints are stored in the platform database (org-scoped; platform rows are global). The module template you write is exactly what lands in the workspace's hydrated module — plus the platform-stamped terraform/provider blocks — rendered to native HCL (main.tf), so "View generated Terraform" always shows the real artifact.