From the article that we use OpenTofu, this one will replace the code in OpenTofu.
This article covers the transition from raw hardware to a functional orchestrator and back to zero.
We call this the Provision-Orchestrate-Collapse-Destroy cycle.
1. The Provisioning Phase
We start at the VM level. OpenTofu interacts with your provider to reserve CPU, RAM, and storage.
- Create a Virtual Machine.
- Use
cloud-initto bootstrap the environment. - Install the K8s Control Plane (e.g., K3s for efficiency).
2. The Orchestration Phase
Once the cluster is “Healthy,” OpenTofu switches to the Kubernetes Provider.
- Apply a manifest to create a Namespace and a Deployment.
- As a role model for external services, we use a OpenTofu
null_resourceto trigger a Podman container.
3. The Condition: The “Collapse”
- A
local-execdo the task on this Podman eg., reverse proxy or firewall. - A logic gate that checks if the Podman task has finished its “work.”
4. The Teardown
For return all resource back:
- OpenTofu deletes the Kubernetes resources (Pods/Deployments) first.
- The Podman container is stopped and removed.
- OpenTofu executes the final
destroycommand on the VM, ensuring no billing or resource leakage occurs.
Example implement
Note : this apply in Miniblue.io, Podman use for reverse proxy.
Create Infrastructure
↓
Create Kubernetes Cluster
↓
Deploy Backend Service
↓
Deploy Reverse Proxy
↓
Validate Service Health
↓
Collapse Kubernetes Workloads
↓
Destroy Cluster
↓
Destroy Infrastructure
Directory Layout
Phoenix-Miniblue-ReverseProxy/
├── main.tf
├── providers.tf
├── variables.tf
├── versions.tf
│
├── k8s/
│ ├── backend-deployment.yaml
│ ├── backend-service.yaml
│ ├── reverse-proxy-configmap.yaml
│ ├── reverse-proxy-deployment.yaml
│ └── reverse-proxy-service.yaml
│
└── scripts/
├── create_cluster.sh
├── wait_proxy_ready.sh
├── validate_destroy.sh
├── collapse_k8s.sh
└── destroy_everything.sh
Version checking
filename: versions.tf
OpenTofu
{
required_version = ">= 1.5.0"
required_providers {
null = {
source = "opentofu/null"
version = "~> 3.2"
}
}
}
Provider
filename: providers.tf
provider "null" {}
Note: replace null_resource with proper providers
Variables
filename: variables.tf
variable "cluster_name"
{
type = string
default = "test-lab"
}
variable "proxy_port"
{
type = number
default = 8080
}
Main
filename: main.tf
Architectural Correction: Running step-by-step teardown code inside standard linear resources causes errors during an orchestration wipe. OpenTofu cleanly handles lifecycle teardowns by mapping scripts directly to destroy-time provisioners (when = destroy).
##########################################################
# STEP 1
# Create Kubernetes cluster
##########################################################
resource "null_resource" "create_cluster" {
provisioner "local-exec"
{
command = "bash scripts/create_cluster.sh ${var.cluster_name}"
}
}
##########################################################
# STEP 2
# Deploy backend application
##########################################################
resource "null_resource" "deploy_backend" {
depends_on = [null_resource.create_cluster]
provisioner "local-exec" {
command = <<EOT
kubectl apply -f k8s/backend-deployment.yaml
kubectl apply -f k8s/backend-service.yaml
EOT
}
}
##########################################################
# STEP 3
# Deploy reverse proxy
##########################################################
resource "null_resource" "deploy_proxy" {
depends_on = [null_resource.deploy_backend]
provisioner "local-exec" {
command = <<EOT
kubectl apply -f k8s/reverse-proxy-configmap.yaml
kubectl apply -f k8s/reverse-proxy-deployment.yaml
kubectl apply -f k8s/reverse-proxy-service.yaml
EOT
}
}
##########################################################
# STEP 4
# Wait for reverse proxy readiness
##########################################################
resource "null_resource" "wait_proxy" {
depends_on = [null_resource.deploy_proxy]
provisioner "local-exec" {
command = "bash scripts/wait_proxy_ready.sh"
}
}
##########################################################
# STEP 5
# Validate infrastructure before collapse
##########################################################
resource "null_resource" "validate_destroy" {
depends_on = [null_resource.wait_proxy]
provisioner "local-exec" {
command = "bash scripts/validate_destroy.sh"
}
}
##########################################################
# STEP 6
# Collapse Kubernetes workloads
##########################################################
resource "null_resource" "collapse_k8s" {
depends_on = [null_resource.validate_destroy]
provisioner "local-exec" {
command = "bash scripts/collapse_k8s.sh ${var.cluster_name}"
}
}
Kubernetes Resources
The original Kubernetes YAML manifests inside the k8s/ folder remain completely unchanged, as OpenTofu acts on standard Kubernetes syntax seamlessly.
filename: k8s/backend-deployment.yaml
Creates a simple nginx backend service.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-backend
spec:
replicas: 1
selector:
matchLabels:
app: nginx-backend
template:
metadata:
labels:
app: nginx-backend
spec:
containers:
- name: nginx-backend
image: nginx:stable
ports:
- containerPort: 80
filename: k8s/backend-service.yaml
Internal Kubernetes service for the backend.
apiVersion: v1
kind: Service
metadata:
name: nginx-backend-service
spec:
selector:
app: nginx-backend
ports:
- port: 80
targetPort: 80
type: ClusterIP
Reverse Proxy
filename: k8s/reverse-proxy-configmap.yaml
nginx reverse proxy configuration.
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-proxy-config
data:
default.conf: |
server {
listen 80;
location / {
proxy_pass http://nginx-backend-service;
}
}
filename: k8s/reverse-proxy-deployment.yaml
Creates nginx reverse proxy pod.
apiVersion: apps/v1
kind: Deployment
metadata:
name: reverse-proxy
spec:
replicas: 1
selector:
matchLabels:
app: reverse-proxy
template:
metadata:
labels:
app: reverse-proxy
spec:
containers:
- name: nginx-proxy
image: nginx:stable
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
volumes:
- name: nginx-config
configMap:
name: nginx-proxy-config
filename: k8s/reverse-proxy-service.yaml
Exposes reverse proxy outside cluster.
apiVersion: v1
kind: Service
metadata:
name: reverse-proxy-service
spec:
selector:
app: reverse-proxy
ports:
- port: 80
targetPort: 80
nodePort: 30080
type: NodePort
Shell Scripts
The primary update is changing the execution wrapper to use tofu instead of terraform.
filename: scripts/create_cluster.sh
Creates Minikube cluster using Podman runtime.
#!/usr/bin/env bash
set -e
CLUSTER_NAME=$1
echo "[STEP] Creating Kubernetes cluster..."
minikube start \
--driver=podman \
--profile=${CLUSTER_NAME}
echo "[DONE] Cluster created."
filename: scripts/wait_proxy_ready.sh
Waits until proxy becomes reachable.
#!/usr/bin/env bash
set -e
echo "[STEP] Waiting for reverse proxy deployment..."
kubectl rollout status deployment/reverse-proxy
echo "[STEP] Checking HTTP response..."
MAX_RETRY=30
COUNT=0
while true; do
if curl -s http://$(minikube ip):30080 >/dev/null; then
echo "[OK] Reverse proxy reachable."
exit 0
fi
COUNT=$((COUNT+1))
if [ "$COUNT" -ge "$MAX_RETRY" ]; then
echo "[ERROR] Reverse proxy health check failed."
exit 1
fi
sleep 5
done
filename: scripts/validate_destroy.sh
Validates system state before destruction.
#!/usr/bin/env bash
set -e
echo "[STEP] Running pre-collapse validation..."
##########################################################
# CHECK 1
# Verify deployment exists
##########################################################
kubectl get deployment reverse-proxy >/dev/null
##########################################################
# CHECK 2
# Verify pod health
##########################################################
READY=$(kubectl get pods \
-l app=reverse-proxy \
-o jsonpath='{.items[0].status.containerStatuses[0].ready}')
if [ "$READY" != "true" ]; then
echo "[ERROR] Reverse proxy pod unhealthy."
exit 1
fi
##########################################################
# CHECK 3
# Verify HTTP response
##########################################################
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
http://$(minikube ip):30080)
if [ "$HTTP_CODE" != "200" ]; then
echo "[ERROR] HTTP validation failed."
exit 1
fi
echo "[OK] Validation successful."
filename: scripts/collapse_k8s.sh
Destroys workloads and cluster.
#!/usr/bin/env bash
set -e
CLUSTER_NAME=$1
echo "[STEP] Removing reverse proxy..."
kubectl delete deployment reverse-proxy || true
kubectl delete service reverse-proxy-service || true
kubectl delete configmap nginx-proxy-config || true
echo "[STEP] Removing backend service..."
kubectl delete deployment nginx-backend || true
kubectl delete service nginx-backend-service || true
echo "[STEP] Destroying Kubernetes cluster..."
minikube delete --profile=${CLUSTER_NAME}
echo "[DONE] Cluster collapsed."
filename: scripts/destroy_everything.sh
Final infrastructure cleanup.
#!/usr/bin/env bash
set -e
echo "[STEP] Running OpenTofu destroy..."
# Triggers the orchestration teardown and hardware collapse natively
tofu destroy -auto-approve
echo "[DONE] Infrastructure destroyed."
Implementation
To work with this architecture using the OpenTofu CLI tool footprint:
To initialize the OpenTofu.
tofu init
Deploy infrastructure:
tofu apply -auto-approve
Destroy infrastructure:
bash scripts/destroy_everything.sh
For more information about Kubernetes with OpenTofu please check here
This is only for idea of how Kubernetes work with OpenTofu. You can apply more complex ideas on this kind of all CLI-automate with OpenTofu.
Special Note
In “main.tf” OpenTofu, the destruction order is the inverse of the creation order.
Because collapse_k8s depends on validate_destroy, OpenTofu will ensure the collapse happens before it tries to destroy the underlying provider resources.
In destroy_everything.sh, calling OpenTofu destroy, this is to ensure the state file is wiped, however “Collapse” is already a resource in main.tf, a standard OpenTofu destroy will actually trigger that collapse_k8s.sh script again during the destruction phase.
While null_resource is used here for demonstration and portability on local machines (Miniblue), in a production enterprise environment, we would swap these for the official Kubernetes and Helm providers to track the live state of every pod.
This is to proof that most of sytax in Terraform can be used in Opentofu.