Terraform Docker Infrastructure: จัดการ Container ด้วย Docker Provider

การจัดการ Docker infrastructure ด้วย Terraform ช่วยให้ประกาศ container, network, volume และ image แบบ declarative ในโค้ด แทนที่จะเขียน docker-compose หรือเรียก CLI ทีละคำสั่ง เหมาะสำหรับทีมที่ต้องการรวม workflow ของ infrastructure ทั้งหมด (VM, network, DNS, container) ให้อยู่ในเครื่องมือเดียว พร้อมมี state file และ plan/apply workflow เหมือนกับการจัดการ cloud ทั่วไป

บทความนี้อธิบายการใช้ kreuzwerker/docker provider ของ Terraform ตั้งแต่การเชื่อมต่อ Docker daemon, การสร้าง container, network, volume, การ pull image, การใช้ร่วมกับ Docker Swarm และข้อจำกัดที่ควรรู้ก่อนใช้ในระดับ production

การตั้งค่า Docker Provider

Terraform ใช้ provider kreuzwerker/docker ซึ่งเป็น provider community ที่ครอบคลุม Docker API ได้ครบถ้วน

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

ถ้า Docker daemon อยู่คนละเครื่อง สามารถเชื่อมต่อผ่าน TCP พร้อม TLS

provider "docker" {
  host      = "tcp://docker.example.com:2376"
  ca_material   = file("~/.docker/ca.pem")
  cert_material = file("~/.docker/cert.pem")
  key_material  = file("~/.docker/key.pem")
}

การสร้าง Container

resource docker_container สร้างและ manage lifecycle ของ container ให้อัตโนมัติ เมื่อ apply ใหม่ container จะถูกรีสตาร์ทเมื่อมีการเปลี่ยนแปลง

resource "docker_image" "nginx" {
  name         = "nginx:1.25-alpine"
  keep_locally = false
}

resource "docker_container" "web" {
  name  = "web"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = 8080
  }

  restart = "unless-stopped"

  env = [
    "APP_ENV=production"
  ]
}

attribute image_id จะทำให้ Terraform รู้ hash ของ image จริง ๆ ไม่ใช่แค่ tag ที่อาจเปลี่ยนเนื้อหาได้ เมื่อ registry push image ใหม่ใน tag เดิม

Network

การสร้าง Docker network แยกต่างหาก ช่วยให้ container คุยกันผ่านชื่อ service ได้ โดยไม่ต้องผูกกับ IP

resource "docker_network" "app_net" {
  name   = "app-network"
  driver = "bridge"
  ipam_config {
    subnet = "172.28.0.0/16"
  }
}

resource "docker_container" "web" {
  name  = "web"
  image = docker_image.nginx.image_id

  networks_advanced {
    name = docker_network.app_net.name
  }
}

ใช้ driver bridge สำหรับ host เดียว, overlay สำหรับ Docker Swarm หลาย node, หรือ macvlan สำหรับให้ container มี IP บน LAN เดียวกับ host

Volume และ Data Persistence

แยก data ออกจาก container lifecycle ด้วย named volume เพื่อให้ข้อมูลไม่หายเมื่อ container ถูกลบ

resource "docker_volume" "db_data" {
  name = "db-data"
}

resource "docker_container" "db" {
  name  = "postgres"
  image = "postgres:15"

  env = [
    "POSTGRES_PASSWORD=${var.db_password}"
  ]

  volumes {
    container_path = "/var/lib/postgresql/data"
    volume_name    = docker_volume.db_data.name
  }
}

สำหรับ bind mount ชี้ path บน host ตรง ๆ ใช้ host_path แทน volume_name แต่ควรใช้ named volume เป็นหลักเพื่อให้ย้าย host ได้ง่าย

Secret และ Environment

ค่าที่เป็นความลับ เช่น password, API token ไม่ควรเขียนตรง ๆ ใน tf file — ให้ส่งผ่านตัวแปรที่ Terraform Cloud, HashiCorp Vault หรือ environment variable

variable "db_password" {
  type      = string
  sensitive = true
}

resource "docker_container" "db" {
  env = [
    "POSTGRES_PASSWORD=${var.db_password}"
  ]
}

flag sensitive = true ทำให้ Terraform log และ state file masking ค่านี้เวลาแสดงผล ลดความเสี่ยงที่ password จะโผล่มาใน CI log

Multi-Container Stack

การ deploy stack web + database + cache ในครั้งเดียว เขียนเป็น resource หลายตัวแล้วใช้ dependency อัตโนมัติ

resource "docker_network" "stack" {
  name = "stack-net"
}

resource "docker_container" "redis" {
  name  = "cache"
  image = "redis:7-alpine"
  networks_advanced { name = docker_network.stack.name }
}

