Suscríbete para recibir notificaciones de nuevas publicaciones:

Generación automática del proveedor Terraform de Cloudflare

2024-09-24

10 min de lectura
Esta publicación también está disponible en English, 繁體中文, Français, Deutsch, 日本語, 한국어 y 简体中文.

En noviembre de 2022, anunciamos la transición a OpenAPI Schemas para la API de Cloudflare. En aquel entonces, teníamos el audaz objetivo de hacer de los esquemas OpenAPI la fuente de información de nuestro ecosistema de SDK y de la documentación de referencia. Durante la Developer Week de 2024, respaldamos esta afirmación al anunciar la generación automática de nuestras bibliotecas SDK a partir de estos esquemas OpenAPI. Hoy nos complace anunciar los últimos componentes del ecosistema que ahora se generan automáticamente: el proveedor Terraform y la documentación de referencia de la API.

Esto significa que tan pronto como se añade un nuevo atributo o función a nuestros productos y el equipo lo documenta, puedes ver cómo se debe utilizar en nuestro ecosistema de SDK y utilizarlo de inmediato. Sin demoras. Se acabó la falta de cobertura de los puntos finales de la API. 

Puedes encontrar el nuevo sitio de la documentación en https://developers.cloudflare.com/api-next/ y probar la versión preliminar candidata del proveedor Terraform instalando 5.0.0-alpha1.

¿Por qué Terraform? 

Para cualquier usuario que no esté familiarizado con Terraform, se trata de una herramienta para gestionar tu infraestructura como código, de forma muy parecida a como lo harías con el código de tu aplicación. Muchos de nuestros clientes (grandes y pequeños) confían en Terraform para orquestar su infraestructura de forma independiente a la tecnología. En el fondo, es esencialmente un cliente HTTP con gestión del ciclo de vida integrada, lo que significa que utiliza nuestras API documentadas públicamente de una manera que entiende cómo crear, leer, actualizar y eliminar durante la vida útil del recurso.

Actualización de Terraform, a la antigua usanza

Históricamente, Cloudflare ha mantenido manualmente un proveedor Terraform, pero dado que los componentes internos del proveedor requieren su propia forma específica de hacer las cosas, la responsabilidad del mantenimiento y el soporte ha recaído en unas pocas personas. Los equipos de servicio siempre tenían dificultades para mantenerse al día con el número de cambios, debido a la cantidad de sobrecarga cognitiva necesaria para enviar un solo cambio en el proveedor. Para que un equipo consiguiera realizar un cambio en el proveedor, necesitaba un mínimo de 3 solicitudes de extracción (4 si se trataba de añadir compatibilidad con cf-terraforming).

Incluso con las 4 solicitudes de extracción completadas, no ofrecía garantías sobre la cobertura de todos los atributos disponibles, lo que significaba que era posible olvidarse de pequeños detalles y no exponerlos a los clientes, lo que causaba frustración al intentar configurar un recurso.

Para solucionar este problema, era necesario que nuestro proveedor Terraform se basara en los mismos esquemas OpenAPI de los que ya se beneficiaba el resto de nuestro ecosistema de SDK.

Actualización automática de Terraform

Lo que diferencia a Terraform de nuestros SDK es que gestiona el ciclo de vida de los recursos. Esto conlleva una nueva serie de problemas relacionados con los valores conocidos y la gestión de las diferencias en las cargas útiles de solicitud y respuesta. Comparemos estos dos enfoques distintos para crear un nuevo registro DNS y recuperarlo.

Con nuestro SDK para Go:

// Create the new record
record, _ := client.DNS.Records.New(context.TODO(), dns.RecordNewParams{
	ZoneID: cloudflare.F("023e105f4ecef8ad9ca31a8372d0c353"),
	Record: dns.RecordParam{
		Name:    cloudflare.String("@"),
		Type:    cloudflare.String("CNAME"),
        Content: cloudflare.String("example.com"),
	},
})


// Wasteful fetch, but shows the point
client.DNS.Records.Get(
	context.Background(),
	record.ID,
	dns.RecordGetParams{
		ZoneID: cloudflare.String("023e105f4ecef8ad9ca31a8372d0c353"),
	},
)

Y con Terraform:

resource "cloudflare_dns_record" "example" {
  zone_id = "023e105f4ecef8ad9ca31a8372d0c353"
  name    = "@"
  content = "example.com"
  type    = "CNAME"
}

