Desplegar una aplicación con Next.js en Azure App Service a través de Azure DevOps

Este artículo es el resultado de una configuración que he estado haciendo para desplegar una aplicación que utiliza el framework Next.js en Azure App Service, utilizando Azure DevOps. Aquí te cuento cómo son mis pipelines de Build y de Release.

Código de ejemplo

El código que voy a utilizar para esta prueba es el que se utiliza en la página de Next.js cuando estás comenzando con el framework. A este le he añadido el archivo web.config, con el siguiente contenido:

<?xml version="1.0" encoding="utf-8"?>
<!--
     This configuration file is required if iisnode is used to run node processes behind
     IIS or IIS Express.  For more information, visit:
     https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config
-->
<configuration>
  <system.webServer>
    <!-- Visit http://blogs.msdn.com/b/windowsazure/archive/2013/11/14/introduction-to-websockets-on-windows-azure-web-sites.aspx for more information on WebSocket support -->
    <webSocket enabled="false" />
    <handlers>
      <!-- Indicates that the server.js file is a node.js site to be handled by the iisnode module -->
      <add name="iisnode" path="server.js" verb="*" modules="iisnode"/>
    </handlers>
    <rewrite>
      <rules>
        <!-- Do not interfere with requests for node-inspector debugging -->
        <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
          <match url="^server.js\/debug[\/]?" />
        </rule>
        <!-- First we consider whether the incoming URL matches a physical file in the /public folder -->
        <rule name="StaticContent">
          <action type="Rewrite" url="public{REQUEST_URI}"/>
        </rule>
        <!-- All other URLs are mapped to the node.js site entry point -->
        <rule name="DynamicContent">
          <conditions>
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
          </conditions>
          <action type="Rewrite" url="server.js"/>
        </rule>
      </rules>
    </rewrite>
    
    <!-- 'bin' directory has no special meaning in node.js and apps can be placed in it -->
    <security>
      <requestFiltering>
        <hiddenSegments>
          <remove segment="bin"/>
        </hiddenSegments>
      </requestFiltering>
    </security>
    <!-- Make sure error responses are left untouched -->
    <httpErrors existingResponse="PassThrough" />
    <!--
      You can control how Node is hosted within IIS using the following options:
        * watchedFiles: semi-colon separated list of files that will be watched for changes to restart the server
        * node_env: will be propagated to node as NODE_ENV environment variable
        * debuggingEnabled - controls whether the built-in debugger is enabled
      See https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config for a full list of options
    -->
    <!--<iisnode watchedFiles="web.config;*.js"/>-->
  </system.webServer>
</configuration>

Y el archivo server.js que servirá el contenido del sitio:

const express = require('express'),
  next = require('next'),
  dev = process.env.NODE_ENV !== 'production',
  port = process.env.PORT || 3000,
  app = next({ dev }),
  handle = app.getRequestHandler()
app.prepare()
  .then(() => {
    const server = express();
    server.get('/p/:id', (req, res) => {
      const actualPage = '/post',
        queryParams = { title: req.params.id };
      app.render(req, res, actualPage, queryParams);
    })
    server.get('*', (req, res) => {
      return handle(req, res);
    })
    server.listen(port, (err) => {
      if (err) throw err;
      console.log('> Ready on http://localhost:' + port);
    })
  })
  .catch((ex) => {
    console.error(ex.stack);
    process.exit(1);
  });

Ambos deben estar en la raíz del proyecto.

Por último, en el archivo package.json he modificado los scripts asociados, para poder utilizarlos luego en el proceso de build:

{
  "name": "hello-next",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "dev": "next",
    "build": "NODE_ENV=production && next build",
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "next": "^8.1.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

La pipeline de tipo Build

Lo primero que tienes que generar es el paquete a desplegar. Para ello he creado la siguiente pipeline, en el apartado de Builds, con estas tareas:

npm install: se van a encargar de instalar todos los paquetes de Node.js Es tan cual añades la tarea de tipo npm, es decir que no tienes que configurar nada en la misma.

npm run build: esta tarea es necesaria para que Next.js genere la carpeta .next con el contenido productivo. En ella he modificado el valor de Command a Custom y en Command and arguments he especificado run build. Este script forma parte de nuestro package.json, por lo que realmente estará lanzando por debajo es NODE_ENV=production && next build.

Configuración de la tarea npm run build

Copy node_modules into prod folder: en este paso el objetivo es crear una carpeta llamada prod folder donde voy a empezar a incluir todo lo que necesito desplegar en mi App Service. Hay diferentes formas de hacer este proceso, claro está, pero en esta veo exactamente lo que necesito. Empiezo con la copia de la carpeta node_modules. Como Source Folder he especificado $(Build.SourcesDirectory)/node_modules y como Target $(System.DefaultWorkingDirectory)/prod/node_modules. Por otro lado, debes asegurarte de que el campo Content tiene el valor **, para que se copien todos los módulos.

Configuración de la tarea Copy node_modules into prod folder

Copy web.config into prod folder: cuando desplegamos una aplicación Node.js en Azure App Service es necesario que en el archivo web.config se indique cómo se gestionan las peticiones. El archivo de configuración que estoy utilizando es el que te mostré al inicio del artículo. Para copiarlo dentro de la carpeta prod, utilizo como Source Folder $(Build.SourcesDirectory), en Contents lo he modificado a **.config y en Target $(System.DefaultWorkingDirectory)/prod.

Configuración de la tarea Copy web.config into prod folder

Copy server.js into prod folder: en esta le toca el turno al archivo server.js, que es el que utilizo para servir el contenido de la aplicación Next.js. En este caso, en la tarea, especifico el mismo Source Folder y Target Folder que en la anterior, pero modifico Contents a directamente server.js, ya que conozco la ruta y el nombre del archivo que quiero copiar.

Configuración de la tarea Copy server.js into prod folder

Copy .next folder into prod folder: la última copia dentro de prod es la carpeta .next, que es la que generaba la tarea npm run build y contiene el contenido de tu aplicación comprimido y listo para producción. En este caso el Source Folder es $(System.DefaultWorkingDirectory)/.next, en Contents decimos que queremos copiar todo con **, y en Target Folder utilizamos $(System.DefaultWorkingDirectory)/prod/.next.

Configuración de la tarea Copy .next folder into prod folder

Archive /prod: ya tenemos todo el contenido listo dentro de la carpeta prod. En esta tarea simplemente voy a crear un zip con todo lo que haya en la ruta $(System.DefaultWorkingDirectory)/prod y en mi caso lo voy a nombrar con el número de la build ($(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip), pero puedes utilizar cualquier otro patrón.

Configuración de la tarea Archive /prod

Copy Files to ArtifactsToBePublished: para que sea más cómodo publicar el zip que acabo de crear, lo último que hago es mover es este mismo a una carpeta llamada ArtifactsToBePublished.

Configuración de la tarea Copy Files to ArtifactsToBePublished

Publish Artifact: drop: para finalizar mi pipeline, a través de esta tarea publico como artefacto todo lo que encuentre en la carpeta $(Build.ArtifactStagingDirectory)\ArtifactsToBePublished y lo llamo drop. La localización del artefacto debe ser Azure pipelines para poder utilizarlo después en mi pipeline del tipo Release.

Configuración de la tarea Publish Artifact: drop

La pipeline de tipo Release

En este artículo no he querido complicarlo mucho, porque lo que más me importaba era la parte de generación del paquete. Aquí podría añadir slots y algunos puntos más, pero hoy me conformo con el despliegue del artefacto que acabamos de publicar. Para ello, en el apartado de Releases, crea una nueva que tenga esta estructura:

En la caja de Artifacts tienes que tener seleccionado la pipeline creada en el apartado anterior. Por otro lado, en este ejemplo, solamente tengo un Stage llamado Dev con una única tarea que es Deploy Azure App Service:

¡Saludos!