Translate

jueves, 8 de octubre de 2026

Locust desde cero: nuestro primer test de carga


Cuando desarrollamos una API REST normalmente hacemos tests para comprobar que funciona correctamente.


Por ejemplo:

¿El GET devuelve los productos?

¿El POST crea correctamente un producto?

¿El DELETE elimina el recurso?


Pero hay otra pregunta importante: ¿Qué pasa si 100 usuarios utilizan nuestra API al mismo tiempo?

Ahí entran las pruebas de carga.

En este post vamos a conocer Locust y hacer nuestro primer test de carga sobre una API REST.

No vamos a construir una API complicada. Vamos a suponer que ya tenemos una API CRUD de productos y nos vamos a concentrar exclusivamente en probarla.


¿Qué es Locust? Locust es una herramienta para realizar pruebas de carga.

Una de sus características más interesantes es que los escenarios de prueba se escriben en Python.

En lugar de tener que configurar un montón de archivos, podemos describir el comportamiento de nuestros usuarios directamente mediante código.


La idea es sencilla: Locust crea usuarios virtuales que realizan requests contra nuestra aplicación y nos permite observar cómo responde el sistema bajo carga.


Para el ejemplo vamos a utilizar una API CRUD para administrar productos.

Un producto tiene:

Product

 ├── id

 ├── name

 ├── price

 └── stock


Y la API expone las operaciones habituales:


GET    /products

GET    /products/{id}

POST   /products

PUT    /products/{id}

DELETE /products/{id}


Por ejemplo:


GET /products


podría devolver:

[

  {

    "id": 1,

    "name": "Notebook",

    "price": 1200,

    "stock": 10

  },

  {

    "id": 2,

    "name": "Mouse",

    "price": 30,

    "stock": 50

  }

]


Para nuestro primer test solamente vamos a utilizar:

GET /products


Locust es una aplicación Python.

Podemos instalarlo con:

pip install locust


Comprobamos que quedó instalado:

locust --version


Ahora creemos un archivo:

locustfile.py


Este archivo contendrá nuestro escenario de prueba.

Comencemos con algo muy simple:


from locust import HttpUser, task, between


class ProductUser(HttpUser):


    wait_time = between(1, 3)


    @task

    def get_products(self):

        self.client.get("/products")


Aunque son pocas líneas, acá ya estamos describiendo un usuario virtual.

Veamos qué significa cada parte.


class ProductUser(HttpUser):


HttpUser representa un usuario que realizará requests HTTP.

Cuando Locust ejecute nuestra prueba, creará usuarios virtuales basándose en esta clase.


Tenemos:

wait_time = between(1, 3)


Esto indica que el usuario esperará entre 1 y 3 segundos entre una tarea y otra.

Es importante porque normalmente un usuario real no realiza requests continuamente sin esperar.


Ahora tenemos:


@task

def get_products(self):

    self.client.get("/products")


La anotación @task indica que este método representa una acción que nuestro usuario puede realizar.

En este caso, la acción es: consultar productos


Y finalmente:

self.client.get("/products")


realiza el request HTTP.

Es decir, nuestro escenario completo básicamente dice: "Soy un usuario que consulta la lista de productos y espero entre 1 y 3 segundos antes de volver a hacerlo."


Supongamos que nuestra API está corriendo en: http://localhost:8080

Podemos iniciar Locust con:


locust -f locustfile.py --host http://localhost:8080


Locust levantará su interfaz web.

Por defecto podremos acceder a: http://localhost:8089

Desde ahí podemos configurar nuestra prueba.


Por ejemplo:

Number of users: 10

Spawn rate:      2


Estamos diciendo: Queremos simular 10 usuarios y crear 2 usuarios nuevos por segundo hasta llegar a los 10.

Cada uno de esos usuarios ejecutará el comportamiento que definimos en ProductUser.


Locust nos permite hacer este tipo de preguntas:

  • ¿Cuánto tarda?
  • ¿Cuántos requests por segundo soporta?
  • ¿Cuántos requests fallan?
  • ¿Qué ocurre cuando aumentamos la cantidad de usuarios?
  • ¿Cuánto tarda el 95% de los requests?


Por ejemplo, podríamos obtener resultados similares a:


GET /products


Requests:        10.000

Failures:             0

Average:            42 ms

Median:             35 ms

95%:                80 ms

99%:               120 ms

Requests/sec:       85


El dato que más llama la atención puede ser el percentil 95.


Si tenemos:

95% → 80 ms


significa que el 95% de los requests respondió en 80 ms o menos.


El 5% restante tardó más.

Esto suele ser mucho más útil que mirar únicamente el promedio.

Pero una aplicación real no tiene usuarios que solamente ejecutan un GET.


Un usuario podría:


GET /products

      ↓

GET /products/10

      ↓

POST /products

      ↓

GET /products


Y probablemente algunas operaciones ocurran con mayor frecuencia que otras.


Por ejemplo:

80% → GET /products

15% → GET /products/{id}

 5% → POST /products


Locust nos permite modelar ese comportamiento mediante diferentes @task.

Pero eso lo dejamos para el próximo paso.


La idea fundamental que quiero llevarme de este primer ejemplo es muy simple: Locust no prueba solamente endpoints.


Podemos utilizar Python para describir el comportamiento de usuarios virtuales y luego ejecutar ese comportamiento contra nuestra aplicación bajo diferentes niveles de carga.


Dejo link: https://locust.io/


miércoles, 7 de octubre de 2026

Terraform avanzado: count, for_each y expresiones


Hasta ahora vimos cómo describir infraestructura con Terraform.


Aprendimos a crear resources:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola!"

}


También vimos variables:


variable "environment" {

  type = string

}



Y módulos:


module "application" {

  source = "./modules/application"

}


Pero hay un problema.

En infraestructura real, muchas veces no queremos crear un único recurso.


Queremos crear:

  • 3 máquinas
  • 5 buckets
  • 10 reglas
  • 2 redes


Y probablemente tampoco queramos escribir diez veces el mismo bloque.

Ahí Terraform empieza a mostrar otra de sus características interesantes: podemos utilizar expresiones para construir infraestructura de forma dinámica.


Empecemos con algo sencillo. Supongamos que queremos crear tres archivos.

Sin count tendríamos que escribir:


resource "local_file" "file1" {

  filename = "file1.txt"

  content  = "Archivo 1"

}


resource "local_file" "file2" {

  filename = "file2.txt"

  content  = "Archivo 2"

}


resource "local_file" "file3" {

  filename = "file3.txt"

  content  = "Archivo 3"

}


Funciona.

Pero estamos repitiendo código.

Terraform nos permite utilizar count:


resource "local_file" "file" {

  count = 3


  filename = "file-${count.index}.txt"

  content  = "Archivo ${count.index}"

}


Ahora Terraform crea tres instancias del resource.


local_file.file[0]

local_file.file[1]

local_file.file[2]


El valor: count.index representa el índice de la instancia actual.


Por lo tanto:

count.index = 0


genera:

file-0.txt


Después:

count.index = 1


genera:

file-1.txt


Y finalmente:

count.index = 2


genera:

file-2.txt


Lo interesante es que count puede depender de una variable.


Por ejemplo:


variable "file_count" {

  type    = number

  default = 3

}


Nuestro resource:


resource "local_file" "file" {

  count = var.file_count


  filename = "file-${count.index}.txt"

  content  = "Archivo ${count.index}"

}


Ahora podemos cambiar:


file_count = 10


y Terraform creará diez recursos.


Tenemos entonces:

variable

    │

    ▼

file_count = 10

    │

    ▼

  count

    │

    ▼

10 resources


Esto ya resulta bastante útil para infraestructura real.


Pero count tiene una limitación


Supongamos que tenemos:

count = 3


Terraform identifica los recursos por posición:

resource[0]

resource[1]

resource[2]


Ahora imaginemos que queremos eliminar el segundo elemento.

Por ejemplo, teníamos:

[ "web", "api", "database" ]


y queremos:

[ "web", "database" ]


Si utilizamos count, las posiciones cambian:


Antes:

0 → web

1 → api

2 → database


Después:

0 → web

1 → database


Terraform puede interpretar que:

resource[1]


cambió de api a database.

Y eso puede provocar cambios que no esperábamos.

Cuando los elementos tienen una identidad propia, count muchas veces no es la mejor opción.

Ahí aparece: for_each


for_each permite crear múltiples instancias utilizando una colección.


Por ejemplo:


variable "files" {

  type = set(string)


  default = [

    "hello",

    "goodbye",

    "config"

  ]

}


Podemos hacer:


resource "local_file" "file" {

  for_each = var.files


  filename = "${each.key}.txt"

  content  = "Archivo ${each.key}"

}



Terraform creará:

hello.txt

goodbye.txt

config.txt


Pero ahora los recursos tienen una identidad basada en su clave:


local_file.file["hello"]

local_file.file["goodbye"]

local_file.file["config"]



Esto es diferente de:


local_file.file[0]

local_file.file[1]

local_file.file[2]


Podemos resumir la diferencia:

  • count : instancias identificadas por índice
  • for_each : instancias identificadas por clave

Por ejemplo:

count:

server[0]

server[1]

server[2]


Mientras que:

for_each:


server["web"]

server["api"]

server["database"]


Cuando cada elemento tiene una identidad propia, for_each suele expresar mejor nuestra intención.

for_each resulta especialmente interesante con mapas.


Por ejemplo:

variable "servers" {

  type = map(string)


  default = {

    web      = "small"

    api      = "medium"

    database = "large"

  }

}


Podemos escribir:


resource "local_file" "server" {

  for_each = var.servers


  filename = "${each.key}.txt"

  content  = "Server type: ${each.value}"

}


Ahora:

each.key


representa:

web

api

database


y:

each.value


representa:

small

medium

large


Por ejemplo, para web:

each.key   = "web"

each.value = "small"


Hasta ahora usamos for_each para crear recursos.

Pero Terraform también tiene expresiones for que permiten transformar colecciones.


Supongamos:


variable "names" {

  default = [

    "juan",

    "pedro",

    "ana"

  ]

}


Podemos crear una nueva lista:


locals {

  upper_names = [

    for name in var.names : upper(name)

  ]

}


El resultado será:

[

  "JUAN",

  "PEDRO",

  "ANA"

]


La sintaxis básica es:


[

  for elemento in coleccion : expresion

]


Es bastante parecida a un map en otros lenguajes o listas por comprensión.


Podemos pensarlo como:


colección

    │

    ▼

   for

    │

    ▼

transformación

    │

    ▼

nueva colección


Las expresiones for también pueden filtrar elementos.


Por ejemplo:


variable "numbers" {

  default = [1, 2, 3, 4, 5, 6]

}


Podemos quedarnos solamente con los números pares:


locals {

  even_numbers = [

    for number in var.numbers : number

    if number % 2 == 0

  ]

}


El resultado: [2, 4, 6]


La parte:

if number % 2 == 0

funciona como un filtro.


También podemos producir mapas.

Por ejemplo:


variable "names" {

  default = [

    "juan",

    "pedro",

    "ana"

  ]

}


Podemos construir:


locals {

  name_lengths = {

    for name in var.names :

    name => length(name)

  }

}


El resultado sería:

{

  juan  = 4

  pedro = 5

  ana   = 3

}



La expresión:

name => length(name)


define:

clave => valor


Esto es particularmente útil cuando necesitamos transformar datos antes de pasarlos a un resource o módulo.

Terraform también permite utilizar expresiones condicionales.

La sintaxis es:

condicion ? valor_si_true : valor_si_false


Por ejemplo:

variable "environment" {

  type = string

}


Podemos definir:

locals {

  instance_size = var.environment == "production"

    ? "large"

    : "small"

}


Esto permite adaptar nuestra infraestructura dependiendo del ambiente.


En los ejemplos anteriores apareció algo nuevo:


locals {

  instance_size = ...

}


Los locals permiten definir valores calculados que queremos reutilizar dentro de nuestra configuración.


Por ejemplo:


locals {

  environment = var.environment


  name_prefix = "myapp-${var.environment}"


  instance_size = var.environment == "production"

    ? "large"

    : "small"

}


Después podemos utilizarlos:


resource "..." "..." {

  name = local.name_prefix

}


La diferencia conceptual es:


variable

   ↓

valor que recibimos


local

   ↓

valor que calculamos


Las variables forman parte de la interfaz de nuestra configuración.

Los locals son principalmente una herramienta para organizar valores derivados.


Ahora podemos hacer algo un poco más interesante.

Supongamos que tenemos:


variable "environment" {

  type    = string

  default = "development"

}


variable "servers" {

  type = map(string)


  default = {

    web = "small"

    api = "small"

  }

}


Podemos definir:


locals {

  name_prefix = "myapp-${var.environment}"

}


Y utilizar for_each:


resource "local_file" "server" {

  for_each = var.servers


  filename = "${local.name_prefix}-${each.key}.txt"


  content = <<EOF

Environment: ${var.environment}

Server: ${each.key}

Size: ${each.value}

EOF

}


En development tendremos:

myapp-development-web.txt

myapp-development-api.txt


Y el contenido de cada archivo será diferente.


¿Cuándo utilizar count?

Una regla práctica puede ser:

Utilizá count cuando las instancias son esencialmente iguales y su identidad puede estar determinada por una posición o simplemente necesitás activar/desactivar un recurso.


Por ejemplo:

count = var.create_monitoring ? 1 : 0


Esto es muy común.


Si:

create_monitoring = true

tenemos:


monitoring[0]

Si es false:

ningún recurso


Este patrón es bastante útil para recursos opcionales.


¿Cuándo utilizar for_each?

Utilizá for_each cuando cada instancia tiene una identidad propia.


Por ejemplo:

  • web
  • api
  • database


es mucho más expresivo que:

  • 0
  • 1
  • 2


Con:

for_each = var.servers


podemos identificar los recursos mediante:

server["web"]

server["api"]

server["database"]


Esto además hace que los cambios en la colección sean más predecibles.


Cuando venimos de lenguajes como Java, C#, C++ o Python, es tentador pensar:

for_each es un for que ejecuta varias veces el resource.

No exactamente.

Terraform es declarativo.

No estamos escribiendo un programa que ejecuta:


for

    crear recurso

    crear recurso

    crear recurso


Estamos describiendo:"Quiero una instancia de este resource por cada elemento de esta colección."

Terraform después determina qué necesita hacer para conseguir ese resultado.

Esta diferencia de mentalidad se vuelve cada vez más importante a medida que las configuraciones crecen.


Entonces, tenemos varias herramientas para trabajar con colecciones:

count: Crea varias instancias indexadas:


resource "..." "server" {

  count = 3

}


Resultado:

server[0]

server[1]

server[2]


for_each: Crea instancias identificadas por claves:


resource "..." "server" {

  for_each = var.servers

}


Resultado:

server["web"]

server["api"]


for: Transforma colecciones:


[

  for item in var.items : transform(item)

]


condicional: Permite seleccionar valores:


condition ? value1 : value2


locals: Permite definir valores calculados reutilizables:


locals {

  name = "myapp-${var.environment}"

}


Hasta ahora utilizábamos Terraform principalmente para entender sus conceptos.

Con count, for_each, for, condicionales y locals empezamos a tener herramientas para construir configuraciones que se adapten a diferentes situaciones.



martes, 6 de octubre de 2026

Terraform: módulos para reutilizar infraestructura


En los artículos anteriores fuimos construyendo las piezas fundamentales de Terraform.


Ya vimos:

  • Resources.
  • Providers.
  • Variables.
  • Outputs.
  • State.


Hasta ahora nuestros ejemplos fueron pequeños.

Pero imaginemos un proyecto real.


Podríamos tener:

  • network
  • database
  • application
  • load balancer
  • monitoring
  • security


Y cada una de esas partes puede tener varios recursos.

Nuestro main.tf podría empezar a crecer. Y además podemos terminar repitiendo configuraciones.

