Docs
Open the console →
Guides

Build your own Terraform blueprint

Write a custom Terraform blueprint for Cloud Control: the JSON definition format, the typed fields schema, the HCL-JSON module template rules, and how it flows through plan, policy, approval and apply.

Cloud Control 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 single JSON document — a typed variables schema plus a self-contained Terraform module template — and it deploys through exactly the same plan → policy → approve → apply pipeline as the built-ins.

The definition format

One JSON object per blueprint. When the platform team syncs the managed catalog from GitHub, each <path>/*.json file in the configured repo is one definition in this exact shape; the customer form and the import-from-repo path validate the same 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 Cloud Control 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.
  • 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.

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 (platform operators)

    Platform staff configure a GitHub repo + directory on /admin/blueprints, then hit Sync now. Every valid *.json in the directory is published to all organizations with a *Managed* badge; a file removed from the repo disables (never deletes) its blueprint, and invalid files are reported per-file, 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 (CC_TF_RUNNER=real + a provisioning-enabled account), the same composed HCL-JSON is what the real OpenTofu runner applies.

Limitations

  • The mock runner records applies 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 main.tf.json — plus the platform-stamped terraform/provider blocks — so "View generated Terraform" always shows the real artifact.