Usar pacotes do sistema


Este tutorial mostra como criar um serviço de publicação do Knative personalizado que transforma um parâmetro de entrada de descrição de um gráfico num diagrama no PNGformato de imagem. Usa o Graphviz que está instalado como um pacote do sistema no ambiente do contentor do serviço. O Graphviz é usado através de utilitários de linha de comandos para processar pedidos.

Objetivos

  • Escreva e crie um contentor personalizado com um Dockerfile
  • Escreva, crie e implemente um serviço de fornecimento do Knative
  • Use a utilidade Graphviz dot para gerar diagramas
  • Teste o serviço publicando um diagrama de sintaxe DOT da coleção ou da sua própria criação

Custos

Neste documento, usa os seguintes componentes faturáveis do Google Cloud:

Para gerar uma estimativa de custos com base na sua utilização projetada, use a calculadora de preços.

Os novos Google Cloud utilizadores podem ser elegíveis para uma avaliação gratuita.

Antes de começar

Obter o exemplo de código

Para obter o exemplo de código para utilização:

  1. Clone o repositório da app de exemplo para a sua máquina local:

    Node.js

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

    Em alternativa, pode transferir o exemplo como um ficheiro ZIP e extraí-lo.

    Python

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

    Em alternativa, pode transferir o exemplo como um ficheiro ZIP e extraí-lo.

    Go

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

    Em alternativa, pode transferir o exemplo como um ficheiro ZIP e extraí-lo.

    Java

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

    Em alternativa, pode transferir o exemplo como um ficheiro ZIP e extraí-lo.

  2. Altere para o diretório que contém o código de exemplo do Knative Serving:

    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 a arquitetura

A arquitetura básica tem o seguinte aspeto:

Diagrama que mostra o fluxo de pedidos do utilizador para o serviço Web e para a utilidade graphviz dot.
Para a origem do diagrama, consulte a descrição do DOT

O utilizador faz um pedido HTTP ao serviço Knative Serving, que executa um utilitário Graphviz para transformar o pedido numa imagem. Essa imagem é enviada ao utilizador como resposta HTTP.

Compreender o código

Definir a configuração do ambiente com o Dockerfile

O seu Dockerfile é específico do idioma e do ambiente de funcionamento base, como o Ubuntu, que o seu serviço vai usar.

Este serviço requer um ou mais pacotes de sistema adicionais que não estão disponíveis por predefinição.

  1. Abra o Dockerfile num editor.

  2. Procure um extrato da Dockerfile RUN Esta declaração permite a execução de comandos de shell arbitrários para modificar o ambiente. Se o Dockerfile tiver várias fases, identificadas pela localização de várias declarações FROM, é encontrado na última fase.

    Os pacotes específicos necessários e o mecanismo para os instalar variam consoante o sistema operativo declarado no contentor.

    Para obter instruções para o seu sistema operativo ou imagem base, clique no separador adequado.

    Debian/Ubuntu
    RUN apt-get update -y && apt-get install -y \
      graphviz \
      && apt-get clean
    Alpine
    O Alpine requer um segundo pacote para suporte de fontes.
    RUN apk --no-cache add graphviz

    Para determinar o sistema operativo da sua imagem de contentor, verifique o nome na declaração FROM ou num ficheiro README associado à sua imagem base. Por exemplo, se estender a partir de node, pode encontrar documentação e o elemento principal Dockerfile no Docker Hub.

  3. Teste a personalização compilando a imagem docker buildlocalmente ou com o Cloud Build.

Processar pedidos recebidos

O serviço de exemplo usa parâmetros do pedido HTTP recebido para invocar uma chamada de sistema que executa o comando de utilitário dot adequado.

No controlador HTTP abaixo, um parâmetro de entrada de descrição do gráfico é extraído da variável de cadeia de consulta dot.

As descrições dos gráficos podem incluir carateres que têm de ser codificados em URL para utilização numa string 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;
    });

Tem de distinguir entre erros internos do servidor e entradas do utilizador inválidas. Este serviço de exemplo devolve um erro interno do servidor para todos os erros de linha de comandos, a menos que a mensagem de erro contenha a string syntax, que indica um problema de entrada do utilizador.

A gerar um diagrama

A lógica essencial da geração de diagramas usa a ferramenta de linha de comando dot para processar o parâmetro de entrada de descrição do gráfico num diagrama no formato de imagem 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;
}

Conceber um serviço seguro

Quaisquer vulnerabilidades na ferramenta dot são potenciais vulnerabilidades do serviço Web. Pode mitigar esta situação usando versões atualizadas do pacote graphviz através da recompilação da imagem do contentor de forma regular.

Se estender o exemplo atual para aceitar a entrada do utilizador como parâmetros da linha de comandos, deve proteger-se contra ataques de injeção de comandos. Algumas das formas de evitar ataques de injeção incluem:

  • Mapeamento de entradas para um dicionário de parâmetros suportados
  • Validar se as entradas correspondem a um intervalo de valores conhecidos e seguros, talvez usando expressões regulares
  • Escape às entradas para garantir que a sintaxe da shell não é avaliada