A primera vista, parece que el enfoque de Terraform es más sencillo, y tendrías razón. La complejidad de saber cómo crear un nuevo recurso y mantener los cambios la gestionas tú. Sin embargo, el problema es que para que Terraform ofrezca esta abstracción y garantía de datos, se deben conocer todos los valores en el momento de la aplicación. Eso significa que incluso si no estás utilizando el valor redireccionando mediante proxy, Terraform necesita saber cuál debe ser el valor para guardarlo en el archivo de estado y gestionar ese atributo en el futuro. El siguiente error es lo que los operadores de Terraform suelen ver en los proveedores cuando no se conoce el valor en el momento de la aplicación.

Error: Provider produced inconsistent result after apply

When applying changes to example_thing.foo, provider "provider[\"registry.terraform.io/example/example\"]"
produced an unexpected new value: .foo: was null, but now cty.StringVal("").

Mientras que cuando se utilizan los SDK, si no necesitas un campo, simplemente lo omites y nunca tendrás que preocuparte por mantener los valores conocidos.

No fue fácil abordar esta cuestión para nuestros esquemas OpenAPI. Desde la incorporación de la compatibilidad con la generación de Terraform, la calidad de nuestros esquemas ha mejorado considerablemente. Ahora llamamos explícitamente a todos los valores predeterminados presentes, las propiedades variables de la respuesta basadas en la carga útil de la solicitud y cualquier atributo calculado del lado del servidor. Todo esto se traduce en una mejor experiencia para cualquier usuario que interactúe con nuestras API.

Dar el salto de terraform-plugin-sdk a terraform-plugin-framework

Para crear un proveedor Terraform y exponer recursos o fuentes de datos a los operadores, necesitas dos elementos principales: un servidor de proveedor y un proveedor.

El servidor del proveedor se encarga de exponer un servidor gRPC que el núcleo de Terraform (a través de la CLI) utiliza para comunicarse cuando gestiona recursos o lee fuentes de datos de la configuración proporcionada por el operador.

El proveedor es responsable de encapsular los recursos y las fuentes de datos, comunicarse con los servicios remotos y gestionar el archivo de estado. Para ello, te basas en terraform-plugin-sdk (comúnmente conocido como SDKv2) o terraform-plugin-framework, que incluye todas las interfaces y métodos proporcionados por Terraform para gestionar correctamente los componentes internos. La decisión de qué complemento utilizas depende de la antigüedad de tu proveedor. SDKv2 existe desde hace más tiempo y es lo que utilizan la mayoría de los proveedores Terraform, pero debido a su antigüedad y complejidad tiene muchos problemas básicos sin resolver que se deben mantener para facilitar la compatibilidad con versiones anteriores a aquellos usuarios que dependen de él. terraform-plugin-framework es la nueva versión que, aunque carece de la amplitud de funciones que tiene SDKv2, proporciona un enfoque más parecido a Go para crear proveedores y soluciona muchos de los errores subyacentes en SDKv2.

(Si quieres ver una comparación más detallada entre SDKv2 y el marco, puedes consultar mi conversación con John Bristowe de Octopus Deploy).

La mayor parte del proveedor Terraform de Cloudflare se desarrolla con SDKv2. Sin embargo, a principios de 2023 dimos el paso a la multiplexación y ofrecemos ambos en nuestro proveedor. Para entender por qué esto era necesario, debemos conocer un poco SDKv2. La forma en que está estructurado SDKv2 no es realmente propicia para representar valores nulos o "no establecidos" de forma coherente y fiable. Puedes utilizar el ResourceData.GetRawConfig experimental para comprobar si el valor está establecido, es nulo o es desconocido en la configuración, pero no puedes reescribirlo como nulo.

Esta advertencia nos fue evidente por primera vez cuando el motor de reglas perimetrales (conjuntos de reglas) comenzó a incorporar nuevos servicios y era necesario que esos servicios fueran compatibles con las respuestas de la API que contenían valores booleanos en un estado no establecido (o que faltaba), true, o false, cada uno con su propio razonamiento y propósito. Si bien este no es un diseño de API convencional en Cloudflare, es un procedimiento válido con el que deberíamos poder trabajar. Sin embargo, como mencioné anteriormente, el proveedor SDKv2 no podía. Esto se debe a que cuando un valor no está presente en el estado de respuesta o lectura, obtiene por defecto un valor cero compatible con Go. Esto se manifestó con la imposibilidad de anular valores después de que se hubieran escrito para indicar valores falsos (y viceversa). 

La única solución que tenemos aquí para utilizar de forma fiable los tres estados de esos valores booleanos es migrar a terraform-plugin-framework, que tiene la implementación correcta de reescribir valores no establecidos.

Una vez que empezamos a añadir más funciones utilizando terraform-plugin-framework en el proveedor anterior, resultaba evidente que mejoraba la experiencia para los desarrolladores, así que añadimos un refuerzo para evitar el uso de SDKv2 en el futuro a fin de anticiparnos a cualquier usuario que, sin saberlo, estuviera en vías de encontrarse con este problema.

Cuando decidimos que generaríamos automáticamente el proveedor Terraform, era lógico que también incorporáramos todos los recursos para que se basaran en terraform-plugin-framework y dijéramos adiós definitivamente a los problemas de SDKv2. Esto complicó la migración, ya que la mejora de los componentes internos comportó cambios en los componentes principales, como el esquema y las operaciones CRUD, con los que necesitábamos familiarizarnos. Sin embargo, la inversión ha valido la pena porque ahora las bases del proveedor están preparadas para el futuro y debemos hacer menos concesiones para ofrecer una fantástica experiencia Terraform debido a componentes internos existentes que presenten errores.

Búsqueda iterativa de errores  

Uno de los problemas habituales de los procesos de generación de código es que, a menos que tengas herramientas existentes que implementen tu nuevo elemento, es difícil saber si funciona o si su uso es razonable. Por supuesto, también puedes generar tus pruebas para poner en práctica el nuevo elemento, pero si hay un error en el proceso, es muy probable que no lo veas como un error, ya que estarás generando aserciones de prueba que muestran que el error es el comportamiento esperado. 

Uno de los bucles de retroalimentación básicos que hemos utilizado es el conjunto de pruebas de aceptación existente. Todos los recursos del proveedor existente tenían una combinación de pruebas de regresión y de funcionalidad. Lo mejor de todo es que, dado que el conjunto de pruebas crea y gestiona recursos reales, era muy fácil saber si el resultado era una implementación funcional o no observando el tráfico HTTP para ver si los puntos finales remotos aceptaban las llamadas API. Migrar el conjunto de pruebas solo era cuestión de copiar todas las pruebas existentes y comprobar si había alguna diferencia de aserción de tipo (p. ej., de lista a lista anidada única) antes de iniciar una ejecución de prueba para determinar si el recurso funcionaba correctamente. 

Aunque el proceso centralizado de esquemas supuso una gran mejora de la calidad de vida, ya que las correcciones de esquemas se propagaban a todo el ecosistema casi al instante, no podía ayudarnos a resolver el mayor obstáculo, es decir, a identificar aquellos errores que ocultaban otros errores. Esto requería mucho tiempo porque para solucionar un problema en Terraform, hay tres lugares donde puedes encontrar un error:

  1. Antes de realizar cualquier llamada API, Terraform implementa la validación del esquema lógico y, cuando encuentra errores de validación, se detiene inmediatamente.

  2. Si alguna llamada API falla, se detendrá en la operación CRUD y devolverá los diagnósticos, deteniéndose inmediatamente.

  3. Una vez ejecutada la operación CRUD, Terraform realiza comprobaciones para garantizar que se conocen todos los valores.

Eso significa que si encontrábamos el error en el paso 1 y luego lo solucionábamos, no había garantía ni forma de saber que no teníamos dos más esperándonos. Por no hablar de que si encontrábamos un error en el paso 2 y enviábamos una solución, no identificaría un error en el primer paso de la siguiente ronda de pruebas.

Aquí no hay una fórmula mágica y nuestra solución fue detectar patrones de problemas en los comportamientos del esquema y aplicar reglas de CI lint en los esquemas OpenAPI antes de que entrara en el proceso de generación de código. La adopción de este enfoque redujo gradualmente el número de errores en los pasos 1 y 2 hasta que en gran medida solo teníamos que ocuparnos del tipo en el paso 3.

Un enfoque más reutilizable para la conversión de modelos y estructuras

En las operaciones CRUD del proveedor Terraform es bastante habitual ver un texto modelo como el siguiente:

var plan ThingModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
	return
}

