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-init to 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_resource to trigger a Podman container.

3. The Condition: The “Collapse”

  • A local-exec do 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:

  1. OpenTofu deletes the Kubernetes resources (Pods/Deployments) first.
  2. The Podman container is stopped and removed.
  3. OpenTofu executes the final destroy command 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.