Keri sisuni

Terraform Lisapraktika

Eeldused: Terraform Alused Labor tehtud, SSH võtmed töötavad, Ubuntu-1 ja Ubuntu-2 saadaval

Platvorm: Terraform + SSH → Ubuntu serverid

Kestus: ~60-90 min


Ülevaade

Kolm progresseerunud harjutust, mis ehitavad põhilabori peale. Igaüks õpetab üht olulist päris-maailma tehnikat:

  1. Multi-Server Deployment - Count vs For_Each
  2. Template-Based Configuration - Templatefile() ja dynamic content
  3. Conditional Deployment - Environment-based logic

Iga harjutus järgib struktuuri: Probleem → Lahendus → Harjutus.


1. Multi-Server Deployment

1.1 Probleem

Põhilabors deploy'sime ühe serveri käsitsi. Aga päris elus on mitu: web servers, API servers, database replicas. Copy-paste on aeglane ja veaaldis - kui muudad ühte, pead muutma kõiki.

Terraform'il on kaks meetodit mitme ressursi loomiseks: count (lihtne numbriline loop) ja for_each (map-based, stabiilsem).

1.2 Lahendus

Count näide:

# main.tf
variable "server_ips" {
  type    = list(string)
  default = ["10.0.208.20", "10.0.208.21"]
}

resource "null_resource" "web_servers" {
  count = length(var.server_ips)

  connection {
    type        = "ssh"
    host        = var.server_ips[count.index]
    user        = "kasutaja"
    private_key = file("~/.ssh/id_ed25519")
  }

  provisioner "remote-exec" {
    inline = [
      "sudo apt update -qq && sudo apt install -y nginx",
      "echo '<h1>Server ${count.index + 1}</h1>' | sudo tee /var/www/html/index.html",
      "sudo systemctl restart nginx",
    ]
  }
}

For_each näide (parem):

variable "servers" {
  type = map(object({
    ip   = string
    port = number
  }))
  default = {
    web1 = { ip = "10.0.208.20", port = 8080 }
    web2 = { ip = "10.0.208.21", port = 8081 }
  }
}

resource "null_resource" "web_servers" {
  for_each = var.servers

  connection {
    type        = "ssh"
    host        = each.value.ip
    user        = "kasutaja"
    private_key = file("~/.ssh/id_ed25519")
  }

  provisioner "remote-exec" {
    inline = [
      "sudo apt update -qq && sudo apt install -y nginx",
      "echo 'server { listen ${each.value.port}; root /var/www/html; }' | sudo tee /etc/nginx/sites-available/${each.key}",
      "sudo ln -sf /etc/nginx/sites-available/${each.key} /etc/nginx/sites-enabled/",
      "echo '<h1>${each.key}</h1>' | sudo tee /var/www/html/index.html",
      "sudo systemctl reload nginx",
    ]
  }
}

Erinevus:

  • count: ressursid on [0], [1] → kui kustutad esimese, nihkub teine
  • for_each: ressursid on ["web1"], ["web2"] → stabiilne

1.3 Harjutus: Deploy Nginx Mõlemale

Nõuded:

  • Deploy nginx Ubuntu-1 (10.0.208.20:8080) ja Ubuntu-2 (10.0.208.21:8081)
  • Kasuta for_each (mitte count)
  • Ubuntu-1 HTML: "Frontend Server"
  • Ubuntu-2 HTML: "Backend Server"
  • Output mõlemad URL'id

Näpunäiteid:

variable "servers" {
  default = {
    frontend = { ip = "10.0.208.20", port = 8080, title = "Frontend Server" }
    backend  = { ip = "10.0.208.21", port = 8081, title = "Backend Server" }
  }
}

Testimine:

terraform apply
# Kontrolli:
# http://10.0.208.20:8080
# http://10.0.208.21:8081

Boonus:

  • Lisa kolmas server (Alma-1: 10.0.208.30:8082)

2. Template-Based Configuration

2.1 Probleem

Inline strings provisioner'ites on loetamatud ja raskesti muudetavad. Päris nginx config on 50+ rida. Kuidas hallata seda elegantelt?