out, err := r.client.UpdateThingModel(ctx, client.ThingModelRequest{
	AttrA: plan.AttrA.ValueString(),
	AttrB: plan.AttrB.ValueString(),
	AttrC: plan.AttrC.ValueString(),
})
if err != nil {
	resp.Diagnostics.AddError(
		"Error updating project Thing",
		"Could not update Thing, unexpected error: "+err.Error(),
	)
	return
}

result := convertResponseToThingModel(out)
tflog.Info(ctx, "created thing", map[string]interface{}{
	"attr_a": result.AttrA.ValueString(),
	"attr_b": result.AttrB.ValueString(),
	"attr_c": result.AttrC.ValueString(),
})

diags = resp.State.Set(ctx, result)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
	return
}

A grandes rasgos:

  • Obtenemos las actualizaciones propuestas (lo que se conoce como plan) mediante req.Plan.Get()

  • Realizamos la llamada API de actualización con los nuevos valores.

  • Manipulamos los datos de un tipo Go en un modelo Terraform (convertResponseToThingModel)

  • Establecemos el estado llamando a resp.State.Set()

Inicialmente, esto no parece demasiado problemático. Sin embargo, el tercer paso, en el que manipulamos el tipo Go en el modelo Terraform, se vuelve rápidamente engorroso, propenso a errores y complejo, porque todos tus recursos necesitan hacer esto para cambiar entre el tipo y los modelos Terraform asociados.

Para evitar generar código más complejo de lo necesario, una de las mejoras que presenta nuestro proveedor es que todos los métodos CRUD utilizan métodos unificados apijson.Marshal, apijson.Unmarshal, y apijson.UnmarshalComputed que resuelven este problema centralizando la lógica de conversión y manejando lógica basada en las etiquetas struct.

var data *ThingModel

resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
	return
}

dataBytes, err := apijson.Marshal(data)
if err != nil {
	resp.Diagnostics.AddError("failed to serialize http request", err.Error())
	return
}
res := new(http.Response)
env := ThingResultEnvelope{*data}
_, err = r.client.Thing.Update(
	// ...
)
if err != nil {
	resp.Diagnostics.AddError("failed to make http request", err.Error())
	return
}

bytes, _ := io.ReadAll(res.Body)
err = apijson.UnmarshalComputed(bytes, &env)
if err != nil {
	resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
	return
}
data = &env.Result

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)

En lugar de tener que generar cientos de instancias de métodos de conversión de tipo a modelo, podemos complementar el modelo Terraform con las etiquetas correctas y gestionar la clasificación y desclasificación de los datos de forma coherente. Es un cambio menor en el código que, a la larga, mejora la reutilización y la legibilidad de la generación. Como ventaja adicional, este enfoque es excelente para la corrección de errores, ya que una vez que identificas un error con un tipo determinado de campo, al corregirlo en la interfaz unificada se corrigen otras apariciones del mismo error que quizás aún no hayas detectado.

Pero espera, ¡hay más (la documentación)!

Para complementar nuestro uso del esquema OpenAPI, estamos reforzando la integración de los SDK con nuestro nuevo sitio de la documentación de la API. Utiliza el mismo proceso en el que hemos invertido durante los últimos dos años, al mismo tiempo que aborda algunos de los problemas de uso más comunes.

Compatible con SDK 

Si has utilizado nuestro sitio de la documentación de la API, sabrás que te proporcionamos ejemplos de cómo interactuar con la API utilizando herramientas de línea de comandos como curl. Este es un excelente punto de partida, pero si utilizas una de las bibliotecas SDK, debes hacer acrobacias mentales para convertirla al método o definición de tipo que deseas utilizar. Ahora que utilizamos el mismo proceso para generar los SDK y la documentación, resolvemos ese problema proporcionando ejemplos en todas las bibliotecas que podrías utilizar, no solo curl.

Ejemplo de uso de cURL para obtener todas las zonas.

Ejemplo de uso de la biblioteca Typescript para obtener todas las zonas.

Ejemplo de uso de la biblioteca de Python para obtener todas las zonas.

Ejemplo de uso de la biblioteca Go para obtener todas las zonas.

Con esta mejora, también recordamos la selección de lenguaje, por lo que si has seleccionado ver la documentación utilizando nuestra biblioteca Typescript y sigues haciendo clic, seguiremos mostrándote ejemplos utilizando Typescript hasta que lo cambies.

Lo mejor de todo es que cuando introducimos nuevos atributos en los puntos finales existentes o añadimos lenguajes de SDK, este sitio de la documentación se mantiene automáticamente sincronizado con el proceso. Ya no supone un gran esfuerzo mantenerlo todo actualizado.

