Introducción

En este módulo aprenderás la sintaxis de HCL, el lenguaje que usa Terraform para definir infraestructura. Comprender bien HCL es fundamental: es la base sobre la que construirás todas tus configuraciones, desde las más simples hasta las más complejas.


¿Qué es HCL? Historia y propósito

HCL (HashiCorp Configuration Language) es un lenguaje de configuración creado por HashiCorp, la empresa detrás de herramientas como Terraform, Vault, Consul y Nomad. Fue diseñado con un objetivo claro: ser legible tanto por humanos como por máquinas.

Historia breve

  • 2014: HashiCorp lanza Terraform y con él la primera versión de HCL.
  • 2019: Se lanza HCL2, una reescritura completa que añade expresiones, funciones y mayor flexibilidad.
  • Actualidad: HCL2 es el estándar utilizado en Terraform desde la versión 0.12.

¿Por qué HCL y no JSON o YAML?

Característica HCL JSON YAML
Legibilidad humana Alta Media Alta
Soporte para comentarios No
Expresiones y funciones No No
Interpolación de strings No Limitado
Tipado dinámico Parcial Parcial
Compatible con JSON Sí (HCL puede leer JSON) Nativo No

HCL permite escribir comentarios, usar expresiones complejas, y tiene una sintaxis más cercana a los lenguajes de programación modernos, lo que lo hace más potente que JSON o YAML para definir infraestructura.


Estructura básica de HCL: bloques, argumentos y expresiones

Bloques

Un bloque es la unidad fundamental de HCL. Todo en Terraform se organiza en bloques. Un bloque tiene:

  1. Un tipo de bloque (por ejemplo: resource, variable, output).
  2. Opcionalmente, una o más etiquetas que identifican el bloque.
  3. Un cuerpo delimitado por llaves {} que contiene los argumentos.
# Estructura general de un bloque
tipo_de_bloque "etiqueta_1" "etiqueta_2" {
  argumento_1 = valor_1
  argumento_2 = valor_2
}

Ejemplo real con un recurso de AWS:

# Tipo de bloque: resource
# Etiqueta 1: "aws_instance" (tipo de recurso de AWS)
# Etiqueta 2: "servidor_web" (nombre local que le damos nosotros)
resource "aws_instance" "servidor_web" {
  # Argumentos que configuran la instancia EC2
  ami           = "ami-0c55b159cbfafe1f0"  # ID de la imagen de sistema operativo
  instance_type = "t2.micro"               # Tipo de instancia (tamaño del servidor)
}

Argumentos

Los argumentos asignan un valor a un nombre dentro de un bloque. Tienen la forma:

nombre_argumento = expresión

Los argumentos pueden tener valores simples o expresiones complejas.

Expresiones

Las expresiones son los valores que se asignan a los argumentos. Pueden ser literales, referencias a otros recursos, llamadas a funciones, etc.

# Literal simple
region = "us-east-1"

# Referencia a otro recurso
subnet_id = aws_subnet.principal.id

# Llamada a función
nombre = upper("produccion")

# Expresión condicional
instance_type = var.entorno == "produccion" ? "t3.medium" : "t2.micro"

Tipos de datos en HCL

HCL soporta varios tipos de datos primitivos y de colección.

Tipos primitivos

Tipo Descripción Ejemplo
string Cadena de texto "hola mundo"
number Número (entero o decimal) 42, 3.14
bool Valor booleano true, false

Tipos de colección

Tipo Descripción Ejemplo
list Lista ordenada de valores del mismo tipo ["a", "b", "c"]
set Conjunto sin orden ni duplicados Igual que list pero sin orden garantizado
map Mapa clave-valor con valores del mismo tipo { nombre = "juan", ciudad = "Madrid" }

Tipos estructurales

Tipo Descripción Uso
object Estructura con atributos con tipos definidos Configuraciones complejas
tuple Lista con tipos distintos por posición Raramente usado directamente

Ejemplos de cada tipo de dato

# STRING: cadena de texto entre comillas dobles
variable "nombre_proyecto" {
  type    = string        # Declaramos que el tipo es string
  default = "mi-proyecto" # Valor por defecto
}

# NUMBER: número entero o decimal
variable "numero_instancias" {
  type    = number # Tipo numérico
  default = 3      # Valor por defecto: 3 instancias
}

# BOOL: verdadero o falso
variable "habilitar_monitoreo" {
  type    = bool  # Tipo booleano
  default = true  # Por defecto el monitoreo está habilitado
}

# LIST(STRING): lista de cadenas de texto
variable "zonas_disponibilidad" {
  type    = list(string)             # Lista que solo acepta strings
  default = ["us-east-1a", "us-east-1b", "us-east-1c"] # Tres zonas
}

# MAP(STRING): mapa con claves y valores de tipo string
variable "etiquetas_comunes" {
  type = map(string) # Mapa donde todos los valores son strings
  default = {
    entorno    = "desarrollo"  # Clave: entorno, valor: desarrollo
    equipo     = "backend"     # Clave: equipo, valor: backend
    proyecto   = "ecommerce"   # Clave: proyecto, valor: ecommerce
  }
}

# OBJECT: estructura con atributos tipados
variable "configuracion_servidor" {
  type = object({
    tipo_instancia = string # El tipo de instancia debe ser string
    disco_gb       = number # El tamaño del disco debe ser number
    publico        = bool   # Si es público debe ser bool
  })
  default = {
    tipo_instancia = "t2.micro"
    disco_gb       = 20
    publico        = false
  }
}

# LIST(OBJECT): lista de objetos (muy común en configuraciones reales)
variable "reglas_seguridad" {
  type = list(object({
    puerto   = number  # Puerto de la regla
    protocolo = string # Protocolo (tcp, udp, etc.)
    origen   = string  # CIDR de origen
  }))
  default = [
    {
      puerto    = 80
      protocolo = "tcp"
      origen    = "0.0.0.0/0"
    },
    {
      puerto    = 443
      protocolo = "tcp"
      origen    = "0.0.0.0/0"
    }
  ]
}

Comentarios en HCL

HCL soporta dos estilos de comentarios. Los comentarios son ignorados por Terraform y sirven para documentar el código.

# Este es un comentario de una sola línea
# Se usa la almohadilla (#) al inicio

// Este también es un comentario de una sola línea
// Se puede usar doble barra (//) como alternativa

/*
  Este es un comentario
  de múltiples líneas.
  Útil para explicaciones largas
  o para desactivar bloques de código temporalmente.
*/

resource "aws_vpc" "principal" {
  cidr_block = "10.0.0.0/16" # El rango de IPs de la red virtual

  # Habilitamos DNS en la VPC para que los recursos
  # puedan resolver nombres de dominio internos
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-principal" # Nombre visible en la consola de AWS
  }
}

Consejo: Usa comentarios generosamente al principio. Con el tiempo sabrás qué necesita explicación y qué es obvio.


Referencias a otros recursos y atributos

Una de las características más poderosas de Terraform es poder referenciar atributos de un recurso desde otro. Esto crea dependencias implícitas que Terraform resuelve automáticamente.

Sintaxis de referencia

tipo_recurso.nombre_local.atributo

Ejemplo completo con referencias

# Primero creamos la VPC
resource "aws_vpc" "principal" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "vpc-principal"
  }
}

# La subnet referencia el ID de la VPC creada arriba
# Terraform sabe que debe crear la VPC antes que la subnet
resource "aws_subnet" "publica" {
  vpc_id            = aws_vpc.principal.id         # Referencia al ID de la VPC
  cidr_block        = "10.0.1.0/24"               # Subred dentro del rango de la VPC
  availability_zone = "us-east-1a"                 # Zona de disponibilidad

  tags = {
    Name = "subnet-publica"
    VPC  = aws_vpc.principal.id # También podemos referenciar en tags
  }
}

# El security group también pertenece a la misma VPC
resource "aws_security_group" "web" {
  name   = "sg-web"
  vpc_id = aws_vpc.principal.id  # Referencia a la misma VPC

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = [aws_vpc.principal.cidr_block] # Referencia al CIDR de la VPC
  }
}

En este ejemplo, Terraform construye automáticamente el orden de creación:

  1. Primero crea aws_vpc.principal
  2. Luego crea aws_subnet.publica y aws_security_group.web (pueden ser en paralelo)

Interpolación de strings con ${}

La interpolación permite insertar expresiones dentro de cadenas de texto.

# Interpolación básica: insertar el valor de una variable en un string
variable "entorno" {
  type    = string
  default = "produccion"
}

variable "region" {
  type    = string
  default = "us-east-1"
}

resource "aws_s3_bucket" "logs" {
  # Interpolamos variables dentro del nombre del bucket
  # Resultado: "logs-produccion-us-east-1-mi-empresa"
  bucket = "logs-${var.entorno}-${var.region}-mi-empresa"
}

resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  tags = {
    # Combinamos texto literal con referencias
    Name        = "servidor-${var.entorno}"           # "servidor-produccion"
    Descripcion = "Instancia en región ${var.region}" # "Instancia en región us-east-1"
  }
}

# También podemos interpolar expresiones más complejas
locals {
  prefijo     = "app"
  version_app = "2.1"

  # Interpolación con expresión
  nombre_completo = "${local.prefijo}-v${local.version_app}-${var.entorno}"
  # Resultado: "app-v2.1-produccion"
}

Expresiones condicionales

La expresión condicional en HCL tiene la misma sintaxis que el operador ternario en muchos lenguajes de programación:

condicion ? valor_si_verdadero : valor_si_falso
variable "entorno" {
  type    = string
  default = "desarrollo"
}

variable "es_produccion" {
  type    = bool
  default = false
}

resource "aws_instance" "app" {
  ami = "ami-0c55b159cbfafe1f0"

  # Si el entorno es produccion, usar t3.medium; si no, t2.micro
  # Esto nos ahorra dinero en entornos de desarrollo
  instance_type = var.entorno == "produccion" ? "t3.medium" : "t2.micro"

  # Si es producción, usar 100GB de disco; si no, 20GB
  root_block_device {
    volume_size = var.es_produccion ? 100 : 20
  }

  tags = {
    Name = "servidor-${var.entorno}"
    # El tag de backup solo tiene valor útil en producción
    Backup = var.es_produccion ? "habilitado" : "deshabilitado"
  }
}

# Ejemplo con recursos opcionales usando count y condicional
resource "aws_eip" "ip_estatica" {
  # Solo creamos IP estática en producción (count 1 = crear, count 0 = no crear)
  count    = var.es_produccion ? 1 : 0
  instance = aws_instance.app.id
}

Funciones built-in básicas de HCL

Terraform incluye muchas funciones predefinidas. Aquí las más usadas:

Funciones de colecciones

locals {
  # length: devuelve el número de elementos en una lista o mapa
  lista_zonas   = ["us-east-1a", "us-east-1b", "us-east-1c"]
  num_zonas     = length(local.lista_zonas) # Resultado: 3

  # concat: une dos o más listas en una sola
  lista_a       = ["a", "b"]
  lista_b       = ["c", "d"]
  lista_unida   = concat(local.lista_a, local.lista_b) # ["a", "b", "c", "d"]

  # tolist: convierte un set a list (los sets no tienen orden garantizado)
  conjunto      = toset(["z", "a", "m"])
  como_lista    = tolist(local.conjunto)

  # toset: convierte una lista a set (elimina duplicados)
  con_duplicados  = ["a", "b", "a", "c", "b"]
  sin_duplicados  = toset(local.con_duplicados) # {"a", "b", "c"}

  # flatten: aplana una lista de listas en una lista simple
  lista_anidada   = [["a", "b"], ["c", "d"], ["e"]]
  lista_plana     = flatten(local.lista_anidada) # ["a", "b", "c", "d", "e"]

  # distinct: elimina duplicados de una lista manteniendo el orden
  lista_dup       = ["x", "y", "x", "z", "y"]
  lista_unica     = distinct(local.lista_dup) # ["x", "y", "z"]
}

Funciones de strings

