Terraform es una de esas herramientas que una vez la pruebas, la vas a echar en falta cuando no la tengas disponible. En lugar de crear recursos manualmente (haciendo clic en consolas o ejecutando comandos), describes lo que quieres en archivos de texto y Terraform se encarga de crearlo.

En este post te explico qué es, cómo instalarlo y cómo dar tus primeros pasos con esta herramienta esencial para un ingeniero DevOps.

¿Qué es Terraform?

Terraform es una herramienta de código abierto desarrollada por HashiCorp que permite definir y provisionar infraestructura mediante código, lo que se conoce como Infrastructure as Code (IaC). En lugar de lidiar con errores humanos, falta de documentación y dificultad para reproducir entornos, con Terraform describes tu infraestructura en archivos de texto plano que pueden versionarse en Git como cualquier código.

Estos archivos son declarativos: tú defines el estado final que quieres y Terraform calcula automáticamente las acciones necesarias para alcanzar ese estado. No hace falta especificar los pasos intermedios, y si lo ejecutas varias veces llegarás siempre al mismo resultado (lo que se conoce como idempotencia). Además, antes de aplicar cualquier cambio te muestra un plan de ejecución para que puedas revisar qué va a hacer. Nada de sorpresas.

Algunas tareas que Terraform facilita son:

  • Provisionar infraestructura: Crear servidores, redes, bases de datos, contenedores…
  • Gestionar múltiples proveedores: AWS, Azure, GCP, Docker, Kubernetes, y muchos más.
  • Versionar tu infraestructura: Los archivos .tf van a Git como cualquier código, con todo lo que eso implica (revisión de cambios, historial, colaboración…).

El flujo de trabajo

Cuando trabajamos con Terraform, se suelen seguir tres pasos: escribir la configuración, planificar los cambios y aplicarlos:

1. Escribir: Creas archivos .tf que describen los recursos que necesitas:

# ...
resource "docker_container" "nginx" {
  name  = "mi-nginx"
  image = "nginx:latest"
  ports {
    internal = 80
    external = 8080
  }
}
# ...

2. Planificar: Terraform analiza tu configuración y muestra qué cambios va a realizar, como un “dry run” que te evita sorpresas:

terraform plan

3. Aplicar: Si el plan es correcto, lo aplicas y Terraform crea, modifica o destruye los recursos necesarios para que tu infraestructura coincida con la configuración:

terraform apply

Conceptos básicos

Antes de empezar, hay tres conceptos que necesitas entender:

  • Providers: Son los plugins que permiten a Terraform hablar con las APIs de diferentes servicios. Hay providers para AWS, Azure, Google Cloud Platform, Docker, Kubernetes, GitHub, Cloudflare… y miles más. Para el ejemplo práctico usaremos el de kreuzwerker/docker.

  • Resources: Son los componentes de infraestructura que gestionas. Cada recurso se define en un bloque resource con su tipo y un nombre local.

  • State: Terraform guarda el estado de tu infraestructura en un archivo terraform.tfstate. Este archivo mapea lo que has definido en tu código con los recursos reales que existen.

⚠️ Importante: El fichero de estado puede contener datos sensibles (contraseñas, tokens, IPs…). Nunca lo subas a Git. Añade *.tfstate* a tu .gitignore.

Si trabajas en equipo, la forma de gestionar el fichero de estado de la infraestructura es a través de un backend remoto (S3, Azure Blob Storage, etc.) que además de centralizar el acceso, soporta bloqueo de estado (state locking) para evitar que dos personas modifiquen la infraestructura a la vez y corrompan el fichero.

Más adelante veremos también las variables y outputs en el ejemplo práctico.

Requisitos

Instalación

Nota: Si no estás usando Debian/Ubuntu, puedes consultar la documentación oficial en Install Terraform | Terraform | HashiCorp Developer.

Para Debian y Ubuntu tenemos disponible un repositorio oficial de HashiCorp que facilita la instalación y futuras actualizaciones:

# Instalar dependencias
su -

apt update

apt install gnupg

# Añadir la clave GPG de HashiCorp
wget -O- https://apt.releases.hashicorp.com/gpg | \
  gpg --dearmor | \
  tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null

# Añadir el repositorio
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | \
  tee /etc/apt/sources.list.d/hashicorp.list

# Instalar Terraform
apt update

apt install terraform