Representación más rápida y eficiente

Una cuestión con la que siempre hemos tenido dificultades es el gran número de puntos finales de la API y cómo representarlos. A partir de esta publicación, tenemos 1330 puntos finales, y para cada uno de esos puntos finales tenemos una carga útil de solicitud, una carga útil de respuesta y varios tipos asociados a ella. Cuando se trata de representar tanta información, las soluciones que hemos utilizado en el pasado debían hacer concesiones para que algunas partes de la representación funcionaran.

La próxima iteración del sitio de la documentación de la API resuelve este problema de dos maneras:

  • Se implementa como una aplicación React moderna que combina una experiencia interactiva del lado del cliente con contenido estático ya representado, lo que acelera la carga inicial y la navegación. (Sí, ¡incluso funciona sin JavaScript activado!). 

  • Obtiene los datos subyacentes de forma incremental a medida que navegas.

Al resolver este problema fundamental, hemos logrado otras mejoras planificadas en el sitio de la documentación y el ecosistema de SDK para mejorar la experiencia del usuario sin tener que hacer concesiones como en el pasado. 

Permisos

Una de las funciones que más nos han solicitado volver a implementar en el sitio de la documentación ha sido los permisos mínimos necesarios para los puntos finales de la API. Una de las iteraciones anteriores del sitio de la documentación ofrecía esta función. Sin embargo, sin que la mayoría de los usuarios lo supieran, el mantenimiento de los valores se realizaba manualmente y eran habitual que fueran incorrectos, lo que provocaba incidencias de soporte y frustración para los usuarios. 

En el sistema de gestión de identidad y acceso de Cloudflare, responder a la pregunta "¿qué necesito para acceder a este punto final?" no es fácil. La razón es que en el flujo normal de una solicitud al plano de control necesitamos dos sistemas distintos para proporcionar partes de la pregunta que luego se pueden combinar para ofrecerte la respuesta completa. Como inicialmente no podíamos automatizar esta característica como parte del proceso de OpenAPI, decidimos eliminarla en lugar de que ofreciera valores incorrectos sin verificación posible. 

Hoy nos complace anunciar que los permisos de los puntos finales vuelven a estar disponibles. Hemos creado algunas nuevas herramientas que extraen la respuesta a esta pregunta de manera que podemos integrarla en nuestro proceso de generación de código y hacer que todos los puntos finales obtengan automáticamente esta información. Al igual que el resto de la plataforma de generación de código, se centra en que los equipos de servicio posean y mantengan esquemas de alta calidad que se puedan reutilizar con valor añadido sin necesidad de realizar ningún trabajo en su nombre.

Di adiós a la espera de actualizaciones

Con estos anuncios, ponemos fin a la espera de las actualizaciones en el ecosistema de SDK. Estas nuevas mejoras nos permiten agilizar la capacidad de los nuevos atributos y puntos finales tan pronto como los equipos los documentan. ¿A qué estás esperando? Consulta hoy mismo el sitio de la documentación de la API y proveedores Terraform.

Protegemos redes corporativas completas, ayudamos a los clientes a desarrollar aplicaciones web de forma eficiente, aceleramos cualquier sitio o aplicación web, prevenimos contra los ataques DDoS, mantenemos a raya a los hackers, y podemos ayudarte en tu recorrido hacia la seguridad Zero Trust.

Visita 1.1.1.1 desde cualquier dispositivo para empezar a usar nuestra aplicación gratuita y beneficiarte de una navegación más rápida y segura.

Para saber más sobre nuestra misión para ayudar a mejorar Internet, empieza aquí. Si estás buscando un nuevo rumbo profesional, consulta nuestras ofertas de empleo.
Birthday Week (ES)API (ES)SDKTerraformOpen APIDeveloper PlatformAgile Developer ServicesDesarrolladores

Síguenos en X

Jacob Bednarz|@jacobbednarz
Cloudflare|@cloudflare

Publicaciones relacionadas

31 de octubre de 2024, 13:00

Moving Baselime from AWS to Cloudflare: simpler architecture, improved performance, over 80% lower cloud costs

Post-acquisition, we migrated Baselime from AWS to the Cloudflare Developer Platform and in the process, we improved query times, simplified data ingestion, and now handle far more events, all while cutting costs. Here’s how we built a modern, high-performing observability platform on Cloudflare’s network....