Volver al blog

Síguenos y suscríbete

Cómo migramos developer.fastly.com de VCL a Compute

Andrew Betts

Principal Developer Advocate, Fastly

Si desarrollas mediante Fastly, seguramente pasas tiempo en el Developer Hub. Pues bien, el mes pasado lo migramos desde nuestra plataforma de VCL a Compute, y ahora te contamos cómo lo hicimos y qué lecciones se pueden sacar.

En primer lugar, cabe remarcar que dejar VCL no tiene por qué ser lo que más te convenga, y es que nuestras dos plataformas, VCL y Compute, siguen recibiendo soporte y desarrollo activos, y VCL ofrece una excelente experiencia de configuración con ajustes predeterminados ideales para sacar un sitio rápidamente sin tener que tocar código. No obstante, si quieres empezar a realizar tareas más complejas en el edge, lo más probable es que sí que necesites las capacidades informáticas genéricas de Compute.

Antes de empezar a probar lo nuevo, es mejor pasar lo que estés haciendo en VCL al servicio de Compute. En este artículo repasaremos cómo migrar todos los patrones usados por developer.fastly.com de VCL a Compute, lo cual resulta ser un buen caso práctico de ejemplo.

Tareas preliminares

Como el servicio existente de developer.fastly.com es VCL (y es un servicio de producción que no nos queremos cargar), lo primero es crear un nuevo servicio en Compute con el que podamos hacer probaturas. Empezamos instalando la CLI de Fastly e inicializando un nuevo proyecto:

> fastly compute init

Creating a new Compute project.

Press ^C at any time to quit.

Name: [edge] developer-hub
Description: Fastly Developer Hub
Author: devrel@fastly.com
Language:
[1] Rust
[2] AssemblyScript (beta)
[3] JavaScript (beta)
[4] Other ('bring your own' Wasm binary)
Choose option: [1] 3
Starter kit:
[1] Default starter for JavaScript
    A basic starter kit that demonstrates routing, simple synthetic responses and
    overriding caching rules.
    https://github.com/fastly/compute-starter-kit-javascript-default
Choose option or paste git URL: [1] 1

✓ Initializing...
✓ Fetching package template...
✓ Updating package manifest...
✓ Initializing package...

Initialized package developer-hub to:
        ~/repos/Developer-Hub/edge

A grandes rasgos, el Developer Hub es una aplicación de JavaScript que usa el marco Gatsby, de modo que elegimos escribir nuestro código de Compute también en JavaScript. La aplicación generada por el comando init funciona sin ningún retoque, por lo que vale la pena desplegarla en producción de inmediato para dar comienzo a un ciclo de desarrollo-pruebas-iteración:

> fastly compute publish
✓ Initializing...
✓ Verifying package manifest...
✓ Verifying local javascript toolchain...
✓ Building package using javascript toolchain...
✓ Creating package archive...

SUCCESS: Built package 'developer-hub' (pkg/developer-hub.tar.gz)


There is no Fastly service associated with this package. To connect to an existing service add the Service ID to the fastly.toml file, otherwise follow the prompts to create a service now.

Press ^C at any time to quit.

Create new service: [y/N] y

✓ Initializing...
✓ Creating service...

Domain: [some-funky-words.edgecompute.app] 

Backend (hostname or IP address, or leave blank to stop adding backends): 

✓ Creating domain some-funky-words.edgecompute.app'...
✓ Uploading package...
✓ Activating version...

Manage this service at:
        https://manage.fastly.com/configure/services/*****************

View this service at:
        https://some-funky-words.edgecompute.app


SUCCESS: Deployed package (service *****************, version 1)

Ahora ya tenemos un servicio operativo, distribuido desde el edge de Fastly, y podemos implementarle cambios en cuestión de segundos. ¡A migrar!

Google Cloud Storage

El principal contenido del Developer Hub es el sitio Gatsby, que se compila y se despliega en Google Cloud Storage para luego distribuirlo como sitio estático. Podemos empezar añadiendo un backend:

fastly backend create --name gcs --address storage.googleapis.com --version active --autoclone

Luego, editamos el archivo de origen principal de la aplicación de Compute (en este caso, src/index.js) para cargar el contenido desde GCS. Empecemos sustituyendo todo el contenido del archivo por lo siguiente:

const BACKENDS = {
  GCS: "gcs",
}
const GCS_BUCKET_NAME = "fastly-developer-portal"