Envio do código

Para enviar o seu código, compile-o com o Cloud Build, carregue-o para o Container Registry e implemente-o no Knative Serving:

  1. Execute o seguinte comando para criar o contentor e publicar no Container Registry.

    Node.js

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

    Onde PROJECT_ID é o ID do projeto Google Cloud e graphviz é o nome que quer dar ao seu serviço.

    Após a conclusão com êxito, é apresentada uma mensagem SUCCESS com o ID, a hora de criação e o nome da imagem. A imagem é armazenada no Container Registry e pode ser reutilizada, se quiser.

    Python

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

    Onde PROJECT_ID é o ID do projeto Google Cloud e graphviz é o nome que quer dar ao seu serviço.

    Após a conclusão com êxito, é apresentada uma mensagem SUCCESS com o ID, a hora de criação e o nome da imagem. A imagem é armazenada no Container Registry e pode ser reutilizada, se quiser.

    Go

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

    Onde PROJECT_ID é o ID do projeto Google Cloud e graphviz é o nome que quer dar ao seu serviço.

    Após a conclusão com êxito, é apresentada uma mensagem SUCCESS com o ID, a hora de criação e o nome da imagem. A imagem é armazenada no Container Registry e pode ser reutilizada, se quiser.

    Java

    Este exemplo usa o Jib para criar imagens do Docker com ferramentas Java comuns. O Jib otimiza as compilações de contentores sem necessidade de um Dockerfile ou de ter o Docker instalado. Saiba mais sobre como criar contentores Java com o Jib.

    1. Usando o Dockerfile, configure e crie uma imagem de base com os pacotes do sistema instalados para substituir a imagem de base predefinida do 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

      Onde PROJECT_ID é o ID do seu Google Cloud projeto.

    2. Crie o contentor final com o Jib e publique-o no 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

      Onde PROJECT_ID é o ID do seu Google Cloud projeto.

  2. Implemente com o seguinte comando:

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

    Onde PROJECT_ID é o ID do seu projeto, Google Cloud é o nome do contentor acima e graphviz-web é o nome do serviço.graphviz

    Aguarde até que a implementação esteja concluída. Este processo pode demorar cerca de meio minuto.

  3. Se quiser implementar uma atualização de código no serviço, repita os passos anteriores. Cada implementação num serviço cria uma nova revisão e começa automaticamente a publicar tráfego quando estiver pronta.

Experimentar

Experimente o seu serviço enviando pedidos HTTP POST com descrições de sintaxe DOT no payload do pedido.

  1. Envie um pedido HTTP para o seu serviço.

    Pode incorporar o diagrama numa página Web:

    1. Para obter o IP externo do balanceador de carga, execute o seguinte comando:

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

      Substitua ASM-INGRESS-NAMESPACE pelo espaço de nomes onde se encontra a entrada do Cloud Service Mesh. Especifique istio-system se tiver instalado o Cloud Service Mesh com a respetiva configuração predefinida.

      O resultado tem um aspeto semelhante ao seguinte:

      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

      em que o valor EXTERNAL-IP é o seu endereço IP externo do LoadBalancer.

    2. Execute um comando curl com este endereço EXTERNAL-IP no URL. Não inclua o protocolo (por exemplo: http://) em 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. Abra o ficheiro diagram.png resultante em qualquer aplicação que suporte ficheiros PNG, como o Chrome.

    Deve ter esta forma:

    Diagrama que mostra o fluxo de fases
  de Código para Compilar para Implementar para &quot;Executar&quot;.
    Fonte: Descrição do DOT

Pode explorar uma pequena coleção de descrições de diagramas prontas.

  1. Copie o conteúdo do ficheiro .dot selecionado
  2. Cole-o num 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

Limpar

Pode eliminar os recursos criados para este tutorial para evitar incorrer em custos.

Eliminar recursos do tutorial

  1. Elimine o serviço Knative Serving que implementou neste tutorial:

    gcloud run services delete SERVICE-NAME

    Onde SERVICE-NAME é o nome do serviço escolhido.

    Também pode eliminar serviços Knative serving a partir da Google Cloud consola:

    Aceda ao Knative serving

  2. Remova as configurações predefinidas do gcloud que adicionou durante a configuração do tutorial:

     gcloud config unset run/platform
     gcloud config unset run/cluster
     gcloud config unset run/cluster_location
    
  3. Remova a configuração do projeto:

     gcloud config unset project
    
  4. Elimine outros Google Cloud recursos criados neste tutorial:

O que se segue?

  • Experimente a sua app Graphviz:
    • Adicionar suporte para outras utilidades do Graphviz que aplicam algoritmos diferentes à geração de diagramas.
    • Guarde diagramas no Cloud Storage. Quer guardar a imagem ou a sintaxe DOT?
    • Implemente a proteção contra abuso de conteúdo com a API Cloud Natural Language.
  • Explore arquiteturas de referência, diagramas e práticas recomendadas sobre o Google Cloud. Consulte o nosso Centro de arquitetura na nuvem.