OpenTofu for Kubernetes Bootstrapping: GitOps with Argo CD, Hands-On
If you have been following the cluster conversation here, you know the verdict from the Docker Compose vs Kubernetes post: Kubernetes earns its keep once a second host exists, and k3s is the on-ramp when it does. What that post only hinted at is the job that appears the moment the cluster is real: bootstrap it the same way every time, install the GitOps layer, and then get out of its way. That is where OpenTofu earns its keep.
This post is the hands-on version of what OpenTofu actually is and how it relates to Terraform, why the bootstrap job splits cleanly into an IaC half and a GitOps half, the exact configuration that provisions a k3s cluster and installs Argo CD, the app-of-apps handoff that makes Argo CD own everything after, and the honest limits of the pattern.
The short version
- OpenTofu is the open-source fork of Terraform, licensed MPL-2.0 and governed by the Linux Foundation. It is a drop-in CLI replacement: same HCL, same providers, same state format.
- Bootstrap splits into two stages. OpenTofu does day zero: the cluster itself and a one-time install of Argo CD. Argo CD does day one onward: every application, reconciled continuously from git.
- The handoff is the app-of-apps pattern. One bootstrap Application in Argo CD points at a git repository that declares the rest of the cluster.
- The failure mode is two sources of truth. Decide what OpenTofu owns and what Argo CD owns, and never manage the same release from both.
- State discipline matters. Your kubeconfig lands inside the state file, which is sensitive, so use state encryption or a remote backend before you share it.
- OpenTofu
v1.12.6 (latest; 1.12.0 GA 2026-05-14) - OpenTofu 1.11 line
v1.11.14 (final patch) - Argo CD
v3.5.1 (2026-08-12); 3.5 GA 2026-08-04 - Argo CD Helm chart (argo/argo-cd)
10.4.0 - k3s
v1.36.3+k3s1 - hashicorp/kubernetes and hashicorp/helm providers
2.x current on the tofu registry
Checked 2026-08-25 against the OpenTofu releases page (v1.12.6 latest; v1.12.0 announcement of 2026-05-14), opentofu.org What's new in 1.12, the Argo CD releases page and endoflife.date/argo-cd (v3.5.1, 3.5 GA 2026-08-04), the artifacthub argo-cd chart page (10.4.0), and the k3s releases page (v1.36.3+k3s1). Provider versions on the registries move independently; re-check before you pin.
What OpenTofu actually is
OpenTofu started in August 2023, when HashiCorp moved Terraform from the MPL-2.0 license to the Business Source License. A fork was created from the last open-licensed release, it joined the Linux Foundation, and it has been developed in the open ever since, with the same HCL language, the same provider ecosystem, and a state format that Terraform reads without complaint. The license is MPL-2.0, which means anyone can use it commercially, fork it, and ship it in products, and nobody has to buy a seat to run it.
The practical differences from Terraform are smaller than the marketing implies, and they matter in specific places:
- The binary is
tofu, and it is deliberately drop-in. Existing Terraform workflows, modules, and providers mostly work unchanged. - There is no seat licensing and no per-user pricing. The cost is the infrastructure and your time.
- The tofu registry at search.opentofu.org mirrors the provider ecosystem and hosts community modules, including several k3s bootstrap modules for common providers.
- OpenTofu ships a few CLI extras Terraform lacks, most usefully
-exclude(skip one resource in a plan or apply) and provider-defined functions, plus built-in state encryption that predates Terraform's.
What has not changed is the provider model. OpenTofu consumes the same HashiCorp providers from the same registries, which is exactly what makes this post's configuration valid under either binary.
Why bootstrap with IaC at all
The bootstrap problem is a coordination problem. A cluster needs nodes, a kubeconfig, a network, the GitOps operator, and a first application, in that order, reproducibly. A hand-rolled checklist works once and fails on the second environment. IaC gives you three properties a checklist cannot: repeatability (the same config produces the same cluster), reviewability (the bootstrap is a diff, not a memory), and destroyability (tear it down and rebuild from nothing, which is how you find the steps you forgot to write down).
The honest counterweight, and I will say it before the vendors do: for a single throwaway test cluster, a shell script is faster. IaC earns its keep when the cluster is the platform, not the experiment, which is exactly the boundary the Compose vs Kubernetes post drew. If Kubernetes earns its keep on your hardware, the bootstrap earns IaC.
The bootstrap loop: where OpenTofu stops and Argo CD starts
This is the mental model that keeps the whole thing clean. There are exactly three stages, and each one has one owner:
- Day zero, OpenTofu: provision the nodes (directly, or through a k3s module on the tofu registry) and write the kubeconfig.
- Day zero, still OpenTofu: use the helm provider to install Argo CD once. This is the last time OpenTofu touches an application.
- Day one onward, Argo CD: a bootstrap Application points at your git repository, and everything in that repo is reconciled continuously. You never run
tofu applyfor an app again.
Argo CD's own documentation calls the pattern cluster bootstrapping, and it offers two shapes for it: the app-of-apps pattern, where one Application points at a directory of further Application manifests, and ApplicationSets, which generate Applications from a generator. The app-of-apps pattern is the simpler default for a homelab; ApplicationSets pay off once you have many clusters or many apps with the same shape.
Hands-on: install OpenTofu
The installer is a shell script, and the package path is the one I use on Debian and Ubuntu. It adds the OpenTofu apt repository, installs tofu, and the binary is yours:
$ curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh | sh -s -- --install-method deb
$ tofu --version
OpenTofu v1.12.6
# optional shell completion
$ tofu -install-autocompleteHomebrew, winget, and the other package managers are covered in the official install docs, and the verified block above has the version I checked against. Pin the version you build on, and track releases the way the release-tracking post recommends, because OpenTofu ships often and the patch line (1.11.14 was the final 1.11 release) moves faster than you expect.
Hands-on: the bootstrap configuration
Assume the cluster exists and you have a kubeconfig, which is the realistic middle ground. If you are starting from bare VMs, point OpenTofu at a k3s module from the tofu registry instead of the inline provider block below; the rest of the pattern is identical.
Two providers do the work. The kubernetes provider reads your kubeconfig, and the helm provider uses the same kubeconfig to install charts. The kubeconfig path is a variable, because it is the one thing that differs between environments:
# providers.tf
terraform {
required_version = ">= 1.12"
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.0"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.0"
}
}
}
variable "kubeconfig_path" {
type = string
default = "~/.kube/config"
}
provider "kubernetes" {
config_path = var.kubeconfig_path
}
provider "helm" {
kubernetes {
config_path = var.kubeconfig_path
}
}The version constraints above are deliberately loose because the registries move independently. Before you rely on exact numbers, check what the tofu registry reports today and pin that, the same discipline as every other dependency in this stack.
Now the resource that does the whole job: one helm_release that installs Argo CD into its own namespace. Everything Argo CD needs after this is declared in git, not in OpenTofu:
# argocd.tf
resource "helm_release" "argocd" {
name = "argocd"
repository = "https://argoproj.github.io/argo-helm"
chart = "argo-cd"
namespace = "argocd"
create_namespace = true
version = "10.4.0"
set {
name = "server.insecure"
value = "true"
}
}create_namespace = true is the piece that saves you the ordering headache, because the namespace must exist before the chart can install into it, and OpenTofu handles that dependency for you. The server.insecure override is for the plain HTTP bootstrap; put a real ingress and TLS in front of it before you expose it anywhere, exactly as you would for any self-hosted service.
Hands-on: the app-of-apps handoff
After tofu apply finishes, Argo CD is running but it owns nothing yet. The handoff is one manifest. Create a bootstrap Application that points at a directory of further Application manifests in your git repository, usually called apps:
# apps/bootstrap.yaml, committed to your cluster-config repo
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: bootstrap
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/you/cluster-config.git
targetRevision: HEAD
path: apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: trueThe resources-finalizer.argocd.argoproj.io finalizer is the Argo CD convention for an app-of-apps parent: when Argo CD deletes the parent, it deletes the children too, instead of leaving orphans behind. From here on, every new service is a manifest dropped into apps/ and pushed to the repo. No kubectl, no tofu, no SSH into the box. That is the whole payoff of the pattern.
Hands-on: run it
The sequence is boring on purpose:
$ tofu init
$ tofu plan -out bootstrap.tfplan
$ tofu apply bootstrap.tfplan
# grab the initial admin password from the cluster
$ kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
# log in with the CLI (or open the web UI)
$ argocd login argocd.example.com --grpc-web
# register the repo, then apply the bootstrap app by hand once
$ argocd app create apps \
--repo https://github.com/you/cluster-config.git \
--path apps \
--dest-server https://kubernetes.default.svc \
--dest-namespace argocd
$ argocd app sync appsAfter that first sync, Argo CD is self-managing: the bootstrap Application, and everything it points at, lives in the repo, and the only human step left is merging pull requests.
The limits that bite
- OpenTofu is a provisioner, not a reconciler. After
tofu applyreturns, nothing watches for drift. That is by design and it is exactly why Argo CD takes over for applications. Expecting OpenTofu to be the GitOps layer is the mistake this whole post is built to prevent. - Single-node k3s is still a single failure domain. The bootstrap pattern does not change the hardware math from the Compose vs Kubernetes post: the cluster earns its keep when a second node exists.
- Provider versioning is on you. The shared providers mean Terraform blog posts mostly work, but the registries move fast and a provider that lags the newest Kubernetes API can fail in confusing ways. Pin versions, and read release notes before upgrading.
- Not every Terraform feature exists in OpenTofu. The CLI is drop-in, but cloud-only enterprise features like Sentinel policies and the hosted run environment are HashiCorp-side, and OpenTofu's answer to those is its own open tooling. Check the What's new page before you promise a Terraform Cloud feature on OpenTofu.
- The first bootstrap Application has a chicken-and-egg moment. Argo CD will not sync a repo it cannot read, so register the repository and apply the app-of-apps once by hand before the automation takes over. That single manual step is the cost of the pattern.
OpenTofu vs Terraform at a glance
| Feature | OpenTofu | Terraform |
|---|---|---|
| License | MPL-2.0 | BUSL-1.1 since August 2023 |
| Governance | Linux Foundation, community-driven | HashiCorp-led |
| CLI1 | tofu, drop-in compatible | terraform |
| Seat licensing | None | Paid tiers for Terraform Cloud |
| Registry2 | search.opentofu.org plus the shared provider registries | registry.terraform.io |
| State encryption | Built in, shipped earlier | Added in 1.9 |
| Notable CLI extras | -exclude, provider-defined functions | Cloud-first features and policies |
License
- OpenTofu
- MPL-2.0
- Terraform
- BUSL-1.1 since August 2023
Governance
- OpenTofu
- Linux Foundation, community-driven
- Terraform
- HashiCorp-led
CLI1
- OpenTofu
- tofu, drop-in compatible
- Terraform
- terraform
Seat licensing
- OpenTofu
- None
- Terraform
- Paid tiers for Terraform Cloud
Registry2
- OpenTofu
- search.opentofu.org plus the shared provider registries
- Terraform
- registry.terraform.io
State encryption
- OpenTofu
- Built in, shipped earlier
- Terraform
- Added in 1.9
Notable CLI extras
- OpenTofu
- -exclude, provider-defined functions
- Terraform
- Cloud-first features and policies
- Same HCL, providers, and state format
- Providers are shared
The honest framing: for this bootstrap workflow, the choice barely matters, because the configuration is identical. The decision is about governance and licensing, not about features. If you want open governance, MPL licensing, and no seat costs, OpenTofu. If your organization already standardized on HashiCorp tooling and Terraform Cloud, Terraform, and the pattern below works unchanged under both.
Which should you pick?
- Choose OpenTofu when: you want the open-licensed fork, you self-host your state and your pipelines, you dislike per-seat pricing, or you want the tofu registry's community k3s modules.
- Choose Terraform when: your team already runs Terraform Cloud, your compliance story depends on HashiCorp enterprise features, or your modules assume a specific Terraform version and you have not tested the migration.
- Use neither when: the cluster is a throwaway test box. A shell script is faster, and IaC is a tool you adopt when the platform becomes the product, not before.
My own rule, after running both: the bootstrap pattern in this post is the default for any k3s box I expect to still exist in a year, and OpenTofu is the binary I reach for, because the fork is the one that stays open. Everything after the Argo CD handoff is git, and git does not care which binary applied the chart.
Official sources
- OpenTofu: https://opentofu.org/
- OpenTofu releases: https://github.com/opentofu/opentofu/releases
- What's new in OpenTofu 1.12: https://opentofu.org/docs/intro/whats-new/
- OpenTofu registry: https://search.opentofu.org/
- Argo CD releases: https://github.com/argoproj/argo-cd/releases
- Argo CD cluster bootstrapping docs: https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-bootstrapping/
- Argo CD Helm chart: https://artifacthub.io/packages/helm/argo/argo-cd
- k3s releases: https://github.com/k3s-io/k3s/releases
- Our Compose vs Kubernetes post: https://systhoughts.com/posts/docker-compose-vs-kubernetes-self-hosted-apps
- Our release-tracking post: https://systhoughts.com/posts/tracking-software-releases-across-forges
Are you bootstrapping your clusters with OpenTofu, Terraform, or plain scripts, and where did you draw the line between IaC and Argo CD? Drop it in the comments.
Until next time, keep your systems thoughtful.

No comments yet