Por ejemplo, quizás necesitamos crear la misma infraestructura para:

  • development
  • staging
  • production



¿Cómo evitamos copiar y pegar cientos de líneas?

La respuesta son los modules.


¿Qué es un módulo?

Un módulo de Terraform es, básicamente, un conjunto de archivos Terraform que encapsula una determinada funcionalidad o parte de nuestra infraestructura.

Podemos pensar en un módulo como una función.


Una función recibe parámetros:


entrada

   ↓

 función

   ↓

 resultado


Un módulo hace algo conceptualmente parecido:


variables

    ↓

  módulo

    ↓

resources

    ↓

outputs


Por ejemplo, podríamos crear un módulo llamado: database. Que se encargue de crear una base de datos completa.

Desde afuera solamente necesitamos decir:

  • Quiero una database
  • con este nombre
  • con este tamaño
  • en este ambiente


El módulo se ocupa del resto.


Vamos a crear una estructura sencilla:


terraform/

│

├── main.tf

│

└── modules/

    │

    └── file/

        │

        ├── main.tf

        ├── variables.tf

        └── outputs.tf


Nuestro módulo estará en:

modules/file


Y tendrá tres archivos.

En:

modules/file/main.tf


podemos tener:


resource "local_file" "file" {

  filename = var.filename

  content  = var.content

}


El módulo necesita dos valores:

  • filename
  • content


Por eso los definimos como variables.


En:

modules/file/variables.tf


tenemos:


variable "filename" {

  type = string

}


variable "content" {

  type = string

}


Ahora nuestro módulo tiene una interfaz. Desde afuera no necesitamos saber cómo está implementado.

Solamente necesitamos conocer sus entradas.

Podemos devolver información desde el módulo.


En:

modules/file/outputs.tf


escribimos:


output "filename" {

  value = local_file.file.filename

}


Ahora nuestro módulo tiene:


Entradas

   │

   ├── filename

   └── content

        │

        ▼

      módulo

        │

        ▼

     resource

        │

        ▼

     Outputs

        │

        └── filename


Ya tenemos algo parecido a una función.

Ahora podemos utilizar nuestro módulo desde el main.tf principal:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


module "hello" {

  source = "./modules/file"

  filename = "hello.txt"

  content  = "Hola desde un módulo!"

}


La parte importante es:


module "hello" {

  source = "./modules/file"

  filename = "hello.txt"

  content  = "Hola desde un módulo!"

}


Estamos diciendo: Utilizá el módulo que está en `./modules/file`.

Y le pasamos sus parámetros.

Nuestro main.tf ya no necesita conocer los detalles internos del resource. Y esto nos permite separar responsabilidades. El módulo sabe cómo crear el recurso.

El código principal sabe qué recurso quiere utilizar.

Podemos utilizar el mismo módulo varias veces.


Por ejemplo:


module "hello" {

  source = "./modules/file"


  filename = "hello.txt"

  content  = "Hola!"

}


module "goodbye" {

  source = "./modules/file"


  filename = "goodbye.txt"

  content  = "Chau!"

}


Tenemos dos instancias del mismo módulo.

No copiamos la implementación. La reutilizamos.


Recordemos que nuestro módulo tiene:


output "filename" {

  value = local_file.file.filename

}


Desde el módulo principal podemos acceder al output:


output "hello_file" {

  value = module.hello.filename

}


La sintaxis:

module.hello.filename


significa:

module

  │

  └── instancia "hello"

           │

           └── output "filename"


Esto es muy parecido a acceder al resultado de una función.

Este concepto es muy importante.

Nuestro módulo tiene una especie de API.


Entradas:

  • filename
  • content


Salidas:

  • filename


Internamente podría tener:

  • 10 resources
  • 20 resources
  • 100 resources


Pero quien lo utiliza no necesita conocer todos esos detalles. Esto permite encapsular complejidad.

Y en infraestructura real esto es extremadamente útil.


Supongamos que nuestra aplicación necesita una máquina virtual.

Podríamos tener un módulo:


modules/

└── application/

    ├── main.tf

    ├── variables.tf

    └── outputs.tf


Internamente podría crear:

  • Virtual Machine
  • Network Interface
  • Security Group
  • Disk


Desde nuestro proyecto principal podríamos utilizarlo así:


module "application" {

  source = "./modules/application"


  environment = "production"

  instance_type = "small"

}


El módulo podría encargarse de todos los detalles.

El código principal queda mucho más limpio:


main.tf


  application

      │

      ├── network

      ├── machine

      ├── security

      └── disk


En lugar de tener cientos de recursos directamente en el main.tf.


Ahora podemos combinar lo que vimos en artículos anteriores.


Supongamos que queremos:

  • development
  • staging
  • production


Podemos tener diferentes configuraciones que utilizan los mismos módulos.


Por ejemplo:


terraform/

│

├── modules/

│   └── application/

│

├── environments/

│   ├── development/

│   │   └── main.tf

│   │

│   ├── staging/

│   │   └── main.tf

│   │

│   └── production/

│       └── main.tf


En development:


module "application" {

  source = "../../modules/application"


  environment   = "development"

  instance_type = "small"

}


En producción:


module "application" {

  source = "../../modules/application"


  environment   = "production"

  instance_type = "large"

}


El módulo es el mismo.

Lo que cambia es la configuración que le pasamos.

Esto nos permite evitar una práctica peligrosa:


development/

    copia de todo Terraform


production/

    otra copia de todo Terraform


Si copiamos infraestructura, tarde o temprano las copias empiezan a divergir.

Con módulos podemos reutilizar la implementación.

Hasta ahora utilizamos:


source = "./modules/application"


Es decir, un módulo local.

Pero Terraform también permite utilizar módulos almacenados en otros lugares.


Por ejemplo, podemos obtener módulos desde:

  • Git;
  • registros de módulos;
  • repositorios privados;
  • otros sistemas compatibles con Terraform.


Un ejemplo podría ser:

module "application" {

  source = "git::https://example.com/infrastructure.git"

}


La idea es que un módulo puede convertirse en una pieza reutilizable entre diferentes proyectos.


Los módulos también pueden tener versiones. Cuando una infraestructura se vuelve importante, queremos evitar que una actualización de un módulo rompa todos nuestros ambientes.

Por eso es habitual trabajar con versiones.

Un proyecto puede utilizar una versión determinada.

Esto es especialmente importante cuando los módulos son compartidos por diferentes equipos.


Este es un error bastante común cuando empezamos a utilizar módulos.

Podemos terminar haciendo:

module "resource1"

module "resource2"

module "resource3"

module "resource4"


para absolutamente todo.

Y terminamos con una infraestructura más complicada que la original.

Un módulo tiene sentido cuando encapsula una unidad reutilizable o conceptualmente coherente.


Por ejemplo:

  • network
  • database
  • application
  • monitoring


pueden ser buenos candidatos.

Crear un módulo solamente porque tenemos un resource:

module "s3_bucket"

no necesariamente aporta valor.


Depende del proyecto y de cuánto comportamiento adicional encapsule.


Una forma interesante de pensar Terraform es como si estuviéramos construyendo una aplicación.

En una aplicación tenemos:

  • clases
  • funciones
  • componentes
  • librerías


En Terraform podemos tener:

  • resources
  • modules
  • providers


Un módulo puede encapsular una parte de nuestra infraestructura de la misma manera que una librería encapsula funcionalidad de una aplicación.


Por ejemplo:


Aplicación

│

├── UserService

├── PaymentService

└── NotificationService


Terraform:


Infraestructura

│

├── Network

├── Database

└── Application


La comparación no es perfecta, pero ayuda a entender la idea.


Un módulo no es solamente una forma de ordenar archivos. Este punto es importante.

Podríamos pensar:"Entonces los módulos sirven solamente para tener el código más ordenado."

No.

Su verdadero valor aparece cuando conseguimos:

  • reutilizar infraestructura;
  • encapsular detalles;
  • definir interfaces mediante variables;
  • devolver resultados mediante outputs;
  • versionar componentes;
  • compartir componentes entre proyectos.


