Publishing the provider to the Terraform Registry
Publishing the provider to the Terraform Registry: the repository, the signing key and the release.
The provider is written, built and tested in this repository (terraform.md). Publishing it
to the Terraform Registry (and to OpenTofu's) needs three things that are the owner's and that nothing in
this repository creates: a public repository of its own, a GPG signing key, and an account on the
registry. This page is the guide and scripts/prepare_terraform_registry.py is the script that makes the
first of them easy. The script writes a directory on your machine; it creates no repository, pushes nothing,
tags nothing and touches no key.
Why a repository of its own
The registry publishes a provider only from a public GitHub repository named exactly
terraform-provider-<name> (here terraform-provider-noust), under the account or organisation that
becomes the provider's namespace, and only from its releases. It cannot read a subdirectory of another
repository, and its release files have a fixed shape. So the provider is developed here, where the API it
is a client of lives and where its tests find the server they run against, and published from a copy.
What is already done
- The code:
terraform-provider-noust/, with its tests, its generated reference (docs/, written bytfplugindocsand checked fresh by CI) and its examples (examples/), which are the three things the registry shows. - The release machinery, in the directory:
.goreleaser.ymlbuilds a zip per platform, writesSHA256SUMSand signs it with the key in the environment;terraform-registry-manifest.jsonsays the provider speaks protocol 6;.github/workflows/release.ymlruns GoReleaser when av*tag is pushed, and.github/workflows/test.ymlruns the unit tests on every push. They are templates here (GitHub only runs workflows at the root of a repository) and become live in the staged repository. - The whole release, rehearsed.
python tests/integration/terraform_run.py --stage releasestages the public repository with the script below, commits and tags it in a container, runs what its owntestworkflow runs, builds it with GoReleaser from the same.goreleaser.yml, signs the checksums with a key made for the run and thrown away after, and holds the output to what the registry documents: thirteen zips namedterraform-provider-noust_<version>_<os>_<arch>.zipeach holdingterraform-provider-noust_v<version>, aSHA256SUMSthat lists every zip and the manifest, a binary detached signature that verifies, a manifest that says protocol 6, and a binary that starts as a plugin with the version built in. It publishes nothing and uses no key of yours. What it cannot rehearse is the part that is yours: the repository, your key and your registry account. - The module path (
github.com/Perkybeet/terraform-provider-noust) and the registry address (registry.terraform.io/perkybeet/noust, sosource = "perkybeet/noust") are written for the accountPerkybeet. For another account or an organisation, stage with--ownerand they are rewritten.
What needs you
1. Decide
- The namespace. The registry namespace is the lower-cased GitHub account that owns the repository. The
default is
Perkybeet; an organisation (noust?) is a better home for a product, and its name is the one users will write insource. It cannot be moved afterwards without breaking them. - The licence. The provider is AGPL-3.0-or-later like the rest of Noust. A provider runs as its own
process and talks to Terraform over gRPC, so the licence does not reach the configurations that use it, but
the Terraform ecosystem is mostly MPL-2.0 and a copyleft licence is a friction some companies will not
accept. As the copyright holder you may license this directory differently; change
LICENSEbefore the first release. - The first version.
0.1.0is what the examples say.
2. Make the repository
python scripts/prepare_terraform_registry.py # or --owner SomeOrg
cd build/terraform-registry/terraform-provider-noustCreate the repository on GitHub, public and empty (no README, no licence, nothing), named
terraform-provider-noust, and push the staged directory as its first commit. The script prints these
commands for the owner it staged for; it does not run them:
gh repo create Perkybeet/terraform-provider-noust --public --description "Terraform and OpenTofu provider for Noust"
git init -b main && git add -A && git commit -m "terraform-provider-noust 0.1.0"
git remote add origin git@github.com:Perkybeet/terraform-provider-noust.git
git push -u origin main--check on its own (python scripts/prepare_terraform_registry.py --check) says whether the provider has
everything the registry needs without writing anything. The staged copy leaves out e2e/ (it drives this
repository's harness) and makes the links of its README absolute.
3. Make the signing key
The registry only accepts a release whose SHA256SUMS file is signed, with a key whose public half you give
it. It accepts RSA and DSA keys and rejects the elliptic-curve type that recent GPG offers by default, so
choose RSA explicitly. Make the key for the provider, with a passphrase, on a machine you trust:
gpg --full-generate-key # "RSA and RSA", 4096 bits, a long validity, a passphrase
gpg --list-secret-keys --keyid-format=long # note the key id after "rsa4096/"
gpg --armor --export <KEY-ID> # the public half: for the registry
gpg --armor --export-secret-keys <KEY-ID> # the private half: for the repository's secretsThe private half goes to the public repository's Actions secrets and nowhere else: GPG_PRIVATE_KEY
(the armoured block) and PASSPHRASE. It is never committed, never put in this repository's secrets, and
never pasted into a chat or an issue. Keep an offline copy: a lost key means a new key and a new version
that people must trust again.
4. Register with the registry
- Sign in at https://registry.terraform.io with GitHub, and authorise it for the account (or the organisation) that owns the repository.
- Settings, Signing Keys: add the public key you exported, under the namespace.
- Publish, Provider: pick the repository. The registry installs a webhook on it so that every release is
ingested; nothing is published until a release exists. If the webhook is missing afterwards, delete any
registry.terraform.iowebhooks the repository has and press Resync on the provider's settings page.
5. Release
git tag v0.1.0
git push origin v0.1.0The registry reads a tag that is a semantic version with the v in front (v0.1.0, or v0.2.0-rc1 for a
pre-release), from a release that is finished and not a draft, and no branch may carry the tag's name. A
version it has ingested is never changed or replaced: a mistake is a new version.
The release workflow imports the key from the secrets, runs GoReleaser, and the GitHub release it
creates carries the files the registry reads: terraform-provider-noust_0.1.0_<os>_<arch>.zip for each
platform, ..._SHA256SUMS, ..._SHA256SUMS.sig and ..._manifest.json. The registry ingests it within a
few minutes; terraform init with source = "perkybeet/noust" then installs it.
6. OpenTofu
OpenTofu has a registry of its own, registry.opentofu.org, which reads the same GitHub release. A provider
is added to it by two submissions to https://github.com/opentofu/registry, and only through the issue
forms in the browser: its maintainers ask that nobody opens pull requests or creates these issues with the
gh command or the API, because those are not processed.
- "Submit new Provider" (the
provider.ymlform): the repository,Perkybeet/terraform-provider-noust. - "Submit new Provider Signing Key" (the
provider_key.ymlform): the public half of the key you exported for the Terraform registry, so that OpenTofu can check the same signature.
The team reviews and approves them. The forms and their fields are theirs and change; the README of that repository is the source of truth.
Releasing again
The provider is developed here, so a release is a refresh of the public repository:
python scripts/prepare_terraform_registry.py --out /tmp/stage/terraform-provider-noust
rsync -a --delete --exclude .git /tmp/stage/terraform-provider-noust/ ../terraform-provider-noust/
cd ../terraform-provider-noust && git add -A && git commit -m "0.2.0" && git tag v0.2.0 && git push && git push origin v0.2.0Versions follow the provider, not Noust: a release that changes nothing a configuration can see is a patch;
a new resource or attribute is a minor; an attribute renamed, removed or made to replace the resource is a
major (and the plan shows must be replaced). The API is versioned separately (/api/v1), and the provider
refuses a server that speaks another version, so a Noust that adds /api/v2 next to /api/v1 does not break
a provider built for 1.
Checking a release
curl -sL https://github.com/<owner>/terraform-provider-noust/releases/download/v0.1.0/terraform-provider-noust_0.1.0_SHA256SUMS -o SUMS
curl -sL https://github.com/<owner>/terraform-provider-noust/releases/download/v0.1.0/terraform-provider-noust_0.1.0_SHA256SUMS.sig -o SUMS.sig
gpg --verify SUMS.sig SUMSand terraform init in an empty directory with the required_providers block of the README, which verifies
the same signature against the key the registry holds.