Ir al contenido

OpenTofu

Herramienta de infraestructura como código de código abierto y fork comunitario de Terraform.

ComandoDescripción
brew install opentofuInstalar en macOS con Homebrew
snap install --classic opentofuInstalar en Linux con Snap
choco install opentofuInstalar en Windows con Chocolatey
curl -fsSL https://get.opentofu.org/install-opentofu.sh | shInstalar mediante script de shell
docker run -it ghcr.io/opentofu/opentofuEjecutar vía Docker
tofu --versionMostrar versión instalada
tofu -helpMostrar comandos disponibles
# Usando tofuenv (como tfenv para OpenTofu)
git clone https://github.com/tofuutils/tofuenv.git ~/.tofuenv
echo 'export PATH="$HOME/.tofuenv/bin:$PATH"' >> ~/.bashrc

# Listar versiones disponibles
tofuenv list-remote

# Instalar versión específica
tofuenv install 1.8.0
tofuenv use 1.8.0

# Fijar versión en proyecto
echo "1.8.0" > .opentofu-version
ComandoDescripción
tofu initInicializar directorio de trabajo y descargar proveedores
tofu init -upgradeRe-inicializar y actualizar versiones de proveedores/módulos
tofu init -backend-config=prod.hclInicializar con archivo de configuración de backend
tofu planPrevisualizar cambios de infraestructura
tofu plan -out=plan.tfplanGuardar plan en archivo para aplicar después
tofu plan -target=aws_instance.webPlanificar cambios solo para recurso específico
tofu plan -var-file=prod.tfvarsPlanificar con archivo de variables específico
tofu applyAplicar cambios de infraestructura (solicita confirmación)
tofu apply -auto-approveAplicar cambios sin confirmación
tofu apply plan.tfplanAplicar un archivo de plan guardado
tofu destroyDestruir toda la infraestructura gestionada
tofu destroy -auto-approveDestruir sin confirmación
tofu destroy -target=aws_instance.webDestruir solo recurso específico
ComandoDescripción
tofu validateValidar sintaxis de configuración
tofu fmtFormatear archivos de configuración
tofu fmt -checkVerificar si los archivos están formateados (compatible con CI)
tofu fmt -recursiveFormatear todos los archivos en subdirectorios
tofu outputMostrar todos los valores de salida
tofu output -jsonMostrar salidas en formato JSON
tofu output db_passwordMostrar valor de salida específico
tofu showMostrar estado actual en formato legible
tofu show -jsonMostrar estado en formato JSON
tofu show plan.tfplanMostrar detalles del plan guardado
tofu graphGenerar gráfico de dependencias de recursos (formato DOT)
tofu graph | dot -Tpng > graph.pngRenderizar gráfico de dependencias como PNG
tofu refreshReconciliar estado con infraestructura real
tofu providersListar proveedores usados en la configuración
# Bloque de proveedores requeridos
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.5"
    }
  }

  # Cifrado de estado (característica exclusiva de OpenTofu)
  encryption {
    method "aes_gcm" "default" {
      keys = key_provider.pbkdf2.mykey
    }

    state {
      method   = method.aes_gcm.default
      enforced = true
    }

    plan {
      method   = method.aes_gcm.default
      enforced = true
    }

    key_provider "pbkdf2" "mykey" {
      passphrase = var.state_passphrase
    }
  }
}

# Proveedor con configuración predeterminada
provider "aws" {
  region = "us-east-1"

  default_tags {
    tags = {
      ManagedBy   = "opentofu"
      Environment = var.environment
      Project     = var.project_name
    }
  }
}

# Alias de proveedor para multi-región
provider "aws" {
  alias  = "west"
  region = "us-west-2"
}

# Usar proveedor con alias en un recurso
resource "aws_instance" "west_server" {
  provider      = aws.west
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"
}
# Recurso básico
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  tags = {
    Name = "${var.project_name}-web"
  }
}

# Fuente de datos (leer recursos existentes)
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

# Valores locales
locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "opentofu"
  }

  az_count = length(data.aws_availability_zones.available.names)
}

# Count y for_each
resource "aws_subnet" "public" {
  count             = local.az_count
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-public-${count.index}"
    Type = "public"
  })
}

