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-initto 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_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:
- Terraform deletes the Kubernetes resources (Pods/Deployments) first.
- The Podman container is stopped and removed.
- Terraform 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
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.