Migrates Terraform charm and product modules to the CC008 Charm Terraform Standards, enforcing the required file layout, variable and output contracts, MAJOR_VERSION markers, module tests and the operator-workflows reusable CI workflows. Use whenever a repository's Terraform modules must be brought to (or audited against) CC008.
72
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Bring every Terraform module in a repository up to the CC008 — Charm Terraform Standards specification, and wire the repository to the compliance, test and release automation provided by canonical/operator-workflows.
versions.tf, combined endpoints output, unpinned module sources).assets/cc008.spec.md is the specification. Read the
sections relevant to the modules you are migrating before editing anything, and
resolve every question against it. This skill does not restate the spec; it only
reinforces the parts that are most often missed and describes the repository
plumbing the spec does not cover.
This skill ships two companion files next to SKILL.md: assets/cc008.spec.md
and scripts/check_cc008.sh. When the skill is installed as a directory — for
example synced to .github/skills/terraform-cc008-migration/ — they are already
on disk and the relative paths above resolve.
When only SKILL.md was fetched (copilot skill add <url> materializes a single
file), download them first and use the downloaded copies wherever this document
refers to them:
CC008_BASE="https://raw.githubusercontent.com/canonical/copilot-collections/main/groups/platform-engineering/skills/terraform-cc008-migration"
mkdir -p /tmp/cc008
curl -fsSL -o /tmp/cc008/cc008.spec.md "$CC008_BASE/assets/cc008.spec.md"
curl -fsSL -o /tmp/cc008/check_cc008.sh "$CC008_BASE/scripts/check_cc008.sh"
chmod +x /tmp/cc008/check_cc008.shDelete /tmp/cc008 once the migration is verified; it must never be committed.
In order of authority when they disagree:
Treat every directory containing a main.tf (or an existing versions.tf /
terraform.tf) as one module, whether the repository has a single terraform/
directory or several — per-charm <charm>/terraform, product modules under
terraform/<product> or terraform-product/. Enumerate them all before
starting:
find . -type f -name 'main.tf' -not -path '*/.terraform/*' -exec dirname {} \; | sort -uThen classify each module before applying any rule. The categories have different input and output contracts, and applying the wrong one is the most common migration mistake:
| Category | What it deploys | Key outputs |
|---|---|---|
| Charm module | a single charm | application, plus provides / requires |
| Product module | a ready-to-use solution, incl. models and integrations | models, metadata |
| Deployment | one specific environment | n/a — versions state and backend.tf |
CC008 also defines component modules (several charms sharing one release cycle). We have none yet, so this skill carries no rules for them — read the "Component modules" section of the spec directly if you ever meet one.
The universal rules below apply to every category; then follow only the section matching the category you classified.
terraform.tf, variables.tf, outputs.tf,
main.tf and README.md. Rename a legacy versions.tf to terraform.tf,
leaving the content unchanged except where the next rule requires otherwise.terraform.tf, a Terraform required_version and a
juju/juju provider version that admits >= 1.0.0 (for example ~> 1.0, or
> 1.0.0, < 2.0.0).variable blocks in variables.tf and output blocks in
outputs.tf alphabetically by name.nullable = false on every variable that must always resolve to a
concrete value (app_name, channel, model_uuid, …) so callers cannot pass
an explicit null and bypass the default. Leave variables that intentionally
default to null (base, constraints, revision, …) nullable.description.terraform/MAJOR_VERSION file at the root of each independent
module family, containing only the current major version number and no trailing
newline (start at 1 for a first migration). In multi-module repositories,
make additional modules' MAJOR_VERSION files a relative symlink to that root
file only when those modules share the same release train (the
mailserver-operators#48 pattern); otherwise give each independently tagged
module family its own file.tests/main.tftest.hcl per module, using mock_provider "juju"
and asserting the module's key outputs, when the module has no test yet.source = "git::...//terraform..." reference
— in README examples and in product modules — to a ?ref= tag
or commit hash such as ?ref=tf-1.0.0. Floating references such as branches
are not allowed.app_name, channel, config,
constraints, model_uuid (no default) and revision. Add units unless the
charm is a subordinate charm, which must omit it. Determine this
deterministically — never guess from the charm's name or description: read
the module's own charmcraft.yaml (walk up from the terraform/ directory
to the charm root if it lives elsewhere, e.g. ../charmcraft.yaml or
../../charmcraft.yaml) and check its top-level subordinate: key.
subordinate: true means omit units; anything else (including the key
being absent, which defaults to false) means units is mandatory. The
compliance checker treats units as optional either way, so it will not
catch a wrong call — getting the classification right is this skill's
responsibility, not the checker's.base, expose, resources, machines, endpoint_bindings,
storage_directives, offered_endpoints.application as the juju_application resource object itself —
value = juju_application.<name> — not its .name.provides and requires as map(object({...})), one key per
relation endpoint the charm actually declares, each entry carrying at least
kind = "endpoint", name = juju_application.<name>.name and
endpoint = "<relation-endpoint-name>". Add controller = null when the
relation supports cross-model integration. They are mandatory as soon as the
charm declares endpoints of that kind; use value = {} only when it declares
none.endpoint / endpoints output with the
provides / requires split.A product module deploys a ready-to-use solution and owns the juju_model,
secret and integration resources tying its charms together. It does not
declare requires: everything the product needs from the outside is supplied
through input variables, and everything it offers to the outside is exposed
through offers.
requires output for a caller to wire up.proxy and logging-config as mandatory inputs whenever the
module creates or manages its own juju_model resources, plus risk to
control the channel risk of the bundled components.count = 0, so it is dropped when the user supplies
their own endpoint or offer through the corresponding variable.models, mapping each model key to its model_uuid and the
components deployed in it, and metadata carrying at least version,
deployed_at and updated_at. Both are mandatory.offers and, where the solution issues them, credentials.backend.tf with the backend configuration, and version every file
describing the deployment including the state.Use operator-workflows' reusable workflows instead of hand-written scripts.
DO add or update .github/workflows/terraform_modules_release.yaml calling
canonical/operator-workflows/.github/workflows/terraform_modules_release.yaml,
triggered on push to main and on pull requests touching terraform/** (plus
any other module paths), with permissions: contents: write.
DO add .github/workflows/terraform_modules_compliance.yaml calling
canonical/operator-workflows/.github/workflows/terraform_modules_compliance.yaml,
triggered on pull requests touching **/terraform/**, with a
terraform-directories input listing every discovered module directory.
DO update .github/workflows/test_terraform_modules.yaml to call
canonical/operator-workflows/.github/workflows/terraform_modules_test.yaml
with terraform-directories listing every discovered module directory, and
trigger it on pull requests touching **/terraform/**.
DO add .github/workflows/generate_terraform_docs.yaml calling
canonical/operator-workflows/.github/workflows/generate_terraform_docs.yaml.
Trigger it on pushes to main that touch **/terraform/** or this workflow
file, with these caller permissions so it can create the documentation pull
request:
permissions:
contents: write
pull-requests: writeDO pass every discovered module directory as a comma-separated
terraform-directory input to the docs workflow. Do not rely on its default
of terraform when modules live elsewhere. Ensure each module's
README.md contains <!-- BEGIN_TF_DOCS --> and <!-- END_TF_DOCS -->
markers for the generated content.
DO leave the docs workflow's auto-merge input unset to retain its
default of true. It generates the README changes and opens or updates a
terraform-docs pull request after the push to main.
DO add each of these three workflow files to its own paths filter, on
every trigger it declares:
on:
pull_request:
paths:
- '**/terraform/**'
- '.github/workflows/terraform_modules_compliance.yaml'Without this, bumping the pinned reusable-workflow SHA does not run the new checks on the pull request that bumps it, so a breaking change in operator-workflows lands unverified.
DO pin every new — and every pre-existing unpinned — reusable-workflow call
to a commit SHA. Reuse the SHA already used elsewhere in the repository for
canonical/operator-workflows if one exists; otherwise resolve the latest
main commit yourself and pin to it with a # main comment, as
platform-engineering-charm-template does:
git ls-remote https://github.com/canonical/operator-workflows.git mainApply this SHA-pinning requirement to the Terraform docs workflow as well.
**/.terraform/ and **/.terraform.lock.hcl to the top-level
.gitignore if they are not already ignored.**/MAJOR_VERSION to the header.ignore list in .licenserc.yaml
so the version markers are exempt from license headers.docs/changelog.md, or a new file under
docs/release-notes/artifacts/ if the repository uses that convention —
describing the CC008 migration and any breaking default change (for example
expose now defaulting to {} instead of null).app_name/channel/revision variables, no application output, and no
provides / requires.charmcraft.yaml, or workflows unrelated to
the Terraform modules.@main or any floating branch or tag in a reusable-workflow
call in the final diff, not even as a placeholder..terraform/ artefact produced while validating.Do not guess at compliance. Run the checker against every discovered module and iterate until it passes clean:
<skill-dir>/scripts/check_cc008.sh<skill-dir> is wherever this skill is installed — typically
.github/skills/terraform-cc008-migration/. If you downloaded the companion
files instead, run /tmp/cc008/check_cc008.sh.
Called with no arguments the script discovers every module directory itself; pass explicit directories to narrow the run:
<skill-dir>/scripts/check_cc008.sh terraform charms/foo/terraformIt downloads the checker from operator-workflows into a temporary directory and removes it afterwards, so nothing is left behind to commit.
Then run the repository's Terraform quality gates:
tflint --init && tflint --recursive
terraform fmt -recursive -check
terraform -chdir=<module-dir> init -backend=false && terraform -chdir=<module-dir> testtflint --recursive and terraform fmt -recursive -check are clean.terraform test passes for every module.paths filter..terraform/ artefact and no
behavioural change beyond what CC008 requires.d41d8d9
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.