resource "aws_security_group_rule" "ingress" {
  for_each = {
    http  = { port = 80, cidrs = ["0.0.0.0/0"] }
    https = { port = 443, cidrs = ["0.0.0.0/0"] }
    ssh   = { port = 22, cidrs = [var.admin_cidr] }
  }

  type              = "ingress"
  from_port         = each.value.port
  to_port           = each.value.port
  protocol          = "tcp"
  cidr_blocks       = each.value.cidrs
  security_group_id = aws_security_group.main.id
}
ComandoDescripción
variable "name" { type = string }Definir una variable de tipo string
variable "port" { type = number default = 8080 }Variable con valor predeterminado
variable "tags" { type = map(string) }Variable de tipo map
variable "cidrs" { type = list(string) }Variable de tipo lista
variable "config" { type = object({ ... }) }Variable de tipo objeto con estructura
variable "db_pass" { sensitive = true }Marcar variable como sensible (oculta en la salida)
variable "env" { validation { ... } }Variable con validación personalizada
ComandoDescripción
tofu plan -var="name=value"Pasar variable vía CLI
tofu plan -var-file="prod.tfvars"Pasar variables desde archivo
TF_VAR_name=value tofu planPasar vía variable de entorno
Crear archivo terraform.tfvarsValores de variables auto-cargados
Crear archivo *.auto.tfvarsArchivos de variables auto-cargados
# variables.tf
variable "environment" {
  type        = string
  description = "Deployment environment"

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

variable "instance_config" {
  type = object({
    instance_type = string
    volume_size   = number
    encrypted     = bool
  })
  default = {
    instance_type = "t3.micro"
    volume_size   = 20
    encrypted     = true
  }
}

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

# outputs.tf
output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "List of public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "web_url" {
  description = "URL of the web server"
  value       = "https://${aws_lb.web.dns_name}"
}

output "db_connection_string" {
  description = "Database connection string"
  value       = "postgresql://${var.db_username}:${var.database_password}@${aws_db_instance.main.endpoint}/${var.db_name}"
  sensitive   = true
}
# prod.tfvars
environment = "prod"
instance_config = {
  instance_type = "t3.large"
  volume_size   = 100
  encrypted     = true
}

# dev.tfvars
environment = "dev"
instance_config = {
  instance_type = "t3.micro"
  volume_size   = 20
  encrypted     = false
}
ComandoDescripción
tofu state listListar todos los recursos en el estado
tofu state show aws_instance.webMostrar detalles de recurso específico
tofu state mv aws_instance.old aws_instance.newRenombrar recurso en el estado
tofu state rm aws_instance.webEliminar recurso del estado (mantiene la infraestructura real)
tofu state pullObtener estado remoto a stdout
tofu state pushEnviar estado local al backend remoto
tofu import aws_instance.web i-1234567Importar recurso existente al estado
tofu state replace-provider hashicorp/aws registry.opentofu.org/hashicorp/awsReemplazar proveedor en el estado
tofu force-unlock LOCK_IDLiberar un bloqueo de estado atascado
# Backend S3 (más común)
terraform {
  backend "s3" {
    bucket         = "my-tofu-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tofu-locks"
    encrypt        = true
  }
}

# Backend PostgreSQL
terraform {
  backend "pg" {
    conn_str = "postgres://user:pass@db.example.com/tofu_state?sslmode=require"
  }
}

# Backend HTTP (funciona con GitLab, alternativas a Terraform Cloud)
terraform {
  backend "http" {
    address        = "https://gitlab.example.com/api/v4/projects/1/terraform/state/prod"
    lock_address   = "https://gitlab.example.com/api/v4/projects/1/terraform/state/prod/lock"
    unlock_address = "https://gitlab.example.com/api/v4/projects/1/terraform/state/prod/lock"
    username       = "gitlab-ci-token"
    password       = var.ci_token
  }
}

# Backend local (predeterminado, para desarrollo)
terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}
# Cifrado de estado con AWS KMS
terraform {
  encryption {
    key_provider "aws_kms" "main" {
      kms_key_id = "arn:aws:kms:us-east-1:123456789:key/abcd-1234"
      region     = "us-east-1"
      key_spec   = "AES_256"
    }

    method "aes_gcm" "default" {
      keys = key_provider.aws_kms.main
    }

    state {
      method   = method.aes_gcm.default
      enforced = true
    }
  }
}
ComandoDescripción
module "vpc" { source = "./modules/vpc" }Usar un módulo local
module "vpc" { source = "github.com/org/repo//modules/vpc" }Usar un módulo de GitHub
module "vpc" { source = "registry.opentofu.org/..." }Usar módulo del registro de OpenTofu
tofu getDescargar y actualizar módulos
tofu get -updateForzar actualización de todos los módulos
modules/
└── vpc/
    ├── main.tf          # Recursos
    ├── variables.tf     # Variables de entrada
    ├── outputs.tf       # Valores de salida
    ├── versions.tf      # Requisitos de proveedores
    └── README.md        # Documentación de uso
# modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = merge(var.tags, { Name = var.name })
}

resource "aws_subnet" "public" {
  count                   = length(var.public_subnets)
  vpc_id                  = aws_vpc.this.id
  cidr_block              = var.public_subnets[count.index]
  availability_zone       = var.azs[count.index]
  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.name}-public-${count.index}"
  })
}

# modules/vpc/variables.tf
variable "name" { type = string }
variable "cidr" { type = string }
variable "azs" { type = list(string) }
variable "public_subnets" { type = list(string) }
variable "tags" { type = map(string) default = {} }

