Conectarse a una instancia de Redis desde un clúster de Google Kubernetes Engine

Solo puedes conectarte a tu instancia de Redis desde clústeres de Google Kubernetes Engine que usen la misma red autorizada que la instancia de Redis.

Configuración

Si ya has instalado la CLI de Google Cloud y has creado una instancia de Redis, puedes saltarte estos pasos.

  1. Instala gcloud CLI e inicialízala:

    gcloud init
    
  2. Sigue la guía de inicio rápido para crear una instancia de Redis. Anota la zona, la dirección IP y el puerto de la instancia de Redis.

Preparar el clúster de GKE

  1. Si no has creado un clúster de GKE, crea uno con los siguientes comandos de la CLI de Google Cloud:

    1. Designa el proyecto de esta aplicación de ejemplo en gcloud.

      gcloud config set project PROJECT_ID

    2. Define la variable de configuración de la zona de Compute Engine en gcloud.

      gcloud config set compute/zone ZONE

    3. Crea un clúster de GKE llamado visitcount-cluster.

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

  2. Si no has creado el clúster con gcloud, usa el siguiente comando para obtener las credenciales del clúster:

    gcloud container clusters get-credentials CLUSTER_NAME --zone CLUSTER_ZONE --project PROJECT_ID
    1. CLUSTER_NAME es el nombre de tu clúster de GKE.
    2. CLUSTER_ZONE es la zona en la que se encuentra tu clúster.
    3. PROJECT_ID es el proyecto en el que se encuentran tu clúster y tus instancias de Redis.
  3. Si tu clúster tiene la versión 1.8 o una posterior y los alias de IP están habilitados, omite este paso. Si tu clúster tiene la versión 1.7 o una anterior, o bien la versión 1.8 o una posterior, pero no tiene habilitados los alias de IP, sigue estos pasos alternativos antes de intentar conectarte a tu instancia.

    1. Ejecuta estos comandos y sustituye RESERVED_IP_RANGE por el intervalo de IP reservado de tu instancia:

      git clone https://github.com/bowei/k8s-custom-iptables.git
      cd k8s-custom-iptables/
      TARGETS="RESERVED_IP_RANGE" ./install.sh
      cd ..
      
    2. Si no conoces el intervalo de IPs reservadas de tu instancia, puedes averiguarlo mediante la consola (opciones avanzadas) o con el siguiente comando:

      gcloud redis instances describe INSTANCE_ID --region=REGION
      

    Para obtener más información sobre los alias de IP, incluido cómo crear un clúster con este ajuste habilitado, consulta la documentación sobre alias de IP.

Aplicación de ejemplo

Esta aplicación de servidor HTTP de ejemplo establece una conexión con una instancia de Redis desde un clúster de Google Kubernetes Engine.

Clona el repositorio del lenguaje de programación que hayas elegido y ve a la carpeta que contiene el código de ejemplo:

Go

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

Esta aplicación de ejemplo incrementa un contador de Redis cada vez que se accede al endpoint /.

Go

Esta aplicación usa el cliente de github.com/gomodule/redigo/redis. Para instalarlo, ejecuta el siguiente comando:

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

Aplicación de ejemplo:


// Command redis 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

Esta aplicación se basa en servlets de Jetty 3.1.

Usa la biblioteca Jedis:

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

La clase AppServletContextListener se usa para crear un grupo de conexiones de Redis de larga duración:


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

La clase VisitCounterServlet es un servlet web que incrementa un contador de 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

Esta aplicación usa el módulo redis.

{
  "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(REDISPORT, REDISHOST);
client.on('error', err => console.error('ERR:REDIS:', err));

// create a server
http
  .createServer((req, res) => {
    // increment the visit counter
    client.incr('visits', (err, reply) => {
      if (err) {
        console.log(err);
        res.status(500).send(err.message);
        return;
      }
      res.writeHead(200, {'Content-Type': 'text/plain'});
      res.end(`Visitor number: ${reply}\n`);
    });
  })
  .listen(8080);

Python

Esta aplicación usa Flask para el servicio web y el paquete redis-py para comunicarse con la instancia de Redis.

Flask==3.0.3
gunicorn==23.0.0
redis==6.0.0
Werkzeug==3.0.3
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)

Crear la imagen de contenedor

Crea la imagen de contenedor y envíala a Container Registry:

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

Desplegar una aplicación en Google Kubernetes Engine

Para evitar codificar de forma rígida la IP de la instancia de Redis, puedes crear un redishost ConfigMap:

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

Verifica la configuración con el siguiente comando:

kubectl get configmaps redishost -o yaml

Actualiza gke_deployment/visit-counter.yaml y sustituye <REPO_REGION>, <PROJECT_ID> y <REPO_ID> por los valores de la imagen de contenedor que has creado en Crear la imagen de contenedor. Este archivo contiene la configuración del despliegue y del servicio.

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

Aplica la configuración a tu clúster:

kubectl apply -f gke_deployment/visit-counter.yaml

Para determinar la dirección IP externa de esta aplicación de ejemplo, ejecuta el siguiente comando:

kubectl get service visit-counter

Verifica que tu aplicación alojada en la IP externa funciona con tu navegador o envía una solicitud GET con curl o tu navegador:

curl http://EXTERNAL_IP

Eliminar la entrada de tablas de IP de la instancia de Redis

Si has seguido el tercer paso de la sección Preparar tu clúster de GKE de esta guía, habrás instalado el intervalo de IPs reservado de tu instancia de Redis en las tablas de IPs de tu instancia de GKE. Si quieres quitar esta entrada de intervalo de IPs de Redis de las tablas de IPs de tu instancia de GKE, ejecuta el siguiente comando desde el directorio k8s-custom-iptables/:

./uninstall.sh