Terraform templatefile() funktsioon võimaldab eraldi template faile kasutada ja dynamic väärtused sisse süstida.

2.2 Lahendus

Loo kaust ja template:

terraform-advanced/
├── main.tf
├── templates/
│   ├── nginx.conf.tpl
│   └── index.html.tpl
└── files/
    └── (generated configs)

templates/nginx.conf.tpl:

# ${server_name} - Generated by Terraform

server {
    listen ${port};
    server_name _;

    root /var/www/${server_name};
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    %{ if enable_monitoring ~}
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
    %{ endif ~}

    add_header X-Server-Name "${server_name}";
}

templates/index.html.tpl:

<!DOCTYPE html>
<html>
<head>
    <title>${server_name}</title>
    <style>
        body { 
            font-family: Arial; 
            background: ${bg_color}; 
            color: white; 
            padding: 50px; 
            text-align: center; 
        }
    </style>
</head>
<body>
    <h1>🚀 ${server_name}</h1>
    <p>Role: <strong>${role}</strong></p>
    <p>Port: <strong>${port}</strong></p>
    <hr>
    <small>Deployed: ${timestamp}</small>
</body>
</html>

main.tf:

variable "servers" {
  type = map(object({
    ip                = string
    port              = number
    role              = string
    bg_color          = string
    enable_monitoring = bool
  }))
  default = {
    web = {
      ip                = "10.0.208.20"
      port              = 8080
      role              = "frontend"
      bg_color          = "#0066cc"
      enable_monitoring = true
    }
    api = {
      ip                = "10.0.208.21"
      port              = 8081
      role              = "api"
      bg_color          = "#cc6600"
      enable_monitoring = true
    }
  }
}

# Generate config files locally first
resource "local_file" "nginx_configs" {
  for_each = var.servers

  filename = "${path.module}/files/${each.key}-nginx.conf"
  content = templatefile("${path.module}/templates/nginx.conf.tpl", {
    server_name       = each.key
    port              = each.value.port
    enable_monitoring = each.value.enable_monitoring
  })
}

resource "local_file" "html_files" {
  for_each = var.servers

  filename = "${path.module}/files/${each.key}-index.html"
  content = templatefile("${path.module}/templates/index.html.tpl", {
    server_name = each.key
    role        = each.value.role
    port        = each.value.port
    bg_color    = each.value.bg_color
    timestamp   = timestamp()
  })
}

# Deploy to servers
resource "null_resource" "deploy" {
  for_each = var.servers

  depends_on = [
    local_file.nginx_configs,
    local_file.html_files
  ]

  connection {
    type        = "ssh"
    host        = each.value.ip
    user        = "kasutaja"
    private_key = file("~/.ssh/id_ed25519")
  }

  provisioner "file" {
    source      = "${path.module}/files/${each.key}-nginx.conf"
    destination = "/tmp/nginx.conf"
  }

  provisioner "file" {
    source      = "${path.module}/files/${each.key}-index.html"
    destination = "/tmp/index.html"
  }

  provisioner "remote-exec" {
    inline = [
      "sudo apt update -qq && sudo apt install -y nginx",
      "sudo mkdir -p /var/www/${each.key}",
      "sudo mv /tmp/index.html /var/www/${each.key}/",
      "sudo mv /tmp/nginx.conf /etc/nginx/sites-available/${each.key}",
      "sudo ln -sf /etc/nginx/sites-available/${each.key} /etc/nginx/sites-enabled/",
      "sudo rm -f /etc/nginx/sites-enabled/default",
      "sudo nginx -t && sudo systemctl reload nginx",
    ]
  }
}

2.3 Harjutus: Custom Templates