# Salimos de la sesión de root
exit

Tras instalar Terraform, puedes verificar que está listo para usar ejecutando:

terraform version

Deberías ver algo como:

Terraform v1.14.4
on linux_amd64

Ejemplo práctico sencillo: Contenedor de Docker

Vamos a crear un proyecto sencillo que levantará un contenedor de nginx, así podrás tener una visión práctica del funcionamiento.

Primero creamos un directorio para nuestro proyecto y nos movemos a él:

mkdir -p ~/terraform-demo

cd ~/terraform-demo

Creamos un archivo main.tf. Lo nombro así por convención, pero puedes llamarlo como quieras siempre que tenga la extensión .tf. Este fichero será el núcleo del proyecto, donde definiremos los recursos que queremos crear. También puedes dividir la configuración en varios ficheros .tf para organizar mejor el código, veremos esto más adelante:

nano -cl main.tf

Y añade el siguiente contenido:

⚠️ Nota: Si usas Docker Rootless, asegúrate de cambiar la ruta del socket en el bloque provider "docker". En mi caso la ruta sería unix:///run/user/1000/docker.sock. Si no estás seguro de cuál es la tuya, puedes comprobarlo con echo unix://$XDG_RUNTIME_DIR/docker.sock.

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

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

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

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

  ports {
    internal = 80
    external = 8080
  }
}

Vamos a desglosar cada bloque:

  • terraform {}: Declara los providers del proyecto. Aquí indicamos que usaremos kreuzwerker/docker en su versión 3.6.2 o actualizaciones menores.
  • provider "docker" {}: Configura la conexión con Docker a través de su socket Unix.
  • resource "docker_image" "nginx" {}: Define la imagen que queremos descargar (nginx:alpine). Con keep_locally = false le indicamos que no conserve la imagen si destruimos el escenario.
  • resource "docker_container" "nginx" {}: Crea el contenedor a partir de la imagen anterior. Usamos docker_image.nginx.image_id para referenciar el ID de la imagen descargada y mapeamos el puerto 80 del contenedor al 8080 del host.

Cada provider tiene sus propios tipos de recursos y opciones. El tipo (docker_image o docker_container) determina qué parámetros puedes usar y cómo se comporta. Consulta la documentación del que estés usando para ver todos los detalles.

En este ejemplo uso kreuzwerker/docker, el más popular para gestionar Docker con Terraform. Puedes consultar su documentación aquí.

Respecto a las versiones, Terraform tiene un sistema de restricciones que te permite especificar las que son compatibles con tu configuración. Esto es importante para evitar que una actualización rompa tu infraestructura:

Operador Ejemplo Descripción
= = 3.6.2 Exactamente esa versión, no se puede combinar con otros operadores
!= != 3.6.2 Cualquier versión excepto la especificada
> > 3.0.0 Mayor que la versión especificada, sin incluirla
>= >= 3.0.0 Mayor o igual que la versión especificada, incluida
< < 4.0.0 Menor que la versión especificada, sin incluirla
<= <= 3.9.9 Menor o igual que la versión especificada, incluida
~> ~> 3.6.2 Solo permite actualizaciones del número más a la derecha

Para aclarar un poco mejor el operador ~>:

Restricción Significado Acepta No acepta
~> 3.0 Versión 3.x 3.0, 3.1, 3.9.5 4.0, 2.9
~> 3.0.1 Versión 3.0.x 3.0.1, 3.0.2, 3.0.9 3.1.0, 4.0
~> 3.6.2 Versión 3.6.x 3.6.2, 3.6.3, 3.6.15 3.7.0, 4.0

Inicializar el proyecto

terraform init

Este comando descarga los providers necesarios y prepara el directorio de trabajo. Solo es necesario ejecutarlo la primera vez o cuando añadas un nuevo provider.

Terraform init

Ver el plan

terraform plan

Terraform nos muestra qué va a hacer para alcanzar el estado deseado definido en main.tf. En este caso, nos indicará que va a crear una imagen de nginx y un contenedor:

Terraform plan - 1

Terraform plan - 2

Aplicar la configuración

terraform apply

Al ejecutar el apply, nos volverá a mostrar el plan y nos pedirá confirmación para proceder. Solo escribiendo yes se aplicarán los cambios:

Terraform apply - 1

Terraform apply - 2

Verificar el resultado