locals {
  texto_original = "  Hola Mundo  "

  # upper / lower: convierte a mayúsculas o minúsculas
  en_mayusculas = upper("hola")    # "HOLA"
  en_minusculas = lower("MUNDO")   # "mundo"

  # trimspace: elimina espacios al inicio y al final
  sin_espacios  = trimspace(local.texto_original) # "Hola Mundo"

  # replace: reemplaza texto dentro de un string
  reemplazado   = replace("hola-mundo", "-", "_") # "hola_mundo"

  # split: divide un string en una lista usando un separador
  ips_string    = "10.0.1.0,10.0.2.0,10.0.3.0"
  lista_ips     = split(",", local.ips_string) # ["10.0.1.0", "10.0.2.0", "10.0.3.0"]

  # join: une una lista en un string con un separador
  lista_nombres = ["api", "web", "db"]
  nombres_str   = join("-", local.lista_nombres) # "api-web-db"

  # format: formatea un string (similar a printf en otros lenguajes)
  nombre_bucket = format("bucket-%s-%d", "logs", 2024) # "bucket-logs-2024"

  # substr: extrae una subcadena
  subcadena     = substr("terraform", 0, 5) # "terra"

  # contains: verifica si un string contiene una subcadena
  tiene_prod    = strcontains("entorno-produccion", "produccion") # true
}

Funciones numéricas y de tipo

locals {
  # max / min: valor máximo o mínimo de una lista de números
  maximo = max(5, 3, 8, 1, 9) # 9
  minimo = min(5, 3, 8, 1, 9) # 1

  # ceil / floor: redondeo hacia arriba o hacia abajo
  redondeado_arriba = ceil(4.2)  # 5
  redondeado_abajo  = floor(4.8) # 4

  # tostring: convierte número a string
  numero_como_texto = tostring(42) # "42"

  # tonumber: convierte string a número
  texto_como_numero = tonumber("42") # 42

  # tobool: convierte string a booleano
  texto_como_bool = tobool("true") # true
}

Ejemplo completo con explicación detallada

Veamos un ejemplo real que combina todos los conceptos aprendidos:

# Declaramos las variables de entrada del módulo
variable "entorno" {
  type        = string       # Solo acepta texto
  description = "Entorno de despliegue: desarrollo, staging o produccion"
  default     = "desarrollo" # Valor por defecto si no se especifica

  # Validamos que el valor sea uno de los permitidos
  validation {
    condition     = contains(["desarrollo", "staging", "produccion"], var.entorno)
    error_message = "El entorno debe ser: desarrollo, staging o produccion."
  }
}

variable "num_servidores" {
  type        = number
  description = "Número de servidores a crear"
  default     = 1
}

variable "etiquetas_extra" {
  type        = map(string)  # Mapa de texto a texto
  description = "Etiquetas adicionales para los recursos"
  default     = {}           # Por defecto un mapa vacío
}

# Valores locales: cálculos internos que no son variables de entrada
locals {
  # Combinamos las etiquetas comunes con las extra usando merge
  # merge une dos mapas (el segundo sobreescribe el primero en caso de conflicto)
  etiquetas_completas = merge(
    {
      # Etiquetas que siempre queremos en todos los recursos
      Entorno   = var.entorno
      Proyecto  = "mi-aplicacion"
      Gestionado = "terraform"
      # Fecha de creación usando la función formatdate y timestamp
    },
    var.etiquetas_extra  # Añadimos las etiquetas adicionales del usuario
  )

  # Decidimos el tipo de instancia según el entorno
  # produccion -> t3.medium (más potente)
  # cualquier otro -> t2.micro (más barato)
  tipo_instancia = var.entorno == "produccion" ? "t3.medium" : "t2.micro"

  # El nombre base para los recursos
  nombre_base = "app-${var.entorno}"
}

# Recurso: instancia de servidor
resource "aws_instance" "servidor" {
  # count crea múltiples instancias del mismo recurso
  count = var.num_servidores

  ami           = "ami-0c55b159cbfafe1f0"   # Imagen base de Ubuntu
  instance_type = local.tipo_instancia       # Tipo según entorno

  tags = merge(
    local.etiquetas_completas,  # Todas las etiquetas comunes
    {
      # Añadimos un nombre único para cada servidor usando el índice
      # count.index empieza en 0, por eso sumamos 1
      Name = "${local.nombre_base}-servidor-${count.index + 1}"
    }
  )
}

