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.
{
"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}" }
}
}
}
}| Key | Rules |
|---|---|
slug | Unique 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 / description | Human names for the deploy forms (title ≤ 80 chars). |
provider | AWS, GCP, AZR or ALI — the cloud account the workspace must target. |
resourceType | The normalized inventory type the applied resource lands as: compute, storage, network, database, loadbalancer, kubernetes or serverless. |
tfType | The primary Terraform resource type (e.g. aws_s3_bucket) — by convention the primary resource address is <tfType>.this, matching the built-in registry. |
fields | The typed variables schema (max 20) — drives the deploy form and server-side validation. |
module | The 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 type | Form control | Injected as |
|---|---|---|
string | Text input (optionally regex-pattern-checked) | string variable |
select | Dropdown over options | string variable |
boolean | Checkbox | bool variable |
tags | key=value,key2=v2 tag editor | map(string) variable |
Module template rules
- Allowed top-level keys: `resource`, `variable`, `output`, `locals` only. The platform stamps its own
terraformblock (required_providers + the encrypted Cloud Control state backend) and the per-cloudproviderblock — a template that carriesterraform,provideror any backend block is rejected. - At least one `resource` block is required (a template without one is rejected with a 400 / an
invalidsync 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
- 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. - Managed (platform operators)
Platform staff configure a GitHub repo + directory on /admin/blueprints, then hit Sync now. Every valid
*.jsonin 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. - 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
resourceTypebase rate (no per-size multipliers unless the attribute names match the built-ins). slugis 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.
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.