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
dockerpara poder ejecutardockersinsudo. 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
NotaDesde la versión 1.6, Terraform se publica bajo la Business Source License. Si necesitas una licencia de código abierto, OpenTofu es un fork comunitario que acepta el mismo lenguaje de configuración y los mismos comandos (
tofuen lugar deterraform).
Paso 2: Entender cómo funciona Terraform
Antes de escribir configuración, conviene conocer las cuatro piezas que vas a usar:
| Concepto | Qué 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. |
| Plan | La 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
terraformdeclara qué proveedores necesita el proyecto.kreuzwerker/dockeres el proveedor comunitario de Docker del Terraform Registry, y~> 3.0acepta 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.nginxdescarga la imagennginx:stable.keep_locally = falseelimina la imagen cuando destruyes el recurso.docker_container.nginxejecuta 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.
AdvertenciaLos puertos que publica Docker se saltan las reglas de UFW, así que el puerto 8000 es accesible desde internet en un servidor público. Destruye el contenedor al terminar este tutorial, o publícalo solo en localhost si haces pruebas en un servidor público.
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.
