# Terraform in organisations

> Page: https://docs.plotwork.dev/terraform-organisations. Index of every page: https://docs.plotwork.dev/llms.txt

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.

## 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](https://docs.plotwork.dev/terraform-provider.md#2-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.

**Diagram source**

```diagram
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"
```

*Picture: One organisation in Terraform: 5 nodes, 4 connections. 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.*

**Text description**

**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

1. In the portal, open the organisation and choose **Keys and CI**.
2. Create a key named for where it runs, such as `terraform-design`, and tick **Brand, icons and patterns**.
3. Create one key for each application pipeline, such as `terraform-payments`, with **Diagrams** ticked.
4. 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](https://docs.plotwork.dev/diagrams-in-azure-pipelines.md) |

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

```text
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.tftpl
```

The design workspace:

```hcl
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:

```hcl
# 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](https://docs.plotwork.dev/terraform-icon-hosting.md) 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

```yaml
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 tfplan
```

For 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:

```hcl
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](https://docs.plotwork.dev/audit-log.md) 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](https://docs.plotwork.dev/terraform-icon-hosting.md#4-what-plotwork-accepts). |
| 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](https://docs.plotwork.dev/terraform-provider.md) | Install, release channels, and every block |
| [Hosting custom icons for Terraform](https://docs.plotwork.dev/terraform-icon-hosting.md) | Where to keep icon SVGs and how the provider reads them |
| [Organisations](https://docs.plotwork.dev/organisations.md) | Roles, keys and the private library |
| [Custom icons](https://docs.plotwork.dev/custom-icons.md) | What an icon may hold and how diagrams draw it |
| [Approved patterns](https://docs.plotwork.dev/approved-patterns.md) | The pattern file `plotwork_pattern` reads |
| [Brand themes](https://docs.plotwork.dev/brand-themes.md) | How the brand colours paint a diagram |
| [Classification labels and publish approval](https://docs.plotwork.dev/classification-and-approval.md) | What each label allows |
| [Enterprise rollout](https://docs.plotwork.dev/enterprise-rollout.md) | Diagrams as code across many repositories |
