Terraform es una herramienta de infraestructura como código: describes recursos como servidores, redes, registros DNS o contenedores en archivos de configuración, y Terraform calcula qué debe crear, cambiar o eliminar para que la realidad coincida con ellos. En este tutorial instalarás Terraform en Ubuntu 24.04 desde el repositorio oficial de HashiCorp y aprenderás el flujo básico (init, plan, apply, destroy) gestionando un contenedor de Nginx con el proveedor de Docker. Todo se ejecuta en un único servidor, así que puedes aprender los conceptos sin cuenta en ninguna nube y sin coste.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM.
  • Un usuario no root con privilegios sudo.
  • Docker Engine instalado desde el repositorio oficial de Docker, con tu usuario en el grupo docker para poder ejecutar docker sin sudo. Cierra sesión y vuelve a entrar después de añadir el grupo.

Confirma que Docker funciona con tu usuario antes de continuar:

docker run --rm hello-world

La salida debe incluir Hello from Docker!.

Paso 1: Instalar Terraform desde el repositorio de HashiCorp

HashiCorp publica Terraform en su propio repositorio APT, que es la forma soportada de instalarlo en Ubuntu y lo mantiene actualizado con apt upgrade.

Instala las herramientas necesarias para añadir el repositorio:

sudo apt update
sudo apt install gnupg curl lsb-release

Descarga la clave de firma de HashiCorp en /etc/apt/keyrings:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/hashicorp-archive-keyring.gpg

Añade el repositorio, restringido a esa clave. El nombre en clave de la versión (noble en Ubuntu 24.04) se rellena automáticamente:

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

Instala Terraform:

sudo apt update
sudo apt install terraform

Comprueba la instalación:

terraform -version
Terraform v1.13.3
on linux_amd64

Tu versión será más nueva o más antigua según cuándo lo instales. Si quieres, activa el autocompletado para Bash y abre una shell nueva para que surta efecto:

terraform -install-autocomplete

Paso 2: Entender cómo funciona Terraform

Antes de escribir configuración, conviene conocer las cuatro piezas que vas a usar:

ConceptoQué es
Proveedor (provider)Un plugin que habla con una API (AWS, Cloudflare, Docker, Kubernetes...). Lo descarga terraform init.
Recurso (resource)Un objeto gestionado por un proveedor, como un contenedor o un registro DNS.
Estado (state)Un archivo (terraform.tfstate) donde Terraform anota qué objetos reales gestiona y sus atributos.
PlanLa lista de acciones que Terraform calcula comparando tu configuración con el estado y con los objetos reales.

La configuración se escribe en HCL (HashiCorp Configuration Language) en archivos con extensión .tf. Terraform lee todos los archivos .tf del directorio actual, así que la forma de repartirlos es cosa tuya. Una convención habitual es main.tf para los recursos, variables.tf para las entradas y outputs.tf para los valores que se muestran.

Paso 3: Escribir tu primera configuración

Crea un directorio de proyecto:

mkdir ~/terraform-docker
cd ~/terraform-docker

Crea main.tf:

nano main.tf
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:stable"
  keep_locally = false
}

resource "docker_container" "nginx" {
  name  = "tf-nginx"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = 8000
  }
}

Esto es lo que hace cada bloque:

  • El bloque terraform declara qué proveedores necesita el proyecto. kreuzwerker/docker es el proveedor comunitario de Docker del Terraform Registry, y ~> 3.0 acepta cualquier versión 3.x pero nunca la 4.0, que podría traer cambios incompatibles.
  • provider "docker" {} configura el proveedor. Sin argumentos, se conecta al socket local de Docker.
  • docker_image.nginx descarga la imagen nginx:stable. keep_locally = false elimina la imagen cuando destruyes el recurso.
  • docker_container.nginx ejecuta un contenedor con esa imagen y publica el puerto 80 del contenedor en el puerto 8000 del servidor.

docker_image.nginx.image_id es una referencia a un atributo de otro recurso. Terraform usa estas referencias para construir un grafo de dependencias, de modo que sabe que debe descargar la imagen antes de crear el contenedor sin que tengas que indicar un orden.

