Skip to main content

Phase 11 — Terraform / OpenTofu — Infrastructure as Code

Terraform (and its open-source fork OpenTofu) lets you define your entire infrastructure in .tf files — MAAS machines, networks, Kubernetes namespaces, Harbor projects, Vault policies — and apply it with a single command. Your infra becomes version-controlled, reviewable, and reproducible.

:::info OpenTofu vs Terraform OpenTofu is the community fork of Terraform after HashiCorp changed its license in 2023. It is fully compatible, actively maintained, and the recommended choice for new projects. Commands are identical (tofu instead of terraform). :::


The Core Idea

Without IaC:
→ Click in MAAS UI to configure subnet
→ Run commands to create k8s namespace
→ Set up Harbor project manually
→ Next time: start from memory

With Terraform/OpenTofu:
→ Write infra.tf
→ tofu apply
→ Git commit infra.tf
→ Next time: tofu apply again — identical result

Install OpenTofu

curl -fsSL https://get.opentofu.org/install-opentofu.sh | sh

tofu version

Provider Setup

Terraform/OpenTofu uses providers to talk to different systems. Key ones for this cluster:

# versions.tf
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.27"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.13"
}
vault = {
source = "hashicorp/vault"
version = "~> 3.25"
}
}
}

provider "kubernetes" {
config_path = "~/.kube/config"
}

provider "helm" {
kubernetes {
config_path = "~/.kube/config"
}
}

provider "vault" {
address = "http://10.0.0.200:8200"
token = var.vault_token
}

Example 1 — Kubernetes Namespace + RBAC

# namespaces.tf
resource "kubernetes_namespace" "production" {
metadata {
name = "production"
labels = {
environment = "production"
managed-by = "terraform"
}
}
}

resource "kubernetes_namespace" "staging" {
metadata {
name = "staging"
labels = {
environment = "staging"
managed-by = "terraform"
}
}
}

resource "kubernetes_resource_quota" "production_quota" {
metadata {
name = "production-quota"
namespace = kubernetes_namespace.production.metadata[0].name
}
spec {
hard = {
"requests.cpu" = "16"
"requests.memory" = "32Gi"
"limits.cpu" = "20"
"limits.memory" = "40Gi"
}
}
}

Example 2 — Deploy Apps via Helm

# monitoring.tf
resource "helm_release" "prometheus" {
name = "prometheus"
repository = "https://prometheus-community.github.io/helm-charts"
chart = "kube-prometheus-stack"
namespace = "monitoring"
version = "57.0.0"

create_namespace = true

set {
name = "grafana.adminPassword"
value = var.grafana_password
}

set {
name = "prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName"
value = "longhorn"
}
}

Example 3 — Vault Secrets Engine

# vault.tf
resource "vault_mount" "kv" {
path = "secret"
type = "kv-v2"
}

resource "vault_kv_secret_v2" "db_creds" {
mount = vault_mount.kv.path
name = "myapp/database"

data_json = jsonencode({
username = "appuser"
password = var.db_password
})
}

resource "vault_policy" "myapp" {
name = "myapp"
policy = <<EOT
path "secret/data/myapp/*" {
capabilities = ["read"]
}
EOT
}

Project Structure

terraform/
├── versions.tf # providers + versions
├── variables.tf # input variables
├── outputs.tf # output values
├── namespaces.tf # k8s namespaces + quotas
├── monitoring.tf # prometheus + grafana helm
├── harbor.tf # harbor helm release
├── vault.tf # vault secrets + policies
├── argocd.tf # argocd helm release
└── terraform.tfvars # variable values (gitignored)

State Backend — Store State in Kubernetes

Instead of local state files, store Terraform state in your cluster:

terraform {
backend "kubernetes" {
secret_suffix = "cluster-state"
config_path = "~/.kube/config"
namespace = "terraform"
}
}

Workflow

# Initialize
tofu init

# Preview changes
tofu plan

# Apply
tofu apply

# Destroy (careful!)
tofu destroy

GitOps for IaC

Store your .tf files in GitLab. Add a CI pipeline:

# .gitlab-ci.yml
stages:
- plan
- apply

tofu-plan:
stage: plan
script:
- tofu init
- tofu plan -out=plan.tfplan
artifacts:
paths: [plan.tfplan]

tofu-apply:
stage: apply
script:
- tofu apply plan.tfplan
when: manual # human approval before apply
environment: production

Every infra change goes through Git → review → manual approval → apply.


Done When

✔ OpenTofu installed and initialized
✔ Kubernetes namespaces created via tofu apply
✔ At least one Helm chart managed by Terraform
✔ State stored in Kubernetes backend
✔ Plan + apply running in GitLab CI