חיבור למופע Redis מאשכול Google Kubernetes Engine



gcloud container clusters describe

אפשר להתחבר למופע Redis רק מאשכולות Google Kubernetes Engine שמשתמשים באותה רשת מורשית כמו מופע Redis.

הגדרה

אם כבר התקנתם את Google Cloud CLI ויצרתם מופע Redis, אתם יכולים לדלג על השלבים האלה.

  1. מתקינים את ה-CLI של gcloud ומפעילים אותו:

    gcloud init
    
  2. פועלים לפי המדריך למתחילים כדי ליצור מכונת Redis. חשוב לשים לב לאזור, לכתובת ה-IP ולפורט של מכונת Redis.

הכנת אשכול GKE

  1. אם לא יצרתם אשכול GKE, אתם יכולים ליצור אותו באמצעות הפקודות הבאות ב-Google Cloud CLI:

    1. מגדירים את הפרויקט לאפליקציית הדוגמה הזו ב-gcloud.

      gcloud config set project PROJECT_ID

    2. מגדירים את משתנה ההגדרה של אזור Compute Engine ב-gcloud.

      gcloud config set compute/zone ZONE

    3. יוצרים אשכול GKE בשם visitcount-cluster.

      gcloud container clusters create visitcount-cluster --num-nodes=3 --enable-ip-alias

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

    gcloud container clusters get-credentials CLUSTER_NAME --zone CLUSTER_ZONE --project PROJECT_ID
    1. ‫CLUSTER_NAME הוא השם של אשכול GKE.
    2. ‫CLUSTER_ZONE הוא האזור שבו האשכול נמצא.
    3. ‫PROJECT_ID הוא הפרויקט שבו נמצאים האשכול ומופעי Redis.
  3. אם הגרסה של האשכול היא 1.8 ואילך והאפשרות 'כינויי IP' מופעלת, מדלגים על השלב הזה. אם האשכול שלכם הוא מגרסה 1.7 או מגרסה מוקדמת יותר, או אם באשכול מגרסה 1.8 או מגרסה מאוחרת יותר לא מופעלים כינויי IP, צריך לפעול לפי השלבים הבאים לפתרון הבעיה לפני שמנסים להתחבר למופע.

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

      git clone https://github.com/bowei/k8s-custom-iptables.git
      cd k8s-custom-iptables/
      TARGETS="RESERVED_IP_RANGE" ./install.sh
      cd ..
      
    2. אם אתם לא יודעים מהו טווח כתובות ה-IP השמור של המופע, תוכלו לגלות אותו באמצעות המסוף (אפשרויות מתקדמות) או באמצעות הפקודה הבאה:

      gcloud redis instances describe INSTANCE_ID --region=REGION
      

    מידע נוסף על כינויי IP, כולל איך ליצור אשכול עם ההגדרה הזו, זמין במאמרי העזרה בנושא כינויי IP.

אפליקציה לדוגמה

אפליקציית שרת ה-HTTP לדוגמה הזו יוצרת חיבור למופע Redis מאשכול Google Kubernetes Engine.

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

המשך

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

Java

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

Node.js

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

Python

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

באפליקציה לדוגמה הזו, מוסיפים 1 למונה Redis בכל פעם שניגשים לנקודת הקצה /.

המשך

האפליקציה הזו משתמשת בלקוח github.com/gomodule/redigo/redis. מריצים את הפקודה הבאה כדי להתקין אותו:

go get github.com/gomodule/redigo/redis@latest

אפליקציה לדוגמה:


// Package main is a basic app that connects to a managed Redis instance.
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/gomodule/redigo/redis"
)

var redisPool *redis.Pool

func incrementHandler(w http.ResponseWriter, r *http.Request) {
	conn := redisPool.Get()
	defer conn.Close()

	counter, err := redis.Int(conn.Do("INCR", "visits"))
	if err != nil {
		http.Error(w, "Error incrementing visitor counter", http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "Visitor number: %d", counter)
}

func main() {
	redisHost := os.Getenv("REDISHOST")
	redisPort := os.Getenv("REDISPORT")
	redisAddr := fmt.Sprintf("%s:%s", redisHost, redisPort)

	const maxConnections = 10
	redisPool = &redis.Pool{
		MaxIdle: maxConnections,
		Dial:    func() (redis.Conn, error) { return redis.Dial("tcp", redisAddr) },
	}

	http.HandleFunc("/", incrementHandler)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	log.Printf("Listening on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

Java

האפליקציה הזו מבוססת על Jetty 3.1 servlet.

הוא משתמש בספרייה Jedis:

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>5.1.0</version>
</dependency>

המחלקות AppServletContextListener משמשות ליצירת מאגר חיבורים ל-Redis לטווח ארוך:


package com.example.redis;

import java.io.IOException;
import java.util.Properties;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@WebListener
public class AppServletContextListener implements ServletContextListener {

  private Properties config = new Properties();

  private JedisPool createJedisPool() throws IOException {
    String host;
    Integer port;
    config.load(
        Thread.currentThread()
            .getContextClassLoader()
            .getResourceAsStream("application.properties"));
    host = config.getProperty("redis.host");
    port = Integer.valueOf(config.getProperty("redis.port", "6379"));

    JedisPoolConfig poolConfig = new JedisPoolConfig();
    // Default : 8, consider how many concurrent connections into Redis you will need under load
    poolConfig.setMaxTotal(128);

    return new JedisPool(poolConfig, host, port);
  }

  @Override
  public void contextDestroyed(ServletContextEvent event) {
    JedisPool jedisPool = (JedisPool) event.getServletContext().getAttribute("jedisPool");
    if (jedisPool != null) {
      jedisPool.destroy();
      event.getServletContext().setAttribute("jedisPool", null);
    }
  }

  // Run this before web application is started
  @Override
  public void contextInitialized(ServletContextEvent event) {
    JedisPool jedisPool = (JedisPool) event.getServletContext().getAttribute("jedisPool");
    if (jedisPool == null) {
      try {
        jedisPool = createJedisPool();
        event.getServletContext().setAttribute("jedisPool", jedisPool);
      } catch (IOException e) {
        // handle exception
      }
    }
  }
}

המחלקות VisitCounterServlet הן סרוולט אינטרנטי שמגדיל מונה Redis:


package com.example.redis;

import java.io.IOException;
import java.net.SocketException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

@WebServlet(name = "Track visits", value = "")
public class VisitCounterServlet extends HttpServlet {

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    try {
      JedisPool jedisPool = (JedisPool) req.getServletContext().getAttribute("jedisPool");

      if (jedisPool == null) {
        throw new SocketException("Error connecting to Jedis pool");
      }
      Long visits;

      try (Jedis jedis = jedisPool.getResource()) {
        visits = jedis.incr("visits");
      }

      resp.setStatus(HttpServletResponse.SC_OK);
      resp.getWriter().println("Visitor counter: " + String.valueOf(visits));
    } catch (Exception e) {
      resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
    }
  }
}

Node.js

האפליקציה הזו משתמשת במודול redis. זהו קובץ package.json לדוגמה:

{
  "name": "memorystore-redis",
  "description": "An example of using Memorystore(Redis) with Node.js",
  "version": "0.0.1",
  "private": true,
  "license": "Apache Version 2.0",
  "author": "Google Inc.",
  "engines": {
    "node": ">=16.0.0"
  },
  "dependencies": {
    "redis": "^4.0.0"
  }
}

קוד אפליקציה לדוגמה:

'use strict';
const http = require('http');
const redis = require('redis');

const REDISHOST = process.env.REDISHOST || 'localhost';
const REDISPORT = process.env.REDISPORT || 6379;

const client = redis.createClient({
  url: `redis://${REDISHOST}:${REDISPORT}`,
});

client.on('error', err => console.error('ERR:REDIS:', err));

client
  .connect()
  .then(() => {
    console.log('Connected to Redis');
    http
      .createServer(async (req, res) => {
        try {
          const reply = await client.incr('visits');
          res.writeHead(200, {'Content-Type': 'text/plain'});
          res.end(`Visitor number: ${reply}\n`);
        } catch (err) {
          console.error(err);
          res.statusCode = 500;
          res.end(err.message);
        }
      })
      .listen(8080, () => {
        console.log('Server listening on port 8080');
      });
  })
  .catch(err => {
    console.error('Failed to connect to Redis:', err);
    throw err;
  });

Python

האפליקציה הזו משתמשת ב-Flask לאירוח אתרים ובחבילת redis-py כדי לתקשר עם מופע Redis.

Flask==3.1.3; python_version >= '3.9'
gunicorn==23.0.0
redis==6.0.0
Werkzeug==3.1.8; python_version >= '3.9'

קוד אפליקציה לדוגמה:

import logging
import os

from flask import Flask
import redis

app = Flask(__name__)

redis_host = os.environ.get("REDISHOST", "localhost")
redis_port = int(os.environ.get("REDISPORT", 6379))
redis_client = redis.StrictRedis(host=redis_host, port=redis_port)


@app.route("/")
def index():
    value = redis_client.incr("counter", 1)
    return f"Visitor number: {value}"


@app.errorhandler(500)
def server_error(e):
    logging.exception("An error occurred during a request.")
    return (
        """
    An internal error occurred: <pre>{}</pre>
    See logs for full stacktrace.
    """.format(
            e
        ),
        500,
    )


if __name__ == "__main__":
    # This is used when running locally. Gunicorn is used to run the
    # application on Google App Engine and Cloud Run.
    # See entrypoint in app.yaml or Dockerfile.
    app.run(host="127.0.0.1", port=8080, debug=True)

פיתוח קובץ אימג' של קונטיינר

יוצרים את קובץ האימג' של הקונטיינר ומעבירים אותו בדחיפה באמצעות Cloud Build אל Container Registry:

cp gke_deployment/Dockerfile .
gcloud artifacts repositories create --location REPO_REGION --repository-format=docker REPO_ID
gcloud builds submit --tag REPO_REGION-docker.pkg.dev/PROJECT_ID/REPO_ID/visit-counter:v1

פריסת האפליקציה ב-Google Kubernetes Engine

כדי להימנע מקידוד קשיח של כתובת ה-IP של מופע Redis, אפשר ליצור redishost ConfigMap:

export REDISHOST_IP=REDISHOST_IP
kubectl create configmap redishost --from-literal=REDISHOST=${REDISHOST_IP}

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

kubectl get configmaps redishost -o yaml

מעדכנים את gke_deployment/visit-counter.yaml ומחליפים את <REPO_REGION>,‏ <PROJECT_ID> ו-<REPO_ID> בערכים מקובץ האימג' של הקונטיינר שנוצר בשלב פיתוח קובץ האימג' של הקונטיינר. הקובץ הזה מכיל את ההגדרה של הפריסה והשירות.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: visit-counter
  labels:
    app: visit-counter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: visit-counter
  template:
    metadata:
      labels:
        app: visit-counter
    spec:
      containers:
      - name: visit-counter
        image: "<REPO_REGION>-docker.pkg.dev/<PROJECT_ID>/<REPO_ID>/visit-counter:v1"
        env:
        - name: REDISHOST
          valueFrom:
            configMapKeyRef:
              name: redishost
              key: REDISHOST
        ports:
        - name: http
          containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: visit-counter
spec:
  type: LoadBalancer
  selector:
    app: visit-counter
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP

מחילים את ההגדרה על האשכול:

kubectl apply -f gke_deployment/visit-counter.yaml

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

kubectl get service visit-counter

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

curl http://EXTERNAL_IP

הסרת הרשומה של טבלאות ה-IP למכונת Redis

אם פעלתם לפי שלב 3 בקטע הכנת אשכול GKE, התקנתם את טווח כתובות ה-IP השמור של מופע Redis בטבלאות כתובות ה-IP של מופע GKE. אם רוצים להסיר את טווח ה-IP של Redis מהטבלאות של כתובות ה-IP במופע GKE, מריצים את הפקודה הבאה מהספרייה k8s-custom-iptables/:

./uninstall.sh