Usar paquetes del sistema


En este tutorial se muestra cómo crear un servicio Knative serving personalizado que transforma un parámetro de entrada de descripción de un gráfico en un diagrama con el formato de imagen PNG. Usa Graphviz, que se instala como paquete del sistema en el entorno de contenedor del servicio. Graphviz se usa a través de utilidades de línea de comandos para atender solicitudes.

Objetivos

  • Escribe y crea un contenedor personalizado con un Dockerfile
  • Escribir, compilar y desplegar un servicio de Knative Serving
  • Usar la utilidad DOT de Graphviz para generar diagramas
  • Prueba el servicio publicando un diagrama de sintaxis DOT de la colección o una creación propia

Costes

En este documento, se utilizan los siguientes componentes facturables de Google Cloud:

Para generar una estimación de costes basada en el uso previsto, utiliza la calculadora de precios.

Los usuarios nuevos Google Cloud pueden disfrutar de una prueba gratuita.

Antes de empezar

Obtener el código de ejemplo

Para obtener el código de muestra que vas a usar, sigue estos pasos:

  1. Clona el repositorio de aplicaciones de muestra en la máquina local:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    También puedes descargar el ejemplo como un archivo ZIP y extraerlo.

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    También puedes descargar el ejemplo como un archivo ZIP y extraerlo.

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

    También puedes descargar el ejemplo como un archivo ZIP y extraerlo.

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

    También puedes descargar el ejemplo como un archivo ZIP y extraerlo.

  2. Cambia al directorio que contiene el código de ejemplo de servicio de Knative:

    Node.js

    cd nodejs-docs-samples/run/system-package/

    Python

    cd python-docs-samples/run/system-package/

    Go

    cd golang-samples/run/system_package/

    Java

    cd java-docs-samples/run/system-package/

Visualizar la arquitectura

La arquitectura básica es la siguiente:

Diagrama que muestra el flujo de solicitudes desde el usuario hasta el servicio web y la utilidad DOT de Graphviz.
Para ver la fuente del diagrama, consulta la descripción de DOT

El usuario envía una solicitud HTTP al servicio de Knative Serving, que ejecuta una utilidad de Graphviz para transformar la solicitud en una imagen. Esa imagen se envía al usuario como respuesta HTTP.

Información sobre el código

Definir la configuración del entorno con Dockerfile

Tu Dockerfile es específico del idioma y del entorno operativo base, como Ubuntu, que usará tu servicio.

Este servicio requiere uno o varios paquetes de sistema adicionales que no están disponibles de forma predeterminada.

  1. Abre el Dockerfile en un editor.

  2. Busca un extracto de Dockerfile RUN. Esta instrucción permite ejecutar comandos de shell arbitrarios para modificar el entorno. Si el Dockerfile tiene varias fases, identificadas por varias instrucciones FROM, se encontrará en la última fase.

    Los paquetes específicos necesarios y el mecanismo para instalarlos varían en función del sistema operativo declarado en el contenedor.

    Para obtener instrucciones sobre tu sistema operativo o imagen base, haz clic en la pestaña correspondiente.

    Debian/Ubuntu
    RUN apt-get update -y && apt-get install -y \
      graphviz \
      && apt-get clean
    Alpine
    Alpine requiere un segundo paquete para admitir fuentes.
    RUN apk --no-cache add graphviz

    Para determinar el sistema operativo de tu imagen de contenedor, consulta el nombre en la instrucción FROM o en un archivo README asociado a tu imagen base. Por ejemplo, si amplías desde node, puedes encontrar documentación y el elemento principal Dockerfile en Docker Hub.

  3. Para probar la personalización, compila la imagen con docker build localmente o con Cloud Build.

Gestionar solicitudes entrantes

El servicio de ejemplo usa parámetros de la solicitud HTTP entrante para invocar una llamada al sistema que ejecuta el comando de utilidad dot adecuado.

En el controlador HTTP que se muestra a continuación, se extrae un parámetro de entrada de descripción de gráfico de la variable de cadena de consulta dot.

Las descripciones de los gráficos pueden incluir caracteres que deben codificarse como URL para usarse en una cadena de consulta.

Node.js

app.get('/diagram.png', (req, res) => {
  try {
    const image = createDiagram(req.query.dot);
    res.setHeader('Content-Type', 'image/png');
    res.setHeader('Content-Length', image.length);
    res.setHeader('Cache-Control', 'public, max-age=86400');
    res.send(image);
  } catch (err) {
    console.error(`error: ${err.message}`);
    const errDetails = (err.stderr || err.message).toString();
    if (errDetails.includes('syntax')) {
      res.status(400).send(`Bad Request: ${err.message}`);
    } else {
      res.status(500).send('Internal Server Error');
    }
  }
});