# Output: valor que queremos exportar
output "ids_servidores" {
  description = "Lista de IDs de todos los servidores creados"
  # Usamos [*] para obtener el atributo 'id' de todos los elementos de count
  value = aws_instance.servidor[*].id
}

output "nombre_entorno" {
  description = "El entorno donde se desplegó la infraestructura"
  value       = upper(var.entorno) # Lo mostramos en mayúsculas
}

Ejercicio práctico

Enunciado

Crea un archivo variables_ejercicio.tf que contenga:

  1. Una variable proyecto de tipo string con valor por defecto "practica-hcl".
  2. Una variable regiones de tipo list(string) con tres regiones de AWS.
  3. Una variable config_db de tipo object con los campos: motor (string), version (string), puerto (number), multiaz (bool).
  4. Un bloque locals que:
    • Calcule el número de regiones usando length.
    • Cree un nombre de bucket S3 concatenando el proyecto y la primera región.
    • Determine si el proyecto es grande basándose en si el número de regiones es mayor a 2.
  5. Tres outputs que muestren el nombre del proyecto en mayúsculas, el nombre del bucket S3 y si el proyecto es grande.

Solución

# =============================================
# Archivo: variables_ejercicio.tf
# Ejercicio de práctica de sintaxis HCL
# =============================================

# 1. Variable de tipo string
variable "proyecto" {
  type        = string
  description = "Nombre del proyecto"
  default     = "practica-hcl"
}

# 2. Variable de tipo list(string) con regiones de AWS
variable "regiones" {
  type        = list(string)
  description = "Lista de regiones de AWS donde se desplegará el proyecto"
  default     = [
    "us-east-1",    # Virginia del Norte (la más común)
    "eu-west-1",    # Irlanda
    "ap-southeast-1" # Singapur
  ]
}

# 3. Variable de tipo object para configuración de base de datos
variable "config_db" {
  type = object({
    motor   = string # Motor de base de datos (mysql, postgres, etc.)
    version = string # Versión del motor
    puerto  = number # Puerto de conexión
    multiaz = bool   # Si se despliega en múltiples zonas de disponibilidad
  })
  description = "Configuración de la base de datos"
  default = {
    motor   = "mysql"
    version = "8.0"
    puerto  = 3306
    multiaz = false
  }
}

# 4. Bloque locals con cálculos
locals {
  # Calculamos el número de regiones en la lista
  num_regiones = length(var.regiones)
  # Resultado con los defaults: 3

  # Creamos el nombre del bucket usando la primera región (índice 0)
  # replace convierte "us-east-1" en "us-east-1" (ya está bien formateado)
  # pero lo usamos para demostrar la interpolación
  nombre_bucket = "${var.proyecto}-${var.regiones[0]}"
  # Resultado con los defaults: "practica-hcl-us-east-1"

  # Determinamos si el proyecto es "grande" (más de 2 regiones)
  es_proyecto_grande = local.num_regiones > 2
  # Resultado con los defaults: true (3 > 2)
}

# 5. Outputs que exportan los valores calculados

# Output 1: nombre del proyecto en mayúsculas
output "nombre_proyecto_mayusculas" {
  description = "Nombre del proyecto en mayúsculas"
  value       = upper(var.proyecto)
  # Resultado: "PRACTICA-HCL"
}

# Output 2: nombre del bucket S3
output "nombre_bucket_s3" {
  description = "Nombre que tendría el bucket S3 del proyecto"
  value       = local.nombre_bucket
  # Resultado: "practica-hcl-us-east-1"
}

# Output 3: si el proyecto es grande
output "es_proyecto_grande" {
  description = "Indica si el proyecto está desplegado en más de 2 regiones"
  value       = local.es_proyecto_grande
  # Resultado: true
}

