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. Terraform 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,” Terraform 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 Terraform 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. Terraform deletes the Kubernetes resources (Pods/Deployments) first.
  2. The Podman container is stopped and removed.
  3. Terraform 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

	terraform 
	{
		required_version = ">= 1.5.0"

		required_providers {
			null = {
					source  = "hashicorp/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

This part quite long as this the main operation and control the life cycle.

##########################################################
# 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

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

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 Terraform destroy..."

terraform destroy -auto-approve

echo "[DONE] Infrastructure destroyed."

Implementation

To initialize the terraform.

Initialize Terraform:

Deploy infrastructure:

terraform apply -auto-approve

Destroy infrastructure:

bash scripts/destroy_everything.sh

For more information about Kubernetes with Terraform please check here

This is only for idea of how Kubernetes work with IaC. You can apply more complex ideas on this kind of all CLI-automate with Terraform.

Special Note

In “main.tf” Terraform, the destruction order is the inverse of the creation order.

Because collapse_k8s depends on validate_destroy, Terraform will ensure the collapse happens before it tries to destroy the underlying provider resources.

In destroy_everything.sh, calling terraform destroy, this is to ensure the state file is wiped, however “Collapse” is already a resource in main.tf, a standard terraform 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.