Python

@app.route("/diagram.png", methods=["GET"])
def index():
    """Takes an HTTP GET request with query param dot and
    returns a png with the rendered DOT diagram in a HTTP response.
    """
    try:
        image = create_diagram(request.args.get("dot"))
        response = make_response(image)
        response.headers.set("Content-Type", "image/png")
        return response

    except Exception as e:
        print(f"error: {e}")

        # If no graphviz definition or bad graphviz def, return 400
        if "syntax" in str(e):
            return f"Bad Request: {e}", 400

        return "Internal Server Error", 500

Go


// diagramHandler renders a diagram using HTTP request parameters and the dot command.
func diagramHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		log.Printf("method not allowed: %s", r.Method)
		http.Error(w, fmt.Sprintf("HTTP Method %s Not Allowed", r.Method), http.StatusMethodNotAllowed)
		return
	}

	q := r.URL.Query()
	dot := q.Get("dot")
	if dot == "" {
		log.Print("no graphviz definition provided")
		http.Error(w, "Bad Request", http.StatusBadRequest)
		return
	}

	// Cache header must be set before writing a response.
	w.Header().Set("Cache-Control", "public, max-age=86400")

	input := strings.NewReader(dot)
	if err := createDiagram(w, input); err != nil {
		log.Printf("createDiagram: %v", err)
		// Do not cache error responses.
		w.Header().Del("Cache-Control")
		if strings.Contains(err.Error(), "syntax") {
			http.Error(w, "Bad Request: DOT syntax error", http.StatusBadRequest)
		} else {
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		}
	}
}

Java

get(
    "/diagram.png",
    (req, res) -> {
      InputStream image = null;
      try {
        String dot = req.queryParams("dot");
        image = createDiagram(dot);
        res.header("Content-Type", "image/png");
        res.header("Content-Length", Integer.toString(image.available()));
        res.header("Cache-Control", "public, max-age=86400");
      } catch (Exception e) {
        if (e.getMessage().contains("syntax")) {
          res.status(400);
          return String.format("Bad Request: %s", e.getMessage());
        } else {
          res.status(500);
          return "Internal Server Error";
        }
      }
      return image;
    });

Deberás diferenciar entre los errores de servidor internos y las entradas de usuario no válidas. Este servicio de ejemplo devuelve un error de servidor interno para todos los errores de línea de comandos de punto, a menos que el mensaje de error contenga la cadena syntax, que indica un problema de entrada del usuario.

Generar un diagrama

La lógica principal de la generación de diagramas usa la herramienta de línea de comandos dot para procesar el parámetro de entrada de descripción del gráfico y convertirlo en un diagrama en formato de imagen PNG.

Node.js

// Generate a diagram based on a graphviz DOT diagram description.
const createDiagram = dot => {
  if (!dot) {
    throw new Error('syntax: no graphviz definition provided');
  }

  // Adds a watermark to the dot graphic.
  const dotFlags = [
    '-Glabel="Made on Cloud Run"',
    '-Gfontsize=10',
    '-Glabeljust=right',
    '-Glabelloc=bottom',
    '-Gfontcolor=gray',
  ].join(' ');

  const image = execSync(`/usr/bin/dot ${dotFlags} -Tpng`, {
    input: dot,
  });
  return image;
};

Python

def create_diagram(dot):
    """Generates a diagram based on a graphviz DOT diagram description.

    Args:
        dot: diagram description in graphviz DOT syntax

    Returns:
        A diagram in the PNG image format.
    """
    if not dot:
        raise Exception("syntax: no graphviz definition provided")

    dot_args = [  # These args add a watermark to the dot graphic.
        "-Glabel=Made on Cloud Run",
        "-Gfontsize=10",
        "-Glabeljust=right",
        "-Glabelloc=bottom",
        "-Gfontcolor=gray",
        "-Tpng",
    ]

    # Uses local `dot` binary from Graphviz:
    # https://graphviz.gitlab.io
    image = subprocess.run(
        ["dot"] + dot_args, input=dot.encode("utf-8"), stdout=subprocess.PIPE
    ).stdout

    if not image:
        raise Exception("syntax: bad graphviz definition provided")
    return image

Go


