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 | Sí | No | Sí |
| Expresiones y funciones | Sí | No | No |
| Interpolación de strings | Sí | No | Limitado |
| Tipado dinámico | Sí | 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:
- Un tipo de bloque (por ejemplo:
resource,variable,output). - Opcionalmente, una o más etiquetas que identifican el bloque.
- 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:
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
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:
- Primero crea
aws_vpc.principal - Luego crea
aws_subnet.publicayaws_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:
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:
- Una variable
proyectode tipo string con valor por defecto"practica-hcl". - Una variable
regionesde tipolist(string)con tres regiones de AWS. - Una variable
config_dbde tipoobjectcon los campos:motor(string),version(string),puerto(number),multiaz(bool). - Un bloque
localsque:- 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.
- Calcule el número de regiones usando
- 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 -checkResumen
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.
Curso de Terraform
Módulo 1: Introducción a Terraform
- ¿Qué es Terraform?
- Instalando Terraform
- Conceptos Básicos de Terraform
- Primera Configuración de Terraform
Módulo 2: Lenguaje de Configuración de Terraform
Módulo 3: Gestión del Estado
Módulo 4: Módulos de Terraform
Módulo 5: Aprovisionamiento de Recursos
- Conceptos Básicos de Aprovisionamiento
- Aprovisionamiento de Recursos AWS
- Aprovisionamiento de Recursos Azure
- Aprovisionamiento de Recursos GCP
Módulo 6: Funcionalidades Avanzadas de Terraform
Módulo 7: Mejores Prácticas de Terraform
- Organización del Código
- Control de Versiones
- Pruebas del Código de Terraform
- Mejores Prácticas de Seguridad
Módulo 8: Terraform en CI/CD
- Integración de Terraform con CI/CD
- Automatización de Terraform con Jenkins
- Uso de Terraform con GitHub Actions
- Terraform Cloud y Enterprise
