שימוש בחבילות מערכת

במדריך הזה מוסבר איך ליצור שירות מותאם אישית של Knative serving שממיר פרמטר קלט של תיאור גרף לדיאגרמה בפורמט התמונה PNG. הוא משתמש ב-Graphviz שמותקן כחבילת מערכת בסביבת הקונטיינר של השירות. השימוש ב-Graphviz מתבצע באמצעות כלי שורת פקודה כדי לטפל בבקשות.

מטרות

  • לכתוב ולבנות קונטיינר בהתאמה אישית עם Dockerfile
  • כתיבה, פיתוח ופריסה של שירות Knative
  • שימוש בכלי השירות Graphviz dot ליצירת דיאגרמות
  • בודקים את השירות על ידי פרסום תרשים תחביר של DOT מהאוסף או יצירה משלכם

עלויות

במסמך הזה משתמשים ברכיבים הבאים של Google Cloud, והשימוש בהם כרוך בתשלום:

כדי ליצור הערכת עלויות בהתאם לשימוש החזוי, אפשר להשתמש במחשבון התמחור.

יכול להיות שמשתמשים חדשים ב- Google Cloud זכאים לתקופת ניסיון בחינם.

לפני שמתחילים

אחזור של דוגמת הקוד

כדי לאחזר את דוגמת קוד לשימוש:

  1. משכפלים את מאגר האפליקציה לדוגמה ומעבירים אותו למכונה המקומית:

    Node.js

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

    אפשרות נוספת היא להוריד את הדוגמה כקובץ ZIP ולחלץ אותה.

    Python

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

    אפשרות נוספת היא להוריד את הדוגמה כקובץ ZIP ולחלץ אותה.

    Go

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

    אפשרות נוספת היא להוריד את הדוגמה כקובץ ZIP ולחלץ אותה.

    Java

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

    אפשרות נוספת היא להוריד את הדוגמה כקובץ ZIP ולחלץ אותה.

  2. עוברים לספרייה שמכילה את הקוד לדוגמה של 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/

הדמיה של הארכיטקטורה

הארכיטקטורה הבסיסית נראית כך:

דיאגרמה שמוצגת בה זרימת הבקשה מהמשתמש לשירות האינטרנט לכלי Graphviz dot.
למקור הדיאגרמה, ראו תיאור DOT

המשתמש שולח בקשת HTTP לשירות Knative serving שמבצע כלי Graphviz כדי להפוך את הבקשה לתמונה. התמונה הזו מועברת למשתמש כתגובת HTTP.

הסבר על הקוד

הגדרת תצורת הסביבה באמצעות Dockerfile

Dockerfile הוא ספציפי לשפה ולסביבת ההפעלה הבסיסית, כמו Ubuntu, שבה השירות ישתמש.

השירות הזה דורש חבילות מערכת נוספות שלא זמינות כברירת מחדל.

  1. פותחים את Dockerfile בעורך.

  2. מחפשים דף חשבון ב-Dockerfile RUN. ההצהרה הזו מאפשרת להריץ פקודות שרירותיות של מעטפת כדי לשנות את הסביבה. אם Dockerfile מורכב מכמה שלבים, שזוהו על ידי מציאת כמה הצהרות FROM, הוא יימצא בשלב האחרון.

    החבילות הספציפיות שנדרשות והמנגנון להתקנתן משתנים בהתאם למערכת ההפעלה שמוצהרת בתוך הקונטיינר.

    כדי לקבל הוראות למערכת ההפעלה או לקובץ הבסיס, לוחצים על הכרטיסייה המתאימה.

    Debian/Ubuntu
    RUN apt-get update -y && apt-get install -y \
      graphviz \
      && apt-get clean
    Alpine
    ב-Alpine נדרשת חבילה שנייה לתמיכה בגופנים.
    RUN apk --no-cache add graphviz

    כדי לזהות את מערכת ההפעלה של קובץ אימג' של קונטיינר, בודקים את השם בהצהרת FROM או בקובץ README שמשויך לתמונת הבסיס. לדוגמה, אם אתם מרחיבים מ-node, תוכלו למצוא תיעוד ואת האב Dockerfile ב-Docker Hub.

  3. כדי לבדוק את ההתאמה האישית, יוצרים את האימג' באמצעות docker build באופן מקומי או באמצעות Cloud Build.