// createDiagram generates a diagram image from the provided io.Reader written to the io.Writer.
func createDiagram(w io.Writer, r io.Reader) error {
	stderr := new(bytes.Buffer)
	args := []string{
		"-Glabel=Made on Cloud Run",
		"-Gfontsize=10",
		"-Glabeljust=right",
		"-Glabelloc=bottom",
		"-Gfontcolor=gray",
		"-Tpng",
	}
	cmd := exec.Command("/usr/bin/dot", args...)
	cmd.Stdin = r
	cmd.Stdout = w
	cmd.Stderr = stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("exec(%s) failed (%w): %s", cmd.Path, err, stderr.String())
	}

	return nil
}

Java

// Generate a diagram based on a graphviz DOT diagram description.
public static InputStream createDiagram(String dot) {
  if (dot == null || dot.isEmpty()) {
    throw new NullPointerException("syntax: no graphviz definition provided");
  }
  // Adds a watermark to the dot graphic.
  List<String> args = new ArrayList<>();
  args.add("/usr/bin/dot");
  args.add("-Glabel=\"Made on Cloud Run\"");
  args.add("-Gfontsize=10");
  args.add("-Glabeljust=right");
  args.add("-Glabelloc=bottom");
  args.add("-Gfontcolor=gray");
  args.add("-Tpng");

  StringBuilder output = new StringBuilder();
  InputStream stdout = null;
  try {
    ProcessBuilder pb = new ProcessBuilder(args);
    Process process = pb.start();
    OutputStream stdin = process.getOutputStream();
    stdout = process.getInputStream();
    // The Graphviz dot program reads from stdin.
    Writer writer = new OutputStreamWriter(stdin, "UTF-8");
    writer.write(dot);
    writer.close();
    process.waitFor();
  } catch (Exception e) {
    System.out.println(e);
  }
  return stdout;
}

Diseñar un servicio seguro

Las vulnerabilidades de la herramienta dot son vulnerabilidades potenciales del servicio web. Para mitigar este problema, puedes usar versiones actualizadas del paquete graphviz recompilando la imagen del contenedor de forma periódica.

Si amplías la muestra actual para que acepte datos introducidos por el usuario como parámetros de línea de comandos, debes protegerte contra los ataques de inyección de comandos. Estas son algunas de las formas de evitar los ataques de inyección:

  • Asignar entradas a un diccionario de parámetros admitidos
  • Validar que las entradas coincidan con un intervalo de valores seguros conocidos, quizás mediante expresiones regulares
  • Escapar las entradas para asegurarse de que no se evalúe la sintaxis de shell

Envío del código

Para enviar tu código, compila con Cloud Build, sube el contenido a Container Registry y despliégalo en Knative Serving:

  1. Ejecuta el siguiente comando para compilar el contenedor y publicarlo en Container Registry.

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz

    Donde PROJECT_ID es el ID de tu proyecto Google Cloud y graphviz es el nombre que quieres dar a tu servicio.

    Si la operación se realiza correctamente, verás un mensaje de ÉXITO que contiene el ID, la hora de creación y el nombre de la imagen. La imagen se almacena en Container Registry y se puede volver a usar si quieres.

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz

    Donde PROJECT_ID es el ID de tu proyecto Google Cloud y graphviz es el nombre que quieres dar a tu servicio.

    Si la operación se realiza correctamente, verás un mensaje de ÉXITO que contiene el ID, la hora de creación y el nombre de la imagen. La imagen se almacena en Container Registry y se puede volver a usar si quieres.

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz

    Donde PROJECT_ID es el ID de tu proyecto Google Cloud y graphviz es el nombre que quieres dar a tu servicio.

    Si la operación se realiza correctamente, verás un mensaje de ÉXITO que contiene el ID, la hora de creación y el nombre de la imagen. La imagen se almacena en Container Registry y se puede volver a usar si quieres.

    Java

    En este ejemplo se usa Jib para crear imágenes de Docker con herramientas comunes de Java. Jib optimiza las compilaciones de contenedores sin necesidad de usar un Dockerfile ni de tener Docker instalado. Más información sobre cómo crear contenedores Java con Jib

    1. Con el Dockerfile, configura y compila una imagen base con los paquetes del sistema instalados para anular la imagen base predeterminada de Jib:

      # Use the Official eclipse-temurin image for a lean production stage of our multi-stage build.
      # https://hub.docker.com/_/eclipse-temurin/
      FROM eclipse-temurin:17.0.16_8-jre
      
      RUN apt-get update -y && apt-get install -y \
        graphviz \
        && apt-get clean
      gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz-base

      Donde PROJECT_ID es el ID de tu proyecto. Google Cloud

    2. Crea el contenedor final con Jib y publícalo en Container Registry:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.4.0</version>
        <configuration>
          <from>
            <image>gcr.io/PROJECT_ID/graphviz-base</image>
          </from>
          <to>
            <image>gcr.io/PROJECT_ID/graphviz</image>
          </to>
        </configuration>
      </plugin>
      mvn compile jib:build \
       -Dimage=gcr.io/PROJECT_ID/graphviz \
       -Djib.from.image=gcr.io/PROJECT_ID/graphviz-base

      Donde PROJECT_ID es el ID de tu proyecto. Google Cloud

  2. Implementa con el siguiente comando:

    gcloud run deploy graphviz-web --create-if-missing --image gcr.io/PROJECT_ID/graphviz

    Donde PROJECT_ID es el ID de tu proyecto, Google Cloud es el nombre del contenedor de arriba y graphviz es el nombre del servicio.graphviz-web

    Espera a que se complete el despliegue, que puede tardar aproximadamente medio minuto.

  3. Si quieres implementar una actualización de código en el servicio, repite los pasos anteriores. Cada despliegue en un servicio crea una revisión y empieza a servir tráfico automáticamente cuando está listo.