# Output adicional (bonus): información resumida del proyecto
output "resumen_proyecto" {
  description = "Resumen completo del proyecto"
  value = {
    nombre         = upper(var.proyecto)
    num_regiones   = local.num_regiones
    bucket_s3      = local.nombre_bucket
    es_grande      = local.es_proyecto_grande
    motor_db       = var.config_db.motor
    puerto_db      = var.config_db.puerto
  }
}

Para validar los outputs sin crear infraestructura real, puedes usar terraform console:

# Inicializar el directorio de trabajo
terraform init

# Abrir la consola interactiva de Terraform
terraform console

# Dentro de la consola, prueba:
> var.proyecto
"practica-hcl"

> length(var.regiones)
3

> upper(var.proyecto)
"PRACTICA-HCL"

> local.nombre_bucket
"practica-hcl-us-east-1"

Errores comunes de sintaxis en HCL

Error 1: Olvidar las comillas en strings

# INCORRECTO: Terraform interpretará "produccion" como una referencia
entorno = produccion

# CORRECTO: Los strings siempre entre comillas dobles
entorno = "produccion"

Error 2: Usar comillas simples

# INCORRECTO: HCL no acepta comillas simples
nombre = 'mi-recurso'

# CORRECTO: Solo comillas dobles
nombre = "mi-recurso"

Error 3: Mezclar tipos incompatibles

# INCORRECTO: no puedes asignar un número donde se espera una lista
variable "puertos" {
  type    = list(number)
  default = 80  # Error: se espera una lista, no un número
}

# CORRECTO:
variable "puertos" {
  type    = list(number)
  default = [80]  # Lista con un elemento
}

Error 4: Referencia circular

# INCORRECTO: recurso_a referencia recurso_b y viceversa -> bucle infinito
resource "tipo" "recurso_a" {
  valor = tipo.recurso_b.id  # A depende de B
}

resource "tipo" "recurso_b" {
  valor = tipo.recurso_a.id  # B depende de A -> ERROR!
}

Error 5: Interpolación incorrecta

# INCORRECTO: falta el $ antes de las llaves
nombre = "{var.proyecto}-web"

# INCORRECTO: las llaves no están bien cerradas
nombre = "${var.proyecto-web"

# CORRECTO:
nombre = "${var.proyecto}-web"

Error 6: Indentación con tabulaciones mixtas

# HCL acepta tanto espacios como tabulaciones, pero no mezcles ambos
# Por convención, usa 2 espacios por nivel de indentación
resource "aws_instance" "app" {
  ami           = "ami-123"
  instance_type = "t2.micro"  # 2 espacios de indentación
}

Tip: Usa el comando terraform fmt para formatear automáticamente tu código HCL con el estilo oficial.

# Formatea todos los archivos .tf del directorio actual
terraform fmt

# Formatea y muestra los archivos que fueron modificados
terraform fmt -diff

# Verifica el formato sin modificar (útil en CI/CD)
terraform fmt -check

Resumen

En este módulo has aprendido:

  • HCL es un lenguaje de configuración diseñado para ser legible y expresivo, superior a JSON o YAML para definir infraestructura.
  • La estructura básica de HCL se basa en bloques (con tipo y etiquetas), argumentos y expresiones.
  • Los tipos de datos incluyen primitivos (string, number, bool) y de colección (list, set, map, object, tuple).
  • Los comentarios se escriben con # o // para líneas simples, y /* */ para múltiples líneas.
  • Las referencias (tipo.nombre.atributo) crean dependencias implícitas que Terraform resuelve automáticamente.
  • La interpolación (${...}) permite insertar expresiones dentro de strings.
  • Las expresiones condicionales (condicion ? valor_verdadero : valor_falso) permiten configuración dinámica.
  • Las funciones built-in como length, concat, upper, merge, etc., son herramientas esenciales para manipular datos.

Preparación para el siguiente módulo

En el próximo módulo estudiaremos Variables y Salidas en profundidad. Verás que todo lo aprendido aquí sobre tipos de datos y expresiones es la base para definir variables de entrada reutilizables y outputs que comunican información entre módulos. La sintaxis HCL que has aprendido la usarás constantemente, así que practica escribiendo algunos bloques antes de continuar.

© Copyright 2026. Todos los derechos reservados