Voy a hacer una pequeña afirmación y luego respaldarla. Ningún acortador de URL
actualmente envía un proveedor Terraform de primera clase. Bitly, TinyURL,
Rebrandly, Short.io, Dub.co - los cinco publican APIs REST, varios
publican webhooks, ninguno publica un terraform-provider-*. Un proveedor
de la comunidad para la API v3 de Bitly existe en GitHub; no tiene mantenimiento y
cubre quizás un cuarto de la superficie de la API. Esa es la brecha.
Hace unas semanas nos sentamos para cerrarla. El resultado es
terraform-provider-elido, que la API de Elido expone hoy como
elido_link (recurso), elido_workspace (data source) y
elido_custom_domain (data source por ahora - sigue leyendo). Lo que sigue
es un tour por lo que se ha lanzado, las decisiones de ingeniería detrás de ello, y
las partes que deliberadamente no lanzamos en v0.1.0. El proveedor es
código abierto bajo la misma licencia que el resto de Elido y vive en
tools/terraform-provider-elido/.
Por qué los enlaces cortos pertenecen en Terraform#
El argumento es corto. Si estás ejecutando redirecciones de marketing, ya tienes otras piezas de infraestructura que convergen en la misma campaña:
- Un registro DNS de Cloudflare apuntando a un lander.
- Un bucket S3 y una distribución CloudFront sirviendo ese lander.
- Un Lambda o un servicio Cloud Run generando URLs firmadas.
- Una etiqueta de campaña horneada en Google Tag Manager o Segment.
Los cinco, en 2026, se gestionan como Terraform. El enlace corto que se sienta al frente del embudo - el punto de entrada real en el que un usuario hace clic - está en un Google Doc. Esa brecha es de donde viene el drift. Un lander queda obsoleto y la redirección que apunta a él sigue viva, recogiendo 404s, hasta que alguien hace ping al marketing en Slack.
Puedes arreglar esa brecha de dos maneras. Puedes escribir un script glue en
TypeScript que se sienta entre tu salida de Terraform y nuestra API REST.
Eso funciona; tenemos clientes haciendo exactamente eso. O podemos darte
un proveedor Terraform real, donde la redirección es un bloque resource
junto a tu registro de Cloudflare, y terraform plan / terraform destroy lo conocen de la misma manera que conocen todo lo demás.
Elegimos el segundo camino. El primero ya estaba sobre ti.
Qué hace terraform-provider-elido hoy#
La superficie mínima v0.1.0, en HCL:
terraform {
required_providers {
elido = {
source = "elidoapp/elido"
version = "~> 0.1"
}
}
}
provider "elido" {
# api_url defaults to https://api.elido.app
# api_token reads ELIDO_API_TOKEN
}
data "elido_workspace" "main" {
id = 42
}
data "elido_custom_domain" "links" {
workspace_id = data.elido_workspace.main.id
hostname = "links.example.com"
}
resource "elido_link" "spring_campaign" {
workspace_id = data.elido_workspace.main.id
domain_id = data.elido_custom_domain.links.id
slug = "spring-2026"
destination_url = "https://example.com/landing/spring"
title = "Spring 2026 email campaign"
tags = ["spring-2026", "email"]
redirect_status = 301
}
terraform apply y has terminado. La detección de drift funciona en
los campos que la API devuelve como eco. Renombrar la etiqueta del recurso Terraform,
o cambiar el slug a mitad de vuelo, no fuerza un reemplazo -
el proveedor emite un PATCH contra el mismo ID numérico. Cambiar
workspace_id o domain_id sí fuerza el reemplazo, porque en
ese punto estás hablando de una ruta edge diferente. Ese es
el ciclo de vida de sentido común, y es a lo que te empujan las guías del
plugin framework
de HashiCorp.
La forma de despliegue masivo es la parte que justifica el trabajo para la mayoría de los equipos:
locals {
channels = ["email", "twitter", "linkedin", "reddit", "hn"]
regions = ["us", "eu", "apac", "latam"]
}
resource "elido_link" "campaign_launch" {
for_each = {
for pair in setproduct(local.channels, local.regions) :
"${pair[0]}-${pair[1]}" => pair
}
workspace_id = data.elido_workspace.main.id
domain_id = data.elido_custom_domain.links.id
slug = "launch-${each.key}"
destination_url = "https://example.com/launch?ch=${each.value[0]}&r=${each.value[1]}"
tags = ["launch-2026", each.value[0], each.value[1]]
}
Veinte enlaces, un apply. Borra el bloque, veinte borrados, un apply. Ese es aproximadamente el caso de uso que apareció en tres notas de llamadas con clientes el último trimestre: marketing quiere enlaces UTM por canal por región para un lanzamiento, ingeniería construye un script Sheets-to-API cada vez, el script se desactualiza, el autor del script abandona la empresa. La fuerza de Terraform aquí no es la novedad - es que hemos hecho el patrón aburrido.
La guía completa con referencia de atributos y ejemplos de importación está en
/docs/guides/terraform. La fuente del proveedor
se envía con examples/main.tf que es una versión más elaborada del
snippet anterior.
Cómo está construido el proveedor#
Aproximadamente 600 líneas de Go, de las cuales ~200 son definiciones de esquema. La forma:
tools/terraform-provider-elido/
├── main.go # plugin entrypoint
├── internal/provider/
│ ├── provider.go # config + auth
│ ├── link_resource.go # CRUD + import
│ ├── workspace_data_source.go # GET /v1/workspaces/{id}
│ ├── custom_domain_data_source.go # GET /v1/workspaces/{id}/domains
│ ├── helpers.go # tag conversion
│ └── provider_test.go # 7 unit tests
├── go.mod # depends on packages/sdk-go
├── .goreleaser.yml # signed-checksum builds
├── terraform-registry-manifest.json # protocol_versions: ["6.0"]
├── Makefile # build + install-local + testacc
└── examples/main.tf
Algunas decisiones que vale la pena destacar.
Usamos el plugin framework, no el SDK legacy. HashiCorp
dirigió explícitamente a los nuevos proveedores a
terraform-plugin-framework
en 2023. La mayoría de los proveedores populares (aws, cloudflare,
google) están en mitad de la migración; los más pequeños y nuevos son
framework-native. Construir en greenfield sobre el SDK legacy habría
significado asumir una tarea de migración en el momento en que lanzamos. Evitamos
la migración no creando una. El framework tiene un sistema de tipos más estricto,
validación de esquema real a nivel de protocolo de plugin,
y un modelo de planificación mucho más limpio (PlanModifiers en lugar de
callbacks CustomizeDiff). Para un proveedor pequeño, la brecha de ergonomía
es grande.
El proveedor no duplica el SDK. Cada método de recurso
delega a packages/sdk-go,
que es el mismo SDK que publicamos para integraciones plain-Go. El
proveedor es, por diseño, un adaptador Schema-to-SDK delgado. Eso tiene dos
consecuencias. La buena: cualquier bug que arreglemos en el SDK aterriza en el
proveedor gratis. La mala: cualquier brecha en el SDK es una brecha en el
proveedor. El ejemplo honesto son los dominios personalizados. api-core no
expone aún POST/DELETE para /v1/workspaces/{id}/domains; la
ruta de escritura vive en domain-manager detrás del dashboard. Hasta que
api-core haga proxy de las escrituras, el SDK no tiene Domains.Create, y
el proveedor no tiene recurso elido_custom_domain - solo un data
source que busca uno existente por hostname. Cerraremos
esa brecha en v0.2.0; el shim de proxy es un cambio de menos de una semana y
el PR de SDK + proveedor ya está esbozado.
Auth tiene la misma forma que cualquier otro cliente de Elido. Bearer API
key en la cabecera Authorization, fallback a ELIDO_API_TOKEN
en el entorno. No exponemos cookie auth o X-Dev-User-ID
en el proveedor; esas son comodidades de desarrollo local que no
tienen sitio en IaC donde la configuración se sienta en control de versiones y
se ejecuta en CI. Tu CI o tiene un token o no.
Detección de drift: la parte que es más difícil de lo que parece#
Si has leído más allá de las partes obvias, esta es la sección que vale la pena
leer. El diffing de Terraform es fundamentalmente una cuestión de: dado
lo que el usuario escribió (Plan), y lo que el servidor devolvió la última
vez (State), y lo que el servidor devuelve ahora (Read), qué
deberíamos proponer hacer?
Para un recurso como elido_link, tres cosas hacen esto no trivial:
Campos Optional + Computed con valores por defecto del servidor. El usuario puede
omitir redirect_status. El servidor rellena con 302. La siguiente Read
devuelve 302. Sin cuidado, esto parece drift en cada plan -
"pedí nada, obtuve 302 de vuelta, propongo ponerlo a nada
otra vez". El framework te da un plan modifier UseStateForUnknown
que dice "si no tengo un valor planificado, mantén lo que está en
state". Lo usamos en cada campo con default de servidor. Eso suena
trivial; es la fuente de los bugs de proveedor más frecuentes en el
ecosistema ("provider produced inconsistent result after apply").
Tags con normalización del lado del servidor. Nuestra API almacena las etiquetas como un
set; Terraform las ve como una lista ordenada. Por ahora pateamos eso. El servidor preserva el orden en el eco, por lo que el diff es estable en
la práctica, pero un usuario que reordene tags en HCL verá una actualización
no-op. Ese es el comportamiento correcto; la alternativa - ordenar
silenciosamente en la entrada - significaría que terraform plan y terraform apply no están de acuerdo sobre qué cambia, que es el pecado cardinal
de Terraform. Lo revisaremos si los clientes reales se quejan. La guía de
best-practices de HashiCorp
está firmemente del lado de "no hagas nada sorprendente" aquí.
Status como tri-estado. Un enlace puede ser active, paused o
archived. Establecer status = "paused" en HCL pero no en Create
(el servidor por defecto es active) significa que tenemos que emitir un PATCH
de seguimiento dentro del mismo Create. Eso se implementa como un
paso de reconciliación post-Create - tenlo en mente si estás leyendo
el código. La alternativa - exponer status como un recurso
separado (elido_link_status con clave link_id) - es lo que hace el proveedor
AWS para algunos recursos. Lo consideramos; para un
campo opcional, el coste supera al beneficio. Si añadimos un segundo
knob post-Create, lo repensaremos.
Importación. terraform import elido_link.spring_campaign 42:7 -
eso es <workspace_id>:<link_id>. Elegimos la forma separada por dos puntos
porque el callback ImportState del framework te da una
sola cadena y la analizas tú mismo. La forma <id>:<id> es
común en proveedores que codifican recursos por una tupla - ver la
documentación de importación de google_compute_instance
para la referencia canónica. Somos deliberados al no sobrecargar
el slug legible por humanos; el estado del recurso está codificado por el
ID numérico, y eso es lo único que deberías poner en una importación.
Tests, CI, el registro#
La suite de unit (7 tests hoy) cubre la capa de validación de esquema
más los helpers de función pura - splitImportID, linkToModel,
apiErrorString, optString. Se ejecuta en 0,5 segundos y bloquea
cada PR a través de la misma matriz go que construye nuestros 13 servicios.
También hay un objetivo testacc que se ejecuta contra un api-core vivo
cuando TF_ACC=1 está configurado, pero eso es opt-in: requiere un token,
y no lo ejecutamos en cada commit porque cada test crea y
elimina un enlace real. El
framework de testing de HashiCorp
documenta el patrón; no nos desviamos.
El pipeline de release está cableado a goreleaser con la matriz de construcción exacta
que el Terraform Registry espera: linux, darwin, freebsd,
windows × amd64/arm64 (más arm y 386 en Linux),
SHA256SUMS sobre los archivos, firma GPG en SHA256SUMS, y
un terraform-registry-manifest.json declarando protocol_versions: ["6.0"]. Etiqueta un commit terraform-provider-vX.Y.Z, el workflow de GitHub
Actions ejecuta goreleaser release --clean, y el GitHub
Release se publica. El
Terraform Registry
encuesta el release según su propio horario e ingiere la versión. Lo
único que actualmente falta es la clave GPG - estamos generando una
dedicada a los releases del proveedor esta semana, lo que significa
v0.1.0 aterriza en el registro aproximadamente al mismo tiempo que esta publicación.
Mientras tanto, instala vía dev_overrides en ~/.terraformrc:
provider_installation {
dev_overrides {
"elidoapp/elido" = "/Users/<you>/.terraform.d/plugins/elidoapp/elido"
}
direct {}
}
Luego make install-local desde tools/terraform-provider-elido/,
y terraform plan resuelve el binario directamente sin terraform init. Este es el patrón oficial de HashiCorp para el desarrollo de proveedores
y funciona igualmente bien como una ruta de instalación provisional hasta v1.0.0.
Lo que deliberadamente no está en v0.1.0#
Tres cosas que consideramos, no lanzamos, y queremos llamar la atención sobre ellas para que nadie se sorprenda.
Sin elido_custom_domain como recurso. Discutido arriba. El
data source es suficiente para encadenar domain_id en elido_link, que
es el caso de uso load-bearing; la gestión de ciclo de vida completo espera a
api-core. ETA: v0.2.0, mediados de 2026.
Sin elido_folder, sin elido_api_key. El SDK tiene ambos; elegimos
no añadir Schemas en v0.1.0 porque sus ciclos de vida no son
donde está el dolor del cliente. Las carpetas son metadatos organizativos; las claves API
se emiten típicamente una vez y se rotan a través del dashboard.
Las añadiremos cuando alguien pregunte.
Sin generación de código desde la spec OpenAPI. HashiCorp envía
terraform-plugin-codegen-openapi
como herramienta beta. La probamos en nuestra spec; los Schemas generados son
mediocres - cada campo nullable se convierte en Optional + Computed,
cada lista se convierte en un Set, el resultado requiere tanto fixup como un
Schema escrito a mano y es más difícil de evolucionar. Con tres recursos
sobre la mesa, escrito a mano gana. Revisaremos el generador en
seis meses cuando más de nuestros pares lo hayan probado en batalla.
Lo que se rompió mientras lo construíamos#
Tres cosas que hicimos mal en la primera pasada.
La primera fue state en Optional + Computed. Inicialmente modelamos
title como una string Optional plana. Los clientes que lo omitieron de
HCL obtuvieron un Create limpio - y luego cada terraform plan subsiguiente
proponía ponerlo de vuelta a null, porque el servidor almacenaba una cadena vacía
y Terraform leía eso como drift. La solución fue el
plan modifier UseStateForUnknown; la lección fue que la interpretación
del proveedor de "el usuario no especificó" tiene que coincidir
con la idea del servidor de "valor por defecto". La documentación del framework
advierte sobre esto en la introducción; leímos más allá de la advertencia la
primera vez. Te ahorré la vergüenza escribiéndolo aquí.
La segunda fue el formato de importación. Inicialmente lanzamos
<workspace_id>/<link_id> con una barra, en la teoría de que las rutas
se leen más naturalmente. El framework no tuvo problema con ello; los linters de HCL
y las terminales sí. Una ruta con dos barras dentro de un argumento de shell-quoted
se convierte en algo que parece un error tipográfico
en tickets de soporte. Cambiamos a dos puntos, que tiene cero ambigüedad
y coincide con las convenciones del proveedor de Google. Lección: las cadenas de importación
son UI orientada al usuario, diséñalas como UI.
La tercera fue el ordenamiento de tags. Discutido arriba - pateamos, y
seguiremos pateando hasta que alguien pregunte. La versión que casi
lanzamos ordenaba silenciosamente las tags en la entrada, lo que hacía que terraform plan
reportara no cambios cuando el cliente las había reordenado claramente.
Esa es una experiencia peor que un diff ruidoso; lo capturamos durante
las pruebas internas. Vale la pena decir porque la tentación de "ser
útil" normalizando la entrada del usuario es constante cuando escribes un
proveedor, y casi siempre es la decisión equivocada.
Cómo usar esto con el resto de Elido#
El proveedor es una forma. Las otras formas siguen existiendo y no van a ninguna parte:
- La API REST es la fuente de verdad.
Todo lo que hace el proveedor también se puede hacer con
curl. - El Go SDK es lo que el propio proveedor usa internamente; puedes incorporarlo como librería.
- Los SDKs de TypeScript y Python cubren la misma superficie para el lenguaje en el que estés.
- El endpoint GraphQL cubre las mismas lecturas con un único round-trip cuando lo necesitas con la forma de tu pantalla.
Elige lo que se ajuste a la forma del problema. Terraform es correcto cuando tienes un ciclo de vida que gestionar. El SDK es correcto cuando tienes un script. La API REST es correcta cuando estás haciendo una cosa una vez. Pensamos que debería ser así de obvio; mantendremos las cuatro funcionando.
Si tienes un patrón de Terraform favorito que nos falta - importaciones masivas
desde CSV vía for_each sobre un bloque data "external", un
for_each con la forma de una API de Linear para tracking de campañas, un
módulo de wrapper para el caso agencia-gestionando-múltiples-tenants - abre un issue
en el repo de GitHub con la
etiqueta area:terraform. El proveedor existe para hacer esos patrones
aburridos; queremos saber cuáles siguen sintiéndose sorprendentes.
Por dónde empezar#
Si has leído esto y quieres probarlo: instala el proveedor según la
guía, apúntalo a un workspace sandbox,
escribe resource "elido_link" para la redirección que siempre has querido
declarar en código, y terraform apply. Apostamos un café a que la
primera cosa que te sorprende, en el buen sentido, es terraform destroy
funcionando exactamente de la manera que esperas.
Si has leído esto y quieres compararnos con las alternativas - hay un write-up más largo en nuestra publicación Bitly alternatives feature gap , y el side-by-side en /compare/vs-bitly muestra dónde se sitúa Terraform en la matriz. La matriz se ha hecho más corta para ellos desde que aterrizó esta publicación.
- Marius
Relacionados en el blog#
Prueba Elido
Pega una URL, obtén un enlace corto
Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.
Gratis, sin registro · 2 por día