# CAPTF v0.1 is released

Cluster API Provider Terraform has its first releases. v0.1.0 was tagged on October 5, and v0.1.1 followed a day later. Together they put every part of the project into a versioned, installable form: the provider, the two runtime base images, the 17 reference modules on the Terraform Registry, and a module image for each of them, on both runtimes.

You can install it with `clusterctl` today and bring up a cluster with the no-op modules on any management cluster. This post covers what shipped, why it is 0.1 and not 1.0, and how every artifact is built, signed and checked, so you can verify it yourself.

[Quick Start](<https://captf.io/docs/getting-started/quick-start/index.md>) [Release notes](<https://github.com/captf-io/cluster-api-provider-terraform/releases/tag/v0.1.1>)

## At a glance

|  | v0.1.1 |
| --- | --- |
| API | `infrastructure.cluster.x-k8s.io/v1alpha1` |
| Module image contract | `v1alpha1` |
| Cluster API contract | `v1beta2`, built against Cluster API v1.14.2 |
| Built with | Go 1.26, controller-runtime v0.24.1, Kubernetes libraries v0.36.5 |
| Runtimes in the base images | Terraform 1.16.5, OpenTofu 1.12.7 |
| Platforms | `linux/amd64`, `linux/arm64` |

## What shipped

| Artifact | Where |
| --- | --- |
| Provider image: the manager and the in-Job runner | `ghcr.io/captf-io/cluster-api-provider-terraform:v0.1.1` |
| clusterctl assets: components, metadata, templates, identity example | The [GitHub Release](<https://github.com/captf-io/cluster-api-provider-terraform/releases/tag/v0.1.1>) |
| `tfcapi-lint`, the module and image linter | `ghcr.io/captf-io/tfcapi-lint:v0.1.1`, plus binaries for Linux, macOS and Windows on the Release |
| `tfcapi-lint` GitHub Action | `captf-io/cluster-api-provider-terraform/actions/tfcapi-lint` |
| Runtime base images | `ghcr.io/captf-io/opentofu-base`, `ghcr.io/captf-io/terraform-base` |
| Reference modules | 17 on the Terraform Registry as `captf-io/<role>/<provider>` |
| Module images | `ghcr.io/captf-io/module-images/<cloud>-<role>:vX.Y.Z-<runtime>` |

v0.1.1 is v0.1.0 plus the linter’s container image and GitHub Action, and two release-pipeline fixes. The manager did not change. If you build or maintain modules, the action is the reason to take it; it has a [post of its own](<https://captf.io/blog/posts/2026-10-06-tfcapi-lint-action/index.md>).

The modules move on their own schedule. All 17 started at v0.1.0, and the first round of updates landed the next day:

- **v0.1.1 of the AWS, Azure and OCI machine pool modules** adds `autoscaler = "external"`, which hands a pool to the Kubernetes Cluster Autoscaler; see [The Cluster Autoscaler on CAPTF machine pools](<https://captf.io/blog/posts/2026-10-06-cluster-autoscaler-pools/index.md>).
- **v0.2.0 of five modules** tightens defaults. The AWS, Azure and Google cluster modules reject a `/0` in `api_allowed_cidrs`, the Azure cluster denies the kubelet ports to the rest of the VNet on workers, and the Azure machine and machine pool modules turn boot diagnostics off by default, since the serial console log can include the `kubeadm join` command.

## Install it

Register the provider with `clusterctl` and initialize a management cluster:

clusterctl.yaml

```yaml
providers:
- name: terraform
  type: InfrastructureProvider
  url: https://github.com/captf-io/cluster-api-provider-terraform/releases/download/v0.1.1/infrastructure-components.yaml
```

```sh
clusterctl init --config clusterctl.yaml --infrastructure terraform:v0.1.1
```

```sh
CLUSTER_TOPOLOGY=true clusterctl init --config clusterctl.yaml --infrastructure terraform:v0.1.1
```

`clusterctl init` also installs Cluster API’s core, bootstrap and control-plane providers, and cert-manager if it is not there yet. From there the [Quick Start](<https://captf.io/docs/getting-started/quick-start/index.md>) brings up a `TerraformCluster` and a control-plane `TerraformMachine` with the no-op modules, which need no cloud account.

## Why 0.1, not 1.0

The version says what has been proven, and what has not:

> [!WARNING]
>
> **Nothing has been applied to a real cloud yet**
>
> The five cloud module sets pass static analysis, mocked unit tests on both runtimes, `tfcapi-lint` and image smoke tests. None has been applied to a real cloud account. Each module repository’s `DESIGN.md` lists the facts its first real apply must confirm.

- **The API and the contract are `v1alpha1`.** Every kind and the module contract may still change, and the contract is provisional.
- **End-to-end testing is opt-in.** Two suites run on a local kind cluster: a foundation suite that installs and checks the stack, and a no-op suite that drives the published no-op modules through real Cluster API objects. They are not part of CI yet.
- **Some operations are unexercised.** `clusterctl upgrade` and `clusterctl move` have not been run against CAPTF.

[Known Limitations](<https://captf.io/docs/operator-guide/limitations/index.md>) lists everything else, with a workaround for each where one exists. If you try CAPTF, that page is the one to read first.

## How a release is built

Releases are built in CI, and every step on the way refuses to go on when the one before it did not finish cleanly. For the provider, as the pipeline now stands on `main`:

```
flowchart TD
    T["signed tag vX.Y.Z"] --> G{"green CI run<br/>on that commit?"}
    G -- "no, after 30 min" --> X["stop"]
    G -- yes --> P[release preflight]
    P --> I["build amd64 + arm64,<br/>push manager and tfcapi-lint images"]
    I --> S["cosign sign, SLSA provenance,<br/>SPDX SBOM attestation"]
    S --> C{"both images tagged vX.Y.Z<br/>at the pushed digests?"}
    C -- no --> X
    C -- yes --> A["build assets pinned to the manager digest,<br/>attest them"]
    A --> R["GitHub Release with curated notes"]
```

Two of these gates are new since v0.1.0, and both came from looking at what the first releases actually did:

- **Publish only what CI passed.** Publishing used to run on every push to `main` alongside CI, not after it. Images were published and signed for three commits in a row whose CI failed, and a tag published without checking CI at all. Publishing now runs from a successful CI run on `main`, builds that run’s exact commit, and moves `:edge` only while that commit is still `main`’s head. A tag waits up to 30 minutes for a green run on its commit.
- **Check the release before creating it.** v0.1.0 shipped without a `tfcapi-lint` image. The release job now fails, before it creates the Release, unless both images carry `:vX.Y.Z` at the digests it pushed, and it refuses to build assets without the manager’s digest.

The release notes are no longer a raw git log either: they open with the API and contract versions, the install commands, the image digests and the commands to verify them.

The module repositories release the same way, by tag. A release is a signed, annotated `vX.Y.Z` tag on `main`. The Terraform Registry picks it up within about a minute, and once the repository’s checks pass, CI creates the GitHub Release, refusing a lightweight tag, a tag that is not on `main` and a tag whose signature GitHub cannot verify. Then [`module-images`](<https://github.com/captf-io/module-images>) takes over:

```
flowchart TD
    R["module release<br/>vX.Y.Z"] --> D["Dependabot bumps<br/>sources/versions.tf"]
    D --> F["fetch the tag,<br/>git verify-tag"]
    F --> B["build and smoke-test,<br/>both runtimes"]
    B --> M[merge to main]
    M --> P["publish and sign<br/>IMAGE:vX.Y.Z-RUNTIME"]
```

The build fetches each module’s release tag and checks it before using it: it must be an annotated tag, signed by a key in the repository’s `hack/allowed_signers`, and pointing at the commit being built. An unsigned or wrongly signed tag fails the build.

## Verify it yourself

Every signature is keyless cosign, issued through GitHub’s OIDC token to the workflow that built the artifact, so you check *which workflow* signed it rather than holding a key:

```sh
cosign verify ghcr.io/captf-io/cluster-api-provider-terraform:vX.Y.Z \
  --certificate-identity https://github.com/captf-io/cluster-api-provider-terraform/.github/workflows/publish.yaml@refs/tags/vX.Y.Z \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify oci://ghcr.io/captf-io/cluster-api-provider-terraform:vX.Y.Z \
  -R captf-io/cluster-api-provider-terraform
```

The same commands check `ghcr.io/captf-io/tfcapi-lint`.

```sh
gh release download vX.Y.Z -R captf-io/cluster-api-provider-terraform \
  -p infrastructure-components.yaml
gh attestation verify infrastructure-components.yaml \
  -R captf-io/cluster-api-provider-terraform
```

```sh
cosign verify ghcr.io/captf-io/module-images/aws-machine:v0.1.0-opentofu@sha256:<digest> \
  --certificate-identity-regexp '^https://github.com/captf-io/module-images/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
```

```sh
cosign verify ghcr.io/captf-io/opentofu-base:<version>@sha256:<digest> \
  --certificate-identity-regexp '^https://github.com/captf-io/opentofu-base/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
```

For Terraform, use `terraform-base` in both places.

> [!NOTE]
>
> **Signing started on October 6 for module and base images**
>
> The provider image has been signed since its first build. The module and base images gained signing on October 6, so a tag built before then stays unsigned until it is rebuilt. Module image tags move to a new digest on every rebuild anyway, so pin a digest you have verified.

- **Quick Start**

  ---

  Install CAPTF and bring up a cluster with the no-op modules, no cloud account needed.
- **Installation**

  ---

  Register the provider, what gets installed, and how to check it.
- **Known Limitations**

  ---

  What CAPTF does not do yet, or does with a catch.
- **Releasing**

  ---

  The release pipeline, signatures and attestations, in full.