async function handleRequest(event) {
  const req = event.request
  const reqUrl = new URL(req.url)

  let backendName

  backendName = BACKENDS.GCS
  reqUrl.pathname = "/" + GCS_BUCKET_NAME + "/production" + reqUrl.pathname

  // Fetch the index page if the request is for a directory
  if (reqUrl.pathname.endsWith("/")) {
    reqUrl.pathname = reqUrl.pathname + "index.html"
  }

  const beReq = new Request(reqUrl, req);
  let beResp = await fetch(beReq, {
    backend: backendName,
    cacheOverride: new CacheOverride(["GET", "HEAD", "FASTLYPURGE"].includes(req.method) ? "none" : "pass"),
  })

  if (backendName === BACKENDS.GCS && beResp.status === 404) {
    // Try for a directory index if the original request didn't end in /
    if (!reqUrl.pathname.endsWith("/index.html")) {
      reqUrl.pathname += "/index.html"
      const dirRetryResp = await fetch(new Request(reqUrl, req), { backend: BACKENDS.GCS })
      if (dirRetryResp.status === 200) {
        const origURL = new URL(req.url) // Copy of original client URL
        return createRedirectResponse(origURL.pathname + "/")
      }
    }
  }

  return beResp
}

const createRedirectResponse = (dest) =>
  new Response("", {
    status: 301,
    headers: { Location: dest },
  })

addEventListener("fetch", (event) => event.respondWith(handleRequest(event)))

Este código implementa nuestro patrón recomendado como frontend para un cubo de GCS público, pero repasemos paso por paso:

  • Define algunas constantes. El objeto BACKENDS nos permite hacer el enrutamiento extensible a backends posteriores. El valor «gcs» muestra el mismo nombre que le habíamos dado al backend.

  • Para simplificar las cosas, asignaremos la petición de cliente event.request a req, y mediante la clase de URL podemos convertir req.url en un objeto de URL analizado, reqUrl.

  • Para llegar al objeto correcto del cubo de GCS, añadimos el nombre del cubo al principio de la ruta. En nuestro caso, también debemos añadir «production», puesto que también tenemos ramas que no son de producción en nuestro cubo de GCS.

  • Si la petición entrante termina en /, es lógico añadir «index.html» para encontrar páginas de índice de directorios en el cubo.

  • Si la respuesta de Google es un error 404 y la petición del cliente no terminó en barra oblicua, probamos añadiendo «/index.html» a la ruta y lo intentamos de nuevo. Si funciona, devolvemos un redireccionamiento externo para pedir al cliente que añada una barra oblicua.

  • Al final, añadimos una función de controlador de petición al evento «fetch» del cliente.

Compilamos y desplegamos:

> fastly compute publish
✓ Initializing...
✓ Verifying package manifest...
✓ Verifying local javascript toolchain...
✓ Building package using javascript toolchain...
✓ Creating package archive...

SUCCESS: Built package 'developer-hub' (pkg/developer-hub.tar.gz)

✓ Uploading package...
✓ Activating version...

SUCCESS: Deployed package (service *****************, version 3)

Ahora, al cargar palabrejas-varias.edgecompute.app en un navegador, ¡nos aparece el Developer Hub! Empezamos con buen pie.

Página personalizada de 404

Las páginas que ya existen funcionan a las mil maravillas, pero si tratamos de cargar una página inexistente, nada bueno nos espera:

Gatsby genera una página aceptable de «No se encontró la página» y la despliega como «404.html» en el cubo de GCS. Basta con que distribuyamos ese contenido si la petición del cliente generó una respuesta 404 de Google:

if (backendName === BACKENDS.GCS && beResp.status === 404) {
  
  // ... existing code here ... //

  // Use a custom 404 page
  if (!reqUrl.pathname.endsWith("/404.html")) {
    debug("Fetch custom 404 page")
    const newPath = "/" + GCS_BUCKET_NAME + "/404.html"
    beResp = await fetch(new Request(newPath, req), { backend: BACKENDS.GCS })
    beResp = new Response(beResp.body, { status: 404, headers: beResp.headers })
  }
}

Fíjate que no devolvemos la beResp que recibimos directamente desde el origen porque tendrá un código de estado 200 (OK); lo que hacemos es crear una nueva respuesta «404» utilizando el flujo del cuerpo de la respuesta de GCS. Entramos en más detalle en este patrón en nuestra guía de integración de backends.

Igual que antes, ejecutamos fastly compute publish para realizar el despliegue. Ahora, nuestros errores de «No se encontró la página» ya tienen mejor aspecto:

Redireccionamientos

No hace falta decir que developer.fastly.com también cuenta con muchos redireccionamientos, por lo que serán estos los siguientes que migraremos desde VCL. Ya usamos diccionarios Edge para almacenarlos: uno para redireccionamientos exactos y otro para redireccionamientos de prefijo. Ambos se pueden cargar al principio del controlador de peticiones:

async function handleRequest(event) {
  const req = event.request
  const reqUrl = new URL(req.url)
  const reqPath = reqUrl.pathname
  const dictExactRedirects = new Dictionary("exact_redirects")
  const dictPrefixRedirects = new Dictionary("prefix_redirects")

​​  // Exact redirects
  const normalizedReqPath = reqPath.replace(/\/$/, "")
  const redirDest = dictExactRedirects.get(normalizedReqPath)
  if (redirDest) {
    return createRedirectResponse(redirDest)
  }

  // Prefix redirects
  let redirSrc = String(normalizedReqPath)
  while (redirSrc.includes("/")) {
    const redirDest = dictPrefixRedirects.get(redirSrc)
    if (redirDest) {
      return createRedirectResponse(redirDest + reqPath.slice(redirSrc.length))
    }
    redirSrc = redirSrc.replace(/\/[^/]*$/, "")
  }

Los redireccionamientos exactos son muy sencillos, pero los redireccionamientos de prefijo nos obligan a dar un rodeo eliminando un segmento de la URL cada vez hasta que la ruta esté vacía, para luego buscar un prefijo cada vez más corto en el diccionario. Si encontramos un redireccionamiento de prefijo, la parte de la petición del cliente que no coincide se añade al redireccionamiento, de modo que un redireccionamiento de /source => /destination redireccionará las peticiones de /source/foo a /destination/foo).

Asignación de un nombre canónico para el nombre del host

Hubo un momento en el que pensamos que estaría bien que developer.fastly.com también estuviera disponible como fastly.dev. Así, nuestro servicio de VCL redirecciona a los usuarios que solicitan fastly.dev a developer.fastly.com. Eso será lo siguiente que migraremos. Empezamos definiendo, al principio del todo del archivo, cuál es el dominio canónico del sitio:

const PRIMARY_DOMAIN = "developer.fastly.com"

Acto seguido, en el controlador de la petición, leemos el encabezado del host y lo redireccionamos si hace falta. Cuando lo probamos, el dominio que pusimos fue del estilo palabrejas-varias.edgecompute.app. Esto va antes del código de redireccionamientos, justo después de las declaraciones del principio del controlador de peticiones:

async function handleRequest(event) {
  // ... existing code ... //
  const hostHeader = req.headers.get("host")

  // Canonicalize hostname
  if (!req.headers.has("Fastly-FF") && hostHeader !== PRIMARY_DOMAIN) {
    return createRedirectResponse("https://" + PRIMARY_DOMAIN + reqPath)
  }

Si tu servicio de VCL también redirecciona por TLS las peticiones HTTP no seguras,  Compute se encarga de ello automáticamente y ya no envía peticiones a tu código hasta que haya una conexión segura, por lo que no hace falta migrar esta función.

Para probar el código, el servicio necesita tener un segundo dominio que no sea canónico. Para crearlo, usamos fastly domain create:

fastly domain create --name testing-fastly-devhub.global.ssl.fastly.net --version latest --autoclone

Puesto que aquí tenemos un caso de uso ideal para dominios asignados por Fastly, podemos dejar en paz el DNS (de momento). Si ahora vamos a testing-fastly-devhub.global.ssl.fastly.net, nuestro servicio nos redirige al dominio principal.

Encabezados de respuesta

Llegados a este punto, pasamos a las modificaciones que nuestro servicio de VCL realiza en las respuestas que recibimos de GCS. Google añade varios encabezados a las respuestas que no queremos compartir con el cliente, como x-goog-generation. Dado que es posible que el backend añada más en el futuro, tiene sentido que filtremos los encabezados mediante una lista de permitidos. Primero, arriba del todo del archivo, definimos los encabezados de respuesta permitidos:

const ALLOWED_RESP_HEADERS = [
  "cache-control",
  "content-encoding",
  "content-type",
  "date",
  "etag",
  "vary",
]

A continuación, después de la recuperación por parte del backend, podemos insertar código que filtre los encabezados que recibimos:

// Filter backend response to retain only allowed headers
beResp.headers.keys().forEach((k) => {
  if (!ALLOWED_RESP_HEADERS.includes(k)) beResp.headers.delete(k)
})

También hay ciertos encabezados que nos interesa ver en las respuestas del cliente, como Content-Security-Policy, por lo que hay que añadirlos. Muchos de estos solo tienen que estar en las respuestas HTML:

if ((beResp.headers.get("content-type") || "").includes("text/html")) {
  beResp.headers.set("Content-Security-Policy", "default-src 'self'; scrip...")
  beResp.headers.set("X-XSS-Protection", "1")
  beResp.headers.set("Referrer-Policy", "origin-when-cross-origin")
  beResp.headers.append(
    "Link",
    "</fonts/CircularStd-Book.woff2>; rel=preload; as=font; crossorigin=anonymous," +
      "</fonts/Lexia-Regular.woff2>; rel=preload; as=font; crossorigin=anonymous," +
      "<https://www.google-analytics.com>; rel=preconnect"
  )
  beResp.headers.set("alt-svc", `h3-29=":443";ma=86400,h3-27=":443";ma=86400`)
  beResp.headers.set("Strict-Transport-Security", "max-age=86400")
}

Añadir y eliminar encabezados es un caso de uso muy común en los servicios de VCL y Compute.

Backends de acceso directo

Otra función que realiza nuestro servicio de VCL es pasar peticiones a backends que no son GCS, lo cual nos permite invocar funciones de la nube u otros servicios externos. Por ejemplo, teniendo en cuenta que usamos Swiftype para el motor de búsqueda del Developer Hub, si queremos hacer que la API de búsqueda esté disponible en el dominio developer.fastly.com, podemos añadir un nuevo backend para luego dirigir ciertas rutas de peticiones hacia ese backend.

Primero, usamos la CLI de Fastly para añadir el backend:

fastly backend create --name swiftype --address search-api.swiftype.com --version active --autoclone

Acto seguido, añadimos una constante al código fuente que nos permita hacer referencia a ese backend:

const BACKENDS = {
  GCS: "gcs",
  SWIFTYPE: "swiftype",
}

Por último, actualizamos el código de selección de backend para añadir lógica de enrutamiento que seleccione Swiftype cuando corresponda:

let backendName
if (reqPath === "/api/internal/search") {
  backendName = BACKENDS.SWIFTYPE
  reqUrl.pathname = "/api/v1/public/engines/search.json"
} else {
  backendName = BACKENDS.GCS
  reqUrl.pathname = "/" + GCS_BUCKET_NAME + "/production" + reqUrl.pathname

  // Fetch the index page if the request is for a directory
  if (reqUrl.pathname.endsWith("/")) {
    reqUrl.pathname = reqUrl.pathname + "index.html"
  }
}

Puedes repetir este proceso con el resto de backends que tengas. Seguimos esta misma técnica con Sentry y Formkeep, por ejemplo. Hablaremos más en profundidad de este patrón más adelante.

Desplegar con GitHub Actions

Nuestro servicio de VCL no contaba con gestión del código fuente, y podemos aprovechar para corregirlo.  La versión de Compute está almacenada junto con el código fuente principal del Developer Hub, de modo que podemos coordinar los lanzamientos en el edge con actualizaciones del backend y crear dependencias; así, no se enviará ningún cambio a la lógica del edge si no se ha desplegado antes en el backend.

Añadimos dos tareas a nuestro flujo de trabajo de CI; una, para crear la aplicación en el edge para las peticiones «pull»:

  build-fastly:
    name: C@E build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v2
      - name: Set up Fastly CLI
        uses: fastly/compute-actions/setup@main
      - name: Install edge code dependencies
        run: cd edge && npm install
      - name: Build Compute@Edge Package
        uses: fastly/compute-actions/build@main
        with:
          project_directory: ./edge/

Y la segunda, para desplegar la aplicación del edge una vez que se haya completado el despliegue de GCS:

  update-edge-app:
    name: Deploy C@E app
    runs-on: ubuntu-latest
    if: ${{ github.ref_name == 'production' }}
    needs: deploy
    steps:
      - name: Checkout Code
        uses: actions/checkout@v2
      - name: Install edge code dependencies
        run: cd edge && npm install
      - name: Deploy to Compute@Edge
        uses: fastly/compute-actions@main
        with:
          project_directory: ./edge/
        env:
          FASTLY_API_TOKEN: ${{ secrets.FASTLY_API_TOKEN }}

Siguientes pasos

Con la migración completada, ya contamos con las mismas funcionalidades que teníamos con el servicio de VCL. Sin embargo, ahora que Compute actúa como frontend del Developer Hub, podemos pasar todas nuestras funciones de la nube al código de informática en el edge y empezar a realizar tareas de mayor envergadura en el edge.

Es posible que migrar servicios a Compute resulte ser más fácil de lo que crees. Si estás preparándote para hacer tu propia migración, consulta nuestra guía de migración de VCL a Compute, que repasa muchos de los patrones que tuvimos que convertir para el Developer Hub, así como nuestra biblioteca de soluciones, que cuenta con demostraciones, ejemplos de código y tutoriales dedicados a todo tipo de casos de uso.

¿Aún no usas Compute? Descubre cómo nuestra plataforma de informática en el edge sin servidores puede ayudarte a crear aplicaciones más rápidas, seguras y eficientes en el edge.