Por eso un módulo puede convertirse en una verdadera unidad de diseño de infraestructura.


Terraform State: qué es y por qué es tan importante


En los artículos anteriores vimos cómo Terraform permite definir infraestructura como código.

También vimos que, después de ejecutar Terraform, aparece un archivo llamado:


terraform.tfstate



¿Qué es exactamente?

¿Por qué Terraform necesita guardar información sobre los recursos que creó?

Y quizás la pregunta más importante: ¿Por qué no alcanza con mirar nuestra configuración .tf?

La respuesta está en uno de los conceptos fundamentales de Terraform: el State.


Supongamos que tenemos:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola!"

}


Ejecutamos:

terraform apply


Terraform crea:

hello.txt


Hasta acá todo parece sencillo.


Pero imaginemos que mañana ejecutamos nuevamente:

terraform apply


Terraform tiene que decidir: "¿Tengo que crear el archivo otra vez?"

La respuesta es no.

Terraform necesita saber que ese recurso ya existe y está siendo administrado.

Ahí aparece el State.

El State es información que Terraform mantiene sobre la infraestructura que administra.

Por defecto se almacena en:

terraform.tfstate


Podemos pensar que Terraform tiene tres fuentes de información:

  • La configuración describe lo que queremos.
  • La infraestructura representa lo que existe realmente.
  • El State ayuda a Terraform a saber qué recursos está administrando y cómo se relacionan con la configuración.

Podríamos pensar: "Entonces terraform.tfstate es simplemente otra versión de main.tf."

No.


Son cosas diferentes.

Nuestro código podría decir:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola!"

}


Mientras que el State contiene información sobre el recurso que Terraform está administrando.

Por ejemplo, puede almacenar:

  • el tipo de recurso;
  • su identificador;
  • atributos conocidos;
  • información del provider;
  • relaciones entre recursos;
  • valores que Terraform necesita para gestionar ese recurso.


El State funciona como una especie de mapa entre nuestra configuración y la infraestructura real.

Podemos crear un ejemplo pequeño:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola Terraform!"

}


Inicializamos:

terraform init


Y aplicamos:

terraform apply


Ahora tendremos:

hello.txt

terraform.tfstate


Podemos consultar el State mediante:

terraform show


Terraform nos muestra información sobre los recursos que administra.

También podemos listar los recursos que forman parte del State:

terraform state list


Obtendremos algo parecido a:

local_file.hello


Esto nos dice que Terraform está administrando ese recurso.

¿Qué pasa si ejecutamos apply otra vez?

Terraform no debería intentar crear otro archivo.

En lugar de eso veremos algo similar a:

No changes. Your infrastructure matches the configuration.


¿Por qué?

Porque Terraform sabe que:

  1. nuestra configuración declara local_file.hello;
  2. ese recurso ya está registrado en el State;
  3. el recurso existe;
  4. no hay cambios que aplicar.


Por eso Terraform es idempotente en este sentido:


apply

  ↓

crear recurso


apply nuevamente

  ↓

no hay cambios


¿Y si modificamos la configuración?

Supongamos que cambiamos:


content = "Hola Terraform!"


por:


content = "Hola mundo!"



Ahora ejecutamos:

terraform plan


Terraform detecta que nuestra configuración cambió.

Podríamos ver algo parecido a:

~ local_file.hello

  content: "Hola Terraform!" -> "Hola mundo!"


El símbolo: ~ indica que Terraform planea modificar un recurso existente.


Después:

terraform apply


Terraform actualiza el recurso.

¿De dónde sabe Terraform qué cambió?

Esta es justamente una de las razones por las que existe el State.

Terraform puede comparar diferentes piezas de información:


               configuración

                         │

                        ▼

                  Terraform

                         │

        ┌─────┴─────┐

        ▼                             ▼

      State                    infraestructura


A partir de esa información Terraform puede determinar qué acciones necesita realizar.


Por ejemplo:

crear

modificar

destruir

no hacer nada


Esto se conoce como planificación de cambios.

Ahora viene una situación muy interesante.

Supongamos que Terraform creó una infraestructura y todo está funcionando correctamente.

Pero alguien entra directamente a la consola del proveedor cloud y modifica algo.


Por ejemplo:

Terraform:

instance_type = "small"


Pero alguien entra manualmente y cambia la máquina a:

instance_type = "large"


Ahora tenemos una diferencia: La infraestructura real ya no coincide con lo que Terraform espera.

A esto normalmente se lo llama drift.


Es decir: La infraestructura se alejó del estado que Terraform espera administrar.


¿Qué puede hacer Terraform?

Cuando ejecutamos:

terraform plan


Terraform consulta información del provider y puede detectar diferencias entre lo que conoce y lo que existe.


Por ejemplo, podría mostrarnos que una configuración cambió.

Entonces Terraform puede proponer una corrección.


La idea es volver a:

Configuración

      │

      ▼

   Terraform

      │

      ▼

Infraestructura deseada


En otras palabras: Terraform puede utilizarse para mantener la infraestructura alineada con una configuración declarativa.

¿Terraform detecta absolutamente todo?

No. Esto es importante.

Terraform depende de lo que el provider pueda consultar y representar.

Además, no todos los cambios externos necesariamente tienen el mismo comportamiento.

Por eso no deberíamos pensar: "Terraform mágicamente sabe todo lo que ocurrió."

Terraform trabaja con la información que obtiene de los providers y con el State que mantiene.


¿Qué ocurre si eliminamos terraform.tfstate? Esta es una prueba interesante.

Supongamos que tenemos:

terraform.tfstate

hello.txt


Si eliminamos:

terraform.tfstate


pero dejamos:

hello.txt


la infraestructura sigue existiendo.

Pero Terraform perdió su información sobre ese recurso.


Si volvemos a ejecutar:

terraform plan


Terraform puede considerar que el recurso declarado en la configuración no está registrado en el State.

Es decir, tenemos:


Configuración

      │

      ▼

local_file.hello


   State

      │

      ▼

     vacío


Infraestructura

      │

      ▼

  hello.txt


Esto muestra algo importante: El State no es la infraestructura.

Si borramos el State, no necesariamente borramos la infraestructura.

Hemos perdido la información que Terraform utilizaba para administrarla.


Entonces, ¿podemos borrar el State?

No deberíamos hacerlo arbitrariamente.

En un proyecto real, perder el State puede ser un problema serio.


Por eso es habitual:

  • almacenarlo de forma segura;
  • realizar backups;
  • restringir el acceso;
  • utilizar un backend apropiado;
  • evitar modificarlo manualmente.


Y esto nos lleva a otro problema.

¿Qué pasa cuando trabajamos en equipo?

Supongamos que tenemos:


Emanuel

   │

   └── terraform apply


Juan

   │

   └── terraform apply


Si ambos tienen una copia local de:

terraform.tfstate

tenemos un problema.


Cada uno podría tener un State diferente.


Por ejemplo:

Emanuel

terraform.tfstate

      │

      └── versión A


Juan

terraform.tfstate

      │

      └── versión B


¿Cuál es el correcto?

Por eso, en equipos, normalmente no queremos guardar el State únicamente en el disco local de cada desarrollador.


Necesitamos un backend remoto.

Terraform permite almacenar el State en diferentes backends.


Por ejemplo:


Terraform

    │

    ▼

Backend remoto

    │

    ├── State

    └── Locking


Algunos proveedores cloud ofrecen almacenamiento adecuado para esto.

Por ejemplo, un equipo que trabaja con AWS puede utilizar un backend basado en S3.

La configuración puede tener una forma similar a:


terraform {

  backend "s3" {

    bucket = "terraform-state"

    key    = "production/terraform.tfstate"

    region = "us-east-1"

  }

}


La configuración exacta depende de la versión de Terraform y del backend que utilicemos, pero la idea fundamental es: El State deja de estar únicamente en la máquina de un desarrollador y pasa a estar almacenado en un lugar compartido.


¿Y si dos personas ejecutan Terraform al mismo tiempo?

Este problema es todavía más interesante.

Imaginemos:


Emanuel

   │

   ├── terraform apply

   │

   ▼

   State