Que Terraform diga que ha aplicado los cambios está genial, pero no está de más comprobarlo por nosotros mismos:

# Listar el contenedor creado
docker ps --filter "name=nginx-terraform"

# Obtener la página por defecto de nginx
curl http://localhost:8080

Comprobación

Podemos ver que todo ha salido bien, el contenedor está en ejecución y nginx responde correctamente.

Si quieres ver el estado actual de tu infraestructura, puedes usar:

terraform state list

terraform show

Terraform state

Variables y outputs

Terraform ofrece variables (valores de entrada parametrizables) y outputs (valores que se exponen tras aplicar la configuración).

Esto es especialmente útil para proyectos más complejos, pero vamos a añadirlos a nuestro ejemplo para que veas cómo funcionan:

Crea el fichero variables.tf, que como su nombre indica, definirá las variables que usaremos en main.tf:

nano -cl variables.tf

Contenido:

variable "container_name" {
  description = "Nombre del contenedor"
  type        = string
  default     = "nginx-terraform"
}

variable "external_port" {
  description = "Puerto externo para nginx"
  type        = number
  default     = 8080
}

variable "docker_host" {
  description = "Ruta del socket de Docker"
  type        = string
  default     = "unix:///var/run/docker.sock"
}

Sintaxis del bloque variable:

  • variable "nombre" {}: Define una variable con un nombre único dentro del proyecto.
  • description: Explica el propósito de la variable.
  • type: Define el tipo de dato esperado (string, number, bool, list, set, map o null).
  • default: Especifica un valor por defecto. Si no se proporciona un valor al aplicar la configuración, se usará este valor. Si no se define un valor por defecto, la variable será obligatoria y Terraform pedirá que se le asigne un valor al ejecutar terraform apply.

Ahora creamos el fichero outputs.tf, donde se definen los valores que queremos exponer tras aplicar la configuración:

nano -cl outputs.tf

Contenido:

output "container_id" {
  description = "ID del contenedor"
  value       = docker_container.nginx.id
}

output "container_name" {
  description = "Nombre del contenedor"
  value       = docker_container.nginx.name
}

output "nginx_url" {
  description = "URL para acceder a nginx"
  value       = "http://localhost:${var.external_port}"
}

Sintaxis del bloque output:

  • output "nombre" {}: Define un output con un nombre único dentro del proyecto.
  • description: Explica el propósito del output.
  • value: Especifica el valor que se va a exponer. Puede ser cualquier expresión válida en Terraform, incluyendo referencias a recursos, variables u otros outputs.

Finalmente, modificamos main.tf para usar las variables en lugar de los valores hardcodeados. Para llamar a las variables, se usa la sintaxis var.nombre_variable:

nano -cl main.tf

Nuevo contenido:

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

provider "docker" {
  host = var.docker_host
}

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

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

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

Ahora, al ejecutar terraform apply, tenemos la opción de usar los valores por defecto definidos en variables.tf o especificar otros diferentes:

# Usar valores por defecto
terraform apply

# Usar otros valores
terraform apply -var="container_name=tf-nginx" -var="docker_host=unix:///run/user/1000/docker.sock"

Terraform apply con variables - 1

Terraform apply con variables - 2

Terraform apply con variables - 3

Terraform apply con variables - 4

Podemos ver que el nombre del contenedor se ha cambiado a tf-nginx pero el puerto externo sigue siendo 8080 porque no hemos especificado un nuevo valor para esa variable, así que se ha usado el valor por defecto. Si hubiéramos querido cambiarlo, podríamos haber añadido -var="external_port=9090" al apply.

Tras aplicar la configuración, podemos ver los outputs definidos en outputs.tf con:

terraform output

Terraform outputs

Modificar infraestructura

Como ya hemos visto en el apartado anterior, si cambias la configuración, Terraform se encargará del resto.

Por ejemplo, si modificamos el puerto externo en variables.tf:

sed -i 's/= 8080/= 9090/' variables.tf

Al ejecutar terraform plan, verás que Terraform detecta el cambio y te muestra exactamente qué haría en caso de lanzar el terraform apply:

Terraform plan con cambios

No olvides agregar las variables a tus comandos de plan y apply si quieres usar valores diferentes a los definidos en variables.tf, de lo contrario se usarán los valores por defecto y te mostrará un plan diferente al que esperas.

Destruir infraestructura