טיפול בבקשות נכנסות

שירות הדוגמה משתמש בפרמטרים מבקשת ה-HTTP הנכנסת כדי להפעיל קריאה למערכת שמבצעת את פקודת כלי השירות המתאימה dot.

ב-HTTP handler שבהמשך, פרמטר הקלט של תיאור הגרף מחולץ מהמשתנה dot querystring.

תיאורי גרפים יכולים לכלול תווים שצריך לקודד בפורמט URL כדי להשתמש בהם במחרוזת שאילתה.

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;
    });

צריך להבדיל בין שגיאות שרת פנימיות לבין קלט משתמש לא תקין. שירות הדוגמה הזה מחזיר שגיאת שרת פנימית לכל השגיאות בשורת הפקודה של נקודה, אלא אם הודעת השגיאה מכילה את המחרוזת syntax, שמציינת בעיה בקלט של המשתמש.

יצירת תרשים

הלוגיקה המרכזית של יצירת התרשים משתמשת בכלי שורת הפקודה dot כדי לעבד את פרמטר הקלט של תיאור הגרף לתרשים בפורמט תמונה 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;
}

תכנון שירות מאובטח

כל נקודת חולשה בכלי dot היא נקודת חולשה פוטנציאלית בשירות האינטרנט. כדי לצמצם את הסיכון הזה, אפשר להשתמש בגרסאות עדכניות של חבילת graphviz על ידי בנייה מחדש של קובץ אימג' של קונטיינר באופן קבוע.

אם מרחיבים את הדוגמה הנוכחית כדי לקבל קלט של משתמשים כפרמטרים של שורת הפקודה, צריך להגן מפני מתקפות הזרקת פקודות. כמה מהדרכים למניעת מתקפות הזרקה:

  • מיפוי של קלט למילון של פרמטרים נתמכים
  • אימות התאמה של קלט לטווח של ערכים בטוחים ידועים, אולי באמצעות ביטויים רגולריים
  • הוספת תווי Escape לקלט כדי לוודא שתחביר המעטפת לא מוערך

שליחת הקוד

כדי לשלוח את הקוד, יוצרים אותו באמצעות Cloud Build, מעלים אותו ל-Container Registry ופורסים אותו ב-Knative serving:

  1. מריצים את הפקודה הבאה כדי ליצור את הקונטיינר ולפרסם אותו ב-Container Registry.

    Node.js

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

    כאשר PROJECT_ID הוא מזהה הפרויקט שלכם, ו-graphviz הוא השם שאתם רוצים לתת לשירות. Google Cloud

    אם הפעולה תצליח, תופיע הודעה עם המזהה, זמן היצירה ושם התמונה. התמונה מאוחסנת ב-Container Registry ואפשר לעשות בה שימוש חוזר אם רוצים.

    Python

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

    כאשר PROJECT_ID הוא מזהה הפרויקט שלכם, ו-graphviz הוא השם שאתם רוצים לתת לשירות. Google Cloud

    אם הפעולה תצליח, תופיע הודעה עם המזהה, זמן היצירה ושם התמונה. התמונה מאוחסנת ב-Container Registry ואפשר לעשות בה שימוש חוזר אם רוצים.

    Go

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

    כאשר PROJECT_ID הוא מזהה הפרויקט שלכם, ו-graphviz הוא השם שאתם רוצים לתת לשירות. Google Cloud

    אם הפעולה תצליח, תופיע הודעה עם המזהה, זמן היצירה ושם התמונה. התמונה מאוחסנת ב-Container Registry ואפשר לעשות בה שימוש חוזר אם רוצים.

    Java

    בדוגמה הזו נעשה שימוש ב-Jib כדי ליצור תמונות Docker באמצעות כלים נפוצים של Java. ‫Jib מבצע אופטימיזציה של בניית קונטיינרים בלי צורך ב-קובץ Docker או בהתקנה של Docker.

    1. באמצעות קובץ Docker, מגדירים ויוצרים תמונת בסיס עם חבילות המערכת שהותקנו כדי לבטל את תמונת הבסיס שמוגדרת כברירת מחדל ב-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.17_10-jre
      
      RUN apt-get update -y && apt-get install -y \
        graphviz \
        && apt-get clean
      gcloud builds submit --tag gcr.io/PROJECT_ID/graphviz-base

      כאשר PROJECT_ID הוא מזהה הפרויקט. Google Cloud

    2. יוצרים את הקונטיינר הסופי באמצעות Jib ומפרסמים אותו ב-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

      כאשר PROJECT_ID הוא מזהה הפרויקט. Google Cloud

  2. מבצעים פריסה באמצעות הפקודה הבאה:

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

    כאשר PROJECT_ID הוא מזהה הפרויקט, Google Cloud הוא שם הקונטיינר שצוין למעלה ו-graphviz הוא שם השירות.graphviz-web

    מחכים עד שהפריסה תושלם. התהליך עשוי להימשך כחצי דקה.

  3. אם רוצים לפרוס עדכון קוד בשירות, חוזרים על השלבים הקודמים. כל פריסה לשירות יוצרת גרסה חדשה ומתחילה להציג תנועה באופן אוטומטי כשהיא מוכנה.

