A Terraform / OpenTofu provider for Cozystack. It manages Cozystack applications through the aggregated Kubernetes API (apps.cozystack.io), so a Cozystack platform can be driven as Infrastructure as Code with the same kubeconfig you already use.
Every kind is served by the same aggregated API, so the provider is built to grow one kind at a time. Adding a kind is a typed model, a schema, and a one-line resource descriptor — the create/read/update/delete/import/wait logic is shared.
Every kind served by the aggregated apps.cozystack.io API is now covered.
Beyond the namespaced tenant apps, the provider also manages the cluster-scoped platform resources in the cozystack.io group — useful for platform-as-code:
| Kind | Resource | Data source |
|---|---|---|
| Package — install a package variant | cozystack_package |
cozystack_package |
| PackageSource — where packages come from | cozystack_package_source |
cozystack_package_source |
| ApplicationDefinition — register an app kind | cozystack_application_definition |
cozystack_application_definition |
| SchedulingClass — named placement policy | cozystack_scheduling_class |
cozystack_scheduling_class |
These are cluster-scoped (imported by name, no namespace). cozystack_package is fully typed (variant, ignore_dependencies, per-component overrides). The other three are platform-definition documents whose deeply-nested, version-coupled specs are surfaced as a single normalized-JSON spec attribute (write with jsonencode, read back with jsondecode) rather than brittle per-field modeling.
The same JSON-spec passthrough also covers the backup framework and dashboard panels:
| Kind | Resource | Data source |
|---|---|---|
| Plan (typed) | cozystack_backup_plan |
cozystack_backup_plan |
| RestoreJob (typed) | cozystack_restore_job |
cozystack_restore_job |
| BackupClass | cozystack_backup_class |
cozystack_backup_class |
| Backup | cozystack_backup |
cozystack_backup |
| BackupJob | cozystack_backup_job |
cozystack_backup_job |
| MarketplacePanel | cozystack_marketplace_panel |
cozystack_marketplace_panel |
backup_plan (schedule a backup) and restore_job (restore a backup) are user-authored, so they are fully typed (application_ref, backup_class_name, schedule / backup_name, target_application_ref, options). The remaining backups kinds are records or driver config and stay JSON-spec.
These do not follow the spec pattern, so they have purpose-built models:
| Kind | Resource | Shape |
|---|---|---|
| TenantSecret | cozystack_tenant_secret |
Secret-shaped: type + data (plaintext in, stored base64) |
| TenantModule | cozystack_tenant_module |
namespaced marker (existence enables the module) |
| TenantNamespace | cozystack_tenant_namespace |
cluster-scoped marker |
Deliberately not exposed: the strategy.backups.cozystack.io per-engine backup strategies and the dashboard.cozystack.io UI-customization CRDs (Sidebar, Navigation, Factory, …) — platform internals shipped and reconciled by Cozystack itself, with no Infrastructure-as-Code use case.
Authentication mirrors the official kubernetes provider: config_path/config_context, host/token, cluster_ca_certificate, client_certificate/client_key, an in_cluster toggle, and an exec {} credential-plugin block for OIDC login helpers (kubectl oidc-login). OIDC via a kubeconfig already works through config_path with no extra configuration.
The aggregated API is write-oriented: it takes a spec and returns a thin status. The connection details you actually want to reference — endpoints, credentials, a child cluster's kubeconfig, a VM's IP — are materialised by the underlying charts as Secrets, Services, and KubeVirt status. The provider reads those and exposes them as computed (sensitive where appropriate) attributes:
cozystack_kubernetes.<x>.kubeconfig— admin kubeconfig of the provisioned cluster, for chaining thekubernetes/helmproviders into it.cozystack_postgres.<x>.endpoints—{ host, read_host, port }from the CNPGrw/roServices.cozystack_bucket.<x>.credentials["<user>"]—{ endpoint, bucket_name, region, access_key, secret_key }per user.cozystack_vminstance.<x>.ip_address/.ip_addresses— guest addresses from the backing VirtualMachineInstance.
These are populated asynchronously, after the application is ready. Set wait_for_ready = true on the resource you consume so they are available on first apply rather than on a later refresh.
To avoid persisting secrets in state, the provider offers modern Terraform secret handling:
- Ephemeral resources (
ephemeral "cozystack_kubernetes",ephemeral "cozystack_tenant_secret") fetch a cluster kubeconfig or a tenant secret at apply time, usable to configure downstream providers, without ever writing the value to state. - Write-only inputs —
cozystack_tenant_secretacceptsdata_wo(write-only, sent on apply but never stored; bumpdata_wo_versionto push changes) alongside the state-persisteddata.
resource "cozystack_kubernetes" "app" {
name = "app"
namespace = "tenant-root"
node_groups = { md0 = { min_replicas = 1, max_replicas = 3 } }
wait_for_ready = true
}
provider "kubernetes" {
alias = "app"
# chain straight into the cluster this provider just created
# (parse cozystack_kubernetes.app.kubeconfig with the helm/kubernetes provider's
# config_path written from it, or your preferred kubeconfig wiring)
}The names of these backing Secrets/Services are chart conventions (postgres-<name>-rw, bucket-<name>-<user>, kubernetes-<name>-admin-kubeconfig), pinned to the supported Cozystack version — not part of the stable aggregated-API contract.
- Terraform >= 1.0 or OpenTofu >= 1.6
- A running Cozystack cluster and a kubeconfig (or bearer token) that can reach its API
terraform {
required_providers {
cozystack = {
source = "cozystack/cozystack"
}
}
}
provider "cozystack" {
config_path = "~/.kube/config"
config_context = "my-cozystack-cluster"
}
resource "cozystack_tenant" "team_a" {
name = "team-a"
namespace = "tenant-root"
monitoring = true
ingress = true
}
resource "cozystack_redis" "cache" {
name = "cache"
namespace = cozystack_tenant.team_a.status_namespace
replicas = 2
version = "v8"
}Resources are namespaced by tenant, so other applications reference a tenant's status_namespace to deploy inside it.
Connection settings mirror the official kubernetes provider, so existing kubeconfig and environment conventions transfer directly. Every provider attribute is optional and falls back to a KUBE_* environment variable: config_path (KUBE_CONFIG_PATH, then KUBECONFIG), config_context (KUBE_CTX), host (KUBE_HOST), token (KUBE_TOKEN), cluster_ca_certificate (KUBE_CLUSTER_CA_CERT_DATA), and insecure (KUBE_INSECURE). Set in_cluster = true to use a pod's service account instead of a kubeconfig.
Until a release is published to the registry, build the provider and point Terraform/OpenTofu at it with a dev override:
make install # go install into $GOBIN# ~/.terraformrc (or a file referenced by TF_CLI_CONFIG_FILE)
provider_installation {
dev_overrides {
"registry.terraform.io/cozystack/cozystack" = "/path/to/your/gobin"
}
direct {}
}make build # build the provider binary
make test # unit tests with the race detector
make lint # golangci-lint
make docs # regenerate docs/ with tfplugindocsAcceptance tests create and destroy real applications and need a reachable Cozystack cluster:
KUBECONFIG=/path/to/kubeconfig KUBE_CTX=my-context make testaccThe acceptance harness drives the Terraform CLI; the testacc target points TF_ACC_TERRAFORM_PATH at tofu automatically so it runs against OpenTofu when Terraform is not installed.
See CONTRIBUTING.md for the full workflow and SECURITY.md for vulnerability reporting and credential-handling guidance.