resource "docker_container" "api" {
  name  = "api"
  image = "myapp:latest"

  env = [
    "REDIS_HOST=cache",
    "DB_HOST=db"
  ]

  networks_advanced { name = docker_network.stack.name }
  depends_on = [docker_container.redis, docker_container.db]
}

Terraform จะสร้าง network ก่อน, จากนั้น redis/db พร้อมกัน, แล้วค่อย api เพราะ depends_on สามารถชี้ชัด dependency ได้

Docker Swarm Mode

สำหรับ cluster Swarm ใช้ resource docker_service แทน docker_container รองรับ replica, rolling update, และ placement constraint

resource "docker_service" "web" {
  name = "web"

  task_spec {
    container_spec {
      image = "nginx:1.25-alpine"
    }
    placement {
      constraints = ["node.role == worker"]
    }
  }

  mode {
    replicated {
      replicas = 3
    }
  }

  endpoint_spec {
    ports {
      target_port    = 80
      published_port = 80
    }
  }
}

Swarm จัดการ scheduling, health check, rolling update, rollback ให้อัตโนมัติ เหมาะกับ environment เล็กถึงกลางที่ไม่ต้องถึงขั้นใช้ Kubernetes

การใช้งานร่วมกับ VM Provisioning

Use case ที่พบบ่อยคือ Terraform สร้าง VM ก่อน, ติดตั้ง Docker Engine ผ่าน cloud-init, แล้วใช้ docker provider ชี้ไปยัง VM นั้นเพื่อ deploy container

resource "digitalocean_droplet" "app" {
  name   = "app-host"
  region = "sgp1"
  size   = "s-2vcpu-4gb"
  image  = "ubuntu-22-04-x64"

  user_data = <<-EOT
    #!/bin/bash
    curl -fsSL https://get.docker.com | sh
    usermod -aG docker ubuntu
  EOT
}

provider "docker" {
  alias = "app_host"
  host  = "ssh://root@${digitalocean_droplet.app.ipv4_address}"
}

resource "docker_container" "web" {
  provider = docker.app_host
  name     = "web"
  image    = "nginx:alpine"
}

pattern นี้ใช้ SSH tunneling ของ docker-cli ติดต่อ Docker daemon บน remote host ทำให้ไม่ต้องเปิด Docker TCP port ออกสู่อินเทอร์เน็ต

ข้อจำกัดที่ควรรู้

  • provider ไม่รองรับ docker compose file ตรง ๆ ต้องแปลง compose → HCL เอง
  • ไม่มี health check orchestration ระดับ stack เท่า Kubernetes — ถ้าต้องการ self-healing ซับซ้อนให้ใช้ K8s
  • การ scale container ต้องใช้ count หรือ for_each ซึ่งไม่สะดวกเท่า Swarm replica
  • เมื่อเปลี่ยน config ของ container ส่วนใหญ่ Terraform จะ destroy + recreate ไม่ใช่ update — ระวัง downtime
  • Log driver, resource limit, security options ต้องกำหนดผ่าน attribute ที่เฉพาะเจาะจง ไม่มี default sensible

ข้อควรระวัง

  • อย่าเชื่อมต่อ Docker daemon ผ่าน TCP โดยไม่มี TLS — เท่ากับเปิด root shell ให้ใครก็ได้
  • State file จะเก็บ env variable ที่ไม่ได้ mark sensitive เป็น plaintext ตรวจสิทธิ์ backend ให้เข้มงวด
  • ใช้ keep_locally = false ใน docker_image เพื่อไม่ให้ image ค้างบน host หลัง destroy
  • สำหรับ production หลาย node ให้พิจารณา Kubernetes หรือ Nomad แทน Docker provider ล้วน
  • Backup volume ด้วย tool เช่น restic หรือ Velero — Terraform ไม่ได้ backup data ให้

สรุป

Docker provider ของ Terraform เหมาะสำหรับการจัดการ container บน host เดียวหรือ Swarm cluster เล็ก ๆ ให้รวม workflow กับ infrastructure อื่น ๆ ไว้ในที่เดียวได้ เขียน resource container, network, volume, image, service เป็น HCL แล้วปล่อยให้ plan/apply จัดการ lifecycle

สำหรับ workload ที่ต้องการ orchestration ซับซ้อน เช่น auto-scaling, self-healing, multi-tenancy แนะนำใช้ Kubernetes ร่วมกับ Terraform (provision cluster) + Helm/Kustomize (deploy app) จะยืดหยุ่นกว่า — ส่วน Docker provider เหมาะกับ use case ที่ต้องการ container เพียงเพื่อ run service แบบ straightforward