# modules/vpc/outputs.tf
output "vpc_id" { value = aws_vpc.this.id }
output "public_subnet_ids" { value = aws_subnet.public[*].id }

# Usando el módulo
module "vpc" {
  source = "./modules/vpc"

  name           = "production"
  cidr           = "10.0.0.0/16"
  azs            = ["us-east-1a", "us-east-1b", "us-east-1c"]
  public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  tags           = local.common_tags
}
ComandoDescripción
Reemplazar terraform con tofu en comandosLos comandos CLI son compatibles directamente
tofu init -upgradeRe-inicializar con proveedores de OpenTofu
Mantener archivos .tf existentes sin cambiosLa sintaxis HCL es totalmente compatible
Mantener archivos .tfstate existentesEl formato de estado es compatible
Mantener .terraform.lock.hcl existenteEl archivo de bloqueo es compatible
Actualizar CI/CD para usar la acción opentofu/setup-opentofuCambiar GitHub Actions
Migración de Terraform a OpenTofu:

1. Instalar OpenTofu junto a Terraform
2. En el directorio de tu proyecto, ejecutar: tofu init
3. Ejecutar: tofu plan
   - Comparar salida con: terraform plan
   - Debería ser idéntico (sin cambios)
4. Reemplazar 'terraform' con 'tofu' en pipelines de CI/CD
5. Actualizar GitHub Actions:
   - Anterior: hashicorp/setup-terraform
   - Nuevo: opentofu/setup-opentofu
6. Actualizar fuentes de proveedores en el archivo de bloqueo (si es necesario):
   - tofu providers lock
7. Habilitar cifrado de estado (exclusivo de OpenTofu):
   - Agregar bloque encryption al bloque terraform {}
8. Cambiar referencias del registro de módulos:
   - registry.terraform.io → registry.opentofu.org
   - (Ambos siguen funcionando, pero el registro de OpenTofu es preferido)
name: OpenTofu Plan & Apply
on:
  push:
    branches: [main]
  pull_request:

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: opentofu/setup-opentofu@v1
        with:
          tofu_version: 1.8.0

      - name: Init
        run: tofu init
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Plan
        run: tofu plan -out=plan.tfplan -no-color
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Apply (main only)
        if: github.ref == 'refs/heads/main'
        run: tofu apply -auto-approve plan.tfplan
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Cifrar archivos de estado en reposo — no disponible en Terraform. Soporta proveedores de claves PBKDF2, AWS KMS, GCP KMS y Azure Key Vault.

A diferencia del cifrado del lado del servidor de Terraform Cloud, OpenTofu cifra el estado antes de que salga de tu máquina, asegurando que ningún tercero pueda leer el estado de tu infraestructura.

OpenTofu mantiene su propio registro de proveedores y módulos en registry.opentofu.org, asegurando disponibilidad a largo plazo independiente de las decisiones de licenciamiento de HashiCorp.

  1. Usa estado remoto con bloqueo — configura un backend S3 + DynamoDB (o equivalente) para prevenir modificaciones concurrentes y habilitar la colaboración en equipo.

  2. Habilita el cifrado de estado — usa la característica exclusiva de cifrado de OpenTofu con KMS o PBKDF2 para proteger datos sensibles en archivos de estado.

  3. Fija versiones de proveedores y módulos — usa restricciones ~> (ej., ~> 5.0) para permitir actualizaciones de parches mientras previenes cambios incompatibles.

  4. Usa workspaces o separación de directorios para entornos — mantén el estado de dev, staging y producción separado para prevenir cambios accidentales entre entornos.

  5. Escribe módulos para patrones reutilizables — extrae patrones comunes (VPCs, bases de datos, clústeres de Kubernetes) en módulos con entradas y salidas claras.

  6. Valida variables con restricciones — agrega bloques validation a las variables para capturar valores inválidos antes de que lleguen al proveedor de nube.

  7. Guarda planes antes de aplicar — usa tofu plan -out=plan.tfplan luego tofu apply plan.tfplan para asegurar que aplicas exactamente lo que revisaste.

  8. Usa default_tags en proveedores — establece etiquetas a nivel de organización (ManagedBy, Environment, Project) en el nivel del proveedor para asegurar etiquetado consistente.

  9. Importa recursos existentes — usa tofu import para traer infraestructura no gestionada bajo control de OpenTofu en lugar de recrearla.

  10. Ejecuta tofu fmt -check en CI — aplica formateo consistente entre todos los miembros del equipo haciendo fallar las builds de CI en archivos sin formatear.

  11. Usa fuentes de datos para referencias externas — referencia infraestructura existente (AMIs, VPCs, zonas DNS) con bloques data en lugar de codificar IDs directamente.

  12. Revisa los planes cuidadosamente — siempre lee la salida del plan antes de aplicar. Presta atención a acciones de destroy y replace que podrían causar tiempo de inactividad.