Paso 4: Inicializar el proyecto

terraform init descarga los proveedores indicados en la configuración y prepara el directorio de trabajo. Debes ejecutarlo una vez en cada proyecto nuevo y de nuevo cada vez que añadas un proveedor.

terraform init
Initializing the backend...
Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 3.0"...
- Installing kreuzwerker/docker v3.6.2...
- Installed kreuzwerker/docker v3.6.2 (self-signed, key ID BD080C4571C6104C)
...
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above.
...
Terraform has been successfully initialized!

init crea dos cosas:

  • .terraform/, con los binarios de los proveedores descargados. No lo subas al repositorio.
  • .terraform.lock.hcl, que fija las versiones exactas de los proveedores y sus checksums. Súbelo al repositorio para que todo el equipo use las mismas versiones.

Da formato a la configuración y valídala:

terraform fmt
terraform validate
Success! The configuration is valid.

fmt reescribe los archivos con el estilo canónico e imprime el nombre de los que ha modificado. validate comprueba la sintaxis y las referencias sin contactar con ninguna API.

Paso 5: Previsualizar y aplicar cambios

terraform plan muestra lo que haría Terraform, sin hacerlo. Lee siempre el plan antes de aplicarlo.

terraform plan
Terraform will perform the following actions:

  # docker_container.nginx will be created
  + resource "docker_container" "nginx" {
      + name  = "tf-nginx"
      ...
    }

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + name         = "nginx:stable"
      ...
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Los símbolos describen cada acción: + crear, ~ modificar en el sitio, - destruir y -/+ destruir y volver a crear.

Aplica la configuración. Terraform vuelve a mostrar el plan y pide confirmación:

terraform apply

Escribe yes cuando te lo pida:

docker_image.nginx: Creating...
docker_image.nginx: Creation complete after 8s [id=sha256:...nginx:stable]
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 1s [id=4f1c...]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Comprueba que el contenedor está en marcha y responde:

docker ps --filter name=tf-nginx
curl -I http://localhost:8000
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                  NAMES
4f1c2a9b7e10   3b25b682ea82   "/docker-entrypoint.…"   30 seconds ago   Up 29 seconds   0.0.0.0:8000->80/tcp   tf-nginx
HTTP/1.1 200 OK
Server: nginx/1.28.0
...

Ejecuta terraform plan otra vez. Como la realidad ya coincide con la configuración, Terraform responde No changes. Your infrastructure matches the configuration.

Paso 6: Añadir variables y outputs

Los valores fijos en el código hacen que una configuración sea difícil de reutilizar. Las variables de entrada permiten cambiarlos sin editar los recursos, y los outputs muestran valores útiles tras cada apply.

Crea variables.tf:

nano variables.tf
variable "container_name" {
  description = "Nombre del contenedor de Nginx"
  type        = string
  default     = "tf-nginx"
}

variable "external_port" {
  description = "Puerto del servidor que expone Nginx"
  type        = number
  default     = 8000

  validation {
    condition     = var.external_port > 1024 && var.external_port < 65536
    error_message = "El puerto externo debe estar entre 1025 y 65535."
  }
}

Crea outputs.tf:

nano outputs.tf
output "container_id" {
  description = "ID del contenedor de Nginx"
  value       = docker_container.nginx.id
}

output "url" {
  description = "URL en la que responde Nginx"
  value       = "http://localhost:${var.external_port}"
}

Ahora actualiza el recurso del contenedor en main.tf para que use las variables:

nano main.tf
resource "docker_container" "nginx" {
  name  = var.container_name
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = var.external_port
  }
}

Las variables se referencian como var.<nombre>, y ${...} inserta una expresión dentro de una cadena.

Aplica pasando un puerto distinto por línea de comandos:

terraform apply -var="external_port=8080"

El puerto publicado no se puede cambiar en un contenedor en marcha, así que el plan muestra una sustitución:

  # docker_container.nginx must be replaced
