Terraform in organisations
How a platform team runs Plotwork for a whole organisation from Terraform or OpenTofu: one workspace owns the brand, the custom icons and the approved patterns, and every application stack publishes its own diagrams to the private library. It is for platform engineers, architects and the owners of an organisation's Plotwork setup.
- GitHub Actions
TL;DR
| Question | Answer |
|---|---|
| What can Terraform manage? | The organisation's brand colours, custom icons and approved patterns, and every diagram a stack publishes to the library. Members, invitations, keys and settings stay in the portal. |
| Which key? | An organisation key from Keys and CI, never a person's key. Give the design workspace a key with Brand, icons and patterns (design:write) and each application stack a key with Diagrams. |
Who can grant design:write? | The organisation's owner or an admin. A key without it is refused with orgs.forbidden. |
| How many workspaces? | One design workspace for the organisation, with its own state, and one workspace per application stack for that stack's diagrams. |
| How does CI get the key? | As a sensitive environment variable, PLOTWORK_API_KEY: a GitHub Actions secret, an Azure Pipelines secret variable, or an HCP Terraform or Atlantis variable set. Never in a committed .tfvars file. |
| What happens to a change made in the portal? | The next plan shows it as drift, and the next apply puts the configuration back. |
| Can I take over what is already there? | Yes. terraform import and import blocks take the ids <organisation> for the brand and <organisation>/<name> for icons and patterns. |
| Which plan? | Brand themes and custom icons need the owner on Pro or Enterprise. Approved patterns need Enterprise. Publishing diagrams works on every plan; plotwork_import and plotwork_render need Pro or above. |
| Is the provider stable? | Not yet. Only beta releases exist; pin the exact version. See Release channels. |
1. The shape of the setup
The platform team keeps the organisation's look in one root module, reviewed like the rest of the platform. Application teams keep their diagrams beside their own infrastructure and publish them to the library. Neither needs the other's key.
flow "One organisation in Terraform" direction=right desc="The platform team's design workspace sets the organisation's brand, icons and patterns with a design key. Each application stack publishes its own diagrams to the library with a diagrams key, and those diagrams are drawn with the brand and icons."
group platform "Platform repository" { node design service "Design workspace" sub="brand, icons, patterns" icon=lucide:layers}group apps "Application repositories" { node payments service "Payments stack" icon=lucide:layers node checkout service "Checkout stack" icon=lucide:layers}group org "Plotwork organisation" { node look service "Brand, icons and patterns" icon=lucide:stamp node library storage "Private library" icon=lucide:book-open}
design -> look : "design:write key"payments -> library : "diagrams key"checkout -> library : "diagrams key"look -> library : "paints"Nodes:
- Group: Platform repository
- Design workspace (brand, icons, patterns), service
- Group: Application repositories
- Payments stack, service
- Checkout stack, service
- Group: Plotwork organisation
- Brand, icons and patterns, service
- Private library, storage
Connections:
- Design workspace to Brand, icons and patterns: design:write key
- Payments stack to Private library: diagrams key
- Checkout stack to Private library: diagrams key
- Brand, icons and patterns to Private library: paints
| Workspace | Owns | Key scope | State |
|---|---|---|---|
| Design, one for the organisation | plotwork_organization_brand, plotwork_organization_icon, plotwork_pattern | Brand, icons and patterns (design:write) | Its own. Nothing else writes these resources. |
| An application stack, one for each | plotwork_diagram, with plotwork_import or templatefile() | Diagrams | The stack's own, beside its infrastructure |
Keep the design resources in exactly one state. Two workspaces that both declare the brand, or the same icon name, undo each other's apply in turn.
2. Make the keys
- In the portal, open the organisation and choose Keys and CI.
- Create a key named for where it runs, such as
terraform-design, and tick Brand, icons and patterns. - Create one key for each application pipeline, such as
terraform-payments, with Diagrams ticked. - Copy each key into the secret store of the pipeline that uses it (section 5). An owner or admin can reveal a key again later.
| Scope | Lets the key | Give it to |
|---|---|---|
| Diagrams | Read the library, publish, move, rename and delete diagrams | Application stacks |
Brand, icons and patterns (design:write) | Read the organisation, and manage its brand, custom icons and approved patterns | The design workspace |
Audit log only (audit:read) | Read and export the audit log | A SIEM, never Terraform |
Drift (drift:write) | Report drift runs from CI and read the style rules | The drift check of the GitHub Action or the Azure Pipelines task |
An organisation key belongs to the organisation, not to the person who made it, so it keeps working when that person leaves. It acts only in its own organisation. The organisation holds at most ten active keys. A key belongs to one environment: a key made on the preview portal works with the beta provider's default API, and a production key needs api_url = "https://api.plotwork.dev".
3. Lay out the modules
platform-design/ # the design workspace, one per organisation versions.tf main.tf # brand, icons, patterns icons/ payments-api.svg ledger.svg patterns/ payments-service.pattern.json
payments/infra/ # an application stack versions.tf main.tf # the stack's own resources diagrams.tf # plotwork_import and plotwork_diagram network.diagram.tftplThe design workspace:
terraform { required_providers { plotwork = { source = "plotwork-official/plotwork" version = "0.1.0-beta.2" } }}
# Reads PLOTWORK_API_KEY: the design key.provider "plotwork" { organization = "acme"}
resource "plotwork_organization_brand" "this" { primary = "#2a78d6"}
resource "plotwork_organization_icon" "this" { for_each = fileset("${path.module}/icons", "*.svg")
name = trimsuffix(each.value, ".svg") svg = file("${path.module}/icons/${each.value}")}
resource "plotwork_pattern" "this" { for_each = fileset("${path.module}/patterns", "*.pattern.json")
content = file("${path.module}/patterns/${each.value}")}
output "icon_refs" { value = { for file, icon in plotwork_organization_icon.this : file => icon.ref }}An application stack:
# Reads PLOTWORK_API_KEY: this stack's diagrams key.provider "plotwork" { organization = "acme"}
data "plotwork_import" "infra" { format = "terraform" files = { for f in fileset(path.module, "*.tf") : f => file("${path.module}/${f}") }}
resource "plotwork_diagram" "infra" { source = data.plotwork_import.infra.source name = "Payments infrastructure" folder = "Payments/Infrastructure" classification = "internal"}A stack draws the organisation's icons by name, as icon=org:payments-api, in a hand-written template. It needs no reference to the design workspace's state: the name is the contract. Hosting custom icons for Terraform covers where the SVGs can live.
4. Names, folders and labels
| Setting | Convention | Why |
|---|---|---|
folder | <Team>/<Area>, outermost first, such as Payments/Infrastructure | Members browse the library by folder, and a folder rule can restrict who sees it. A restricted folder takes a key only when the rule names key. |
name | What a reader looks for, such as Payments infrastructure. Keep it fixed. | The library lists diagrams by name. Removing name later keeps the old label. |
classification | Set it on every diagram: internal by default, confidential for anything that names security controls or data flows of personal data | Left out, the folder's or the organisation's default applies. A confidential diagram is never shared at a public link. |
Icon name | Lower-case words joined by hyphens, after the service, such as payments-api | It is what diagrams write after org:. Changing it replaces the icon, and every diagram that names the old one draws a placeholder. |
Icon aliases | The words people write in labels, such as payments service | Search and automatic icon matching find the icon by them. |
alias | Not for an organisation's diagram | A stable link is a public address in your own library. An organisation's diagram has none; members open it with view_url, which changes when the source does. |
5. Run it in CI
Set the key as the environment variable PLOTWORK_API_KEY, marked sensitive, in the system that runs the plan. Leave api_key out of the configuration, and never write a key in a .tfvars file that is committed. If a module takes the key as a variable, declare it sensitive = true.
GitHub Actions
name: plotwork-design
on: pull_request: push: branches: [main]
permissions: contents: read
jobs: terraform: runs-on: ubuntu-latest env: PLOTWORK_API_KEY: ${{ secrets.PLOTWORK_DESIGN_KEY }} PLOTWORK_ORG: acme steps: - uses: actions/checkout@v7 - uses: hashicorp/setup-terraform@v4 - run: terraform init - run: terraform plan -out=tfplan - if: github.ref == 'refs/heads/main' run: terraform apply tfplanFor OpenTofu, use opentofu/setup-opentofu@v2 and tofu in place of terraform. Keep the secret in a protected environment when pull requests from forks can run the workflow.
HCP Terraform, Terraform Enterprise and Atlantis
| Runner | Where the key goes |
|---|---|
| HCP Terraform or Terraform Enterprise | A variable set with an environment variable PLOTWORK_API_KEY, marked Sensitive, applied to the design workspace only. A second variable set with a diagrams key, applied to the application workspaces. |
| Atlantis | The server's environment, or a secret the server reads into PLOTWORK_API_KEY. Give the design repository its own server or workflow when it must not share a key with application repositories. |
| Spacelift, env0, Scalr | The same: an environment variable marked secret, scoped to the stacks that need it. |
6. Take over what is already there
An organisation that set its brand, icons or patterns in the portal can move them into code without deleting anything. Write the resources, then import each one:
import { to = plotwork_organization_brand.this id = "acme"}
import { to = plotwork_organization_icon.this["payments-api.svg"] id = "acme/payments-api"}
import { to = plotwork_pattern.this["payments-service.pattern.json"] id = "acme/payments-service"}| Resource | Import id |
|---|---|
plotwork_organization_brand | <organisation>, its slug or id |
plotwork_organization_icon | <organisation>/<name> |
plotwork_pattern | <organisation>/<name>, the pattern's name |
plotwork_diagram | <organisation>/<id>, or the id alone for a personal diagram |
The command form works too: terraform import 'plotwork_organization_icon.this["payments-api.svg"]' acme/payments-api, or tofu import. Plotwork keeps a checked copy of an icon, not the file, so the first apply after importing an icon uploads the configured file once. Download the icons from the portal first if the repository does not have them yet.
7. Drift: the configuration wins
Terraform reads each resource on every plan. A change made outside it shows as drift, and the next apply puts the configuration back.
| Changed in the portal | Next plan | Next apply |
|---|---|---|
| A brand colour | An update to the brand | Saves the configured colours |
| An icon replaced | An update: content_sha256 is empty | Uploads the configured file again |
| An icon or pattern deleted | A create | Saves it again |
| A pattern edited | An update | Saves the configured file |
| A diagram deleted, or expired by the plan | A create | Publishes it again |
| An icon or pattern added in the portal | Nothing | Nothing: Terraform manages only what it declares |
Tell admins that the design workspace owns the brand, icons and patterns, so a fix belongs in a pull request. On Pro and Enterprise the organisation's audit log records who saved or deleted each icon and pattern.
8. What each plan includes
| Resource or data source | Free | Pro | Enterprise |
|---|---|---|---|
plotwork_diagram | Yes | Yes | Yes |
plotwork_import, plotwork_render | No, plan.rest_not_included | Yes | Yes |
plotwork_organization_brand | No, plan.brand_not_included | Yes | Yes |
plotwork_organization_icon | No, plan.icons_not_included | Up to 100 icons | Custom |
plotwork_pattern | No | No, plan.governance_not_included | Up to 50 patterns |
An organisation has its owner's plan, and an organisation key is held to it. If the owner moves to Free, the brand and icons are kept but not drawn, and active on the brand turns false.
9. Best practices
| Practice | Why |
|---|---|
| One design workspace per organisation | The brand, each icon and each pattern have exactly one owner, so two applies never fight. |
| A separate key for each pipeline, named for it | You can revoke one pipeline without touching the others, and the call history names it. |
design:write only on the design key | An application pipeline that leaks its key cannot change the organisation's look. |
| Keys as sensitive environment variables | They never reach the state, a plan file or a log. |
Pin the exact provider version and commit .terraform.lock.hcl | Every run uses the same build, and a new beta is a reviewed change. |
| Review icon SVGs and pattern files in pull requests | They are what every team's diagrams draw. Run check_pattern on a pattern file before merging. |
Send configuration, not state, to plotwork_import | State can hold secrets. |
Set classification on every organisation diagram | The label decides how far a diagram may travel. |
10. Troubleshooting
| Error | Cause | Fix |
|---|---|---|
401 auth.invalid | The key is wrong or revoked, or it was made in the other environment | Use a key from the portal of the API the provider talks to, or set api_url. A beta talks to preview by default. |
403 orgs.forbidden | The key lacks the scope the resource needs: a design resource without design:write, or a diagram without Diagrams. A personal key of a member who is not an owner or admin. | Make a key with the right scope under Keys and CI. |
403 orgs.key_wrong_org | An organisation key was used with no organisation, so the call went to a personal library | Set organization on the provider, or PLOTWORK_ORG. |
404 not_found | The organisation's slug is wrong, or the key belongs to another organisation | Check organization against the organisation's handle. |
403 plan.rest_not_included | plotwork_import or plotwork_render with an owner on Free | Upgrade the owner to Pro, or publish with plotwork_diagram from a hand-written source. |
403 plan.brand_not_included, plan.icons_not_included, plan.governance_not_included | The owner's plan does not include brand themes, custom icons or approved patterns | See section 8. |
403 orgs.icon_limit_reached, orgs.pattern_limit_reached | The organisation holds as many as its plan allows | Remove some, or move the owner up a plan. |
403 orgs.folder_restricted | The folder is restricted and its rule does not name key | Ask the owner to add key to the folder rule, or publish to another folder. |
422 icon.invalid | The SVG holds something the check refuses | details lists every reason. See Hosting custom icons for Terraform. |
422 orgs.pattern_invalid | A template in the pattern file has a mistake | Run check_pattern and fix each problem it names. |
See also
| Page | Why |
|---|---|
| Terraform provider | Install, release channels, and every block |
| Hosting custom icons for Terraform | Where to keep icon SVGs and how the provider reads them |
| Organisations | Roles, keys and the private library |
| Custom icons | What an icon may hold and how diagrams draw it |
| Approved patterns | The pattern file plotwork_pattern reads |
| Brand themes | How the brand colours paint a diagram |
| Classification labels and publish approval | What each label allows |
| Enterprise rollout | Diagrams as code across many repositories |