Juan

   │

   ├── terraform apply

   │

   ▼

   State


Si ambos intentan modificar la infraestructura simultáneamente, podemos terminar con operaciones que interfieren entre sí.


Por eso aparece otro concepto importante: State locking

Algunos backends permiten bloquear el State mientras una operación está en curso.

Conceptualmente:


Emanuel

   │

   └── apply

         │

         ▼

       LOCK 🔒

         │

         ▼

       State

         

Juan

   │

   └── apply

         │

         ▼

    espera/rechazo


De esta manera evitamos que dos operaciones modifiquen simultáneamente el mismo State.


¿Podemos guardar terraform.tfstate en Git?


En general, no es una buena práctica para proyectos reales.

Hay varias razones.

La primera es que el State puede contener información sensible.


Por ejemplo:

  • identificadores;
  • atributos de infraestructura;
  • información de configuración;
  • y, dependiendo del provider y de los recursos, valores que no queremos distribuir libremente.


Además, Git no es un backend diseñado para coordinar el acceso concurrente al State.

Por eso normalmente tenemos:


Código Terraform

      │

      ▼

     Git


pero:


Terraform State

      │

      ▼

Backend remoto


Son responsabilidades diferentes.

Después de conocer el State, podemos entender mejor qué ocurre cuando ejecutamos:

terraform plan


No es simplemente: "Leé los `.tf` y ejecutá los comandos."

Terraform tiene que construir un plan considerando:


                         Configuración

                                  │

                                 ▼

                           Terraform

                                 │

        ┌────────┴────────┐

        ▼                                              ▼

      State                                  infraestructura

        │                                                │

        └────────┬────────┘

                                 ▼

                               Plan

                                 │

                                ▼

                     cambios necesarios


Por ejemplo:

+ crear

~ modificar

- destruir


Y recién después:

terraform apply


se aplican esos cambios.


Podemos resumir todo esto con una frase: La configuración dice lo que queremos, la infraestructura dice lo que existe y el State permite a Terraform relacionar ambas cosas.

Por eso el State es tan importante.

No es un simple archivo temporal.

Es una parte fundamental del funcionamiento de Terraform.

sábado, 3 de octubre de 2026

Terraform desde cero: variables, outputs y recursos


En el artículo anterior vimos qué problema intenta resolver Terraform y qué significa Infrastructure as Code.

La idea fundamental era: En lugar de configurar nuestra infraestructura manualmente, podemos describirla mediante código.

Ahora vamos a dar el siguiente paso.

En este artículo vamos a conocer tres conceptos que aparecen prácticamente en cualquier proyecto de Terraform:

  • Resources
  • Variables
  • Outputs

Y vamos a terminar viendo dónde aparece un concepto fundamental: el state.


Resources: los recursos que queremos crear

El concepto más importante de Terraform probablemente sea el de resource.

Un resource representa algo que queremos administrar.


Por ejemplo:

  • una máquina virtual;
  • una base de datos;
  • un bucket;
  • una red;
  • una regla de firewall;
  • un DNS;
  • un archivo local.


En el artículo anterior utilizamos:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


La estructura general es:


resource "TIPO" "NOMBRE" {

  ...

}


Por ejemplo:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Tenemos dos nombres importantes:


local_file

    │

    └── tipo de recurso


hello

    │

    └── nombre del recurso


Por lo tanto, dentro de Terraform podemos referirnos a este recurso como:

local_file.hello


Es importante entender que local_file no es algo que Terraform conozca mágicamente.

Lo proporciona un provider.


Providers

Terraform utiliza providers para comunicarse con diferentes plataformas y servicios.

Por ejemplo, existen providers para trabajar con:

  • AWS
  • Azure
  • Google Cloud
  • Kubernetes
  • GitHub
  • Docker
  • bases de datos
  • recursos locales


Por ejemplo, para nuestro ejemplo local:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


Y entonces podemos utilizar:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Podemos pensar en el provider como el componente que sabe cómo comunicarse con el sistema que estamos administrando.


Terraform sabe interpretar nuestra configuración.

El provider sabe cómo convertir esa configuración en operaciones sobre un servicio concreto.

Supongamos ahora que queremos crear dos archivos:


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


resource "local_file" "goodbye" {

  filename = "goodbye.txt"

  content  = "Chau desde Terraform!"

}


Funciona perfectamente.

Pero imaginemos que el nombre de nuestro ambiente aparece en varios lugares:


resource "local_file" "config" {
  filename = "production-config.txt"
  content  = "Configuración de production"
}


¿Qué pasa si mañana queremos utilizar exactamente la misma configuración para development?

Podríamos copiar el archivo y modificar los valores.

Pero eso rápidamente empieza a convertirse en un problema.

Queremos parametrizar nuestra infraestructura.

Ahí aparecen las variables.


Variables

Una variable permite definir un valor que puede cambiar sin modificar directamente nuestro resource.

Por ejemplo:

variable "environment" {
  type    = string
  default = "development"
}

Ahora podemos utilizarla:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = "Configuración de ${var.environment}"
}


Terraform reemplazará:

${var.environment}

por el valor correspondiente.


Con el valor por defecto:

development

terminaremos teniendo:

development-config.txt

y su contenido será:

Configuración de development

No siempre queremos definir un valor por defecto.

Por ejemplo:

variable "environment" {
  type = string
}

Ahora Terraform necesita que le proporcionemos un valor.

Podemos hacerlo mediante un archivo:

terraform.tfvars

con:

environment = "production"

Y nuestro resource puede seguir siendo exactamente el mismo:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = "Configuración de ${var.environment}"
}

Esto es interesante porque podemos utilizar la misma infraestructura para diferentes ambientes.


Por ejemplo:

development
staging
production

cambiando solamente los valores de entrada.

¿Por qué no poner todo directamente en el resource?

Podríamos escribir:

resource "local_file" "config" {
  filename = "production-config.txt"
  content  = "Configuración de production"
}

Pero entonces nuestro código está acoplado a un ambiente concreto.

Con variables podemos separar:

Infraestructura
      +
Configuración


Por ejemplo:

main.tf
    ↓
define cómo funciona la infraestructura

terraform.tfvars
    ↓
define los valores para este ambiente


Esta separación se vuelve especialmente importante cuando trabajamos con infraestructura real.

Las variables pueden tener diferentes tipos.

Por ejemplo:

variable "environment" {
  type = string
}

Una cadena.

También podemos tener números:

variable "instance_count" {
  type = number
}

Booleanos:

variable "enabled" {
  type = bool
}

Y estructuras más interesantes.

Por ejemplo, una lista:

variable "availability_zones" {
  type = list(string)
}

Podemos asignarle:

availability_zones = [
  "us-east-1a",
  "us-east-1b"
]

Más adelante, cuando empecemos a trabajar con configuraciones más complejas, estos tipos van a ser muy importantes.


Hasta ahora vimos cómo introducir información en Terraform mediante variables.

Pero muchas veces queremos hacer lo contrario.

Queremos que Terraform nos muestre información sobre lo que acaba de crear.

Para eso tenemos los outputs.

Por ejemplo:

output "environment" {
  value = var.environment
}

Después de ejecutar:

terraform apply

Terraform puede mostrar:

Outputs:

environment = "development"


Los outputs sirven para exponer información que resulta útil después de crear nuestros recursos.

Los outputs se vuelven mucho más interesantes cuando apuntan a recursos.


Por ejemplo:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = "Configuración de ${var.environment}"
}


Podemos definir:

output "config_file" {
  value = local_file.config.filename
}


Ahora Terraform conoce el nombre del archivo creado y podemos mostrarlo como output.


La referencia:

local_file.config.filename


significa:

resource
   │
   └── local_file.config
            │
            └── atributo filename


Esta forma de referenciar recursos es fundamental en Terraform.

Terraform entiende las dependencias

Supongamos que tenemos:

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"
  content  = "Configuración de ${var.environment}"
}

y:

output "config_file" {
  value = local_file.config.filename
}


El output depende del resource.

Terraform puede detectar esa relación.

Podemos imaginarlo así:

local_file.config
        │
        ▼
output.config_file


En configuraciones más grandes tendremos muchos recursos relacionados:


network
   │
   ├── subnet
   │      │
   │      └── virtual machine
   │
   └── database


Terraform construye y utiliza estas dependencias para determinar el orden en el que debe realizar las operaciones.

Veamos un ejemplo completo

Nuestro main.tf puede quedar así:

terraform {
  required_providers {
    local = {
      source = "hashicorp/local"
    }
  }
}

provider "local" {}

variable "environment" {
  type    = string
  default = "development"
}

resource "local_file" "config" {
  filename = "${var.environment}-config.txt"

  content = <<EOF
Environment: ${var.environment}
Application: MyApplication
EOF
}

output "config_file" {
  value = local_file.config.filename
}


Ahora ejecutamos:

terraform init


Después:

terraform plan


Y finalmente:

terraform apply


Terraform crea:

development-config.txt

con:

Environment: development
Application: MyApplication

Y además podemos obtener:

Outputs:

config_file = "development-config.txt"

Tenemos entonces las tres piezas:

                                       Terraform
                                             │
        ┌────────────┼────────────┐
        │                                  │                                  │
        ▼                                ▼                                 ▼
     Variable                     Resource                       Output
        │                                  │                                  │
        │                                  │                                  │
        └────────────┼────────────┘
                                             │
                                            ▼
                                     infraestructura


¿Dónde entra el State?

Hay un concepto que todavía no explicamos en profundidad, pero que ya aparece cuando ejecutamos Terraform:

terraform apply

Terraform necesita saber qué recursos administra y cuál es el estado conocido de esos recursos.

Para eso utiliza el state.

Normalmente, después de ejecutar nuestro ejemplo veremos un archivo:

terraform.tfstate

Este archivo contiene información que Terraform utiliza para relacionar nuestra configuración con los recursos que administra.

Por ahora no necesitamos entrar en todos sus detalles.

Lo importante es entender que Terraform trabaja con tres cosas:

  Configuración
       │
       │
       ▼
   Terraform
       ▲
       │
       │
     State
       │
      ▼
Infraestructura real


La configuración dice lo que queremos.

El state ayuda a Terraform a saber qué está administrando.

Y la infraestructura real es lo que existe efectivamente.

Este concepto merece otro post... 

viernes, 2 de octubre de 2026

Terraform: ¿qué problema resuelve? Una introducción a Infrastructure as Code


Cuando hablamos de Terraform, muchas veces la primera reacción es: ¿Otra herramienta más para configurar servidores?

No exactamente.

Terraform intenta resolver un problema bastante más interesante: ¿cómo podemos definir, versionar y reproducir nuestra infraestructura de la misma manera que hacemos con nuestro código?


Imaginemos que tenemos que desplegar una aplicación.

Necesitamos:

  • una máquina virtual;
  • una base de datos;
  • una red;
  • algunos permisos;
  • un almacenamiento;
  • quizás un balanceador.


Una forma de hacerlo es entrar a la consola de nuestro proveedor cloud y empezar a crear recursos.

  1. Hacemos clic.
  2. Configuramos una opción.
  3. Creamos una red.
  4. Después una máquina virtual.
  5. Después cambiamos una configuración.
  6. Y finalmente tenemos nuestra infraestructura funcionando.


El problema aparece unos meses después.

Queremos crear exactamente el mismo ambiente para testing.

¿Recordamos todos los pasos?

¿Todas las opciones?

¿Todas las reglas de firewall?

¿Todas las dependencias?

Probablemente no.


Y aunque recordemos todo, aparece otro problema: ¿cómo sabemos exactamente qué cambió entre producción y testing?


Ahí aparece una idea importante: Infrastructure as Code

Infrastructure as Code, o IaC, consiste en tratar la infraestructura como código.


En lugar de decir: Entrá a esta consola y creá una máquina con estas opciones, podemos escribir algo que describa nuestra infraestructura:


resource "..." "..." {

    ...

}


Ese archivo puede:

  • guardarse en Git;
  • revisarse mediante Pull Requests;
  • versionarse;
  • reutilizarse;
  • automatizarse;
  • compartirlo con otros desarrolladores.


Nuestra infraestructura deja de estar solamente en una consola y pasa a estar también descrita en código.

HashiCorp Terraform es una herramienta de Infrastructure as Code desarrollada originalmente por HashiCorp.


La idea fundamental es bastante sencilla: Describimos el estado que queremos y Terraform se encarga de realizar los cambios necesarios para llegar a ese estado.

Esto es importante porque Terraform utiliza un enfoque declarativo.

No necesitamos describir paso por paso cómo crear la infraestructura.

Describimos qué queremos tener.


Por ejemplo:

resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Estamos diciendo: Quiero un recurso local_file llamado hello cuyo archivo sea hello.txt y cuyo contenido sea Hola desde Terraform!.

Terraform se ocupa del resto.


Para empezar no necesitamos AWS, Azure ni Google Cloud.

Podemos utilizar un provider que trabaja con archivos locales.

Nuestro primer ejemplo completo puede ser:


terraform {

  required_providers {

    local = {

      source = "hashicorp/local"

    }

  }

}


provider "local" {}


resource "local_file" "hello" {

  filename = "hello.txt"

  content  = "Hola desde Terraform!"

}


Guardamos el archivo como:

main.tf


Y ejecutamos:

terraform init


Terraform descarga y configura el provider que necesitamos.

Después podemos preguntarle qué va a hacer:


terraform plan


Terraform nos muestra un plan de ejecución.


Finalmente:

terraform apply


Terraform aplica los cambios.


Y ahora tenemos:

hello.txt


con el contenido:


Hola desde Terraform!


¿Por qué plan es interesante?

Este es uno de los conceptos más importantes de Terraform.

Antes de modificar nuestra infraestructura podemos ejecutar:


terraform plan


Terraform analiza la configuración y el estado actual y nos muestra qué cambios pretende realizar.


Por ejemplo:

Plan: 1 to add, 0 to change, 0 to destroy.


Esto nos permite revisar los cambios antes de aplicarlos.

En una infraestructura real esto es extremadamente importante.


Imaginemos que alguien modifica un archivo Terraform y, sin revisar nada, ejecuta directamente la aplicación de los cambios.

Podría terminar eliminando un recurso que no debía eliminarse.

Con plan, podemos ver qué piensa hacer Terraform antes de hacerlo.


Terraform no es un script. Esta diferencia merece una explicación.


jueves, 1 de octubre de 2026

Fundamentos de lenguajes de programación cuánticos parte 2

 

Seguimos con : https://emanuelpeg.blogspot.com/2026/10/fundamentos-de-lenguajes-de.html

 ¿Y si el sistema de tipos controlara los qubits?

Acá aparece una conexión muy interesante con un área clásica de los lenguajes de programación: los sistemas de tipos.

Supongamos que tenemos:

qubit q

y hacemos:

measure(q)


Ahora podríamos preguntarnos:

xt

use(q)


¿Es válido?

¿Y qué pasa con:

copy(q)


¿Debería permitirlo el compilador?

Estas preguntas nos llevan a una idea conocida en lenguajes funcionales y sistemas de tipos: los tipos pueden utilizarse para controlar recursos.


Un tipo lineal introduce una restricción interesante: un recurso debe utilizarse exactamente de acuerdo con las reglas establecidas por el sistema de tipos.


Una forma simplificada de imaginarlo sería:

q : Qubit

y determinadas operaciones consumen o transforman ese recurso.


En vez de pensar solamente: variable → valor

podemos pensar: variable → recurso


Esto resulta especialmente interesante para la computación cuántica porque los qubits no se comportan como valores clásicos que podemos copiar libremente.


Por ejemplo, conceptualmente queremos evitar situaciones como:

q = createQubit()

q1 = q

q2 = q


si eso implica que q1 y q2 son copias independientes del mismo estado cuántico.

El sistema de tipos puede ayudar a expresar estas restricciones.


Esta diferencia permite ver algo más profundo.

En muchos lenguajes pensamos principalmente en: valor

Por ejemplo:

x : Int


Pero en determinados lenguajes podemos necesitar pensar también en: t recurso

Un qubit puede considerarse un recurso cuyo uso está restringido.

Esto tiene relación conceptual con otras ideas que ya existen en lenguajes modernos:

  • tipos lineales
  • ownership
  • borrowing
  • affine types
  • resource management


Por supuesto, estas ideas no son idénticas entre sí ni todos los lenguajes cuánticos utilizan exactamente el mismo mecanismo.


Pero comparten una preocupación: controlar qué podemos hacer con determinados valores o recursos.


Los programas cuánticos tampoco necesariamente reemplazan a los programas clásicos.

En muchos casos tenemos una combinación:


programa clásico


       │


       ▼


preparar qubits


       │


       ▼


ejecutar circuito cuántico


       │


       ▼


    medir


       │


       ▼


resultado clásico



Podemos imaginar un programa clásico que decide qué circuito ejecutar:


if condition:

    ejecutarCircuitoA()

else:

    ejecutarCircuitoB()


y dentro de esos circuitos aparecen las operaciones cuánticas.

Esto genera una característica importante de los lenguajes cuánticos: el mundo clásico y el mundo cuántico deben coexistir.


Podemos pensar entonces en dos mundos:

                         Programa

                                │

       ┌────────┴────────┐

       │                                                │

    Clásico                                    Cuántico

       │                                              │

     Int                                           Qubit

     Bool                                        Estado

     List                                         Circuito

     String                                     Medición



El lenguaje debe establecer reglas sobre cómo interactúan ambos.


Por ejemplo:

resultado = measure(q)

convierte información del mundo cuántico en información clásica.


Pero el camino inverso no es simplemente:

q = resultado


Hay que preparar un estado cuántico y aplicar las operaciones correspondientes.


Acá aparece otro punto de contacto con los lenguajes de programación tradicionales.

Podemos imaginar una cadena de compilación como:


Código fuente

     ↓

   AST

     ↓

Representación intermedia

     ↓

Optimización

     ↓

Circuito cuántico

     ↓

Instrucciones

     ↓

Hardware cuántico



Esto es muy parecido a un compilador tradicional.

Pero aparecen problemas nuevos.


Por ejemplo:

¿Podemos eliminar una operación?

 ¿Podemos reordenar dos operaciones?

 ¿Dos circuitos son equivalentes?

 ¿Podemos reducir la cantidad de puertas?

 ¿Cómo traducimos una operación abstracta a las operaciones que soporta un hardware específico?


El compilador cuántico no solamente traduce código.

También puede transformar y optimizar circuitos.


Fundamentos de lenguajes de programación cuánticos


Cuando pensamos en computación cuántica, normalmente aparecen palabras como qubit, superposición, entrelazamiento o puertas cuánticas.

Pero hay otra pregunta interesante:

¿Cómo se programa una computadora cuántica?

Y, más específicamente:

¿Qué cambia en un lenguaje de programación cuando la máquina que ejecuta nuestros programas es cuántica?

La respuesta es interesante porque no alcanza simplemente con agregar un nuevo tipo de dato llamado Qubit.

La computación cuántica introduce restricciones y conceptos que afectan algunas de las ideas fundamentales de los lenguajes de programación: valores, variables, tipos, copia, efectos, recursos y compilación.


En una computadora clásica, la unidad básica de información es el bit.

Un bit puede tener dos valores: 0 o 1

Podemos imaginar entonces una variable:


x = 0


y posteriormente:


x = 1


Un qubit es diferente.

Su estado puede representarse como:


α|0⟩ + β|1⟩


donde α y β son números complejos llamados amplitudes.


No significa simplemente que el qubit contiene un 0 y un 1.

Significa que su estado cuántico es una combinación de ambos estados posibles.


Las amplitudes deben cumplir:


|α|² + |β|² = 1


Cuando medimos el qubit obtenemos un resultado clásico: 0 o 1

La probabilidad de cada resultado depende de las amplitudes.


Por ejemplo:

1/√2 |0⟩ + 1/√2 |1⟩


produce un 0 o un 1 con igual probabilidad.

Esto ya introduce una diferencia importante para un lenguaje de programación.

Una variable clásica tiene un valor.

Un qubit representa un estado cuántico que puede transformarse y posteriormente medirse.


En un lenguaje clásico estamos acostumbrados a operaciones como:

x = x + 1


o:


x = !x


En un lenguaje cuántico aparecen operaciones como las puertas cuánticas.


Por ejemplo:

X(q)


La puerta X tiene un comportamiento similar al NOT clásico sobre los estados básicos:


|0⟩ → |1⟩

|1⟩ → |0⟩


Otra puerta fundamental es H, la puerta de Hadamard.


Aplicada a |0⟩, produce una superposición:


H(|0⟩) = 1/√2 |0⟩ + 1/√2 |1⟩


Es decir, una operación que en un lenguaje clásico podríamos imaginar como una transformación de un valor, en el mundo cuántico transforma un estado.


Una forma habitual de representar un programa cuántico es mediante un circuito.


Por ejemplo:


q ── H ─────●──── M

             │

q ───────────X──── M


Aquí tenemos dos qubits.

Al primero le aplicamos H, después utilizamos ambos en una operación CNOT y finalmente medimos los qubits.

Desde el punto de vista de un lenguaje de programación, esto es bastante interesante.

El programa no solamente describe valores y operaciones. También describe una secuencia de transformaciones sobre estados cuánticos.

Por eso muchos lenguajes y frameworks cuánticos tienen una fuerte relación con la representación de circuitos.


La medición es uno de los conceptos que más claramente rompe nuestras intuiciones clásicas.

Podemos imaginar algo como:

q = |+⟩

donde:

|+⟩ = 1/√2 |0⟩ + 1/√2 |1⟩


Si hacemos: measure(q) obtenemos un resultado clásico: 0 o 1

Pero el proceso de medición también modifica el estado cuántico.


Esto genera una pregunta interesante para el diseño de lenguajes: ¿Qué significa exactamente utilizar nuevamente q después de medirlo?


En un lenguaje clásico, leer una variable normalmente no destruye su valor:


x = 10

print(x)

print(x)

print(x)


En computación cuántica no podemos asumir automáticamente ese comportamiento.

La medición es un efecto que debemos tener en cuenta.


Esta es probablemente una de las diferencias más interesantes desde el punto de vista de los lenguajes de programación.


En un lenguaje clásico podemos hacer:

a = 10

b = a


Tenemos dos valores.

Con un qubit no existe una operación general equivalente a:

q2 = copy(q1)


para copiar arbitrariamente el estado cuántico.


Esto se conoce como el teorema de no clonación cuántica.

No se trata simplemente de que la API de un lenguaje haya decidido prohibir una operación.

Es una propiedad fundamental de la mecánica cuántica.

Y esto tiene consecuencias importantes para el diseño de lenguajes.


Me quedo relargo el post vamos a tener que tener parte 2. 

miércoles, 30 de septiembre de 2026

De C# a Q#: buscando un elemento con Grover


Cuando hablamos de computación cuántica es fácil caer en ejemplos demasiado artificiales. Podemos hablar de superposición, entrelazamiento, qubits e interferencia, pero queda una pregunta bastante más interesante: ¿Cómo se programa realmente un algoritmo cuántico?

Para verlo, vamos a resolver un problema muy sencillo de dos maneras:

Tenemos una colección de elementos y queremos encontrar uno que cumple una determinada condición.


Primero lo hacemos de la manera clásica, con C#. Después veremos cómo abordar el mismo problema con Q# utilizando el algoritmo de Grover.


Supongamos que tenemos:

int[] values = { 3, 8, 12, 17, 21, 42, 55, 61 };


y queremos encontrar el 42.


La solución más sencilla es una búsqueda lineal:


int Search(int[] values, int target)

{

    for (int i = 0; i < values.Length; i++)

    {

        if (values[i] == target)

            return i;

    }


    return -1;

}


No hay ningún misterio.


El algoritmo va recorriendo los elementos:


3  → no

8  → no

12 → no

17 → no

21 → no

42 → sí


En el peor caso tendremos que consultar todos los elementos.

Para una colección de N elementos, esto requiere:


O(N)


consultas.


Acá aparece una diferencia fundamental.


No podemos simplemente escribir:


for cada qubit

    buscar()


y esperar que la computadora cuántica haga lo mismo más rápido.


La programación cuántica utiliza otro modelo.

Uno de los algoritmos más conocidos para búsquedas no estructuradas es Grover's algorithm.


Su idea fundamental es utilizar:

  • superposición
  • un oráculo cuántico
  • interferencia
  • amplificación de amplitud
  • medición


para aumentar la probabilidad de obtener la respuesta que estamos buscando.


La cantidad de consultas necesarias pasa de: O(N)

a aproximadamente: O(√N)


Para utilizar Grover tenemos que formular la búsqueda de otra manera.

En lugar de preguntarnos: ¿Es este elemento igual a 42?

con cada elemento individualmente, definimos una función que identifica la solución.


Por ejemplo:

f(x) = 1  si x es la solución

f(x) = 0  en otro caso


Esta función se implementa como un oráculo cuántico.

Conceptualmente:


                     ┌─────────────┐

x ─────►│          ORACLE           │─────► marca x

                     └─────────────┘


El oráculo no nos devuelve directamente la solución.

Lo que hace es marcarla.

Y después Grover utiliza interferencia para aumentar su amplitud.


Primero necesitamos una superposición

Supongamos que tenemos 8 posibilidades:


000

001

010

011

100

101

110

111


Con tres qubits podemos representar esas ocho posibilidades.

Inicialmente tenemos un estado:

|000>


Aplicando una compuerta Hadamard a cada qubit obtenemos una superposición de todos ellos:


|000> + |001> + |010> + |011>

+ |100> + |101> + |110> + |111>


No significa que tengamos ocho computadoras ejecutándose independientemente.

Significa que nuestro estado cuántico es una combinación de esas posibilidades.


El oracle. Ahora necesitamos marcar nuestra solución.

Supongamos que queremos encontrar: 101


El oracle modifica la fase asociada a ese estado.


Conceptualmente:

000   +

001   +

010   +

011   +

100   +

101   -

110   +

111   +


La solución no apareció mágicamente.

Simplemente fue marcada mediante su fase.

Y esto es importante porque todavía no podemos medir y decir: ¡101!


Si medimos ahora, seguimos teniendo una probabilidad distribuida entre los estados.


Grover aplica una operación llamada difusión o inversión sobre la media.

Su objetivo es modificar las amplitudes de los estados.

Después de una iteración:


solución      ↑↑↑

otros estados ↓


La amplitud de la solución aumenta mientras las amplitudes de los estados incorrectos disminuyen.


Repetimos este proceso aproximadamente: π/4 × √N veces.


Finalmente medimos.

La probabilidad de obtener la solución es mucho mayor.


¿Y dónde está Q#?

Ahora viene la parte que más nos interesa.

Q# no intenta esconder que estamos trabajando con un modelo diferente.


Un programa cuántico puede trabajar con Qubit: use qs = Qubit[3];

Tenemos tres qubits.

Podemos ponerlos en superposición:

ApplyToEach(H, qs);


H es la compuerta Hadamard.

Después podemos aplicar nuestro oracle y la operación de difusión.

Una versión conceptual de Grover puede verse así:


operation Grover(qs : Qubit[]) : Unit {

    ApplyToEach(H, qs);


    // Oracle

    Oracle(qs);


    // Difusión

    Diffusion(qs);

}


En una implementación real necesitamos definir cuidadosamente el oracle y la difusión, pero la estructura fundamental ya está ahí:


superposición

      ↓

   oracle

      ↓

  difusión

      ↓

   medir


Comparemos las dos soluciones.

En C# escribimos:


for (int i = 0; i < values.Length; i++)

{

    if (values[i] == target)

        return i;

}


El programa piensa en términos de:


elemento

elemento

elemento

elemento

...

Es una búsqueda secuencial.


En Q# pensamos en:


qubits

  ↓

superposición

  ↓

oracle

  ↓

interferencia

  ↓

medición


No estamos escribiendo una versión más complicada del mismo for.

Estamos utilizando un modelo computacional diferente.

Sería muy fácil concluir: Entonces una computadora cuántica puede buscar en una lista de un millón de elementos en √1.000.000 = 1.000 pasos.


No es tan sencillo.

Grover proporciona una ventaja en el número de consultas al oracle.

Eso no significa que toda la aplicación se ejecute en O(√N).


Tenemos que:

  • preparar el estado cuántico;
  • construir el oracle;
  • ejecutar las operaciones cuánticas;
  • repetir Grover el número adecuado de veces;
  • realizar la medición.


Además, las computadoras cuánticas reales tienen limitaciones físicas y de hardware.

Por eso es más correcto decir: Grover reduce el número de consultas necesarias para una búsqueda no estructurada de O(N) a O(√N).

No significa que cualquier programa de búsqueda pueda reemplazarse automáticamente por Grover.


Lo interesante de Q# no es simplemente que tenga un tipo llamado Qubit.

Lo interesante es que el lenguaje permite expresar conceptos que no aparecen en un lenguaje clásico tradicional:

  • Qubit
  • Hadamard
  • superposición
  • medición
  • operaciones unitarias
  • oracle


Y eso cambia la forma en que pensamos el algoritmo.

En C# pensamos:

datos → operaciones → resultado


En Q# podemos pensar:


estado cuántico

      ↓

transformación

      ↓

interferencia

      ↓

medición

      ↓

resultado



No podemos decir simplemente eso.

Estamos comparando dos modelos de computación diferentes.


Para nuestra búsqueda:

Clásico: O(N)


frente a Grover: O(√N) consultas al oracle


Pero para aprovechar esa ventaja necesitamos una computadora cuántica y un problema que pueda expresarse adecuadamente mediante un oracle.

Además, si los datos están almacenados en una memoria clásica y tenemos que cargarlos en la computadora cuántica, esa carga también forma parte del problema.


Lo verdaderamente interesante

Quizás la parte más interesante de este ejemplo no sea que:

O(N) → O(√N)


sino por qué es posible hacerlo.


Una computadora clásica trabaja con bits:

0

1


Una computadora cuántica trabaja con qubits, que pueden encontrarse en superposición:

α|0> + β|1>


y las operaciones cuánticas pueden modificar las amplitudes de esos estados.

Grover aprovecha precisamente esto.

No estamos ejecutando un for` más rápido.


Estamos diseñando un algoritmo alrededor de las propiedades de la mecánica cuántica.

Y acá aparece una cuestión interesante para los lenguajes de programación.

En C# podemos abstraernos completamente del hardware:

int result = Search(values, 42);


En Q#, en cambio, muchas de las abstracciones del lenguaje están relacionadas directamente con el modelo cuántico:

use qs = Qubit[3];


ApplyToEach(H, qs);


Oracle(qs);

Diffusion(qs);


El programador tiene que pensar en:

¿Qué estado tengo?

¿Qué transformación estoy aplicando?

¿Qué amplitudes quiero aumentar?

¿Qué amplitudes quiero cancelar?

¿Cuándo puedo medir?


Es decir, el paradigma de programación cambia.


Grover es un buen ejemplo para entender que la computación cuántica no consiste simplemente en ejecutar programas clásicos más rápido.


Tenemos el mismo problema: encontrar un elemento que cumple una condición.


Pero utilizamos dos modelos diferentes.

En el modelo clásico:


buscar

   ↓

comparar

   ↓

buscar

   ↓

comparar

   ↓

...



En el modelo cuántico:


superposición

      ↓

    oracle

      ↓

 interferencia

      ↓

amplificación

      ↓

   medición


Y ahí está, quizás, la diferencia más importante entre programar para una computadora clásica y programar para una computadora cuántica: No se trata solamente de aprender nuevas instrucciones. Hay que aprender a pensar en términos de estados cuánticos y transformaciones sobre esos estados.