רוצה לנסות?

כדי לנסות את השירות, שולחים בקשות HTTP POST עם תיאורים של תחביר DOT במטען הייעודי (payload) של הבקשה.

  1. שליחת בקשת HTTP לשירות.

    אפשר להטמיע את הדיאגרמה בדף אינטרנט:

    1. כדי לקבל את כתובת ה-IP החיצונית של מאזן העומסים, מריצים את הפקודה הבאה:

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

      מחליפים את ASM-INGRESS-NAMESPACE במרחב השמות שבו נמצאת הכניסה של Cloud Service Mesh. מציינים istio-system אם התקנתם את Cloud Service Mesh באמצעות הגדרת ברירת המחדל שלו.

      הפלט שיתקבל ייראה כך:

      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

      כאשר הערך EXTERNAL-IP הוא כתובת ה-IP החיצונית של מאזן העומסים.

    2. מריצים פקודת curl באמצעות הכתובת EXTERNAL-IP בכתובת ה-URL. אסור לכלול את הפרוטוקול (לדוגמה: http://) ב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. פותחים את קובץ diagram.png שנוצר בכל אפליקציה שתומכת בקובצי PNG, כמו Chrome.

    הוא אמור להיראות כך:

    דיאגרמה שמציגה את זרימת השלבים: Code (קוד) > Build (בנייה) > Deploy (פריסה) > Run (הפעלה).
    מקור: תיאור DOT

אתם יכולים לעיין באוסף קטן של תיאורים מוכנים מראש של דיאגרמות.

  1. העתקת התוכן של קובץ .dot שנבחר
  2. מדביקים אותו בפקודה 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

הסרת המשאבים

כדי להימנע מחיובים, אפשר למחוק את המשאבים שנוצרו במסגרת המדריך הזה.

מחיקת משאבי הדרכה

  1. מוחקים את שירות Knative serving שפרסתם במדריך הזה:

    gcloud run services delete SERVICE-NAME

    כאשר SERVICE-NAME הוא שם השירות שבחרתם.

    אפשר גם למחוק שירותי Knative serving מהמסוף:Google Cloud

    מעבר אל Knative serving

  2. מסירים את הגדרות ברירת המחדל של gcloud שהוספתם במהלך ההגדרה של המדריך:

     gcloud config unset run/platform
     gcloud config unset run/cluster
     gcloud config unset run/cluster_location
    
  3. מסירים את הגדרות הפרויקט:

     gcloud config unset project
    
  4. מחיקה של משאבים אחרים Google Cloud שנוצרו במדריך הזה:

המאמרים הבאים

  • ניסוי עם אפליקציית graphviz:
    • הוספנו תמיכה בכלי עזר אחרים של Graphviz שמחילים אלגוריתמים שונים על יצירת דיאגרמות.
    • שמירת תרשימים ב-Cloud Storage. רוצה לשמור את התמונה או את תחביר ה-DOT?
    • הטמעה של הגנה מפני התנהלות פוגעת בתוכן באמצעות Cloud Natural Language API.
  • כדאי להעמיק את הקריאה ולהכיר דוגמאות לארכיטקטורות, תרשימים ושיטות מומלצות בנושאי Google Cloud. כל אלה זמינים במרכז הארכיטקטורה של Cloud.