Quick Start¶
This tutorial takes CAPTF from an empty management cluster to a TerraformCluster and a control-plane TerraformMachine that both report Ready, using the no-op modules that ship with CAPTF: they create no real infrastructure, so the tutorial needs no cloud account and no credentials.
This flow has not been run end to end
This tutorial has not been run end to end against a live management cluster.
Because the no-op modules provision nothing real, nothing ever boots a kubelet: no Node ever joins, so KubeadmControlPlane never initializes and the Machine never reaches its Running phase. What you watch come up in this tutorial is CAPTF’s own objects finishing their applies, not a usable Kubernetes cluster. See Your First Module for writing a module that does create something, and the module contract for turning one into a real cloud provider.
Before you begin
- A Kubernetes cluster to use as the management cluster, and
kubectlpointed at it. clusterctl,make, Go andpodmanordocker: CAPTF has no release yet, so this tutorial builds the provider’s manager image from a clone of this repository instead of fetching it.- A container registry you can push to, and that the management cluster can pull from, for the manager image.
- A management cluster that can pull from
ghcr.io, where the no-op module images are published.
Run every command below from the root of that clone: the make targets and the templates/... paths are relative to it.
1. Install the provider¶
CAPTF has not published a release, so clusterctl cannot fetch its manifest from a URL yet; build one into a local repository instead. Build and push the manager image, then render the manifest against it:
export IMG=registry.example.com/you/cluster-api-provider-terraform:v0.1.0
make docker-build docker-push IMG="${IMG}"
make manifests-release RELEASE_DIR="${HOME}/local-repository/infrastructure-terraform/v0.1.0" \
RELEASE_IMG="${IMG}" VERSION=v0.1.0
Point a clusterctl config at that directory; the config entry’s name is terraform, CAPTF’s registered provider name:
providers:
- name: terraform
type: InfrastructureProvider
url: file:///home/<you>/local-repository/infrastructure-terraform/v0.1.0/infrastructure-components.yaml
<you> is your home directory’s user name, so the url is the absolute path of the directory manifests-release just wrote.
This also installs Cluster API’s core, bootstrap and control-plane providers, and cert-manager itself if a compatible version is not already present, since CAPTF’s webhooks need it. See Installation for what this creates and how to confirm it, and Installing from a local repository for the general form of the local-repository steps above.
2. Apply an identity¶
Cloud credentials come from a cluster-scoped TerraformClusterIdentity. An admin applies one per set of credentials, naming the namespaces allowed to use it:
export TERRAFORM_IDENTITY_NAME=aws-prod NAMESPACE=team-a
clusterctl generate yaml --from templates/identity.yaml | kubectl apply -f -
The no-op modules read no credentials, so the generated Secret’s placeholder keys can stay as they are; a module that calls a real cloud provider reads its credentials from the same Secret. kubectl get terraformclusteridentity aws-prod shows Ready=True once the Secret exists and whoever applied the identity was allowed to get it — the admission webhook checks. See Identities and Credentials for creating, rotating and revoking credentials, and how they reach a Job.
3. Choose the no-op module images¶
The no-op modules are published as images by captf-io/noop-modules, so there is nothing to build. The cluster and machine roles are:
export NOOP_CLUSTER_IMAGE=ghcr.io/captf-io/noop-cluster:terraform
export NOOP_MACHINE_IMAGE=ghcr.io/captf-io/noop-machine:terraform
Each image comes in two tags, one per base image: <version>-terraform and <version>-opentofu, such as vX.Y.Z-terraform. Either satisfies the image contract. The bare terraform and opentofu tags move to the newest release; pin a release tag, or a digest, in anything you keep. This tutorial uses the terraform tag. See No-op for what the modules return.
4. Generate and apply a cluster¶
Cluster objects and everything they own live in a namespace clusterctl does not create:
Generate the default flavor and apply it. This tutorial asks for one control-plane machine and no workers, since a worker never gets bootstrap data until a real control plane initializes, which the no-op modules never do:
export TERRAFORM_CLUSTER_IMAGE="${NOOP_CLUSTER_IMAGE}"
export TERRAFORM_MACHINE_IMAGE="${NOOP_MACHINE_IMAGE}"
export TERRAFORM_IDENTITY_NAME=aws-prod
clusterctl generate cluster my-cluster --from templates/cluster-template.yaml \
--target-namespace team-a \
--kubernetes-version v1.36.3 \
--control-plane-machine-count 1 --worker-machine-count 0 \
| kubectl apply -f -
--from renders the template file directly, so this command needs no provider registration and works the same before or after a release exists. A ClusterClass-based flavor is also available; see Templates and ClusterClass, which also covers every variable this template accepts, and clusterctl variables for their defaults and built-in safeguards.
5. Watch it come up¶
kubectl get clusters,machines,machinepools,terraformclusters,terraformmachines,terraformmachinepools -n team-a
machinepools returns nothing: the default flavor creates none. See Add an autoscaled pool to a generated cluster to add one to this cluster.
Each Terraform* kind reports a Ready condition, the only one Cluster API reads (it is mirrored into the owning Cluster’s or Machine’s InfrastructureReady). Ready is Unknown while an object waits on dependencies or on its apply to finish, and True once the module’s apply succeeds:
kubectl get terraformcluster -n team-a my-cluster \
-o jsonpath='{.status.conditions[?(@.type=="Ready")]}'
Expect TerraformCluster my-cluster and the control-plane TerraformMachine to both reach Ready=True, and their PHASE columns to reach Provisioned on Cluster my-cluster and its control-plane Machine too, since the no-op modules do return a control-plane endpoint and a provider ID. What you will not see, because nothing real ever boots: the Machine reaching phase Running (no Node ever registers), and KubeadmControlPlane reporting itself initialized. That gap is expected here and is exactly what a module that creates real infrastructure closes.
Watch within about 30 minutes
Past that, the control-plane MachineHealthCheck’s node-startup timeout fires because no Node ever registers, and KubeadmControlPlane starts remediating the Machine (see Remediation).
For what each condition type and reason means, see Conditions; for the reconcile flow behind these states, see The Reconcile Lifecycle.
6. Clean up¶
Delete the Cluster first, not the namespace
Delete the Cluster first, and wait for it to be gone, rather than deleting the namespace outright: once a namespace starts terminating, the API server refuses to create the destroy Jobs each Terraform* object still needs to run before its own finalizer clears.
kubectl delete cluster my-cluster -n team-a
kubectl wait --for=delete cluster/my-cluster -n team-a --timeout=10m
That wait only returns once every descendant — KubeadmControlPlane, the MachineDeployment, and both TerraformCluster and the control-plane TerraformMachine — is gone too, the latter two only after their own destroy Job finished.
An identity cannot be deleted while a TerraformCluster, TerraformMachine or TerraformMachinePool still references it, so delete it only once the wait above returns, then the now-empty namespace:
clusterctl generate yaml --from templates/identity.yaml | kubectl delete -f -
kubectl delete namespace team-a
To remove the provider itself as well: