Elido
14 min de lecturaIngeniería
Esencial

Gestiona tus enlaces cortos como Terraform

Lanzamos el único proveedor Terraform en el espacio de acortadores de URL - terraform-provider-elido. Aquí está lo que hace, cómo funciona el ciclo de vida del recurso y las compensaciones de ingeniería detrás de él.

Marius Voß
DevRel · edge infra
Three-stage diagram: HCL config flowing into terraform-provider-elido, then into the api-core REST surface, with a side panel listing the four resources and data sources shipped today

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
}
Ciclo de vida declarativo del enlace: un bloque HCL elido_link alimenta a terraform plan, que produce un diff, luego terraform apply llama a la superficie REST de api-core de Elido para crear, actualizar o eliminar el enlace, con PATCH en un cambio de slug y reemplazo solo cuando workspace_id o domain_id se mueve a una ruta edge diferente

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?

La deteccion de drift compara tres entradas: el estado deseado en el plan HCL, el estado de Terraform registrado y la lectura en vivo de api-core. Cuando la lectura en vivo diverge, el proveedor propone un PATCH correctivo; los campos con valor por defecto del servidor se mantienen con el modificador de plan UseStateForUnknown para que nunca aparezcan como drift falso

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.

Flujo de trabajo GitOps para enlaces cortos: una edicion de la configuracion HCL abre un pull request, CI ejecuta terraform plan y tests unitarios de Go, un revisor aprueba el diff y hace merge a main, y el merge activa terraform apply contra api-core para que el edge recoja el cambio en el siguiente clic

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

Prueba Elido

Acortador de URL alojado en la UE: dominios personalizados, análisis profundo y API abierta. Plan gratuito - sin tarjeta de crédito.

Etiquetas
terraform
infrastructure as code
url shortener
developer experience
devops
iac

Seguir leyendo