Pruébalo

Prueba tu servicio enviando solicitudes HTTP POST con descripciones de sintaxis DOT en la carga útil de la solicitud.

  1. Envía una solicitud HTTP a tu servicio.

    Puedes insertar el diagrama en una página web:

    1. Para obtener la IP externa del balanceador de carga, ejecuta el siguiente comando:

      kubectl get svc istio-ingressgateway -n ASM-INGRESS-NAMESPACE

      Sustituye ASM-INGRESS-NAMESPACE por el espacio de nombres en el que se encuentra tu entrada de Cloud Service Mesh. Especifica istio-system si has instalado Cloud Service Mesh con su configuración predeterminada.

      La salida resultante tiene un aspecto similar al siguiente:

      NAME                   TYPE           CLUSTER-IP     EXTERNAL-IP  PORT(S)
      istio-ingressgateway   LoadBalancer   XX.XX.XXX.XX   pending      80:32380/TCP,443:32390/TCP,32400:32400/TCP

      donde el valor EXTERNAL-IP es la dirección IP externa del balanceador de carga.

    2. Ejecuta un comando curl con esta dirección EXTERNAL-IP en la URL. No incluyas el protocolo (por ejemplo, http://) en SERVICE_DOMAIN.

      curl -G -H "Host: SERVICE_DOMAIN" http://EXTERNAL-IP/diagram.png \
         --data-urlencode "dot=digraph Run { rankdir=LR Code -> Build -> Deploy -> Run }" \
         > diagram.png
  2. Abre el archivo diagram.png resultante en cualquier aplicación que admita archivos PNG, como Chrome.

    Debería tener este aspecto:

    Diagrama que muestra el flujo de fases de &quot;Codificar&quot;, &quot;Compilar&quot;, &quot;Implementar&quot; y &quot;Ejecutar&quot;.
    Fuente: Descripción de DoT

Puedes consultar una pequeña colección de descripciones de diagramas prediseñadas.

  1. Copia el contenido del archivo .dot seleccionado.
  2. Pégalo en un comando curl:

    curl -G -H "Host: SERVICE_DOMAIN" http://EXTERNAL-IP/diagram.png \
    --data-urlencode "dot=digraph Run { rankdir=LR Code -> Build -> Deploy -> Run }" \
    > diagram.png

Limpieza

Puedes eliminar los recursos creados para este tutorial para evitar que se te cobren.

Eliminar recursos del tutorial

  1. Elimina el servicio de Knative que has desplegado en este tutorial:

    gcloud run services delete SERVICE-NAME

    Donde SERVICE-NAME es el nombre del servicio que has elegido.

    También puedes eliminar servicios de Knative Serving desde la consola deGoogle Cloud :

    Ir a Knative serving

  2. Elimina las configuraciones predeterminadas de gcloud que has añadido durante la configuración del tutorial:

     gcloud config unset run/platform
     gcloud config unset run/cluster
     gcloud config unset run/cluster_location
    
  3. Elimina la configuración del proyecto:

     gcloud config unset project
    
  4. Elimina otros recursos de Google Cloud que hayas creado en este tutorial:

Siguientes pasos

  • Experimenta con tu aplicación Graphviz:
    • Añade compatibilidad con otras utilidades de Graphviz que aplican diferentes algoritmos a la generación de diagramas.
    • Guarda los diagramas en Cloud Storage. ¿Quieres guardar la imagen o la sintaxis DOT?
    • Implementa la protección contra abusos de contenido con la API Cloud Natural Language.
  • Consulta arquitecturas de referencia, diagramas y prácticas recomendadas sobre Google Cloud. Consulta nuestro Centro de arquitectura de Cloud.