Nõuded:

  • Loo template nginx.conf ja index.html
  • Ubuntu-1: sinine taust (#0066cc), "Web Server"
  • Ubuntu-2: roheline taust (#00cc66), "API Server"
  • HTML näitab deployment aega (timestamp())
  • Nginx config sisaldab /health endpoint

Näpunäiteid:

  • Template syntax: ${variable} asendab, %{ if } on conditional
  • templatefile(path, vars) genereerib sisu
  • Esmalt loo local_file, siis upload provisioner "file"

Testimine:

curl http://10.0.208.20:8080/health
# Peaks tagastama: healthy

Boonus:

  • Lisa /metrics endpoint, mis näitab serveri info't
  • Conditional SSL block template'is (kuigi cert'e pole)

3. Conditional Deployment

3.1 Probleem

Dev ja prod keskkonnad vajavad erinevat konfiguratsiooni. Dev'is debug mode, prod'is SSL ja monitoring. Kuidas hallata ühes Terraform projektis?

Terraform toetab conditionals: count = condition ? 1 : 0 ja dynamic blocks.

3.2 Lahendus

variable "environment" {
  type    = string
  default = "dev"

  validation {
    condition     = contains(["dev", "prod"], var.environment)
    error_message = "Must be dev or prod"
  }
}

variable "servers" {
  type = map(object({
    ip   = string
    port = number
  }))
}

locals {
  # Environment-specific configs
  config = {
    dev = {
      replicas          = 1
      enable_ssl        = false
      enable_monitoring = false
      log_level         = "debug"
    }
    prod = {
      replicas          = 2
      enable_ssl        = true
      enable_monitoring = true
      log_level         = "error"
    }
  }

  current_config = local.config[var.environment]
}

resource "null_resource" "deploy" {
  for_each = var.servers

  connection {
    type        = "ssh"
    host        = each.value.ip
    user        = "kasutaja"
    private_key = file("~/.ssh/id_ed25519")
  }

  provisioner "remote-exec" {
    inline = concat(
      [
        "sudo apt update -qq && sudo apt install -y nginx",
        "echo 'Environment: ${var.environment}' | sudo tee /var/www/html/env.txt",
      ],

      # Conditional: only in prod
      local.current_config.enable_monitoring ? [
        "sudo apt install -y prometheus-node-exporter",
        "sudo systemctl enable prometheus-node-exporter",
      ] : [],

      [
        "echo 'log_level: ${local.current_config.log_level}' | sudo tee -a /var/www/html/env.txt",
        "sudo systemctl reload nginx",
      ]
    )
  }
}

# Conditional resource: monitoring only in prod
resource "null_resource" "monitoring" {
  count = var.environment == "prod" ? 1 : 0

  connection {
    type        = "ssh"
    host        = var.servers["web"].ip
    user        = "kasutaja"
    private_key = file("~/.ssh/id_ed25519")
  }

  provisioner "remote-exec" {
    inline = [
      "echo 'Setting up production monitoring...'",
      "# Install monitoring tools here",
    ]
  }
}

Kasutamine:

# Dev deployment
terraform apply -var="environment=dev"

# Prod deployment
terraform apply -var="environment=prod"

3.3 Harjutus: Dev vs Prod

Nõuded:

  • Loo terraform.tfvars.dev ja terraform.tfvars.prod
  • Dev: ainult Ubuntu-1, port 8080, debug mode
  • Prod: mõlemad Ubuntu'd, port 443, monitoring enabled
  • Kasuta count või for_each conditional'iga
  • HTML näitab environment'i

Näpunäiteid:

# terraform.tfvars.dev
environment = "dev"
servers = {
  web = { ip = "10.0.208.20", port = 8080 }
}

# terraform.tfvars.prod
environment = "prod"
servers = {
  web1 = { ip = "10.0.208.20", port = 443 }
  web2 = { ip = "10.0.208.21", port = 443 }
}

Testimine:

# Dev
terraform apply -var-file="terraform.tfvars.dev"

# Prod
terraform apply -var-file="terraform.tfvars.prod"

Boonus:

  • Prod'is enable firewall (ufw) automaatselt
  • Dev'is install development tools (curl, vim, htop)

Kasulikud Ressursid

Dokumentatsioon:

Tööriistad:

  • Terraform Console - testi expressions: terraform console
  • Terraform fmt - vorminda kood: terraform fmt
  • Terraform validate - kontrolli syntax: terraform validate

Näited:

  • HashiCorp Learn: https://learn.hashicorp.com/terraform