-/+ resource "docker_container" "nginx" {
      ...
      ~ ports { # forces replacement
          ~ external = 8000 -> 8080 # forces replacement
            ...
        }
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Escribe yes. Cuando termina el apply, Terraform muestra los outputs:

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Outputs:

container_id = "9d2e..."
url = "http://localhost:8080"

En lugar de pasar -var cada vez, puedes poner los valores en un archivo terraform.tfvars, que Terraform carga automáticamente:

nano terraform.tfvars
external_port = 8080

Consulta los outputs en cualquier momento con:

terraform output url
"http://localhost:8080"

Paso 7: Inspeccionar el estado

Lo que Terraform sabe de tu infraestructura vive en terraform.tfstate, en el directorio del proyecto. No lo edites nunca a mano; usa los comandos state.

Lista los recursos que gestiona Terraform:

terraform state list
docker_container.nginx
docker_image.nginx

Muestra todos los atributos de un recurso:

terraform state show docker_container.nginx

El estado puede contener valores sensibles, como contraseñas o claves que devuelven los proveedores. En proyectos de equipo, guárdalo en un backend remoto con bloqueo (por ejemplo un bucket compatible con S3 o PostgreSQL) en lugar de en el disco de una persona, y mantenlo fuera de Git.

Crea ya un .gitignore para el proyecto, para no subir los archivos equivocados más adelante:

nano .gitignore
.terraform/
*.tfstate
*.tfstate.*
crash.log
*.tfvars

Mantén .terraform.lock.hcl y todos los archivos .tf en el control de versiones. Deja fuera los *.tfvars si contienen secretos.

Paso 8: Destruir la infraestructura

terraform destroy elimina todos los recursos del estado. Muestra un plan con entradas - y pide confirmación:

terraform destroy

Escribe yes:

docker_container.nginx: Destroying... [id=9d2e...]
docker_container.nginx: Destruction complete after 1s
docker_image.nginx: Destroying... [id=sha256:...nginx:stable]
docker_image.nginx: Destruction complete after 0s

Destroy complete! Resources: 2 destroyed.

Terraform ha eliminado el contenedor antes que la imagen, el orden inverso al de creación. Confirma que no queda nada:

docker ps -a --filter name=tf-nginx
terraform state list

Ninguno de los dos comandos devuelve recursos.

Solución de problemas

permission denied while trying to connect to the Docker daemon socket: tu usuario no está en el grupo docker o no has vuelto a iniciar sesión desde que lo añadiste. Compruébalo con groups, cierra sesión y vuelve a entrar.

Error: Inconsistent dependency lock file o provider ... is not available: has añadido o cambiado un proveedor después del último init. Ejecuta terraform init -upgrade.

Bind for 0.0.0.0:8000 failed: port is already allocated: otro contenedor o proceso usa ese puerto. Elige otro external_port o detén el otro proceso (sudo ss -tlnp | grep 8000 muestra cuál es).

Error acquiring the state lock: hay otro comando de Terraform en marcha en el mismo directorio, o una ejecución anterior se interrumpió. Espera a que termine la otra ejecución; solo si estás seguro de que no hay ninguna activa, libera el bloqueo con terraform force-unlock LOCK_ID, usando el ID del mensaje de error.

Para ver más detalle de cualquier error, ejecuta el comando con TF_LOG=DEBUG, por ejemplo TF_LOG=DEBUG terraform plan.

Conclusión

Has instalado Terraform desde el repositorio de HashiCorp, has escrito una configuración con un proveedor y dos recursos dependientes y has recorrido el ciclo de vida completo: init, plan, apply, cambios mediante variables, inspección del estado y destroy. El mismo flujo sirve para cualquier proveedor, tanto si gestiona contenedores como registros DNS o servidores en la nube.

Como siguientes pasos, prueba un proveedor de un servicio que ya uses (por ejemplo los registros DNS de Cloudflare), mueve el estado a un backend remoto con bloqueo y agrupa los recursos relacionados en módulos reutilizables.