Cuando ya no necesites los recursos, puedes destruirlo todo con un solo comando:

terraform destroy

Terraform muestra qué recursos va a destruir y pide confirmación:

Terraform destroy - 1

Terraform destroy - 2

En mi caso, debo incluir la variable docker_host por ser diferente al valor que puse por defecto en el fichero variables.tf, así el provider sabrá cuál es el socket de Docker a utilizar, de lo contrario no podrá interactuar con Docker ni realizar ninguna acción.

Estructura de un proyecto más complejo

El ejemplo que hemos visto está bien para comenzar a entender cómo funciona Terraform, pero para proyectos más grandes conviene organizar los archivos de forma más estructurada.

No hay una única forma de hacerlo, pero un ejemplo de estructura podría ser:

mi-proyecto/
├── main.tf           # Recursos principales
├── variables.tf      # Definición de variables
├── prod.tfvars       # Valores de las variables para el entorno de producción
├── dev.tfvars        # Valores de las variables para el entorno de desarrollo
├── outputs.tf        # Outputs del proyecto
├── providers.tf      # Configuración de providers
└── modules/          # Módulos reutilizables
    └── nginx/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Los ficheros prod.tfvars y dev.tfvars son opcionales, pero muy útiles para gestionar diferentes entornos sin tener que usar -var cada vez como hicimos en el ejemplo con Docker.

En ellos puedes definir valores específicos para cada entorno, como credenciales, nombres de recursos, configuraciones particulares, etc. Recuerda no commitear estos archivos si contienen información sensible.

Su estructura sería la siguiente:

# dev.tfvars
container_name = "nginx-dev"
external_port  = 8080
docker_host    = "unix:///run/user/1000/docker.sock"

Y para usarlos, se pasa el parámetro -var-file al ejecutar terraform plan, terraform apply o terraform destroy de esta forma:

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

Comandos más comunes

Comando Descripción
terraform init Inicializa el proyecto y descarga providers
terraform plan Muestra qué cambios se aplicarán
terraform apply Aplica los cambios
terraform show Muestra el estado actual de la infraestructura según el fichero .tfstate
terraform destroy Destruye toda la infraestructura
terraform fmt Formatea los archivos .tf
terraform validate Valida la sintaxis de la configuración
terraform output Muestra los outputs
terraform state list Lista los recursos
terraform state show <recurso> Muestra los detalles de un recurso concreto
terraform state rm <recurso> Elimina un recurso del .tfstate (sin destruirlo en la infraestructura real)
terraform import <recurso> <id> Importa un recurso existente (creado manualmente) al .tfstate para que Terraform lo gestione a partir de ese momento
terraform apply -refresh-only -auto-approve Actualiza el estado con la infraestructura real, equivalente al antiguo terraform refresh

Consejos para cuando trabajes con Terraform

  • Versiona el código desde el principio. Los archivos .tf son código y se merecen control de versiones como cualquier otro.
  • Pero NO hagas commit del estado. Añade el *.tfstate* a tu .gitignore o equivalente. Este archivo puede contener contraseñas, tokens y otros datos sensibles que no querrás exponer.
  • Usa variables para todo lo que pueda cambiar. No hardcodees valores que mañana puedan ser diferentes.
  • Ejecuta terraform fmt antes de hacer commits. Formatea el código automáticamente y te ahorra problemas de estilo.
  • Revisa siempre el plan antes de hacer apply. Es tu última oportunidad de ver qué va a cambiar antes de que lo haga de verdad.
  • Si trabajas en equipo, recuerda usar un backend remoto (S3, Blob Storage, etc.) para almacenar el fichero de estado. Además de centralizar el acceso, los backends remotos soportan bloqueo de estado (state locking), lo que evita que dos personas modifiquen la infraestructura a la vez y corrompan el fichero. Si trabajas en solitario, con el fichero local será suficiente.

Conclusión

Terraform cambiará completamente cómo gestionas la infraestructura. En lugar de procesos manuales que pueden ser propensos a errores, tienes código versionado, revisable y reproducible.

Mi consejo es que empieces con algo pequeño, como el ejemplo de Docker que hemos visto, y vayas escalando conforme te sientas cómodo.

Si quieres practicar con servicios de AWS sin gastar un céntimo, echa un vistazo a mi post sobre LocalStack donde explico cómo poner en funcionamiento este emulador de AWS en local.

Referencias